Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions plugins/issue-driven-dev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions plugins/issue-driven-dev/MANIFESTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。)
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ idd-verify #98 --branch <name> # diff against origin/<default>
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)
Expand Down
3 changes: 3 additions & 0 deletions plugins/issue-driven-dev/references/pr-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ Identical to PR path: `<type>: <description> (#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)
```

Expand Down
53 changes: 53 additions & 0 deletions plugins/issue-driven-dev/references/pr-issue-matching.md
Original file line number Diff line number Diff line change
@@ -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` | 文件範例 | 會被照抄 |
3 changes: 3 additions & 0 deletions plugins/issue-driven-dev/references/usecase-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 行為)
Expand Down
10 changes: 6 additions & 4 deletions plugins/issue-driven-dev/skills/idd-all/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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。
>
Expand All @@ -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")
Expand Down Expand Up @@ -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

```
Expand Down
21 changes: 17 additions & 4 deletions plugins/issue-driven-dev/skills/idd-close/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand Down Expand Up @@ -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`)

Expand Down
3 changes: 3 additions & 0 deletions plugins/issue-driven-dev/skills/idd-report/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
3 changes: 3 additions & 0 deletions plugins/issue-driven-dev/skills/idd-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 行為)
Expand Down
Loading