diff --git a/plugins/issue-driven-dev/CHANGELOG.md b/plugins/issue-driven-dev/CHANGELOG.md index 8f73364..9614d1a 100644 --- a/plugins/issue-driven-dev/CHANGELOG.md +++ b/plugins/issue-driven-dev/CHANGELOG.md @@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.104.0] - 2026-08-14 + +### Fixed + +- **`/idd-plan` was unreachable from `/idd-all` (#292)** — the issue reported "two skill docs contradict each other"; + the contradiction was the symptom. `EnterPlanMode` lives in `/idd-plan` (Step 4 `enter_plan_mode_for_approval`). + `/idd-implement` says so itself and, when handed a Plan-tier issue, prints a *warning* suggesting `/idd-plan` — it has + no gate. And `grep "idd-plan" idd-all/SKILL.md` returned **nothing**: the orchestrator never routed there. So under + `/idd-all`, Plan tier's entire approval checkpoint silently vanished while three separate places claimed it fired — + under attended mode the user got a warning telling them to use a skill the orchestrator would not call, and under + unattended mode the injected directive told the sub-skill to "skip the EnterPlanMode approval gate" it never had. + Routing table now sends attended Plan tier to **Phase 3p `/idd-plan`**; unattended is stated as a deliberate downgrade + rather than skipping a gate that does not exist. + +- **`in:body "#N"` is not exact matching, in seven places (#293, #305)** — GitHub tokenizes `#N`; quoting does not make + it precise. Measured on `PsychQuant/perspective-writer`: `in:body "#7"` matched a PR whose body contains only + `codex-pro#7`, and `in:body "#10"` matched a PR whose body contains no `#10` at all. Every call site took `.[0]` as + the answer. The two issues named two sites; the pattern was in **seven** — the close gate (blocks a legitimate close), + branch resolution (verifies the wrong branch), verify's input auto-detect (reviews the wrong PR), the report stats, + and three reference files that get copied. New contract `references/pr-issue-matching.md`: search is a coarse filter, + the decision is made client-side with a regex that excludes cross-repo forms, plus #305's orthogonal check that a PR + predating the issue cannot be its feature branch. It is documented as a pragmatic answer, not an authoritative one — + GraphQL's `CrossReferencedEvent` is closer but not semantically identical. + +- **`idd-close` Step 3.6 now files residue by default (#304)** — the default was `acknowledge as-is`, whose stated + rationale ("closure is a wrap-up moment, not a deliberation moment") was written for **Step 3.5**, a keyword scan over + prose that may have surfaced an offhand mention. Residue is not surfaced — it is *declared* at diagnose time, with the + template requiring an explicit `(none)` when empty. And residue is by definition the part that cannot be + operationalized, so nothing but a follow-up issue will ever touch it again: leaving it in the closing summary reopens + the write-only loop #105 was created to close. Empirically the default was on the wrong side — three residues in one + batch, the user deviated all three times. Aligned with IC_R011 §1.1; skip still requires a reason. + +### Added + +- **MANIFESTO: 設計階段的抽象度,作者自己看不出來 (#144)** — codified with the evidence that produced it: #135's design + phase went four rounds over-abstract before a third party named it, and PR #297 needed **seven** verify rounds where + every round the author believed it was fixed and every round external review found real defects. The heaviest one — + `gh` returning only the oldest 100 comments, so a real closing summary is truncated away and the issue is classified + `missing`, inviting a duplicate post — surfaced only after round six, because it was never in the layer the author + kept re-reading. Round seven then demonstrated the recursive form: the machine check added to prevent prose drift + carried the same blind spot, and all three of that round's new mechanisms had zero test weight. Hence the three rules: + third-party surfacing of design shape, a positive control for every guard, and "I enumerated exhaustively" is not + evidence. ## [2.103.1] - 2026-08-14 ### Fixed diff --git a/plugins/issue-driven-dev/MANIFESTO.md b/plugins/issue-driven-dev/MANIFESTO.md index 066ba62..eed6bf5 100644 --- a/plugins/issue-driven-dev/MANIFESTO.md +++ b/plugins/issue-driven-dev/MANIFESTO.md @@ -338,3 +338,22 @@ Ad-hoc analysis script 同理:當 question 答完,script 進 archive,不會被 f 維護者:Che Cheng 首次版本:2026-04-28(針對 issue-driven-dev v2.32.0 的方法論論述) + +## 設計階段的抽象度,作者自己看不出來 + +AI 在設計階段會**穩定地過度抽象**,而且**當事人審不出來** —— 這不是偶發失誤,是可預期的系統性偏差,所以流程要對它有安排,不能靠「這次多想一下」。 + +證據不是印象,是兩次量到的: + +- **#135 的 design 階段連四輪過度抽象**(每一輪都獨立提出「做成獨立 skill」的更抽象版本),直到第三方點出才收斂。 +- **PR #297 連七輪 verify**:每一輪作者都認為修好了,每一輪外部審查都找到真缺陷。最重的一個(`gh` 只回最舊 100 則 comment,導致真 summary 被截、判 `missing`、觸發重複貼文)**直到第六輪之後才浮出來**,因為它根本不在作者反覆檢視的那一層。同一個 PR 裡,「散文與實作分岔」這個缺陷連續四輪復發,而作者每一輪都做過「完整枚舉」。 + +共同結構:**自審能驗「我打算做的有沒有做到」,驗不了「我做出來的東西自己有沒有矛盾」** —— 矛盾正是作者看不見的那部分。第七輪甚至示範了它的遞迴形式:作者為了防止分岔而新增的機器檢查,本身也帶著同一個盲點(清單只認作者記得加進去的東西),而且新加的三個機制**全部沒有測試重量**,還原任何一個測試都不會紅。 + +因此: + +1. **設計輪廓要由第三方 surface**,不是由作者自評。可以是 ensemble、可以是人,但不能是同一個腦袋再讀一遍。 +2. **每個新加的防護機制都要有 positive control** —— 證明它在被破壞時會失敗。不會失敗的檢查不是檢查。 +3. **「我已經完整枚舉過了」不是證據。** 四輪的枚舉都是誠實的,也都漏了東西。要嘛讓枚舉機械化,要嘛承認它會漏並安排第二雙眼睛。 + +(#144;證據來自 #135 與 PR #297。) diff --git a/plugins/issue-driven-dev/references/external-agent-delegation.md b/plugins/issue-driven-dev/references/external-agent-delegation.md index ba09540..600e49c 100644 --- a/plugins/issue-driven-dev/references/external-agent-delegation.md +++ b/plugins/issue-driven-dev/references/external-agent-delegation.md @@ -77,6 +77,9 @@ idd-verify #98 --branch # diff against origin/ N>0 → local mode HEAD~N..HEAD N=0 → continue b. Search open PRs ref'ing #N: gh pr list --search "#N in:body" --state open + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + 1 PR found → AskUserQuestion "Verify PR #X or local diff?" 2+ PRs → AskUserQuestion list all 0 PRs → fall back HEAD~1 (preserves v2.36 behavior) diff --git a/plugins/issue-driven-dev/references/pr-flow.md b/plugins/issue-driven-dev/references/pr-flow.md index 3776b2f..14ce067 100644 --- a/plugins/issue-driven-dev/references/pr-flow.md +++ b/plugins/issue-driven-dev/references/pr-flow.md @@ -221,6 +221,9 @@ Identical to PR path: `: (#NNN)`, no auto-close trailers. ```bash OPEN_PRS=$(gh pr list --repo "$GITHUB_REPO" --state open \ --search "in:body \"#${N}\" OR in:body \"Refs #${N}\"" \ + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + --json number,url,headRefName) ``` diff --git a/plugins/issue-driven-dev/references/pr-issue-matching.md b/plugins/issue-driven-dev/references/pr-issue-matching.md new file mode 100644 index 0000000..c9d06d5 --- /dev/null +++ b/plugins/issue-driven-dev/references/pr-issue-matching.md @@ -0,0 +1,53 @@ +# 找「引用某 issue 的 PR」——精確比對契約(#293 / #305) + +## 問題 + +GitHub 的 issue/PR search **對 `#N` 做 tokenize 後的比對,不是 issue-reference 解析**。加引號也不會變成精確比對。實測(`PsychQuant/perspective-writer`): + +``` +$ gh pr list --state merged --search 'in:body "#7"' --json number +[{"number":2}] # PR #2 body 內實際出現的是 codex-pro#7,不是 #7 + +$ gh pr list --state merged --search 'in:body "#10"' --json number +[{"number":2}] # PR #2 body 內根本沒有 #10 +``` + +所有呼叫點都直接把 `.[0]` 當答案用,所以誤配會安靜地變成錯誤的 gate 判定、錯誤的 branch、錯誤的 verify 目標。 + +## 契約 + +**search 只當粗篩,判定一律在 client 端做。** + +```bash +# 1. 粗篩(縮小結果集,允許誤中) +CANDIDATES=$(gh pr list --repo "$REPO" --state "$STATE" \ + --search "in:body \"#${N}\"" \ + --json number,body,createdAt,headRefOid,mergedAt) + +# 2. 精篩:#N 前面不得緊鄰 [A-Za-z0-9_/-],後面不得緊鄰數字 +# —— 這一條同時排除 owner/repo#N 與 repo#N 的跨 repo 形式,以及 #12 誤中 #123 +MATCHED=$(printf '%s' "$CANDIDATES" | jq --argjson n "$N" ' + map(select((.body // "") | test("(^|[^A-Za-z0-9_/-])#\($n)([^0-9]|$)")))') + +# 3. 時序檢查(#305):PR 若在 issue 開立之前就建立,不可能是它的 feature branch +ISSUE_CREATED=$(gh issue view "$N" --repo "$REPO" --json createdAt --jq .createdAt) +MATCHED=$(printf '%s' "$MATCHED" | jq --arg t "$ISSUE_CREATED" 'map(select(.createdAt >= $t))') +``` + +**多筆命中時不得預設取 `.[0]`** —— 依呼叫點的語意決定(最新 merge、或提示使用者),並把「有多筆」這件事印出來。 + +## 為什麼不用 GraphQL 的 `CrossReferencedEvent` + +那是最接近權威的來源,但語意不完全一致(它包含任何 cross-reference,不限於「這個 PR 要修這個 issue」),且需要 GraphQL 分頁。**本契約是務實解,不是權威解** —— 這一點要留在實作註解裡,免得後人以為問題已從根解決。 + +## 呼叫點(全部七處) + +| 位置 | 用途 | 誤配後果 | +|---|---|---| +| `skills/idd-close/SKILL.md` Step 1.5 | PR gate | 誤以為有未 merge 的 PR → **擋住合法的 close** | +| `skills/idd-close/SKILL.md` Step 1.55 | branch resolution | 拿到別的 PR 的 headRefOid → merge-completeness 比對錯的 branch | +| `skills/idd-verify/SKILL.md` Step 0.5 | auto-detect input source | 對錯的 PR 跑 verify | +| `skills/idd-report/SKILL.md` | 統計 | 數字偏誤 | +| `references/pr-flow.md` | 文件範例 | 會被照抄 | +| `references/usecase-routing.md` | 文件範例 | 會被照抄 | +| `references/external-agent-delegation.md` | 文件範例 | 會被照抄 | diff --git a/plugins/issue-driven-dev/references/usecase-routing.md b/plugins/issue-driven-dev/references/usecase-routing.md index ceee57c..024cb18 100644 --- a/plugins/issue-driven-dev/references/usecase-routing.md +++ b/plugins/issue-driven-dev/references/usecase-routing.md @@ -100,6 +100,9 @@ 2. N=0 → 查 open PRs ref'ing #98: gh pr list --search "#98 in:body" --state open + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + 找到 1 PR → AskUserQuestion「Verify PR #X 還是本地 diff?」 找到 2+ PR → AskUserQuestion 列全部 找不到 → fall back HEAD~1(保留 v2.36 行為) diff --git a/plugins/issue-driven-dev/skills/idd-all/SKILL.md b/plugins/issue-driven-dev/skills/idd-all/SKILL.md index d59d72b..f59601f 100644 --- a/plugins/issue-driven-dev/skills/idd-all/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-all/SKILL.md @@ -530,7 +530,7 @@ print(m.group(1).strip() if m else 'UNKNOWN') | Complexity 值 | 下一步 | |--------------|--------| | `Simple` | Phase 3a: idd-implement | -| `Plan` | Phase 3a: idd-implement(unattended → Plan deliberation 跳過;attended → Plan tier `EnterPlanMode` approval gate 自然 fire) | +| `Plan` | **attended → Phase 3p: `/idd-plan`**(該 skill 擁有 `EnterPlanMode` 閘門,approve 後自己 chain 到 idd-implement);**unattended → Phase 3a: idd-implement**,並在 final report 標記 `[Plan tier deliberation skipped under unattended mode]` | | `Plan via Layer V` (v2.50+) | 視同 `Plan` 處理 — verdict 是 user 在 idd-diagnose Step 3.4 選 escalate 觸發,routing 行為跟 bare `Plan` 一致 | | `Spectra` | Phase 3b: spectra-discuss → spectra-propose → spectra-apply(unattended → 一輪收斂;attended → multi-turn 對話自然進行) | | `SDD-warranted` (legacy alias) | 視同 `Spectra` 處理(v2.36.0+ backward compat) | @@ -542,7 +542,9 @@ print(m.group(1).strip() if m else 'UNKNOWN') > > **Plan tier under (PR, unattended)**: Plan tier 的核心價值是 user approval via `EnterPlanMode`/`ExitPlanMode`。unattended 沒有 user 在 review plan,所以 Plan path 直接跳到 idd-implement,**在 final report 標記 `[Plan tier deliberation skipped under unattended mode]`**。 > -> **Plan tier under (direct-commit, attended)**: 不傳 unattended hint,idd-implement 進 Plan tier、`EnterPlanMode` 呈現 plan 給 user,user `ExitPlanMode` approve 後才繼續。這是 attended mode 的設計目的之一。 +> **Plan tier under (direct-commit, attended)**: 路由到 **`/idd-plan`**,由它 `EnterPlanMode` 呈現 plan 給 user,user `ExitPlanMode` approve 後由 `/idd-plan` 自己 chain 到 `/idd-implement`。這是 attended mode 的設計目的之一。 +> +> **修正紀錄(#292)**:本表過去把 Plan tier 直送 `idd-implement`,並宣稱該 skill 的 `EnterPlanMode` 閘門會「自然 fire」。**那個閘門不在 `idd-implement` 裡** —— 它住在 `/idd-plan`(見該 skill Step 4 `enter_plan_mode_for_approval`)。`idd-implement` 收到 Plan tier 時做的是**印一句警告**並用 AskUserQuestion 問要不要繼續(見其 SKILL.md 的 Complexity 表)。而 `grep "idd-plan" idd-all/SKILL.md` 在修正前**沒有任何命中** —— `/idd-plan` 從 `/idd-all` 根本到不了。結果是 Plan tier 的 approval checkpoint 在 orchestrated 路徑上整個消失,而三處文件都宣稱它在。 > > **SDD path under (PR, unattended)**: 三步 chain `spectra-discuss → spectra-propose → spectra-apply`,每步傳 `UNATTENDED MODE` directive 抑制 `AskUserQuestion`,一輪收斂、不停 Park。 > @@ -567,7 +569,7 @@ if [ "$INTERACTION" = "unattended" ]; then # Inline directive — sub-skill suppresses AskUserQuestion / EnterPlanMode prompts IMPL_ARGS="$IMPL_ARGS -UNATTENDED MODE — called by /idd-all orchestrator. Suppress AskUserQuestion. If complexity is Plan, skip the EnterPlanMode approval gate and proceed straight to TDD; mark in your final comment that Plan deliberation was skipped." +UNATTENDED MODE — called by /idd-all orchestrator. Suppress AskUserQuestion. If complexity is Plan, this is a DELIBERATE DOWNGRADE to the Simple path — there is no approval gate in this skill to skip (it lives in /idd-plan); proceed straight to TDD and mark in your final comment that Plan deliberation was skipped." fi Skill(skill="issue-driven-dev:idd-implement", args="$IMPL_ARGS") @@ -1086,7 +1088,7 @@ Next: review PR https://github.com/owner/repo/pull/87, merge after acceptance, t Phase 0.5 印 `→ Path: direct-commit (attended) — pr_policy=never`,sub-skill args **不帶** unattended hint。 中間流程: -- Phase 2 diagnose 判定 `Plan` → Phase 3a `idd-implement` 進 Plan tier、`EnterPlanMode` 把 plan 呈現給 user → user `ExitPlanMode` approve 後才繼續 TDD +- Phase 2 diagnose 判定 `Plan` → Phase 3p `/idd-plan` 用 `EnterPlanMode` 把 plan 呈現給 user → user `ExitPlanMode` approve → `/idd-plan` chain 到 `/idd-implement` 跑 TDD - Phase 4 verify 找到 1 個 P3 finding → 不 auto-fix(attended mode trade-off),直接 surface 給 user ``` diff --git a/plugins/issue-driven-dev/skills/idd-close/SKILL.md b/plugins/issue-driven-dev/skills/idd-close/SKILL.md index a2eb57f..98bf25f 100644 --- a/plugins/issue-driven-dev/skills/idd-close/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-close/SKILL.md @@ -315,6 +315,9 @@ Exit code: ```bash OPEN_PRS=$(gh pr list --repo "$GITHUB_REPO" --state open \ --search "in:body \"#${NUMBER}\"" \ + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](../../references/pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + --json number,url,headRefName,mergeable) ``` @@ -606,10 +609,20 @@ When `RESIDUE_TRIGGER == "true"`, AskUserQuestion per IC_R011 canonical pattern. > > 該 residue 在 #${NUMBER} 完成期間有變動嗎?" > -> Options (default = first): -> - **`still residue — acknowledge as-is`** — record acknowledgement in closing summary that residue stayed as residue. No new issue. Audit trail: `Acknowledged as still residue (text quoted in audit block).` -> - **`file as follow-up issue(s)`** — surface candidate decompositions if residue has multiple distinct items; user picks which to file. Each filed issue gets `**Source**: residue from #${NUMBER} at /idd-close time` for traceability. -> - **`skip — record in audit trail only`** — no new issue, no acknowledgement; just log the user's choice. Audit trail: `Skipped per user choice (residue not addressed).` +> Options (default = first, flipped in #304): +> - **`file as follow-up issue(s)`** — **預設**。surface candidate decompositions if residue has multiple distinct items; user picks which to file. Each filed issue gets `**Source**: residue from #NNN` footer. Audit trail: `Filed as follow-up: #XXX`. +> - **`still residue — acknowledge as-is`** — record acknowledgement in closing summary that residue stayed as residue. No new issue. Audit trail: `Acknowledged as still-residue`. +> - **`skip — record in audit trail only`** — no new issue, no acknowledgement; just log the user's choice. Audit trail: `Skipped per user choice (residue not addressed)`. + +**為什麼預設是 file,不是 acknowledge(#304)**:三個理由,第三個是決定性的。 + +1. **原本的理由是從別處借來的。** Step 3.6 過去寫著「closure 是 wrap-up moment 非 deliberation moment」—— 那句話是為 **Step 3.5**(closing-summary keyword scan)寫的。那一步撈的是散文裡的 orphan mention,可能只是順口提到,預設不 file 合理。但 residue 不是撈出來的:它是 `idd-diagnose` 時**被明確宣告過的實體**,模板還強制無殘留要寫 `(none)`。對一個已經正式宣告的東西,預設「不追蹤」說不通。 + +2. **residue 的定義本身就指向它最容易被遺忘。** Foresay §4.6 的 residue = issue 意圖中*無法 operationalize* 的部分。無法 operationalize 就是接不進 checklist、接不進 commit、接不進測試 —— 除了 follow-up issue,**沒有任何機制會再碰它**。停在 closing summary 的散文裡等於宣告完就丟,而 #103 F3 指認的 write-only loop 正是這件事,#105 建本 step 就是要關掉它。 + +3. **實測與預設系統性相反。** 同一批 close 的三個 residue,使用者三次全部偏離預設(Akashic-Library #235/#219/#224,2026-08-13)。一次是偏好,三次同向是預設站錯邊 —— 而代價不對稱:多開一張 follow-up 的成本是三十秒與一次關票,漏追一個 residue 的成本是三個月後重新考古。 + +與 [`references/ic-r011-checkpoint.md`](../../references/ic-r011-checkpoint.md) §1.1 的 file-by-default 對齊;skip 需比照 §1.4 給理由。 #### File issues (if user picks `file`) diff --git a/plugins/issue-driven-dev/skills/idd-report/SKILL.md b/plugins/issue-driven-dev/skills/idd-report/SKILL.md index 2d1633d..95b95e2 100644 --- a/plugins/issue-driven-dev/skills/idd-report/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-report/SKILL.md @@ -137,6 +137,9 @@ git log --all --oneline --grep="#$NUMBER" | head -5 # 找對應 PR gh pr list --repo $GITHUB_REPO --state merged --search "#$NUMBER" \ + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](../../references/pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + --json number,title,mergedAt,additions,deletions ``` diff --git a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md index 7944658..7d77bba 100644 --- a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md @@ -336,6 +336,9 @@ TaskCreate(name="triage_followup_issues", description="Step 5b: 分類 non-block N>0 → 本地 mode HEAD~N..HEAD N=0 → b b. PRS=$(gh pr list --search "#$NUMBER in:body" --state open --json number,headRefName,author) + +> ⚠ **`in:body "#N"` 不是精確比對**(#293 / #305)——它會誤中跨 repo 引用(`codex-pro#7` → `#7`)與無關的 PR。search 只能當粗篩,判定必須照 [`references/pr-issue-matching.md`](../../references/pr-issue-matching.md) 在 client 端精篩,並檢查 PR 不早於 issue。 + 1 PR → AskUserQuestion「Verify PR #X 還是本地 diff?」 2+ PR → AskUserQuestion 列全部 0 PR → fall back HEAD~1(保留 v2.36 行為)