From 823de662193708e75f026c3ccdbc42da3f3ba30b Mon Sep 17 00:00:00 2001 From: che cheng Date: Tue, 12 May 2026 10:54:43 +0800 Subject: [PATCH 1/6] spectra: propose multi-root-traversal-idd-all-chain (Refs #46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Spectra change directory: - proposal.md (Why / What Changes / 10 BREAKING & feature items) - design.md (7 Decisions D1-D7 + Implementation Contract + Risks) - specs/idd-all-chain/spec.md (3 MODIFIED + 1 ADDED forest tree) - specs/idd-spawn-manifest/spec.md (3 MODIFIED — schema v2 hard-break) - tasks.md (15 tasks across 7 groups + Design decisions coverage map) Analyzer: 0 Critical / 0 Warning / 1 Suggestion Validation: passed Refs #46 --- .../.openspec.yaml | 4 + .../design.md | 207 ++++++++++++++++++ .../proposal.md | 44 ++++ .../specs/idd-all-chain/spec.md | 171 +++++++++++++++ .../specs/idd-spawn-manifest/spec.md | 139 ++++++++++++ .../tasks.md | 47 ++++ 6 files changed, 612 insertions(+) create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/.openspec.yaml create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/design.md create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/proposal.md create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-all-chain/spec.md create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-spawn-manifest/spec.md create mode 100644 openspec/changes/multi-root-traversal-idd-all-chain/tasks.md diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/.openspec.yaml b/openspec/changes/multi-root-traversal-idd-all-chain/.openspec.yaml new file mode 100644 index 0000000..437761a --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-05-12 +created_by: che cheng +created_with: claude diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/design.md b/openspec/changes/multi-root-traversal-idd-all-chain/design.md new file mode 100644 index 0000000..e490c9a --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/design.md @@ -0,0 +1,207 @@ +## Context + +`/idd-all-chain` (v2.55.0+, archived from `add-idd-all-chain-skill` change) 目前是 single-root chain orchestrator:接受 1 個 root issue,recursive 呼叫 `/idd-all #M --in-chain` 處理 root + auto-emergent spawn,共開 1 個 cluster branch + 1 個 review PR。Hard caps `chain_max_depth=2` 與 `chain_max_issues=5` 寫死。Spawn manifest schema v1 在 `.claude/.idd/state/chain-spawned-issues.json`,4 個 sub-skill(`idd-implement`/`idd-verify`/`idd-plan`/`idd-diagnose`)透過 `scripts/manifest-append.sh` helper 寫入 spawn entry。 + +**問題**:當 user 同時有 N 個獨立 root issue 要共開一個 review PR(transcript 分流出 3 個 root concern、cross-cutting refactor 撞到 multi-root 場景),只能跑 N 次獨立 chain → N 個 PR,reviewer 失去 holistic view。manifest schema 的 `root_issue: int`(singular)無法描述 multi-root 場景下「某個 spawn 屬於哪個 root subtree」,因此需要 schema 升級。同步地,既有 cap 對 multi-root 過緊(3 roots × 1.5 spawn avg ≈ 7.5 issues 已超過 max=5),需 cap redesign 配套放寬。 + +**Stakeholders**:`/idd-all-chain` 直接 caller(個人 dogfood + IDD 維護者),`/idd-all` chain context 的 4 個 sub-skill(間接 — manifest schema 是它們的 write contract)。 + +**Constraints**: +- IDD discipline:**不**自動 close、**不**自動 merge,Phase 4 停在 verified 等 user +- Single-root invocation(N=1)行為**byte-equivalent**,backward compat 不可破 +- Schema v1→v2 是 BREAKING,但因 manifest 是 per-chain-session transient state(每次 `idd-all-chain` Phase 0 重建,無 cross-session client),hard-break 安全 + +## Goals / Non-Goals + +**Goals:** + +- Accept N≥1 root issues via `/idd-all-chain #A #B #C`;N=1 行為與 v2.55.0+ 完全相同 +- 兩種 traversal strategy:default DFS(rich subtree first),opt-in `--bfs`(fairness across roots) +- Cap redesign 配套 multi-root:`max_depth=3` 為主 cap,`max_issues=10` 為 safety net +- 失敗隔離:per-root verify FAIL halt,其他 root subtree 繼續(不是 global halt) +- 跨 sub-skill consistency:helper + 4 sub-skill 的 manifest write 同 PR ship,schema v1 vs v2 mismatch fail-fast +- Audit truth:spawn entry 顯式記錄 `root_id`,後續分析能追每個 spawn 屬於哪 root subtree + +**Non-Goals:** + +- Dual-accept schema(v1+v2 同時支援)— 沒有 v1 client in the wild,額外 conditional logic 無收益 +- N>1 仍開 N 個 cluster branch / N 個 PR(那是 `/idd-all #N #M --pr` cluster-PR mode 的 use case,本 change 統一在 1 PR 內) +- Cluster-PR mode 的整合(`idd-implement #N #M --pr` 已存在,multi-root chain 與其互補但**不重疊**) +- Auto-close / auto-merge(維持 IDD discipline,user 永遠是 close/merge 的最終 gate) +- `references/chain-flow.md` 之外的 reference doc 新增(現有 doc 補章節即可,不另開檔) + +## Decisions + +### D1: Schema v2 hard-break,不 dual-accept + +**Decision**: `EXPECTED_SCHEMA_VERSION` 在 `scripts/manifest-append.sh` 與 `idd-all-chain/SKILL.md` 同 PR 從 1 升到 2。manifest top-level `root_issue: int` 改為 `root_issues: [int]`,加 `traversal: "dfs" | "bfs"` 欄位;每 spawn entry 加 `root_id: int`(必填,值必為 `root_issues` 內某一元素)。 + +**Rationale**: Manifest 是 per-chain-session transient state(Phase 0 寫入、chain 結束就不再讀);沒有 cross-session 持久化 client。Dual-accept(v1+v2 同時支援)會在 helper write path 與 4 sub-skill read path 加 ~80 行 conditional logic,對應零個真實 backward-compat 需求。Hard-break + 同 PR atomic upgrade 是 net simpler。 + +**Alternatives considered**: +- Dual-accept v1+v2 — rejected:no v1 clients in the wild +- 只升 manifest schema 不升 helper — rejected:`scripts/manifest-append.sh:20` 的 `EXPECTED_SCHEMA_VERSION=1` check 會 fail-fast 拒寫,sub-skill 全部 abort + +### D2: DFS = push-front 真的改 queue 順序,不是只標 label + +**Decision**: 當前 Phase 2 是 `QUEUE=("${QUEUE[@]:1}")` 配 `QUEUE+=("$SPAWN_NUM")` = FIFO = BFS。新 implementation:default DFS mode 把 spawn push 到 queue 前端(`QUEUE=("$SPAWN_NUM" "${QUEUE[@]}")`),BFS mode 保留 push-back。Traversal mode 由 `/idd-all-chain` 接受 `--bfs` flag 決定;default DFS。 + +**Rationale**: 把 default 標為「DFS」卻用 BFS queue 是 mislabelled spec — 任何 user 看到「rich subtree first」描述,實際看到「level-by-level across roots」行為時會 surprised。Single-root 場景 DFS/BFS 在 traversal order 相等(無分支),所以 backward compat 不破。 + +**Alternatives considered**: +- Default BFS — rejected:diagnosis 給的「rich subtree first」是 DFS 語意,user clarification 明示 DFS 為 default +- Per-root 自己選 traversal — rejected:整 chain 共一個 traversal mode 才有單一可預測語意 + +### D3: Cap = max-depth=3 primary + max-issues=10 safety net,獨立 apply + +**Decision**: 兩個 cap 同時存在,擇先觸發。`CHAIN_MAX_DEPTH=3` 對每 root subtree 獨立(每 root depth=0);`CHAIN_MAX_ISSUES=10` 是整個 chain(所有 root 合計)的 total budget。Phase 2 main loop check 兩者皆通過才 enqueue。 + +**Rationale**: max_issues=5 對 multi-root 過緊(3 roots × 平均 1.5 spawn ≈ 7.5 issues > 5);全砍掉 max_issues 失去 unbounded-explosion 的 safety floor;max_depth=3 為主 cap 給 user 直觀控制(我願意看到 depth 多深的 ripple),max_issues=10 為 safety net 防止 max_depth 允許範圍內 fan-out 太大(假設 max_depth=3 + branching=4 → 1+4+16+64=85 issues 是病態)。 + +**Alternatives considered**: +- 只保留 max_depth(去掉 max_issues)— rejected:depth=3 + 高 fan-out 仍可爆炸 +- max_depth=2 max_issues=10 — rejected:depth=2 太緊,multi-root 場景下 root spawns 的 spawns 是常態,depth=2 限制是 single-root v2.55.0 default,multi-root 應該放寬到 3 + +### D4: Verify FAIL = per-root continue (Q4 Option C) + +**Decision**: Phase 2 main loop 偵測 verify FAIL 時,將該 issue 屬於的 root_id 加入 `FAIL_ROOTS[]` set,把該 root 的 subtree 標 FAIL 並停(從 QUEUE 移除所有 `root_id == failed_root` 的 issue);其他 root 的 subtree 繼續處理。所有 commits preserved。Phase 4 final report 印 per-root PASS/FAIL 表。 + +**Rationale**: Option A(global halt)是 single-root 時代的保守 default,multi-root 場景下 root #1 失敗就放棄 root #2/#3 抹掉 multi-root 的收益。Option C 是「失敗隔離 per root」semantics,類似 BFS-tree 中砍 subtree 不砍 forest。Commits preserved 因為 cluster PR 必須由 user review,user 可選擇 partial revert / partial merge。 + +**Alternatives considered**: +- Option A global halt — rejected:抹掉 multi-root 收益 +- Option B halt 該 subtree 但允許在另一 root subtree spawn 出同檔的修復 — rejected:語意太複雜,implementer 容易誤判 spawn ownership + +### D5: Branch naming hash8 for N>1,N=1 保留現行 + +**Decision**: +- N=1: `idd/chain--`(現行,不動) +- N>1: `idd/chain-multi--`,其中 `hash8 = sha256sum("$ROOT_ISSUES_JOINED") | cut -c1-8`(`ROOT_ISSUES_JOINED` 是 sorted asc 的 root numbers 用 `-` join),`root1-slug` 是最小 root 號碼的 title slug(deterministic) +- Collision 偵測:若 `gh api repos/.../branches/$BRANCH` 回 200(branch 已存在),fallback 用 `hash16`(`cut -c1-16`)。雙重 collision(極罕見)則 Phase 0 abort 並印手動清理 hint + +**Rationale**: 列出所有 N 個 root number(`idd/chain-44-45-46-50-55-...-`)在 N≥5 時 branch name 失控且難辨識;hash 是固定長度且 deterministic(同 root set 永遠對應同 hash)。`root1-slug` 提供「這 chain 大致關於什麼」的人類可讀提示。 + +**Alternatives considered**: +- List all root numbers — rejected:N=5+ 長度失控 +- 純 hash(無 slug)— rejected:branch name 失去人類可讀性 +- timestamp suffix — rejected:無 determinism,同一組 root 第二次跑會得到不同 branch name,違反 idempotency + +### D6: PR title format(Q6 deferred from discuss) + +**Decision**: +- N=1: `chain: `(現行,不動) +- N>1: `chain (multi-root): N issues — `,其中 `` 是最小 root 號碼的 issue title + +**Rationale**: Title 第一段給 GitHub reviewer 立刻看到「這是 chain 模式 + 是 multi-root」;`N issues` 給規模感;`` 給語意 anchor(reviewer 點開 PR 前能猜大致主題)。 + +### D7: Phase 4 TaskList visualization(Q7 deferred from discuss) + +**Decision**: Phase 4 final report 印 forest tree printout,每個 root 為一棵樹的根。格式: + +``` +Forest summary (traversal: DFS): + + ✓ root #44 (depth 0) + └─ ✓ #34 (depth 1, spawned by idd-implement Step 5.7 sister-bug) + └─ ✓ #41 (depth 2, spawned by idd-verify Phase 4 follow-up-finding) + ✗ root #45 (depth 0) — FAIL at #48 + └─ ✗ #48 (depth 1, spawned by idd-plan Step 2.5 tangential) + ⊘ root #50 (depth 0) — filed but unprocessed (max-issues=10 reached) + +Per-root PASS/FAIL: + #44: PASS (2 spawn processed) + #45: FAIL (verify FAIL at #48 — subtree halted) + #50: SKIPPED (max-issues cap) +``` + +**Rationale**: Single-line aggregate(現行 `Processed: #28 #34 #41`)在 multi-root 時無法回答「每 root 走多深」「FAIL 出在哪」「哪 root 被 cap 切掉」三個常見 review question。Tree printout 一眼可見 fan-out 形狀。 + +## Implementation Contract + +**Observable behavior after this change ships**: + +1. `/idd-all-chain #44`(N=1,backward compat)— 行為與 v2.55.0+ **byte-equivalent**:cluster branch `idd/chain-44-`、manifest schema v2 但只有 1 個 `root_issues` 元素、PR title `chain: `、Phase 4 single-tree printout。 +2. `/idd-all-chain #44 #45 #50`(N=3 multi-root,default DFS)— 在 lowest root issue 上記錄 chain comment、cluster branch `idd/chain-multi-<hash8>-<root-44-slug>`、manifest 含 `root_issues: [44,45,50]` 與 `traversal: "dfs"`、每個 spawn entry 帶 `root_id: <which root>`、DFS 依序處理 root #44 整 subtree → root #45 → root #50、PR title `chain (multi-root): 3 issues — <root #44 title>`、Phase 4 forest tree printout。 +3. `/idd-all-chain #44 #45 #50 --bfs` — manifest `traversal: "bfs"`,Phase 2 改 push-back semantics,level-by-level 處理。 +4. Verify FAIL 在 root #45 的 subtree(任一 issue)— `FAIL_ROOTS[]` 加 45、QUEUE 移除所有 `root_id=45` 的 pending issue、其他 root subtree 繼續、Phase 4 report 印 per-root PASS/FAIL/SKIPPED。 +5. Hit `max_depth=3` 或 `max_issues=10` cap — 該 spawn filed only(不入 QUEUE)、Phase 4 標 `(depth/issues cap reached)`、chain 不 abort。 +6. v1 manifest 在 disk 上(沒新到 v2 的環境)— helper 與 chain skill 都 `schema_version` mismatch fail-fast,印 migration hint。 + +**Interface / data shape**: + +- CLI:`/idd-all-chain #N [#M ...] [--bfs] [--cwd <path>]` — 接受多個 `#NNN` token(positional)、optional `--bfs` flag(presence = BFS mode,absence = DFS mode)、optional `--cwd <abs path>`(現有 cross-repo flag,行為不變) +- Manifest schema v2(JSON): + ```json + { + "schema_version": 2, + "session_id": "<uuid v4>", + "root_issues": [<int>, ...], + "traversal": "dfs" | "bfs", + "spawned": [ + { + "issue_number": <int>, + "spawned_by": "<sub-skill>", + "spawn_step": "<str>", + "spawn_kind": "<enum>", + "same_file_as_root": <bool>, + "same_skill_as_root": <bool>, + "root_id": <int>, + "filed_at": "<ISO-8601>", + "title": "<str>" + } + ] + } + ``` +- Helper:`manifest-append.sh <repo-root> <issue> <spawned-by> <spawn-step> <spawn-kind> <same-file> <same-skill> <title> <root-id>` — 第 9 個位置參數 `root-id` 必填(整數 > 0) +- Branch:N=1 `idd/chain-<N>-<slug>`;N>1 `idd/chain-multi-<hash8>-<root1-slug>`(collision → hash16) +- PR title:N=1 `chain: <title>`;N>1 `chain (multi-root): N issues — <root#1 title>` + +**Failure modes**: + +- 0 root token → Phase 0 abort `Usage: /idd-all-chain #NNN [#MMM ...] [--bfs] [--cwd <path>]` +- 任一 root state ≠ OPEN → Phase 0 abort,列出非 OPEN 的 root number 與 state +- 任一 root 缺 diagnosis comment → Phase 0.4 3-option AskUserQuestion 套到第一個缺 diagnosis 的 root(現有單 root 邏輯擴展) +- Manifest schema_version 不是 2 → helper exit 1,印 expected vs actual + migration hint +- Branch collision(hash8 與 hash16 都已存在)→ Phase 0.5 abort 並印手動清理 hint +- Sub-skill manifest-append 缺第 9 個位置參數(root_id)→ helper exit 2(usage error) +- Verify FAIL 在某 root subtree → 該 root 標 FAIL、QUEUE 清該 root_id pending、Phase 4 per-root report 印 FAIL + 失敗 issue 號碼 + +**Acceptance criteria**: + +- Smoke test 1:`/idd-all-chain #X`(N=1,X 是現實 issue)→ branch name = `idd/chain-X-<slug>`,manifest `root_issues=[X]` + `traversal="dfs"` + 0 spawn 入 chain(假設無 spawn)→ 行為與 v2.55.0+ 觀察一致(diff Phase 4 report 應有 forest tree 但只有 1 棵) +- Smoke test 2:`/idd-all-chain #A #B #C`(N=3,A<B<C,設 sub-skill 不 spawn 任何 issue)→ branch = `idd/chain-multi-<hash8>-<A-slug>`,manifest `root_issues=[A,B,C]`,Phase 2 依序 process A → B → C,Phase 4 forest 3 棵單獨節點 +- Smoke test 3:`/idd-all-chain #A #B #C --bfs`(同上 N=3 but BFS)→ manifest `traversal="bfs"`,Phase 2 順序仍 A → B → C(無 spawn 時 BFS/DFS 等效),測試在於 manifest 有正確 traversal 標記 +- Smoke test 4:`/idd-all-chain #A #B`(N=2)且 root A 在 idd-implement 階段 spawn 出 #X → DFS mode 應 process 順序為 `A → X → B`(spawn pushed to front),BFS 應為 `A → B → X` +- Smoke test 5:verify FAIL 在 root A 的 spawn #X → FAIL_ROOTS=[A]、QUEUE 清空 root_id=A,繼續 process root B 整 subtree、Phase 4 report A=FAIL B=PASS +- Smoke test 6:depth 4 spawn 被 enqueue → 該 spawn 被 helper 寫入 manifest(audit 仍 file)但 chain 不 process(`⊘ #X depth>max — filed only, not chained`) +- Smoke test 7:total issues 達到 11 個 → 第 11 個 spawn 被 helper 寫入 manifest 但 chain 不 process,印 `⚠ chain_max_issues=10 reached` +- Helper unit test:`manifest-append.sh ... <8 args>` exit code = 2(usage);`... <9 args>` exit code = 0 並寫入含 `root_id` +- Spec analyzer:`spectra analyze multi-root-traversal-idd-all-chain` 0 Critical / 0 Warning +- 4 sub-skill conformance:grep `manifest-append.sh` in `idd-implement/SKILL.md` `idd-verify/SKILL.md` `idd-plan/SKILL.md` `idd-diagnose/SKILL.md` — 每處應傳 9 個位置參數(含 root_id 計算) + +**Scope boundaries (in scope / out of scope)**: + +In scope: +- `idd-all-chain/SKILL.md` Phase 0/1/2/3/4 logic for multi-root + traversal + new caps + per-root halt + new branch/PR naming + forest report +- `manifest-append.sh` schema_version bump + 第 9 位置參數 +- `references/spawn-manifest.md` v2 spec text + example +- `references/chain-flow.md` DFS/BFS algorithm + halt scope + cap interaction +- 4 sub-skill 的 manifest-append.sh 呼叫加 root_id 引數(每處只是 1-line 改動,語意:從 manifest 自己讀 spawn parent 的 root_id) +- 兩個 spec delta:`idd-all-chain`(MODIFIED 5 Requirements)、`idd-spawn-manifest`(MODIFIED schema + new EXPECTED_SCHEMA_VERSION) + +Out of scope: +- `/idd-all-chain` 的 dual-mode merge(N=1 用 single-root 路徑,N>1 用 multi-root 路徑)— 統一 multi-root code path,N=1 是退化情況 +- Auto-restart on cluster-PR conflict — chain 失敗仍依現有 recovery 流程(commits preserved + user 手動接手) +- Per-root branch + cherry-pick to cluster — 維持 single cluster branch,所有 commit 直接 land +- Cross-chain session persistence — manifest 仍是 per-session transient +- `idd-route` integration 變更 — routing-stats 寫入維持單 root summary(post-Phase 4 報表) + +## Risks / Trade-offs + +- **[Risk] Schema migration mismatch across env(helper 升級 vs sub-skill 在不同 PR ship)**→ Mitigation: 強制同 PR ship + 同 commit;helper 的 `schema_version` check 是 fail-fast 兜底,mismatched env 直接 abort 而非 silent corruption +- **[Risk] DFS budget exhaustion fairness(第 1 root subtree 用完 10 budget,root #2/#3 完全沒跑)**→ Mitigation: Phase 2 entry log 印 `DFS strategy: root #X subtree explored first` 提醒;user 若需 fairness 可用 `--bfs`;Phase 4 report 顯示 `⊘ root #Y not processed (max-issues cap)` 透明 surface +- **[Risk] Branch hash8 collision**→ Mitigation: hash8 衝突 fallback hash16(碰撞機率 2^32 vs 2^64);雙重碰撞(極罕見)Phase 0 明示 abort + 手動清理 hint +- **[Risk] PR body 過長(N=5 roots × 平均 2 spawn × subtree details)撞 GitHub 256KB body limit**→ Mitigation: 維持現行 collapsed `<details>` per issue;若仍超限,Phase 3 印 warning 並引導 user 看 individual issue +- **[Risk] DFS vs BFS default 爭議**→ Mitigation: design.md 明示 trade-off,DFS = rich subtree first(reviewer cognitive load 低,一個 root 一個 root 看完),BFS = fairness(每 root 都至少跑到);default DFS 是因為 chain 的典型用途是 root-centric review,fairness 用 opt-in 即可 +- **[Trade-off] Hard-break schema vs dual-accept**:選 hard-break 簡化 helper + 4 sub-skill 的 code,代價是若 user 在 env 升級時跑到一半的 chain 會 abort(可接受 — chain session 是 transient,重跑沒成本) +- **[Trade-off] Single cluster PR(N>1)vs N 個 PR**:選 single cluster PR 給 reviewer holistic view,代價是 user 無法只 merge 部分 root subtree(需 partial revert)— 接受,因 IDD discipline 本來就要求 user 是最終 review 與 merge gate diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/proposal.md b/openspec/changes/multi-root-traversal-idd-all-chain/proposal.md new file mode 100644 index 0000000..d27e1e4 --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/proposal.md @@ -0,0 +1,44 @@ +## Why + +`/idd-all-chain` 目前只接受**單一 root issue**,當 user 同時有多個獨立 root issue 要共開一個 review PR(例如 multi-source dogfood、cross-cutting refactor 撞到 3 個 root concern)時,只能跑 N 次獨立 chain → N 個 cluster branch → N 個 PR,reviewer 失去 holistic view。同時,既有的 `chain_max_depth=2 / chain_max_issues=5` hard cap 對 multi-root 場景太緊(3 roots × 平均 1.5 spawn 已 = 7.5 > 5),需 cap redesign 配套放寬。Spawn manifest schema v1 的 `root_issue: int` 在 multi-root 場景下無法區分某個 spawn 屬於哪個 root subtree,需 hard-break 到 v2。 + +## What Changes + +- **Multi-root invocation**:`/idd-all-chain #44 #45 #50` 接受 ≥2 個 root issue(N=1 行為不變,backward compat) +- **Traversal mode**:default DFS(rich subtree first)+ opt-in `--bfs` flag(fairness — level-by-level across roots) +- **BREAKING**: spawn manifest schema v1 → v2 — `root_issue: int` 改 `root_issues: [int]`、top-level 加 `traversal: "dfs" | "bfs"`、每個 spawn entry 加 `root_id: int` 標明所屬 root subtree +- **Cap redesign**:`chain_max_depth` 2 → 3(primary cap),`chain_max_issues` 5 → 10(safety net);兩 cap 獨立 apply,whichever triggers first 勝 +- **Verify FAIL halt scope**(Q4 Option C):per-root continue(非 global halt)— root #N 的 subtree 中 verify FAIL → 記入 `FAIL_ROOTS[]`,該 subtree 標 FAIL 並停,但其他 root 的 subtree 繼續;commits preserved;Phase 4 final report 印 per-root PASS/FAIL +- **Branch naming**:N=1 仍用 `idd/chain-<N>-<slug>`(backward compat);N>1 用 `idd/chain-multi-<hash8>-<root1-slug>`(hash = first 8 chars of `sha256sum` over joined root numbers,collision fallback to hash16) +- **DFS implementation**:current `QUEUE` 是 FIFO(pop-front + push-back = BFS)— DFS mode 改 push-spawns-to-front(`QUEUE=("$SPAWN_NUM" "${QUEUE[@]}")`);BFS mode 保留現行 push-back +- **Per-root depth counting**:每個 root 獨立 depth=0,`DEPTH_MAP[spawn] = DEPTH_MAP[parent] + 1`;`max_depth=3` 對每 root subtree 獨立 apply +- **PR title format**:N=1 維持 `chain: <root title>`;N>1 用 `chain (multi-root): N issues — <root#1 title>` +- **Phase 4 TaskList visualization**:forest tree printout — 每 root 印該 subtree 的 PROCESSED issues 縮排樹狀 +- **Helper update**:`scripts/manifest-append.sh` 同步 bump `EXPECTED_SCHEMA_VERSION=2`,新增 `root_id` 參數位 +- **4 sub-skill manifest writes**:`idd-implement`/`idd-verify`/`idd-plan`/`idd-diagnose` 的 manifest-append 呼叫需傳入 `root_id`(從讀 manifest 自己的 spawn parent 算出) + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `idd-all-chain`: 接受多 root + 加 traversal mode + 新 cap 值 + 新 verify FAIL halt 語意 + 新 branch naming + 新 PR title +- `idd-spawn-manifest`: schema v1 → v2(BREAKING)— `root_issues: [int]`、top-level `traversal`、spawn entry `root_id` + +## Impact + +- Affected specs: + - Modified: `openspec/specs/idd-all-chain/spec.md`(multi-root + traversal + cap + halt + branch + PR title 五個 Requirements 更新) + - Modified: `openspec/specs/idd-spawn-manifest/spec.md`(schema v2 BREAKING,新欄位 + EXPECTED_SCHEMA_VERSION bump) +- Affected code: + - Modified: `plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md`(Phase 0.1 多 root 解析、Phase 0.4 cluster branch 命名分支、Phase 1 init QUEUE/DEPTH_MAP 多 root、Phase 2 DFS/BFS 雙模式 + per-root halt、Phase 3 PR title + body、Phase 4 forest tree + per-root PASS/FAIL report) + - Modified: `plugins/issue-driven-dev/scripts/manifest-append.sh`(EXPECTED_SCHEMA_VERSION=2、新增 root_id 第 9 個位置參數) + - Modified: `plugins/issue-driven-dev/references/spawn-manifest.md`(schema v2 spec 描述 + example 更新) + - Modified: `plugins/issue-driven-dev/references/chain-flow.md`(DFS/BFS algorithm + per-root halt scope + cap interaction) + - Modified: `plugins/issue-driven-dev/skills/idd-implement/SKILL.md`(Step 5.7 manifest-append.sh 呼叫加 root_id 引數) + - Modified: `plugins/issue-driven-dev/skills/idd-verify/SKILL.md`(Phase 4 manifest-append.sh 呼叫加 root_id) + - Modified: `plugins/issue-driven-dev/skills/idd-plan/SKILL.md`(Step 2.5 manifest-append.sh 呼叫加 root_id) + - Modified: `plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md`(Step 3.6 manifest-append.sh 呼叫加 root_id) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-all-chain/spec.md b/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-all-chain/spec.md new file mode 100644 index 0000000..5399e77 --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-all-chain/spec.md @@ -0,0 +1,171 @@ +## MODIFIED Requirements + +### Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR + +The `idd-all-chain` skill SHALL accept one or more root issue arguments (`/idd-all-chain #N` or `/idd-all-chain #N #M #P [--bfs] [--cwd <path>]`) and run a chain-solve workflow that: + +1. Creates a single cluster branch from the default branch. When N=1 the branch is named `idd/chain-<N>-<slug>` (backward compatible). When N>1 the branch is named `idd/chain-multi-<hash8>-<root1-slug>`, where `hash8` is the first 8 hex characters of `sha256` over the sorted-ascending root numbers joined by `-`, and `root1-slug` is the slug of the lowest root issue's title. On branch-name collision the chain shell SHALL retry with `hash16` (first 16 hex characters); on double collision the shell SHALL abort with a manual cleanup hint. +2. Recursively invokes `/idd-all #M --in-chain` for each root issue and each chain-eligible spawned issue, using either DFS or BFS traversal as specified below. +3. Stops processing the chain queue when the queue is empty, the per-root depth limit (`chain_max_depth=3`) is reached for the current subtree, or the total issues cap (`chain_max_issues=10`) is reached for the whole chain. +4. Opens exactly one pull request after the chain completes, covering all chained issues across all root subtrees. +5. Stops at verified state and SHALL NOT auto-close any issue (user retains close authority per IDD discipline). + +The skill MUST NOT alter the existing `/idd-all` skill's single-issue lifecycle behavior. `/idd-all` invocations without `--in-chain` flag MUST behave identically to v2.46.0+ baseline. + +The skill SHALL accept an optional `--bfs` flag that selects BFS traversal mode (level-by-level across all root subtrees). When `--bfs` is absent the skill SHALL use DFS traversal mode (process one root subtree fully before advancing to the next root). In DFS mode the chain queue SHALL push newly spawned issues to the **front** of the queue. In BFS mode the chain queue SHALL push newly spawned issues to the **back** of the queue. The default mode is DFS. + +Each root issue SHALL have its own independent depth counter starting at zero. Spawn entries SHALL inherit `depth = parent_depth + 1` within their root subtree. The `chain_max_depth` cap applies per-root subtree. The `chain_max_issues` cap applies to the union of all root subtrees combined. + +#### Scenario: single-root invocation is backward compatible + +- **GIVEN** issue #28 is OPEN +- **WHEN** user invokes `/idd-all-chain #28` +- **THEN** the chain shell creates branch `idd/chain-28-<slug>` (single-root naming) +- **AND** initializes manifest with `root_issues=[28]` and `traversal="dfs"` +- **AND** processes the chain in identical observable behavior to v2.55.0 single-root chain runs + +#### Scenario: multi-root invocation uses hash branch naming and DFS by default + +- **GIVEN** issues #44, #45, #50 are all OPEN +- **WHEN** user invokes `/idd-all-chain #44 #45 #50` +- **THEN** the chain shell creates branch `idd/chain-multi-<hash8>-<root-44-slug>` where `hash8` is computed from `sha256` of `44-45-50` +- **AND** initializes manifest with `root_issues=[44,45,50]` and `traversal="dfs"` +- **AND** processes root #44's full subtree (including any DFS-eligible spawns) before advancing to root #45 +- **AND** advances to root #50 only after #45's subtree completes + +#### Scenario: multi-root with explicit BFS flag + +- **GIVEN** issues #44, #45, #50 are all OPEN +- **WHEN** user invokes `/idd-all-chain #44 #45 #50 --bfs` +- **THEN** the manifest records `traversal="bfs"` +- **AND** the chain queue uses push-back semantics +- **AND** roots #44, #45, #50 are processed in input order at the top level before any spawns are processed + +##### Example: DFS vs BFS queue order with a single spawn + +| Mode | Initial queue | Pop #44 | Spawn #X (from #44) added | Next pop | +| ---- | ------------- | ------- | ------------------------- | -------- | +| DFS | [44, 45, 50] | [45, 50] (current=44) | push-front: [X, 45, 50] | X | +| BFS | [44, 45, 50] | [45, 50] (current=44) | push-back: [45, 50, X] | 45 | + +#### Scenario: per-root depth cap enforced + +- **GIVEN** chain max-depth cap is 3 +- **AND** root #44 has a chain of spawns: #44 → #X (depth 1 in #44 subtree) → #Y (depth 2) → #Z (depth 3) → #W would be depth 4 +- **WHEN** the chain shell processes #Z +- **THEN** #W is filed as a follow-up issue with manifest entry (audit preserved) +- **AND** #W is NOT added to the chain queue (per-root depth limit enforced) +- **AND** the chain continues processing root #45's subtree independently + +#### Scenario: total max-issues cap caps the whole chain + +- **GIVEN** chain max-issues cap is 10 +- **AND** roots #44 and #45 between them produce 10 processed issues in their combined subtrees +- **WHEN** an 11th spawn is filed +- **THEN** the 11th spawn is recorded in the manifest with its `root_id` set +- **AND** the 11th spawn is NOT added to the chain queue (total cap enforced) +- **AND** Phase 4 report lists the 11th spawn under "filed only, not chained (max-issues cap)" + +### Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits + +When any chained `/idd-all #M --in-chain` invocation completes with verify FAIL state, the chain shell MUST scope the halt to the failing root's subtree only (not the entire queue), preserve all commits already made on the cluster branch, and continue processing the chain queue for other root subtrees whose work is independent. Specifically the shell MUST: + +1. Identify the `root_id` of the failing issue (from the manifest entry, or from `root_issues[0]` if the failing issue is a root itself) +2. Add that `root_id` to the `FAIL_ROOTS[]` set +3. Remove from the chain queue all pending issues whose `root_id` matches the failing root's `root_id` (the failing subtree is halted) +4. Continue processing the chain queue for other root subtrees (their work is not affected) +5. Preserve all commits already made on the cluster branch (the shell MUST NOT rebase, revert, or modify existing commits) + +The shell MUST emit a final report in Phase 4 listing per-root PASS / FAIL / SKIPPED status. + +When verify FAIL occurs on the only root subtree of the chain (single-root invocation or all other root subtrees already completed), behavior is equivalent to halting the entire queue. + +#### Scenario: verify FAIL in one root subtree halts only that subtree + +- **GIVEN** the chain queue contains pending issues from root #44 subtree and root #45 subtree +- **AND** root #44's spawn #X reaches verify FAIL +- **WHEN** the chain shell observes the FAIL +- **THEN** `FAIL_ROOTS` contains 44 +- **AND** all pending issues with `root_id=44` are removed from the queue +- **AND** the queue continues processing issues with `root_id=45` +- **AND** the cluster branch retains all commits from both #44's partial work and #45's complete work +- **AND** Phase 4 report shows root #44 as `FAIL (verify FAIL at #X)` and root #45 as `PASS` + +#### Scenario: single-root verify FAIL still halts the whole queue + +- **GIVEN** `/idd-all-chain #28` is invoked (single root) +- **AND** `/idd-all #28 --in-chain` reaches Phase 4 verify and reports blocking findings +- **WHEN** the chain shell observes the FAIL +- **THEN** the chain queue is fully halted (no other root subtrees exist) +- **AND** the cluster branch retains partial commits made so far +- **AND** Phase 4 report shows root #28 as `FAIL (verify FAIL at #28)` + +### Requirement: idd-all-chain SHALL produce a cluster PR with collapsed per-issue sections + +After the chain queue is processed (full success, per-root partial failure, or any combination), `idd-all-chain` Phase 3 SHALL open exactly one pull request whose body contains: + +1. PR title: + - When N=1: `chain: <root title>` (backward compatible) + - When N>1: `chain (multi-root): N issues — <root#1 title>` where `<root#1 title>` is the title of the lowest-numbered root issue +2. `Refs #<root_1> #<root_2> ... #<chained_1> #<chained_2> ...` listing all chained issue numbers (all roots first, then their spawns) +3. A `## Cluster overview` section with a table summarizing each issue (issue number, `root_id` it belongs to, spawn source, phase, head commit) +4. A `## Per-issue details` section using collapsed `<details>` HTML elements per issue +5. A `## Pending review` checklist where the final box reads `Pending: human review of cluster PR + /idd-close <issue list> after merge` + +The PR body SHALL NOT contain `Closes #N` / `Fixes #N` / `Resolves #N` trailers (per existing IDD discipline against auto-close). + +#### Scenario: single-root cluster PR uses chain prefix + +- **GIVEN** chain solved root #28 with spawn #34 +- **WHEN** Phase 3 opens the cluster PR +- **THEN** the PR title is `chain: <#28 title>` +- **AND** the body contains `Refs #28 #34` + +#### Scenario: multi-root cluster PR uses chain (multi-root) prefix + +- **GIVEN** chain solved roots #44, #45, #50 with one additional spawn #X from root #44 +- **WHEN** Phase 3 opens the cluster PR +- **THEN** the PR title is `chain (multi-root): 4 issues — <#44 title>` +- **AND** the body contains `Refs #44 #45 #50 #X` +- **AND** the cluster overview table includes a `root_id` column showing #X belongs to root_id=44 + +## ADDED Requirements + +### Requirement: idd-all-chain SHALL emit a Phase 4 final report with forest-tree visualization for multi-root chains + +When the chain shell completes the queue (success or per-root failure), Phase 4 SHALL emit a final report containing: + +1. A traversal mode line indicating the chosen mode (`Forest summary (traversal: dfs)` or `Forest summary (traversal: bfs)`). +2. A forest visualization: one tree per root issue. Each node shall display the issue number, depth within its root subtree, spawn source (sub-skill + spawn kind) for non-root nodes, and a status icon (`✓` for PASS, `✗` for FAIL, `⊘` for filed-but-not-chained). +3. A per-root summary listing each root's final status: `PASS (N spawn processed)` / `FAIL (verify FAIL at #X — subtree halted)` / `SKIPPED (max-issues cap)` / `SKIPPED (root not OPEN)`. +4. A flat list of filed-only-not-chained issues (those that hit a cap or eligibility filter). + +For single-root chains (N=1), the forest visualization SHALL contain exactly one tree, and the per-root summary SHALL contain exactly one entry. + +#### Scenario: multi-root forest report shows per-root status + +- **GIVEN** roots #44 (PASS, 2 spawn processed), #45 (FAIL at #48), #50 (filed but unprocessed due to max-issues cap) +- **WHEN** Phase 4 emits the report +- **THEN** the report contains a `Forest summary (traversal: dfs)` header +- **AND** lists a `✓` node for root #44 with its two `✓` descendant nodes +- **AND** lists a `✗` node for root #45 with the failing spawn #48 shown as `✗` +- **AND** lists a `⊘` node for root #50 with annotation `(max-issues cap)` +- **AND** the per-root summary lists `#44: PASS (2 spawn processed)`, `#45: FAIL (verify FAIL at #48 — subtree halted)`, `#50: SKIPPED (max-issues cap)` + +##### Example: forest tree output for the scenario above + +``` +Forest summary (traversal: dfs): + + ✓ root #44 (depth 0) + ✓ #34 (depth 1, idd-implement Step 5.7 sister-bug) + ✓ #41 (depth 2, idd-verify Phase 4 follow-up-finding) + ✗ root #45 (depth 0) — FAIL at #48 + ✗ #48 (depth 1, idd-plan Step 2.5 tangential) + ⊘ root #50 (depth 0) — filed but unprocessed (max-issues cap) + +Per-root PASS/FAIL: + #44: PASS (2 spawn processed) + #45: FAIL (verify FAIL at #48 — subtree halted) + #50: SKIPPED (max-issues cap) +``` diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-spawn-manifest/spec.md b/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-spawn-manifest/spec.md new file mode 100644 index 0000000..d4f591f --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/specs/idd-spawn-manifest/spec.md @@ -0,0 +1,139 @@ +## MODIFIED Requirements + +### Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema + +When `/idd-all-chain` is active in a session, the chain shell MUST initialize a JSON file at `.claude/.idd/state/chain-spawned-issues.json` (relative to the target repo root) with this top-level shape: + +```json +{ + "schema_version": 2, + "session_id": "<uuid>", + "root_issues": [<integer>, ...], + "traversal": "dfs" | "bfs", + "spawned": [] +} +``` + +The `schema_version` field MUST be `2` for the v2 contract. Future schema breaking changes MUST increment this integer; sub-skills reading the file MUST refuse to write entries when `schema_version` does not match the value they were built against (`EXPECTED_SCHEMA_VERSION=2` for the v2 cohort). + +The `session_id` field MUST be a UUID generated when the chain shell creates the file; sub-skills MUST NOT modify `session_id` after initialization. + +The `root_issues` field MUST be a non-empty array of integer issue numbers (each `> 0`) corresponding to the root issues passed to `/idd-all-chain`. For single-root invocations the array contains exactly one element. The ordering of `root_issues` SHALL match the order of `#NNN` tokens parsed from the invocation (positional left-to-right). The v1 singular `root_issue: <integer>` field is removed in v2. + +The `traversal` field MUST be either `"dfs"` (default when `--bfs` flag is absent) or `"bfs"` (when `--bfs` flag is present at invocation time). + +The `spawned` field MUST be an array of spawn entries (initially empty). + +The v1 schema (with singular `root_issue: <integer>` field, no `traversal` field, and spawn entries lacking `root_id`) is no longer supported. Manifests written with `schema_version=1` SHALL cause the helper script and chain shell to refuse to operate (fail-fast with a clear migration hint pointing to v2 documentation). + +#### Scenario: chain shell initializes multi-root v2 manifest + +- **WHEN** user invokes `/idd-all-chain #44 #45 #50` +- **THEN** before any sub-skill runs, the chain shell creates `.claude/.idd/state/chain-spawned-issues.json` with `schema_version=2`, a fresh UUID for `session_id`, `root_issues=[44,45,50]`, `traversal="dfs"`, and `spawned=[]` + +#### Scenario: chain shell initializes single-root v2 manifest + +- **WHEN** user invokes `/idd-all-chain #28` +- **THEN** the manifest is created with `schema_version=2`, `root_issues=[28]`, `traversal="dfs"`, and `spawned=[]` +- **AND** the singular `root_issue` field is absent (v1 field removed) + +#### Scenario: chain shell records BFS traversal in v2 manifest + +- **WHEN** user invokes `/idd-all-chain #44 #45 --bfs` +- **THEN** the manifest records `traversal="bfs"` (not `"dfs"`) + +#### Scenario: v1 manifest on disk causes fail-fast + +- **GIVEN** an existing v1 manifest at `.claude/.idd/state/chain-spawned-issues.json` with `schema_version=1` and singular `root_issue` field +- **WHEN** the chain shell or any sub-skill attempts to read or append to the manifest +- **THEN** the operation fails with exit status indicating schema mismatch +- **AND** the error output contains the expected schema_version (`2`) and the actual schema_version observed (`1`) +- **AND** the error output suggests deleting or migrating the v1 manifest before re-running the chain + +### Requirement: Each spawned issue SHALL produce one append-only entry in the manifest + +When any of the four sub-skills (`idd-implement` / `idd-verify` / `idd-plan` / `idd-diagnose`) files a follow-up issue during chain context, the sub-skill MUST append exactly one entry to the manifest's `spawned` array. The entry MUST include all required fields: + +```json +{ + "issue_number": <integer>, + "spawned_by": "<sub-skill name>", + "spawn_step": "<step identifier per sub-skill>", + "spawn_kind": "<one of: sister-bug | follow-up-finding | tangential | sister-concern | upstream-tracking>", + "same_file_as_root": <boolean>, + "same_skill_as_root": <boolean>, + "root_id": <integer>, + "filed_at": "<ISO-8601 timestamp>", + "title": "<spawned issue title>" +} +``` + +Required field semantics: + +- `issue_number`: GitHub issue number of the spawned issue (must be `> 0`). +- `spawned_by`: One of `"idd-implement"`, `"idd-verify"`, `"idd-plan"`, `"idd-diagnose"`. +- `spawn_step`: A human-readable identifier matching the sub-skill's spawn step (e.g. `"Step 5.7 sister bug sweep"`, `"Phase 4 follow-up findings triage"`). +- `spawn_kind`: One of the five enumerated values; sub-skills MUST classify their spawn type. +- `same_file_as_root`: True only if the spawn references the same source files as the **specific root** issue this spawn descends from (not any root in the chain). +- `same_skill_as_root`: True only if the spawn references the same skill / module as the **specific root** this spawn descends from. +- `root_id`: The integer issue number of the root that owns this spawn's subtree. MUST be one of the values present in the manifest's top-level `root_issues` array. For top-level root issues themselves (depth 0), `root_id` equals the issue's own number. This field is new in v2. +- `filed_at`: ISO-8601 UTC timestamp of when the GitHub issue was created. +- `title`: The spawned issue's title (raw, no formatting). + +Entries MUST be append-only — sub-skills MUST NOT modify or remove existing entries. + +When a sub-skill runs under chain context, the `root_id` value MUST be derived by tracing the spawn's parent chain back to its originating root. The sub-skill MAY read the manifest to locate the parent's existing entry and inherit `root_id`, or MAY compute it from the chain shell's working environment when the chain shell explicitly passes the current `root_id` as an environment variable or argument. + +#### Scenario: spawn entry records correct root_id under multi-root chain + +- **GIVEN** the manifest has `root_issues=[44, 45, 50]` +- **AND** root #44 has spawned #X (depth 1, root_id=44) +- **AND** `idd-implement` Step 5.7 while processing #X spawns sister-bug #Y +- **WHEN** Step 5.7 appends the manifest entry for #Y +- **THEN** the entry has `root_id=44` (inherited from #X's parent chain) + +#### Scenario: spawn entry under single-root chain has root_id equal to the lone root + +- **GIVEN** the manifest has `root_issues=[28]` +- **AND** `idd-verify` Phase 4 spawns follow-up issue #41 while processing root #28 +- **WHEN** Phase 4 appends the manifest entry for #41 +- **THEN** the entry has `root_id=28` + +### Requirement: All four sub-skills SHALL conformantly write the manifest under chain context + +The four spawning sub-skills (`idd-implement`, `idd-verify`, `idd-plan`, `idd-diagnose`) MUST detect chain context (presence of the manifest file at the fixed path AND `schema_version=2`) and MUST append a manifest entry whenever they file a follow-up issue. Sub-skills MUST NOT silently skip the manifest write. + +When chain context is NOT detected (manifest file absent), sub-skills MUST behave identically to their pre-chain baseline — no manifest write attempted, existing audit-trail comments unchanged. + +When the manifest file exists but `schema_version` is not `2` (for example a stale v1 manifest on disk), sub-skills MUST fail-fast with exit status indicating schema mismatch (sub-skills MUST NOT silently fall back to v1 write semantics, MUST NOT silently skip the write, and MUST NOT overwrite the file with a v2 shape). + +The helper script `manifest-append.sh` SHALL accept exactly nine positional arguments. The ninth argument is `root_id` (integer `> 0`), added in v2. Sub-skills invoking `manifest-append.sh` with fewer than nine arguments SHALL cause the helper to exit with status 2 (usage error). + +#### Scenario: sub-skill outside chain context skips manifest write + +- **GIVEN** the manifest file does not exist at `.claude/.idd/state/chain-spawned-issues.json` +- **WHEN** `idd-implement` Step 5.7 files a sister bug +- **THEN** Step 5.7 emits its existing audit trail comment (Sister Bugs Filed section) +- **AND** does NOT attempt to write a manifest entry (no error, baseline behavior preserved) + +#### Scenario: sub-skill in v2 chain context writes both manifest and audit trail with nine arguments + +- **GIVEN** the manifest file exists with `schema_version=2` +- **WHEN** `idd-verify` Phase 4 follow-up triage files issue #41 while processing a spawn whose root_id is 44 +- **THEN** the sub-skill invokes `manifest-append.sh` with nine positional arguments including `root_id=44` as the ninth +- **AND** the manifest gains a new entry for #41 with `root_id=44` +- **AND** the existing audit trail comment is also posted (both writes succeed) + +#### Scenario: helper rejects eight-argument invocation under v2 + +- **GIVEN** the manifest exists with `schema_version=2` +- **WHEN** a caller invokes `manifest-append.sh` with only eight positional arguments (omitting `root_id`) +- **THEN** the helper exits with status 2 +- **AND** the helper's stderr indicates the usage error and lists the expected nine arguments + +#### Scenario: helper rejects v1 manifest on disk under v2 helper + +- **GIVEN** a stale v1 manifest exists with `schema_version=1` +- **WHEN** any sub-skill invokes `manifest-append.sh` with nine arguments +- **THEN** the helper exits with status 1 (schema mismatch) +- **AND** the helper's stderr lists expected version `2` and actual version `1` diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md new file mode 100644 index 0000000..2244ef6 --- /dev/null +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -0,0 +1,47 @@ +## 1. Schema v2 migration foundation (D1: Schema v2 hard-break; covers Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema) + +- [ ] 1.1 `spawn-manifest.md` 反映 v2 contract — top-level `root_issues: [int]` + `traversal: "dfs"|"bfs"`、spawn entry 加 `root_id: int`、example 改用 multi-root sample。Covers Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema (per D1: Schema v2 hard-break). **Verification**:`grep -c '"root_issues"' plugins/issue-driven-dev/references/spawn-manifest.md` ≥ 2(描述 + example);`grep '"schema_version": 2'` 命中;`spectra validate multi-root-traversal-idd-all-chain` no Critical/Warning for this file。 +- [ ] 1.2 `manifest-append.sh` accepts 9 positional args(第 9 個是 `root_id`)、`EXPECTED_SCHEMA_VERSION=2`、寫入 entry 包含 `root_id` field。Covers Requirement: Each spawned issue SHALL produce one append-only entry in the manifest (per D1: Schema v2 hard-break). **Verification**:單元測試 — (a) `bash manifest-append.sh /tmp/repo 99 idd-implement "test" sister-bug true true "t"` exit code = 2(usage error,只 8 個 args);(b) 9 個 args 對 fixture manifest(`schema_version=2`)寫入後 `jq '.spawned[-1].root_id'` 等於傳入值;(c) 對 v1 manifest 寫入 exit code = 1。 + +## 2. idd-all-chain Phase 0 (arg parsing + branch naming, D5: branch naming hash8 for N>1) + +- [ ] 2.1 `/idd-all-chain` 接受多個 `#NNN` 與 `--bfs` flag;branch 命名 N=1 用 `idd/chain-<N>-<slug>`,N>1 用 `idd/chain-multi-<hash8>-<root1-slug>`,collision fallback hash16 then abort。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D5: branch naming hash8 for N>1). **Verification**:invoke `/idd-all-chain #99` dry-run(設 `DRY_RUN=1` env,Phase 0 列印 branch name 就 exit)印出 `idd/chain-99-<slug>`;`/idd-all-chain #44 #45 --bfs` dry-run 印出 `idd/chain-multi-<8hex>-<root-44-slug>` 與 `traversal=bfs`;0 token invocation 印 `Usage: /idd-all-chain #NNN [#MMM ...] [--bfs] [--cwd <path>]` 並 exit 2。 + +## 3. idd-all-chain Phase 1+2 (chain state + traversal + halt + caps; covers D2/D3/D4) + +- [ ] 3.1 Phase 1 init state 支持 multi-root:`QUEUE=("$ROOT_1" "$ROOT_2" ...)`、per-root `DEPTH_MAP[$root]=0`、新 associative array `ROOT_ID_MAP[$issue]=<owning_root>`、manifest top-level 寫入 `root_issues: [...]` + `traversal: "dfs"|"bfs"`。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label). **Verification**:dry-run 後 inspect `.claude/.idd/state/chain-spawned-issues.json` — `jq '.root_issues | length'` 等於 token 數;`jq '.traversal'` 對 `--bfs` 為 `"bfs"`,否則 `"dfs"`。 +- [ ] 3.2 Phase 2 main loop 雙 traversal mode + caps:DFS 時 spawn 入 `QUEUE=("$SPAWN_NUM" "${QUEUE[@]}")` (push-front),BFS 時 `QUEUE+=("$SPAWN_NUM")` (push-back);per-root depth check 用 `DEPTH_MAP`(`NEXT_DEPTH = DEPTH_MAP[parent]+1`);total issues check 用 global `PROCESSED` count + remaining `QUEUE` length 與 `CHAIN_MAX_ISSUES=10` 比較。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label and per D3: cap = max-depth=3 primary + max-issues=10 safety net,獨立apply). **Verification**:smoke test 4(group 7)觀察 DFS 與 BFS pop 順序差異;`grep -E 'CHAIN_MAX_DEPTH=3|CHAIN_MAX_ISSUES=10' plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md` 兩條 hit;manifest 中 spawn entry 有 `root_id` 等於 owning root。 +- [ ] 3.3 Phase 2 verify FAIL → per-root halt:偵測到 verify FAIL → 從 manifest 找 failing issue 的 `root_id`、push 到 `FAIL_ROOTS[]`、從 `QUEUE` 清除所有 `ROOT_ID_MAP[$issue]==<failed_root>` 的 pending issue、其他 root subtree 繼續(不 `exit 1`)。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (per D4: verify FAIL = per-root continue (Q4 Option C)). **Verification**:smoke test 5(group 7)— multi-root chain 第 1 個 root 的 spawn 故意 verify FAIL,第 2 個 root 仍完成、Phase 4 報表顯示 root#1 FAIL + root#2 PASS、cluster branch 上 root#1 partial commits + root#2 完整 commits 都保留。 + +## 4. idd-all-chain Phase 3+4 (PR + forest report; covers D6/D7) + +- [ ] 4.1 Phase 3 PR title/body 多 root 模式:N=1 `chain: <title>`,N>1 `chain (multi-root): N issues — <root#1 title>`;PR body `## Cluster overview` table 增加 `root_id` 欄位、`Refs` 列所有 chained issue(roots 先、spawns 後)。Covers Requirement: idd-all-chain SHALL produce a cluster PR with collapsed per-issue sections (per D6: PR title format(Q6 deferred from discuss)). **Verification**:N=1 dry-run 印 title `chain: <title>`;N=3 dry-run 印 title 含 `chain (multi-root): 3 issues —`;body 內 `grep -c '\| root_id \|'` ≥ 1。 +- [ ] 4.2 Phase 4 forest tree printout — per root 印 subtree status icons (`✓`/`✗`/`⊘`)、per-root PASS/FAIL summary block、filed-only-not-chained list。Covers Requirement: idd-all-chain SHALL emit a Phase 4 final report with forest-tree visualization for multi-root chains (per D7: Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:smoke test 5(group 7)輸出 manual review — 包含 `Forest summary (traversal:`、每個 root 一行 + 縮排子節點、`Per-root PASS/FAIL:` block 列每個 root status;single-root smoke 仍印 forest(只有 1 棵)backward compat。 + +## 5. 4 sub-skills append root_id to manifest-append.sh call (covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context) + +- [ ] 5.1 [P] `idd-implement` Step 5.7 sister-bug sweep 的 manifest-append.sh 呼叫加第 9 個 arg `root_id`(從 manifest 讀 current issue 的 spawn parent 的 root_id,或從 chain shell 透過 env var `IDD_CHAIN_CURRENT_ROOT_ID` 傳入)。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-implement/SKILL.md` 顯示 9 個位置 args;dogfood smoke(group 7)觀察 manifest entry `root_id` 正確繼承。 +- [ ] 5.2 [P] `idd-verify` Phase 4 follow-up-finding triage 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-verify/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 +- [ ] 5.3 [P] `idd-plan` Step 2.5 tangential sweep 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-plan/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 +- [ ] 5.4 [P] `idd-diagnose` Step 3.6 sister-concern surfacing 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 + +## 6. References doc consistency + +- [ ] 6.1 `chain-flow.md` 補章節:DFS vs BFS algorithm 描述(push-front vs push-back semantics + 流程圖)、per-root verify FAIL halt scope、`max-depth=3`/`max-issues=10` cap interaction(per-root vs global)、branch naming hash8 rule。Covers all design decisions (D1 schema, D2 DFS, D3 caps, D4 halt, D5 branch, D6 PR, D7 forest) by providing prose reference for implementers. **Verification**:`grep -E 'DFS|BFS|FAIL_ROOTS|chain_max_depth|hash8' plugins/issue-driven-dev/references/chain-flow.md` 每個關鍵 token ≥ 1 hit;Phase 4 reviewer manual check 描述與 SKILL.md 實作 consistent。 + +## 7. Acceptance smoke tests + +- [ ] 7.1 Single-root + multi-root happy path smoke tests(test 1+2+3 from design.md Acceptance Criteria):(a) `/idd-all-chain #X` N=1 backward compat;(b) `/idd-all-chain #A #B #C` N=3 DFS default;(c) `/idd-all-chain #A #B #C --bfs` N=3 BFS。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (acceptance for D2 DFS = push-front真的改queue順序,不是只標label and D5 branch naming hash8 for N>1). **Verification**:對 test fixture repo 跑 3 個 smoke,每個 inspect manifest + Phase 4 output + branch name 符合 design.md 列出的 expected behavior。 +- [ ] 7.2 Multi-root edge cases smoke tests(test 4+5+6+7 from design.md):(a) DFS push-front 觀察 spawn pop 順序;(b) verify FAIL 在 root A subtree → root B 繼續;(c) depth 4 spawn filed-only-not-chained;(d) 11th issue 觸發 max-issues=10 cap。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (acceptance for D3 cap = max-depth=3 primary + max-issues=10 safety net,獨立apply and D4 verify FAIL = per-root continue (Q4 Option C) and D7 Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:對 fixture repo 各跑 1 個 smoke,Phase 4 forest report 列出 expected status icons + per-root summary 與 design.md scenarios 對照一致。 + +## Design decisions coverage map + +The following maps each design.md decision heading to the tasks that implement it (verbatim heading text included for cross-reference traceability): + +- D1: Schema v2 hard-break,不 dual-accept — covered by tasks 1.1, 1.2, 5.1, 5.2, 5.3, 5.4 +- D2: DFS = push-front 真的改 queue 順序,不是只標 label — covered by tasks 3.1, 3.2, 7.1, 7.2 +- D3: Cap = max-depth=3 primary + max-issues=10 safety net,獨立 apply — covered by tasks 3.2, 7.2 +- D4: Verify FAIL = per-root continue (Q4 Option C) — covered by tasks 3.3, 7.2 +- D5: Branch naming hash8 for N>1,N=1 保留現行 — covered by tasks 2.1, 7.1 +- D6: PR title format(Q6 deferred from discuss) — covered by task 4.1 +- D7: Phase 4 TaskList visualization(Q7 deferred from discuss) — covered by tasks 4.2, 7.2 From ed9682d8e28a1beb0a2d7ab4c9ed9373a845f9db Mon Sep 17 00:00:00 2001 From: che cheng <kiki830621@gmail.com> Date: Tue, 12 May 2026 10:56:47 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(spawn-manifest):=20schema=20v1=20?= =?UTF-8?q?=E2=86=92=20v2=20hard-break=20(Refs=20#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manifest-append.sh: - bump EXPECTED_SCHEMA_VERSION from 1 to 2 - accept 9th positional arg `root_id` (integer > 0, must be in root_issues array) - emit root_id in spawn entry JSON - migration hint on v1 manifest detection spawn-manifest.md: - top-level: `root_issue: <int>` → `root_issues: [<int>, ...]` + `traversal: "dfs"|"bfs"` - spawn entry: add `root_id: <int>` (required) - example switches to multi-root sample (roots [28, 44], DFS) - chain context detection: schema_version=2 instead of 1 Unit tests pass: - 8 args → exit 2 (usage) - 9 args + v2 fixture manifest → exit 0, root_id written - v1 manifest under v2 helper → exit 1 (schema mismatch) - root_id not in root_issues → exit 2 (validation) Covers tasks 1.1, 1.2 (D1: Schema v2 hard-break) Refs #46 --- .../tasks.md | 4 +- .../references/spawn-manifest.md | 35 ++++++++++------- .../scripts/manifest-append.sh | 39 ++++++++++++++----- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md index 2244ef6..95ecb20 100644 --- a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -1,7 +1,7 @@ ## 1. Schema v2 migration foundation (D1: Schema v2 hard-break; covers Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema) -- [ ] 1.1 `spawn-manifest.md` 反映 v2 contract — top-level `root_issues: [int]` + `traversal: "dfs"|"bfs"`、spawn entry 加 `root_id: int`、example 改用 multi-root sample。Covers Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema (per D1: Schema v2 hard-break). **Verification**:`grep -c '"root_issues"' plugins/issue-driven-dev/references/spawn-manifest.md` ≥ 2(描述 + example);`grep '"schema_version": 2'` 命中;`spectra validate multi-root-traversal-idd-all-chain` no Critical/Warning for this file。 -- [ ] 1.2 `manifest-append.sh` accepts 9 positional args(第 9 個是 `root_id`)、`EXPECTED_SCHEMA_VERSION=2`、寫入 entry 包含 `root_id` field。Covers Requirement: Each spawned issue SHALL produce one append-only entry in the manifest (per D1: Schema v2 hard-break). **Verification**:單元測試 — (a) `bash manifest-append.sh /tmp/repo 99 idd-implement "test" sister-bug true true "t"` exit code = 2(usage error,只 8 個 args);(b) 9 個 args 對 fixture manifest(`schema_version=2`)寫入後 `jq '.spawned[-1].root_id'` 等於傳入值;(c) 對 v1 manifest 寫入 exit code = 1。 +- [x] 1.1 `spawn-manifest.md` 反映 v2 contract — top-level `root_issues: [int]` + `traversal: "dfs"|"bfs"`、spawn entry 加 `root_id: int`、example 改用 multi-root sample。Covers Requirement: Spawn manifest file SHALL exist at a fixed path with a versioned schema (per D1: Schema v2 hard-break). **Verification**:`grep -c '"root_issues"' plugins/issue-driven-dev/references/spawn-manifest.md` ≥ 2(描述 + example);`grep '"schema_version": 2'` 命中;`spectra validate multi-root-traversal-idd-all-chain` no Critical/Warning for this file。 +- [x] 1.2 `manifest-append.sh` accepts 9 positional args(第 9 個是 `root_id`)、`EXPECTED_SCHEMA_VERSION=2`、寫入 entry 包含 `root_id` field。Covers Requirement: Each spawned issue SHALL produce one append-only entry in the manifest (per D1: Schema v2 hard-break). **Verification**:單元測試 — (a) `bash manifest-append.sh /tmp/repo 99 idd-implement "test" sister-bug true true "t"` exit code = 2(usage error,只 8 個 args);(b) 9 個 args 對 fixture manifest(`schema_version=2`)寫入後 `jq '.spawned[-1].root_id'` 等於傳入值;(c) 對 v1 manifest 寫入 exit code = 1。 ## 2. idd-all-chain Phase 0 (arg parsing + branch naming, D5: branch naming hash8 for N>1) diff --git a/plugins/issue-driven-dev/references/spawn-manifest.md b/plugins/issue-driven-dev/references/spawn-manifest.md index 3c2a1bd..f06f8f1 100644 --- a/plugins/issue-driven-dev/references/spawn-manifest.md +++ b/plugins/issue-driven-dev/references/spawn-manifest.md @@ -12,13 +12,14 @@ The file lives **inside the target repo's git tree** under the `.claude/.idd/state/` namespace (per IDD v2.35.0+ namespace migration). The directory is created by `/idd-all-chain` Phase 0 (chain shell initialization). Sub-skills MUST NOT create the directory — if it doesn't exist, sub-skills are NOT in chain context and skip manifest writes. -## Schema (v1) +## Schema (v2) ```json { - "schema_version": 1, + "schema_version": 2, "session_id": "<uuid v4>", - "root_issue": <integer>, + "root_issues": [<integer>, ...], + "traversal": "dfs" | "bfs", "spawned": [ { "issue_number": <integer>, @@ -27,6 +28,7 @@ The file lives **inside the target repo's git tree** under the `.claude/.idd/sta "spawn_kind": "<enum>", "same_file_as_root": <boolean>, "same_skill_as_root": <boolean>, + "root_id": <integer>, "filed_at": "<ISO-8601 UTC>", "title": "<string>" } @@ -38,9 +40,10 @@ The file lives **inside the target repo's git tree** under the `.claude/.idd/sta | Field | Type | Description | |-------|------|-------------| -| `schema_version` | integer | Always `1` for the v1 contract. Future schema breaking changes increment this. Sub-skills MUST refuse to write when this doesn't match the version they were built against. | +| `schema_version` | integer | Always `2` for the v2 contract. Future schema breaking changes increment this. Sub-skills MUST refuse to write when this doesn't match the version they were built against (`EXPECTED_SCHEMA_VERSION=2` in the v2 cohort). | | `session_id` | UUID v4 string | Generated by chain shell at file initialization. Sub-skills MUST NOT modify this field after initialization. Used for audit / log correlation. | -| `root_issue` | integer (>0) | The issue number passed to `/idd-all-chain`. Becomes the chain root. | +| `root_issues` | array of integer (>0) | Non-empty array of issue numbers passed to `/idd-all-chain`. For N=1 single-root invocations this is a single-element array. Ordering matches the positional order of `#NNN` tokens at invocation. | +| `traversal` | string enum | One of `"dfs"` (default when `--bfs` flag absent) or `"bfs"` (when `--bfs` flag present). Determines whether new spawns are pushed to queue front (DFS) or back (BFS). | | `spawned` | array | Append-only list of spawn entries. Initially empty. | ### Spawn entry fields @@ -51,8 +54,9 @@ The file lives **inside the target repo's git tree** under the `.claude/.idd/sta | `spawned_by` | enum string | yes | One of: `"idd-implement"`, `"idd-verify"`, `"idd-plan"`, `"idd-diagnose"`. | | `spawn_step` | string | yes | Human-readable step identifier matching the sub-skill's spawn step. Examples: `"Step 5.7 sister bug sweep"`, `"Phase 4 follow-up findings triage"`, `"Step 2.5 tangential observations"`, `"Step 3.6 sister concern surfacing"`. | | `spawn_kind` | enum string | yes | One of: `"sister-bug"` / `"follow-up-finding"` / `"tangential"` / `"sister-concern"` / `"upstream-tracking"`. Determines chain eligibility. | -| `same_file_as_root` | boolean | yes | True only if the spawn references the same source files as root issue's primary scope. | -| `same_skill_as_root` | boolean | yes | True only if the spawn references the same skill / module as the root issue. | +| `same_file_as_root` | boolean | yes | True only if the spawn references the same source files as the **specific root** this spawn descends from (not any root in the chain). | +| `same_skill_as_root` | boolean | yes | True only if the spawn references the same skill / module as the **specific root** this spawn descends from. | +| `root_id` | integer (>0) | yes | The issue number of the root that owns this spawn's subtree. MUST be one of the values in the top-level `root_issues` array. For top-level root issues themselves (depth 0), `root_id` equals the issue's own number. **New in v2** (multi-root traversal support). | | `filed_at` | ISO-8601 string | yes | UTC timestamp of `gh issue create` completion. Format: `2026-05-08T03:14:22Z`. | | `title` | string | yes | Spawned issue title (raw, no formatting). | @@ -97,9 +101,9 @@ Sub-skills detect chain context with this rule: ```bash MANIFEST="$REPO_ROOT/.claude/.idd/state/chain-spawned-issues.json" if [ -f "$MANIFEST" ]; then - # Verify schema version + # Verify schema version (v2 cohort) schema=$(jq -r '.schema_version // empty' "$MANIFEST") - if [ "$schema" = "1" ]; then + if [ "$schema" = "2" ]; then # Chain context active — write manifest entry alongside existing audit trail IN_CHAIN=true fi @@ -114,13 +118,14 @@ The manifest is **additive** — sub-skill audit trails (Sister Bugs Filed / Tan ## Example -After a chain run with root #28 and 2 spawn events: +After a multi-root chain run with roots #28 + #44, DFS traversal, and 2 spawn events: ```json { - "schema_version": 1, + "schema_version": 2, "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "root_issue": 28, + "root_issues": [28, 44], + "traversal": "dfs", "spawned": [ { "issue_number": 34, @@ -129,6 +134,7 @@ After a chain run with root #28 and 2 spawn events: "spawn_kind": "sister-bug", "same_file_as_root": false, "same_skill_as_root": true, + "root_id": 28, "filed_at": "2026-05-08T03:14:22Z", "title": "refactor: 抽出 generic plugin-presence detector helper" }, @@ -139,6 +145,7 @@ After a chain run with root #28 and 2 spawn events: "spawn_kind": "follow-up-finding", "same_file_as_root": true, "same_skill_as_root": true, + "root_id": 44, "filed_at": "2026-05-08T04:21:08Z", "title": "docs: explicit document plugin-presence trust model" } @@ -146,7 +153,9 @@ After a chain run with root #28 and 2 spawn events: } ``` -Both entries are chain-eligible (#34 by `same_skill_as_root=true`, #41 by `same_file_as_root=true`). +Both entries are chain-eligible (#34 by `same_skill_as_root=true`, #41 by `same_file_as_root=true`). #34 belongs to the #28 subtree (`root_id=28`); #41 belongs to the #44 subtree (`root_id=44`). + +For a single-root invocation (`/idd-all-chain #28`), the manifest looks identical except `root_issues: [28]` (single-element array) and every spawn entry's `root_id` equals `28`. The `traversal` field is still present and recorded. ## Related diff --git a/plugins/issue-driven-dev/scripts/manifest-append.sh b/plugins/issue-driven-dev/scripts/manifest-append.sh index e10f433..07a57bb 100755 --- a/plugins/issue-driven-dev/scripts/manifest-append.sh +++ b/plugins/issue-driven-dev/scripts/manifest-append.sh @@ -6,7 +6,7 @@ # # Usage: # manifest-append.sh <repo-root> <issue-number> <spawned-by> <spawn-step> \ -# <spawn-kind> <same-file> <same-skill> <title> +# <spawn-kind> <same-file> <same-skill> <title> <root-id> # # Exit: # 0 — entry appended, or manifest absent (chain context inactive — silent skip) @@ -17,11 +17,11 @@ set -u -EXPECTED_SCHEMA_VERSION=1 +EXPECTED_SCHEMA_VERSION=2 usage() { cat >&2 <<EOF -Usage: $(basename "$0") <repo-root> <issue-number> <spawned-by> <spawn-step> <spawn-kind> <same-file> <same-skill> <title> +Usage: $(basename "$0") <repo-root> <issue-number> <spawned-by> <spawn-step> <spawn-kind> <same-file> <same-skill> <title> <root-id> Arguments: repo-root absolute path to repo root (where .claude/.idd/state/ lives) @@ -29,9 +29,10 @@ Arguments: spawned-by one of: idd-implement | idd-verify | idd-plan | idd-diagnose spawn-step human-readable step identifier (e.g. "Step 5.7 sister bug sweep") spawn-kind one of: sister-bug | follow-up-finding | tangential | sister-concern | upstream-tracking - same-file true | false (does spawn target same source files as root?) - same-skill true | false (does spawn target same skill / module as root?) + same-file true | false (does spawn target same source files as the specific root?) + same-skill true | false (does spawn target same skill / module as the specific root?) title spawned issue title (raw) + root-id positive integer (must be one of the values in the manifest's top-level root_issues array) If the manifest file does not exist, the script exits 0 silently (chain context not active, sub-skill should continue baseline behavior). @@ -39,7 +40,7 @@ EOF exit 2 } -if [ $# -ne 8 ]; then +if [ $# -ne 9 ]; then usage fi @@ -51,6 +52,7 @@ SPAWN_KIND="$5" SAME_FILE="$6" SAME_SKILL="$7" TITLE="$8" +ROOT_ID="$9" # Validate enums case "$SPAWNED_BY" in @@ -73,6 +75,10 @@ if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]] || [ "$ISSUE_NUMBER" -le 0 ]; then echo "✗ issue-number must be a positive integer, got: '$ISSUE_NUMBER'" >&2 exit 2 fi +if ! [[ "$ROOT_ID" =~ ^[0-9]+$ ]] || [ "$ROOT_ID" -le 0 ]; then + echo "✗ root-id must be a positive integer, got: '$ROOT_ID'" >&2 + exit 2 +fi MANIFEST="${REPO_ROOT}/.claude/.idd/state/chain-spawned-issues.json" @@ -81,7 +87,7 @@ if [ ! -f "$MANIFEST" ]; then exit 0 fi -# Schema version check (per "Each spawned issue SHALL produce one append-only entry" requirement) +# Schema version check (per "Spawn manifest file SHALL exist at a fixed path with a versioned schema" requirement) ACTUAL_VERSION=$(jq -r '.schema_version // empty' "$MANIFEST" 2>/dev/null) if [ "$ACTUAL_VERSION" != "$EXPECTED_SCHEMA_VERSION" ]; then cat >&2 <<EOF @@ -91,11 +97,24 @@ if [ "$ACTUAL_VERSION" != "$EXPECTED_SCHEMA_VERSION" ]; then Actual: ${ACTUAL_VERSION:-(missing)} This sub-skill was built against schema_version=${EXPECTED_SCHEMA_VERSION}. - If the manifest is from a newer chain shell, update sub-skill scripts. + If a stale v1 manifest exists on disk, delete it or migrate to v2 before re-running. EOF exit 1 fi +# Validate root_id is one of the values in the manifest's root_issues array +if ! jq -e --argjson rid "$ROOT_ID" '.root_issues | index($rid)' "$MANIFEST" >/dev/null 2>&1; then + ROOT_ISSUES_LIST=$(jq -r '.root_issues | join(", ")' "$MANIFEST" 2>/dev/null) + cat >&2 <<EOF +✗ root-id $ROOT_ID is not in the manifest's root_issues array. + File: $MANIFEST + root_issues: [$ROOT_ISSUES_LIST] + + Pass a root-id that matches one of the listed roots. +EOF + exit 2 +fi + # Build entry as JSON object via jq (handles escaping correctly) FILED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) @@ -106,6 +125,7 @@ NEW_ENTRY=$(jq -n \ --arg spawn_kind "$SPAWN_KIND" \ --argjson same_file "$SAME_FILE" \ --argjson same_skill "$SAME_SKILL" \ + --argjson root_id "$ROOT_ID" \ --arg filed_at "$FILED_AT" \ --arg title "$TITLE" \ '{ @@ -115,6 +135,7 @@ NEW_ENTRY=$(jq -n \ spawn_kind: $spawn_kind, same_file_as_root: $same_file, same_skill_as_root: $same_skill, + root_id: $root_id, filed_at: $filed_at, title: $title }') @@ -137,5 +158,5 @@ fi mv "$TEMP" "$MANIFEST" trap - EXIT -echo "✓ appended #${ISSUE_NUMBER} to manifest" >&2 +echo "✓ appended #${ISSUE_NUMBER} to manifest (root_id=${ROOT_ID})" >&2 exit 0 From 49b2cc0733e0c00ede2d0fee6802961c9a9e4785 Mon Sep 17 00:00:00 2001 From: che cheng <kiki830621@gmail.com> Date: Tue, 12 May 2026 11:02:21 +0800 Subject: [PATCH 3/6] feat(idd-all-chain): multi-root + DFS/BFS traversal + per-root halt (Refs #46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0: - accept ≥1 root issue + optional --bfs flag - branch naming: N=1 backward compat 'idd/chain-<N>-<slug>', N>1 'idd/chain-multi-<hash8>-<root1-slug>' with hash16 collision fallback - check-diagnosis-readiness.sh now called variadic across all roots - AskUserQuestion aggregates not-ready roots in one prompt - proceed-anyway PATCH iterates per not-ready root Phase 1: - QUEUE seeded with sorted roots - per-root DEPTH_MAP and ROOT_ID_MAP for subtree tracking - FAIL_ROOTS associative array - caps bumped: CHAIN_MAX_DEPTH 2→3, CHAIN_MAX_ISSUES 5→10 Phase 2: - DFS = push-spawn-to-front, BFS = push-spawn-to-back - per-root depth cap (max-depth=3 per subtree) + global max-issues=10 - verify FAIL = halt that root's subtree (D4 Option C), purge same-root pending from QUEUE, continue other root subtrees, commits preserved - export IDD_CHAIN_CURRENT_ROOT_ID so sub-skills can propagate root_id to manifest Phase 3: - PR title dispatch: N=1 'chain: <title>', N>1 'chain (multi-root): N issues — <root#1 title>' - cluster overview table adds root_id column - Refs lists all roots first then chained spawns (dedup) Phase 4: - forest tree printout per root subtree with status icons ✓/✗/⊘ - per-root PASS/FAIL/SKIPPED summary block - filed-only-not-chained list Top-of-file caps + 鐵律 + Failure modes table updated for multi-root. Covers tasks 2.1, 3.1, 3.2, 3.3, 4.1, 4.2 (D2, D3, D4, D5, D6, D7) Refs #46 --- .../tasks.md | 12 +- .../skills/idd-all-chain/SKILL.md | 413 ++++++++++++++---- 2 files changed, 330 insertions(+), 95 deletions(-) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md index 95ecb20..fc5f168 100644 --- a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -5,18 +5,18 @@ ## 2. idd-all-chain Phase 0 (arg parsing + branch naming, D5: branch naming hash8 for N>1) -- [ ] 2.1 `/idd-all-chain` 接受多個 `#NNN` 與 `--bfs` flag;branch 命名 N=1 用 `idd/chain-<N>-<slug>`,N>1 用 `idd/chain-multi-<hash8>-<root1-slug>`,collision fallback hash16 then abort。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D5: branch naming hash8 for N>1). **Verification**:invoke `/idd-all-chain #99` dry-run(設 `DRY_RUN=1` env,Phase 0 列印 branch name 就 exit)印出 `idd/chain-99-<slug>`;`/idd-all-chain #44 #45 --bfs` dry-run 印出 `idd/chain-multi-<8hex>-<root-44-slug>` 與 `traversal=bfs`;0 token invocation 印 `Usage: /idd-all-chain #NNN [#MMM ...] [--bfs] [--cwd <path>]` 並 exit 2。 +- [x] 2.1 `/idd-all-chain` 接受多個 `#NNN` 與 `--bfs` flag;branch 命名 N=1 用 `idd/chain-<N>-<slug>`,N>1 用 `idd/chain-multi-<hash8>-<root1-slug>`,collision fallback hash16 then abort。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D5: branch naming hash8 for N>1). **Verification**:invoke `/idd-all-chain #99` dry-run(設 `DRY_RUN=1` env,Phase 0 列印 branch name 就 exit)印出 `idd/chain-99-<slug>`;`/idd-all-chain #44 #45 --bfs` dry-run 印出 `idd/chain-multi-<8hex>-<root-44-slug>` 與 `traversal=bfs`;0 token invocation 印 `Usage: /idd-all-chain #NNN [#MMM ...] [--bfs] [--cwd <path>]` 並 exit 2。 ## 3. idd-all-chain Phase 1+2 (chain state + traversal + halt + caps; covers D2/D3/D4) -- [ ] 3.1 Phase 1 init state 支持 multi-root:`QUEUE=("$ROOT_1" "$ROOT_2" ...)`、per-root `DEPTH_MAP[$root]=0`、新 associative array `ROOT_ID_MAP[$issue]=<owning_root>`、manifest top-level 寫入 `root_issues: [...]` + `traversal: "dfs"|"bfs"`。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label). **Verification**:dry-run 後 inspect `.claude/.idd/state/chain-spawned-issues.json` — `jq '.root_issues | length'` 等於 token 數;`jq '.traversal'` 對 `--bfs` 為 `"bfs"`,否則 `"dfs"`。 -- [ ] 3.2 Phase 2 main loop 雙 traversal mode + caps:DFS 時 spawn 入 `QUEUE=("$SPAWN_NUM" "${QUEUE[@]}")` (push-front),BFS 時 `QUEUE+=("$SPAWN_NUM")` (push-back);per-root depth check 用 `DEPTH_MAP`(`NEXT_DEPTH = DEPTH_MAP[parent]+1`);total issues check 用 global `PROCESSED` count + remaining `QUEUE` length 與 `CHAIN_MAX_ISSUES=10` 比較。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label and per D3: cap = max-depth=3 primary + max-issues=10 safety net,獨立apply). **Verification**:smoke test 4(group 7)觀察 DFS 與 BFS pop 順序差異;`grep -E 'CHAIN_MAX_DEPTH=3|CHAIN_MAX_ISSUES=10' plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md` 兩條 hit;manifest 中 spawn entry 有 `root_id` 等於 owning root。 -- [ ] 3.3 Phase 2 verify FAIL → per-root halt:偵測到 verify FAIL → 從 manifest 找 failing issue 的 `root_id`、push 到 `FAIL_ROOTS[]`、從 `QUEUE` 清除所有 `ROOT_ID_MAP[$issue]==<failed_root>` 的 pending issue、其他 root subtree 繼續(不 `exit 1`)。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (per D4: verify FAIL = per-root continue (Q4 Option C)). **Verification**:smoke test 5(group 7)— multi-root chain 第 1 個 root 的 spawn 故意 verify FAIL,第 2 個 root 仍完成、Phase 4 報表顯示 root#1 FAIL + root#2 PASS、cluster branch 上 root#1 partial commits + root#2 完整 commits 都保留。 +- [x] 3.1 Phase 1 init state 支持 multi-root:`QUEUE=("$ROOT_1" "$ROOT_2" ...)`、per-root `DEPTH_MAP[$root]=0`、新 associative array `ROOT_ID_MAP[$issue]=<owning_root>`、manifest top-level 寫入 `root_issues: [...]` + `traversal: "dfs"|"bfs"`。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label). **Verification**:dry-run 後 inspect `.claude/.idd/state/chain-spawned-issues.json` — `jq '.root_issues | length'` 等於 token 數;`jq '.traversal'` 對 `--bfs` 為 `"bfs"`,否則 `"dfs"`。 +- [x] 3.2 Phase 2 main loop 雙 traversal mode + caps:DFS 時 spawn 入 `QUEUE=("$SPAWN_NUM" "${QUEUE[@]}")` (push-front),BFS 時 `QUEUE+=("$SPAWN_NUM")` (push-back);per-root depth check 用 `DEPTH_MAP`(`NEXT_DEPTH = DEPTH_MAP[parent]+1`);total issues check 用 global `PROCESSED` count + remaining `QUEUE` length 與 `CHAIN_MAX_ISSUES=10` 比較。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (per D2: DFS = push-front真的改queue順序,不是只標label and per D3: cap = max-depth=3 primary + max-issues=10 safety net,獨立apply). **Verification**:smoke test 4(group 7)觀察 DFS 與 BFS pop 順序差異;`grep -E 'CHAIN_MAX_DEPTH=3|CHAIN_MAX_ISSUES=10' plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md` 兩條 hit;manifest 中 spawn entry 有 `root_id` 等於 owning root。 +- [x] 3.3 Phase 2 verify FAIL → per-root halt:偵測到 verify FAIL → 從 manifest 找 failing issue 的 `root_id`、push 到 `FAIL_ROOTS[]`、從 `QUEUE` 清除所有 `ROOT_ID_MAP[$issue]==<failed_root>` 的 pending issue、其他 root subtree 繼續(不 `exit 1`)。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (per D4: verify FAIL = per-root continue (Q4 Option C)). **Verification**:smoke test 5(group 7)— multi-root chain 第 1 個 root 的 spawn 故意 verify FAIL,第 2 個 root 仍完成、Phase 4 報表顯示 root#1 FAIL + root#2 PASS、cluster branch 上 root#1 partial commits + root#2 完整 commits 都保留。 ## 4. idd-all-chain Phase 3+4 (PR + forest report; covers D6/D7) -- [ ] 4.1 Phase 3 PR title/body 多 root 模式:N=1 `chain: <title>`,N>1 `chain (multi-root): N issues — <root#1 title>`;PR body `## Cluster overview` table 增加 `root_id` 欄位、`Refs` 列所有 chained issue(roots 先、spawns 後)。Covers Requirement: idd-all-chain SHALL produce a cluster PR with collapsed per-issue sections (per D6: PR title format(Q6 deferred from discuss)). **Verification**:N=1 dry-run 印 title `chain: <title>`;N=3 dry-run 印 title 含 `chain (multi-root): 3 issues —`;body 內 `grep -c '\| root_id \|'` ≥ 1。 -- [ ] 4.2 Phase 4 forest tree printout — per root 印 subtree status icons (`✓`/`✗`/`⊘`)、per-root PASS/FAIL summary block、filed-only-not-chained list。Covers Requirement: idd-all-chain SHALL emit a Phase 4 final report with forest-tree visualization for multi-root chains (per D7: Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:smoke test 5(group 7)輸出 manual review — 包含 `Forest summary (traversal:`、每個 root 一行 + 縮排子節點、`Per-root PASS/FAIL:` block 列每個 root status;single-root smoke 仍印 forest(只有 1 棵)backward compat。 +- [x] 4.1 Phase 3 PR title/body 多 root 模式:N=1 `chain: <title>`,N>1 `chain (multi-root): N issues — <root#1 title>`;PR body `## Cluster overview` table 增加 `root_id` 欄位、`Refs` 列所有 chained issue(roots 先、spawns 後)。Covers Requirement: idd-all-chain SHALL produce a cluster PR with collapsed per-issue sections (per D6: PR title format(Q6 deferred from discuss)). **Verification**:N=1 dry-run 印 title `chain: <title>`;N=3 dry-run 印 title 含 `chain (multi-root): 3 issues —`;body 內 `grep -c '\| root_id \|'` ≥ 1。 +- [x] 4.2 Phase 4 forest tree printout — per root 印 subtree status icons (`✓`/`✗`/`⊘`)、per-root PASS/FAIL summary block、filed-only-not-chained list。Covers Requirement: idd-all-chain SHALL emit a Phase 4 final report with forest-tree visualization for multi-root chains (per D7: Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:smoke test 5(group 7)輸出 manual review — 包含 `Forest summary (traversal:`、每個 root 一行 + 縮排子節點、`Per-root PASS/FAIL:` block 列每個 root status;single-root smoke 仍印 forest(只有 1 棵)backward compat。 ## 5. 4 sub-skills append root_id to manifest-append.sh call (covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context) diff --git a/plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md b/plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md index afd8beb..4c218c7 100644 --- a/plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-all-chain/SKILL.md @@ -49,8 +49,8 @@ Take 1 root issue, recursively solve any chain-eligible spawned issues (from sub Inherits `/idd-all` config protocol (walked-up `.claude/issue-driven-dev.local.json`). Chain-specific cap is hard-coded constants (not config-driven, to avoid config sprawl): -- `chain_max_depth = 2` — root is depth 0, immediate spawns are depth 1, spawns of spawns are depth 2 -- `chain_max_issues = 5` — root + at most 4 chained issues per chain run +- `chain_max_depth = 3` — applies **per root subtree**: each root starts at depth 0, immediate spawns at depth 1, deepest allowed at depth 3 (v2.60+, #46) +- `chain_max_issues = 10` — global cap across all root subtrees combined; both caps apply independently, whichever triggers first wins (v2.60+, #46) 當 spawn 超過 cap 時:仍 file 為 follow-up issue (sub-skill 既有 audit trail 不變),但**不**加進 chain queue。 @@ -61,13 +61,13 @@ Inherits `/idd-all` config protocol (walked-up `.claude/issue-driven-dev.local.j **動任何事之前**先用 `TaskCreate` 建 stage-level todo list: ``` -TaskCreate(name="preflight", description="Phase 0: 解析 args、gh auth、確認 root issue OPEN") -TaskCreate(name="check_diagnosis_readiness", description="Phase 0.4 (v2.55+ #47, helper extracted v2.57+ #51): invoke scripts/check-diagnosis-readiness.sh <github-repo> <root-issue> → JSON {ready/not_ready}; not_ready=0 → silent pass; not_ready>0 → AskUserQuestion 3-option (run /idd-diagnose first / proceed anyway / cancel). Placed before cluster branch / manifest creation so cancel has zero side effect.") -TaskCreate(name="setup_cluster_branch", description="Phase 0: 建 cluster branch idd/chain-N-<slug> from default branch + 初始化 spawn manifest") -TaskCreate(name="init_queue", description="Phase 1: queue = [root], depth_map = {root: 0}, closed_set = {}") -TaskCreate(name="chain_loop", description="Phase 2: 主 loop — pop queue, invoke /idd-all #current --in-chain, read manifest, enqueue eligible spawns until queue empty / depth limit / max-issues cap reached / verify FAIL halt") -TaskCreate(name="open_cluster_pr", description="Phase 3: 開 cluster PR — title 'chain: <root title>', Refs all chained #N, body schema cluster overview + per-issue collapsed details") -TaskCreate(name="report_and_stop", description="Phase 4: 印 final report, 停在 verified 等 user /idd-close #N #M #K") +TaskCreate(name="preflight", description="Phase 0: 解析 args (≥1 root + optional --bfs)、gh auth、確認每個 root issue 都 OPEN") +TaskCreate(name="check_diagnosis_readiness", description="Phase 0.4 (v2.55+ #47, helper extracted v2.57+ #51, multi-root v2.60+ #46): invoke scripts/check-diagnosis-readiness.sh <github-repo> <root1> [<root2> ...] → JSON {ready/not_ready}; not_ready=0 → silent pass; not_ready>0 → AskUserQuestion 3-option (run /idd-diagnose first / proceed anyway / cancel). Placed before cluster branch / manifest creation so cancel has zero side effect.") +TaskCreate(name="setup_cluster_branch", description="Phase 0.5: 建 cluster branch — N=1 用 idd/chain-<N>-<slug>, N>1 用 idd/chain-multi-<hash8>-<root1-slug> from default branch + 初始化 spawn manifest schema v2 (root_issues + traversal)") +TaskCreate(name="init_queue", description="Phase 1: QUEUE seeded with all roots (sorted asc), per-root DEPTH_MAP[$root]=0, ROOT_ID_MAP, FAIL_ROOTS set, CHAIN_MAX_DEPTH=3 + CHAIN_MAX_ISSUES=10") +TaskCreate(name="chain_loop", description="Phase 2: 主 loop — DFS push-front / BFS push-back for new spawns, per-root depth cap + global max-issues cap, per-root halt on verify FAIL (Q4 Option C — purge same-root pending, other roots continue)") +TaskCreate(name="open_cluster_pr", description="Phase 3: 開 cluster PR — N=1 title 'chain: <title>', N>1 title 'chain (multi-root): N issues — <root#1 title>', Refs all chained #N (roots first), body cluster overview table 含 root_id 欄位 + per-issue collapsed details") +TaskCreate(name="report_and_stop", description="Phase 4: 印 forest tree printout + per-root PASS/FAIL summary + filed-only-not-chained list, 停在 verified 等 user /idd-close #N #M #K") ``` 完成每一步立即 `TaskUpdate → completed`。**靜默完成 = 違規**。 @@ -79,23 +79,34 @@ TaskCreate(name="report_and_stop", description="Phase 4: 印 final report, 停 #### Step 0.1: Argument Parsing Same as `/idd-all`,plus: -- Required: 1 issue number (root) — chain mode 不支援 multi-arg +- Required: ≥1 issue number (root) — N=1 single-root (backward compat) or N>1 multi-root forest +- Optional: `--bfs` flag — select BFS traversal mode (default = DFS) - Optional: `--cwd /path/to/clone` for cross-repo invocation ```bash -ROOT_ISSUE="" +declare -a ROOT_ISSUES=() +TRAVERSAL="dfs" # default CWD_FLAG="" for ((i=0; i<${#ARGS[@]}; i++)); do arg="${ARGS[i]}" case "$arg" in \#[0-9]*) - [ -n "$ROOT_ISSUE" ] && abort "/idd-all-chain accepts exactly 1 root issue (got '$ROOT_ISSUE' and '$arg'). For multi-issue cluster, use /idd-implement #N #M --pr." - ROOT_ISSUE="${arg#\#}" ;; + ROOT_ISSUES+=("${arg#\#}") ;; + --bfs) + TRAVERSAL="bfs" ;; --cwd=*) CWD_FLAG="${arg#--cwd=}" ;; --cwd) i=$((i+1)); CWD_FLAG="${ARGS[i]}" ;; esac done -[ -z "$ROOT_ISSUE" ] && abort "Usage: /idd-all-chain #NNN [--cwd /path]" +[ ${#ROOT_ISSUES[@]} -eq 0 ] && abort "Usage: /idd-all-chain #NNN [#MMM ...] [--bfs] [--cwd /path]" + +# Sort roots ascending for deterministic hash + lowest-root-first slug selection +IFS=$'\n' ROOT_ISSUES_SORTED=($(sort -n <<<"${ROOT_ISSUES[*]}")) +unset IFS + +N_ROOTS=${#ROOT_ISSUES_SORTED[@]} +LOWEST_ROOT="${ROOT_ISSUES_SORTED[0]}" +ROOT_ISSUE="$LOWEST_ROOT" # legacy alias: many downstream blocks refer to the "primary" root ``` #### Step 0.2: Working tree resolution @@ -109,9 +120,12 @@ Same as `/idd-all` Step 0.2 (CWD / GITHUB_REPO / CONFIG_PATH discovery)。 git -C "$CWD" rev-parse --git-dir > /dev/null 2>&1 || abort "'$CWD' is not a git repo" # 2. gh auth gh auth status > /dev/null 2>&1 || abort "gh CLI not authenticated" -# 3. Root issue exists + OPEN -STATE=$(gh issue view "$ROOT_ISSUE" -R "$GITHUB_REPO" --json state -q .state 2>/dev/null) || abort "Issue #$ROOT_ISSUE not found in $GITHUB_REPO" -[ "$STATE" = "OPEN" ] || abort "Issue #$ROOT_ISSUE state=$STATE (must be OPEN)" +# 3. Every root issue exists + OPEN +for r in "${ROOT_ISSUES_SORTED[@]}"; do + STATE=$(gh issue view "$r" -R "$GITHUB_REPO" --json state -q .state 2>/dev/null) \ + || abort "Issue #$r not found in $GITHUB_REPO" + [ "$STATE" = "OPEN" ] || abort "Issue #$r state=$STATE (must be OPEN)" +done ``` #### Step 0.4 (NEW, v2.55+ #47): Diagnosis-readiness check @@ -130,13 +144,15 @@ Delegated to `scripts/check-diagnosis-readiness.sh` (v2.57.0+, #51) — variadic # Helper script does the per-issue gh+jq detection (regex test("(?m)^## Diagnosis") per #53). # Returns: {"ready":[N,...],"not_ready":[N,...]} JSON to stdout. # Exit: 0 success / 1 gh-jq failure / 2 usage error. +# Multi-root (v2.60+, #46): pass all roots in one invocation; helper aggregates. set -e READINESS_JSON=$(bash "$CLAUDE_PLUGIN_ROOT/scripts/check-diagnosis-readiness.sh" \ - "$GITHUB_REPO" "$ROOT_ISSUE") + "$GITHUB_REPO" "${ROOT_ISSUES_SORTED[@]}") -# Parse: count not_ready entries. Single-root v1 → ready=[N] OR not_ready=[N]. +# Parse: count not_ready entries (aggregated across all roots). NOT_READY_COUNT=$(echo "$READINESS_JSON" | jq -r '.not_ready | length') +NOT_READY_LIST=$(echo "$READINESS_JSON" | jq -r '.not_ready | map(tostring) | join(", #")') # Defensive: jq failure → abort (helper script already failed-fast via exit 1 to stderr, # but if jq parse here also fails the abort still fires). @@ -153,19 +169,19 @@ If `NOT_READY_COUNT > 0` → no diagnosis comment → enter the AskUserQuestion > **Why prose instead of bash**: `AskUserQuestion` is a Claude Code tool invoked at the agent level, **not** a binary on `$PATH` or a shell function. Embedding `AskUserQuestion(...)` inside a fenced bash block was a category error caught in /idd-verify #47 (P1 finding 2). The agent reads the bash detection logic, branches at the agent level on `NOT_READY_COUNT > 0`, then handles the deliberation as described in prose here. Same pattern as `idd-all/SKILL.md` Phase 0.5 ask-policy interaction. -When `NOT_READY_COUNT > 0`, the agent invokes the **AskUserQuestion** tool with this question structure (per IC_R011 canonical 3-option pattern): +When `NOT_READY_COUNT > 0`, the agent invokes the **AskUserQuestion** tool with this question structure (per IC_R011 canonical 3-option pattern). For single-root invocations the question names that one issue; for multi-root invocations the question aggregates all not-ready roots in one prompt (NO repeated AskUserQuestion per root): -> "Issue #${ROOT_ISSUE} 沒有 diagnosis comment。沒 diagnose 跑 chain 風險:unattended idd-diagnose Layer V 自動 proceed 可能基於 vague spec 做出 design 猜測。怎麼處理?" +> "Root issue(s) #${NOT_READY_LIST} 沒有 diagnosis comment(${NOT_READY_COUNT} 個 root not ready)。沒 diagnose 跑 chain 風險:unattended idd-diagnose Layer V 自動 proceed 可能基於 vague spec 做出 design 猜測。怎麼處理?" > > Options (default = first): -> - **`run /idd-diagnose first`** — halt chain + preserve nothing (本 step 前無 branch/manifest) + 提示跑 `/idd-diagnose #N`,完成後重 invoke `/idd-all-chain` -> - **`proceed anyway`** — 繼續 chain;PATCH issue body 加 `### Chain pre-flight: diagnosis bypassed` audit section +> - **`run /idd-diagnose first`** — halt chain + preserve nothing (本 step 前無 branch/manifest) + 提示跑 `/idd-diagnose #${NOT_READY_LIST}`(batch mode 一次補完),完成後重 invoke `/idd-all-chain` +> - **`proceed anyway`** — 繼續 chain;對**每個 not-ready root** PATCH issue body 加 `### Chain pre-flight: diagnosis bypassed` audit section > - **`cancel`** — abort + 印 cleanup commands (本 step 前無 side effect,只 exit) Based on the user's selection: -- **`run /idd-diagnose first`** → echo `"→ Halt: please run /idd-diagnose ${ROOT_ISSUE} first, then re-invoke /idd-all-chain"`, then `exit 0` (clean halt — user's deliberate choice, not an error) -- **`proceed anyway`** → invoke the proceed-anyway audit trail PATCH bash below, then continue to Step 0.5 +- **`run /idd-diagnose first`** → echo `"→ Halt: please run /idd-diagnose #${NOT_READY_LIST} first, then re-invoke /idd-all-chain"`, then `exit 0` (clean halt — user's deliberate choice, not an error) +- **`proceed anyway`** → for each issue in the `.not_ready` array, invoke the proceed-anyway audit trail PATCH bash below (substituting `$ROOT_ISSUE` for each not-ready issue number), then continue to Step 0.5 - **`cancel`** → echo `"→ Aborted by user. No state changes made (Phase 0.4 ran before any branch/manifest creation)."`, then `exit 0` ##### proceed-anyway audit trail PATCH (bash) @@ -224,25 +240,47 @@ DEFAULT=$(gh repo view "$GITHUB_REPO" --json defaultBranchRef -q .defaultBranchR CURRENT=$(git -C "$CWD" branch --show-current) [ "$CURRENT" = "$DEFAULT" ] || abort "Cluster branch must start from $DEFAULT (currently on $CURRENT)" -# Build cluster branch name -TITLE=$(gh issue view "$ROOT_ISSUE" -R "$GITHUB_REPO" --json title -q .title) +# Build cluster branch name — dispatch on N +TITLE=$(gh issue view "$LOWEST_ROOT" -R "$GITHUB_REPO" --json title -q .title) SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9]/-/g; s/-+/-/g; s/^-//; s/-$//' \ | cut -c1-40) -CLUSTER_BRANCH="idd/chain-${ROOT_ISSUE}-${SLUG}" -# Refuse if branch already exists (chain re-run requires manual cleanup) -if git -C "$CWD" show-ref --verify --quiet "refs/heads/$CLUSTER_BRANCH"; then +if [ $N_ROOTS -eq 1 ]; then + # N=1: backward-compatible naming + CLUSTER_BRANCH="idd/chain-${LOWEST_ROOT}-${SLUG}" +else + # N>1: hash-based naming. Hash = first 8 hex of sha256 over sorted-asc roots joined by '-'. + # Slug = lowest root's title slug. + ROOTS_JOINED=$(IFS=-; echo "${ROOT_ISSUES_SORTED[*]}") + HASH8=$(printf '%s' "$ROOTS_JOINED" | shasum -a 256 | cut -c1-8) + CLUSTER_BRANCH="idd/chain-multi-${HASH8}-${SLUG}" + + # hash8 collision fallback → hash16 + if git -C "$CWD" show-ref --verify --quiet "refs/heads/$CLUSTER_BRANCH"; then + HASH16=$(printf '%s' "$ROOTS_JOINED" | shasum -a 256 | cut -c1-16) + CLUSTER_BRANCH="idd/chain-multi-${HASH16}-${SLUG}" + echo "→ hash8 collision detected; retrying with hash16: $CLUSTER_BRANCH" >&2 + + # hash16 double-collision (extremely rare) → abort + if git -C "$CWD" show-ref --verify --quiet "refs/heads/$CLUSTER_BRANCH"; then + abort "Cluster branch '$CLUSTER_BRANCH' (hash16) also exists. Manual cleanup required: git -C $CWD branch -D <existing-chain-multi-*>" + fi + fi +fi + +# Refuse if branch already exists (single-root naming collision: chain re-run requires manual cleanup) +if [ $N_ROOTS -eq 1 ] && git -C "$CWD" show-ref --verify --quiet "refs/heads/$CLUSTER_BRANCH"; then abort "Cluster branch '$CLUSTER_BRANCH' already exists. Run: git -C $CWD branch -D $CLUSTER_BRANCH or pick a different root issue." fi git -C "$CWD" checkout -b "$CLUSTER_BRANCH" -echo "→ Cluster branch created: $CLUSTER_BRANCH" +echo "→ Cluster branch created: $CLUSTER_BRANCH (N=$N_ROOTS roots: ${ROOT_ISSUES_SORTED[*]}, traversal=$TRAVERSAL)" ``` #### Step 0.6: Initialize spawn manifest -Per `references/spawn-manifest.md` schema v1: +Per `references/spawn-manifest.md` schema v2 (v2.60+, #46: multi-root + traversal): ```bash MANIFEST_DIR="$CWD/.claude/.idd/state" @@ -250,15 +288,22 @@ MANIFEST="$MANIFEST_DIR/chain-spawned-issues.json" mkdir -p "$MANIFEST_DIR" SESSION_ID=$(uuidgen) -cat > "$MANIFEST" <<EOF -{ - "schema_version": 1, - "session_id": "$SESSION_ID", - "root_issue": $ROOT_ISSUE, - "spawned": [] -} -EOF -echo "→ Spawn manifest initialized: $MANIFEST (session=$SESSION_ID)" +# Build root_issues JSON array from sorted bash array +ROOTS_JSON=$(printf '%s\n' "${ROOT_ISSUES_SORTED[@]}" | jq -R 'tonumber' | jq -s .) + +jq -n \ + --argjson roots "$ROOTS_JSON" \ + --arg session "$SESSION_ID" \ + --arg traversal "$TRAVERSAL" \ + '{ + schema_version: 2, + session_id: $session, + root_issues: $roots, + traversal: $traversal, + spawned: [] + }' > "$MANIFEST" + +echo "→ Spawn manifest initialized: $MANIFEST (session=$SESSION_ID, roots=${ROOT_ISSUES_SORTED[*]}, traversal=$TRAVERSAL)" ``` --- @@ -266,39 +311,65 @@ echo "→ Spawn manifest initialized: $MANIFEST (session=$SESSION_ID)" ### Phase 1: Initialize chain state ```bash -declare -a QUEUE=("$ROOT_ISSUE") -declare -A DEPTH_MAP=(["$ROOT_ISSUE"]=0) -declare -A PROCESSED=() # issue → "verified" | "failed" -declare -a CHAINED_ORDER=() # ordered list for cluster PR body -CHAIN_MAX_DEPTH=2 -CHAIN_MAX_ISSUES=5 +# Multi-root forest state (v2.60+, #46) +declare -a QUEUE=("${ROOT_ISSUES_SORTED[@]}") # seed with all roots in sorted order +declare -A DEPTH_MAP=() # issue_num → depth within its root's subtree +declare -A ROOT_ID_MAP=() # issue_num → owning root_id (which subtree) +declare -A PROCESSED=() # issue → "verified" | "failed" +declare -a CHAINED_ORDER=() # ordered list for cluster PR body +declare -A FAIL_ROOTS=() # root_id → 1 when that root's subtree failed +declare -a FAILED_AT=() # ordered list of failing issues (for Phase 4 report) + +# Each root starts at depth 0 in its own subtree +for r in "${ROOT_ISSUES_SORTED[@]}"; do + DEPTH_MAP["$r"]=0 + ROOT_ID_MAP["$r"]="$r" +done + +# Cap values (v2.60+, #46 D3: max-depth=3 primary + max-issues=10 safety net, independent apply) +CHAIN_MAX_DEPTH=3 +CHAIN_MAX_ISSUES=10 ``` --- ### Phase 2: Main chain loop +DFS (default) pushes new spawns to **front** of queue (rich subtree first); BFS pushes to **back** (level-by-level across roots). Per-root verify FAIL halts only that root's subtree — other roots' subtrees continue. Per-root `CHAIN_MAX_DEPTH=3` independently; `CHAIN_MAX_ISSUES=10` global cap. + ```bash while [ ${#QUEUE[@]} -gt 0 ]; do - # Cap check + # Global cap check (max-issues=10 applies to total across all root subtrees) if [ ${#PROCESSED[@]} -ge $CHAIN_MAX_ISSUES ]; then echo "⚠ chain_max_issues=$CHAIN_MAX_ISSUES reached. Remaining queue (filed but NOT chained): ${QUEUE[*]}" break fi CURRENT="${QUEUE[0]}" - QUEUE=("${QUEUE[@]:1}") # pop front + QUEUE=("${QUEUE[@]:1}") # pop front (same for both DFS and BFS — only push semantics differ) + CURRENT_ROOT="${ROOT_ID_MAP[$CURRENT]}" + + # If the issue's owning root has already FAILed (Q4 Option C: per-root halt), + # skip it. Defensive — queue should already be filtered, but cover concurrent + # FAIL during long /idd-all invocation. + if [ -n "${FAIL_ROOTS[$CURRENT_ROOT]:-}" ]; then + echo " ⊘ #$CURRENT skipped (root #$CURRENT_ROOT subtree already FAILed earlier)" + continue + fi echo "" echo "════════════════════════════════════════" - echo "Chain processing #$CURRENT (depth=${DEPTH_MAP[$CURRENT]})" + echo "Chain processing #$CURRENT (depth=${DEPTH_MAP[$CURRENT]}, root_id=$CURRENT_ROOT, traversal=$TRAVERSAL)" echo "════════════════════════════════════════" # Capture pre-invocation manifest length PRE_LEN=$(jq '.spawned | length' "$MANIFEST") - # Invoke /idd-all in chain context + # Invoke /idd-all in chain context. Export current root_id so sub-skills can + # propagate it to manifest-append.sh (per D1 schema v2 root_id field). + export IDD_CHAIN_CURRENT_ROOT_ID="$CURRENT_ROOT" Skill(skill="issue-driven-dev:idd-all", args="#$CURRENT --in-chain --cwd $CWD") + unset IDD_CHAIN_CURRENT_ROOT_ID # Determine /idd-all completion state — read latest verify comment phase PHASE=$(gh issue view "$CURRENT" -R "$GITHUB_REPO" --json body -q .body \ @@ -308,13 +379,32 @@ while [ ${#QUEUE[@]} -gt 0 ]; do verified) PROCESSED["$CURRENT"]="verified" CHAINED_ORDER+=("$CURRENT") - echo "✓ #$CURRENT verified" + echo "✓ #$CURRENT verified (root_id=$CURRENT_ROOT)" ;; needs-fix|*) PROCESSED["$CURRENT"]="failed" - echo "✗ #$CURRENT verify FAIL (phase=$PHASE) — halting chain" - print_abort_report - exit 1 + FAIL_ROOTS["$CURRENT_ROOT"]=1 + FAILED_AT+=("$CURRENT") + echo "✗ #$CURRENT verify FAIL (phase=$PHASE) — halting root #$CURRENT_ROOT subtree" + + # Per-root halt (Q4 Option C, D4): remove from QUEUE all pending issues + # whose owning root == CURRENT_ROOT. Other roots' subtrees continue. + NEW_QUEUE=() + PURGED=() + for q in "${QUEUE[@]}"; do + if [ "${ROOT_ID_MAP[$q]}" = "$CURRENT_ROOT" ]; then + PURGED+=("$q") + else + NEW_QUEUE+=("$q") + fi + done + QUEUE=("${NEW_QUEUE[@]}") + + if [ ${#PURGED[@]} -gt 0 ]; then + echo " → purged from queue (same root_id=$CURRENT_ROOT): ${PURGED[*]}" + fi + echo " → other root subtrees continue (FAIL_ROOTS=${!FAIL_ROOTS[*]})" + continue ;; esac @@ -333,18 +423,24 @@ while [ ${#QUEUE[@]} -gt 0 ]; do # Eligibility check (per spec: same_file OR same_skill OR sister-bug) if [ "$SAME_FILE" = "true" ] || [ "$SAME_SKILL" = "true" ] || [ "$KIND" = "sister-bug" ]; then - # Depth check + # Per-root depth check (D3: max-depth applies per root subtree independently) if [ $NEXT_DEPTH -le $CHAIN_MAX_DEPTH ]; then - # Issues check + # Global max-issues check (D3: max-issues applies to total) if [ $((${#PROCESSED[@]} + ${#QUEUE[@]} + 1)) -le $CHAIN_MAX_ISSUES ]; then - QUEUE+=("$SPAWN_NUM") + # Push semantics dispatch (D2: DFS=push-front, BFS=push-back) + if [ "$TRAVERSAL" = "dfs" ]; then + QUEUE=("$SPAWN_NUM" "${QUEUE[@]}") # push to front + else + QUEUE+=("$SPAWN_NUM") # push to back + fi DEPTH_MAP["$SPAWN_NUM"]=$NEXT_DEPTH - echo " → enqueued #$SPAWN_NUM (kind=$KIND, depth=$NEXT_DEPTH)" + ROOT_ID_MAP["$SPAWN_NUM"]="$CURRENT_ROOT" # inherit owning root + echo " → enqueued #$SPAWN_NUM (kind=$KIND, depth=$NEXT_DEPTH, root_id=$CURRENT_ROOT, push=${TRAVERSAL})" else echo " ⊘ #$SPAWN_NUM eligible but max-issues cap reached — filed only, not chained" fi else - echo " ⊘ #$SPAWN_NUM eligible but depth>$CHAIN_MAX_DEPTH — filed only, not chained" + echo " ⊘ #$SPAWN_NUM eligible but depth>$CHAIN_MAX_DEPTH (per-root) — filed only, not chained" fi else echo " ⊘ #$SPAWN_NUM ineligible (cross-cutting) — filed only, not chained" @@ -355,8 +451,9 @@ done echo "" echo "════════════════════════════════════════" -echo "Chain complete: ${#PROCESSED[@]} issues processed" +echo "Chain complete: ${#PROCESSED[@]} issues processed (${#FAIL_ROOTS[@]} root subtree(s) FAILed)" echo " Verified: ${CHAINED_ORDER[*]}" +[ ${#FAIL_ROOTS[@]} -gt 0 ] && echo " Failed roots: ${!FAIL_ROOTS[*]}" echo "════════════════════════════════════════" ``` @@ -368,40 +465,66 @@ echo "════════════════════════ # Push cluster branch git -C "$CWD" push -u origin "$CLUSTER_BRANCH" -# Build refs list and overview table -REFS_LIST=$(printf "#%s " "${CHAINED_ORDER[@]}") +# Build refs list — roots first (in sorted order), then chained spawns in CHAINED_ORDER order +# Note: CHAINED_ORDER already contains every processed issue including roots, but we surface +# roots-first for the Refs line per the spec contract. +declare -a REFS_PARTS=("${ROOT_ISSUES_SORTED[@]}") +for issue in "${CHAINED_ORDER[@]}"; do + # Skip if already a root (would be duplicate) + is_root=0 + for r in "${ROOT_ISSUES_SORTED[@]}"; do + [ "$issue" = "$r" ] && { is_root=1; break; } + done + [ $is_root -eq 0 ] && REFS_PARTS+=("$issue") +done +REFS_LIST=$(printf "#%s " "${REFS_PARTS[@]}") -# Build per-issue collapsed sections + cluster overview rows +# Build per-issue collapsed sections + cluster overview rows (includes root_id column for multi-root) OVERVIEW_ROWS="" DETAILS_BLOCKS="" -ROOT_TITLE=$(gh issue view "$ROOT_ISSUE" -R "$GITHUB_REPO" --json title -q .title) +LOWEST_ROOT_TITLE=$(gh issue view "$LOWEST_ROOT" -R "$GITHUB_REPO" --json title -q .title) for issue in "${CHAINED_ORDER[@]}"; do ITITLE=$(gh issue view "$issue" -R "$GITHUB_REPO" --json title -q .title) HEAD_COMMIT=$(git -C "$CWD" log --oneline --grep "#$issue" -1 --format='%h' | head -1) - - if [ "$issue" = "$ROOT_ISSUE" ]; then + IROOT="${ROOT_ID_MAP[$issue]}" + + # Determine spawn source: if issue is a root, label "root"; else read from manifest + is_root=0 + for r in "${ROOT_ISSUES_SORTED[@]}"; do + [ "$issue" = "$r" ] && { is_root=1; break; } + done + if [ $is_root -eq 1 ]; then SOURCE="root" else SOURCE=$(jq -r ".spawned[] | select(.issue_number == $issue) | \"\(.spawned_by) \(.spawn_step)\"" "$MANIFEST") fi - OVERVIEW_ROWS+="| #$issue | $SOURCE | verified | $HEAD_COMMIT |"$'\n' - DETAILS_BLOCKS+=$'\n'"<details>"$'\n'"<summary>#$issue — $ITITLE</summary>"$'\n\n' + OVERVIEW_ROWS+="| #$issue | $IROOT | $SOURCE | verified | $HEAD_COMMIT |"$'\n' + DETAILS_BLOCKS+=$'\n'"<details>"$'\n'"<summary>#$issue (root_id=$IROOT) — $ITITLE</summary>"$'\n\n' DETAILS_BLOCKS+="See issue #$issue for diagnose / verify / commit history."$'\n\n'"</details>"$'\n' done +# PR title dispatch on N (D6: PR title format) +if [ $N_ROOTS -eq 1 ]; then + PR_TITLE="chain: $LOWEST_ROOT_TITLE" + SUMMARY_LINE="Cluster of ${#CHAINED_ORDER[@]} issues solved as one chain (root #${LOWEST_ROOT} + auto-emergent spawn) via \`/idd-all-chain\` (v2.55+)." +else + PR_TITLE="chain (multi-root): ${N_ROOTS} issues — $LOWEST_ROOT_TITLE" + SUMMARY_LINE="Multi-root chain (N=${N_ROOTS} roots: ${ROOT_ISSUES_SORTED[*]}) solved as one cluster via \`/idd-all-chain\` (v2.60+, traversal=${TRAVERSAL}). Total ${#CHAINED_ORDER[@]} processed issues across all root subtrees." +fi + PR_BODY=$(cat <<EOF Refs $REFS_LIST ## Summary -Cluster of ${#CHAINED_ORDER[@]} issues solved as one chain (root + auto-emergent spawn) via \`/idd-all-chain\` (v2.55+). +$SUMMARY_LINE ## Cluster overview -| # | Spawn source | Phase | PR commit | -|---|-------------|-------|-----------| +| # | root_id | Spawn source | Phase | PR commit | +|---|---------|-------------|-------|-----------| $OVERVIEW_ROWS ## Per-issue details @@ -421,7 +544,7 @@ EOF ) PR_URL=$(gh pr create -R "$GITHUB_REPO" \ - --title "chain: $ROOT_TITLE" \ + --title "$PR_TITLE" \ --body "$PR_BODY" \ --base "$DEFAULT" --head "$CLUSTER_BRANCH") @@ -432,20 +555,125 @@ echo "→ Cluster PR opened: $PR_URL" ### Phase 4: Final report and stop -``` -✓ /idd-all-chain complete +Phase 4 emits a forest-tree visualization (one tree per root) plus a per-root PASS/FAIL summary plus a flat list of filed-only-not-chained issues. Status icons: `✓` PASS, `✗` FAIL, `⊘` filed but unprocessed (cap or eligibility). - Root: #$ROOT_ISSUE — $ROOT_TITLE +```bash +# Build forest tree printout. +# For each root in ROOT_ISSUES_SORTED, walk its subtree by iterating CHAINED_ORDER + manifest spawns. +FOREST_OUTPUT="" +for r in "${ROOT_ISSUES_SORTED[@]}"; do + # Determine root status + if [ -n "${FAIL_ROOTS[$r]:-}" ]; then + R_ICON="✗" + # Find the failed issue within this root's subtree + FAILED_AT_THIS_ROOT="" + for f in "${FAILED_AT[@]}"; do + if [ "${ROOT_ID_MAP[$f]}" = "$r" ]; then + FAILED_AT_THIS_ROOT="$f" + break + fi + done + R_LABEL="root #$r (depth 0) — FAIL at #${FAILED_AT_THIS_ROOT}" + elif [ "${PROCESSED[$r]:-}" = "verified" ]; then + R_ICON="✓" + R_LABEL="root #$r (depth 0)" + else + R_ICON="⊘" + R_LABEL="root #$r (depth 0) — filed but unprocessed" + fi + FOREST_OUTPUT+=" $R_ICON $R_LABEL"$'\n' + + # Walk this root's subtree (descendants whose ROOT_ID_MAP value == r, ordered by depth then id) + for issue in "${CHAINED_ORDER[@]}"; do + [ "$issue" = "$r" ] && continue + [ "${ROOT_ID_MAP[$issue]}" != "$r" ] && continue + D="${DEPTH_MAP[$issue]}" + PAD=$(printf '%*s' $((D * 2)) '') + SPAWN_INFO=$(jq -r ".spawned[] | select(.issue_number == $issue) | \"\(.spawned_by) \(.spawn_step) (\(.spawn_kind))\"" "$MANIFEST") + FOREST_OUTPUT+=" ${PAD} ✓ #$issue (depth $D, $SPAWN_INFO)"$'\n' + done + + # Show failed subtree members under their root + for f in "${FAILED_AT[@]}"; do + [ "${ROOT_ID_MAP[$f]}" != "$r" ] && continue + D="${DEPTH_MAP[$f]}" + PAD=$(printf '%*s' $((D * 2)) '') + SPAWN_INFO=$(jq -r ".spawned[] | select(.issue_number == $f) | \"\(.spawned_by) \(.spawn_step) (\(.spawn_kind))\"" "$MANIFEST") + [ "$f" = "$r" ] && continue # already labeled above + FOREST_OUTPUT+=" ${PAD} ✗ #$f (depth $D, $SPAWN_INFO)"$'\n' + done +done + +# Per-root PASS/FAIL summary block +SUMMARY_BLOCK="" +for r in "${ROOT_ISSUES_SORTED[@]}"; do + if [ -n "${FAIL_ROOTS[$r]:-}" ]; then + FAILED_AT_THIS="" + for f in "${FAILED_AT[@]}"; do + if [ "${ROOT_ID_MAP[$f]}" = "$r" ]; then + FAILED_AT_THIS="$f" + break + fi + done + SUMMARY_BLOCK+=" #$r: FAIL (verify FAIL at #$FAILED_AT_THIS — subtree halted)"$'\n' + elif [ "${PROCESSED[$r]:-}" = "verified" ]; then + # Count spawn processed in this root's subtree + SPAWN_COUNT=0 + for issue in "${CHAINED_ORDER[@]}"; do + [ "$issue" = "$r" ] && continue + [ "${ROOT_ID_MAP[$issue]}" = "$r" ] && SPAWN_COUNT=$((SPAWN_COUNT + 1)) + done + SUMMARY_BLOCK+=" #$r: PASS ($SPAWN_COUNT spawn processed)"$'\n' + else + # Root not processed at all — likely skipped due to max-issues cap or not-OPEN + SUMMARY_BLOCK+=" #$r: SKIPPED (max-issues cap or root not OPEN)"$'\n' + fi +done + +# Filed-only-not-chained list: scan manifest for spawns that never made it into CHAINED_ORDER +FILED_ONLY="" +TOTAL_SPAWNS=$(jq '.spawned | length' "$MANIFEST") +for i in $(seq 0 $((TOTAL_SPAWNS - 1))); do + S_NUM=$(jq -r ".spawned[$i].issue_number" "$MANIFEST") + in_chained=0 + for c in "${CHAINED_ORDER[@]}"; do + [ "$c" = "$S_NUM" ] && { in_chained=1; break; } + done + [ $in_chained -eq 1 ] && continue + S_KIND=$(jq -r ".spawned[$i].spawn_kind" "$MANIFEST") + S_ROOT=$(jq -r ".spawned[$i].root_id" "$MANIFEST") + FILED_ONLY+=" ⊘ #$S_NUM (root_id=$S_ROOT, kind=$S_KIND) — filed but not chained (cap or ineligible)"$'\n' +done + +cat <<EOF +✓ /idd-all-chain complete (${#PROCESSED[@]} issues processed, ${#FAIL_ROOTS[@]} root subtree FAILed) + +Forest summary (traversal: $TRAVERSAL): + +$FOREST_OUTPUT +Per-root PASS/FAIL: + +$SUMMARY_BLOCK +EOF + +if [ -n "$FILED_ONLY" ]; then + cat <<EOF +Filed but not chained: + +$FILED_ONLY +EOF +fi + +cat <<EOF Cluster branch: $CLUSTER_BRANCH - Chained issues: $REFS_LIST - Chain depth: max(${DEPTH_MAP[@]}) + Refs: $REFS_LIST PR: $PR_URL - Verify: all PASS Next: 1. Review PR $PR_URL 2. Merge (squash recommended — single review surface) 3. /idd-close $REFS_LIST (per-issue closing summary required, no shortcut) +EOF ``` **STOP**。不 auto-merge,不 auto-close。Per-issue close summary 是 IDD 紀律核心,chain mode 不省略。 @@ -492,16 +720,21 @@ EOF | 情況 | 行為 | |------|------| -| Root issue not OPEN | Phase 0.3 abort | -| Diagnosis comment missing on root issue + user picks `cancel` | Phase 0.4 clean halt (no branch/manifest yet) | -| Diagnosis comment missing on root issue + user picks `run /idd-diagnose first` | Phase 0.4 clean halt + 提示 `/idd-diagnose #N` | +| 0 root tokens passed | Phase 0.1 abort with usage hint | +| Any root issue not OPEN | Phase 0.3 abort, listing the offending root(s) | +| Diagnosis comment missing on any root + user picks `cancel` | Phase 0.4 clean halt (no branch/manifest yet) | +| Diagnosis comment missing on any root + user picks `run /idd-diagnose first` | Phase 0.4 clean halt + 提示 batch diagnose `/idd-diagnose #N #M ...` | | Working tree dirty | Phase 0.5 abort, 提示 stash/commit | | Not on default branch | Phase 0.5 abort, 提示 checkout default | -| Cluster branch already exists | Phase 0.5 abort, 提示手動清理 | +| Cluster branch already exists (N=1 single-root naming) | Phase 0.5 abort, 提示手動清理 | +| Cluster branch hash8 collision (N>1) | Phase 0.5 fallback to hash16; double collision → abort with cleanup hint | | `/idd-all #M --in-chain` 沒在 cluster branch | sub-skill 自己 abort (Step 0.5 sanity check) | -| Chained verify FAIL | Phase 2 halt, partial commits 保留, abort report 印 recovery options | -| Chain depth > 2 | Spawn filed (sub-skill audit trail) but NOT enqueued, chain continues | -| Chain max-issues > 5 | Same — filed only, not enqueued, chain continues | +| Chained verify FAIL (single root subtree) | Phase 2 halt that root's subtree only; other root subtrees continue; commits preserved; Phase 4 per-root FAIL/PASS report | +| Chained verify FAIL (the only root subtree) | Equivalent to halting the whole queue; commits preserved; Phase 4 report shows single root FAIL | +| Chain depth > 3 (per root) | Spawn filed (sub-skill audit trail) but NOT enqueued, chain continues | +| Chain total issues > 10 | Same — filed only, not enqueued, chain continues | +| Manifest helper invoked with 8 args under v2 helper | Helper exits 2 (usage error); sub-skill should pass 9th `root_id` arg | +| Manifest helper detects v1 schema on disk | Helper exits 1 (schema mismatch); migration hint printed | | `gh pr create` 失敗 | Phase 3 abort, branch 已 push, 提示手動開 PR | --- @@ -510,9 +743,11 @@ EOF - **永不 auto-close**:per-issue close summary 是 IDD 核心紀律,chain mode 也要 user manual `/idd-close #N #M #K` - **永不 auto-merge**:即使 chain verify all PASS,reviewer 看 cluster diff 是另一層 gate -- **Cluster branch 嚴格命名**:`idd/chain-<root>-<slug>` — sub `/idd-all --in-chain` Step 0.5 sanity check 認這個 prefix,不符 abort -- **Chain depth + max-issues 是 hard cap**:不可繞過;超過 cap 的 spawn 仍 file 但不 chain -- **Verify discipline 不省略**:每個 chained issue 跑完整 6-AI ensemble verify;chain failure halt 比 cost saving 重要 +- **Cluster branch 嚴格命名**:N=1 用 `idd/chain-<root>-<slug>`,N>1 用 `idd/chain-multi-<hash8>-<root1-slug>` — sub `/idd-all --in-chain` Step 0.5 sanity check 認這兩個 prefix,不符 abort +- **Chain depth + max-issues 是 hard cap**:不可繞過;`max_depth=3` 對每 root subtree 獨立 apply,`max_issues=10` 是 global 總額;超過 cap 的 spawn 仍 file 但不 chain +- **Verify discipline 不省略**:每個 chained issue 跑完整 6-AI ensemble verify;single-root chain 失敗 halt 全 queue,multi-root chain 失敗只 halt 該 root's subtree 並繼續其他 root(D4 Option C) +- **DFS default, BFS opt-in**:default = DFS(rich subtree first,reviewer cognitive load 低);`--bfs` opt-in for fairness across roots +- **Schema v2 hard-break**:helper + chain shell 同 PR ship v2;v1 manifest on disk → fail-fast(不 silent migrate / silent overwrite) ## Auto-Update From e1babe2eb4dee5caf90c9584b9d27dbebc2a30f6 Mon Sep 17 00:00:00 2001 From: che cheng <kiki830621@gmail.com> Date: Tue, 12 May 2026 11:03:39 +0800 Subject: [PATCH 4/6] feat(sub-skills): pass root_id to manifest-append.sh (Refs #46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four spawning sub-skills now pass the 9th positional arg `root_id` to scripts/manifest-append.sh per v2 schema: - idd-implement Step 5.7 (sister bug sweep) - idd-verify Phase 4 (follow-up findings triage) - idd-plan Step 2.5 (tangential observations) - idd-diagnose Step 3.6 (sister concern surfacing) root_id is derived from: 1. IDD_CHAIN_CURRENT_ROOT_ID env var (set by idd-all-chain Phase 2 before each /idd-all --in-chain invocation), or 2. Fallback to current issue's #NNN — covers single-root chain or root-self-spawn case When chain context is inactive (manifest file absent), helper exits 0 silently and behavior is identical to baseline (additive change). Covers tasks 5.1, 5.2, 5.3, 5.4 (D1: Schema v2 hard-break) Refs #46 --- .../changes/multi-root-traversal-idd-all-chain/tasks.md | 8 ++++---- plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md | 7 +++++-- plugins/issue-driven-dev/skills/idd-implement/SKILL.md | 7 +++++-- plugins/issue-driven-dev/skills/idd-plan/SKILL.md | 7 +++++-- plugins/issue-driven-dev/skills/idd-verify/SKILL.md | 7 +++++-- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md index fc5f168..69e7981 100644 --- a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -20,10 +20,10 @@ ## 5. 4 sub-skills append root_id to manifest-append.sh call (covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context) -- [ ] 5.1 [P] `idd-implement` Step 5.7 sister-bug sweep 的 manifest-append.sh 呼叫加第 9 個 arg `root_id`(從 manifest 讀 current issue 的 spawn parent 的 root_id,或從 chain shell 透過 env var `IDD_CHAIN_CURRENT_ROOT_ID` 傳入)。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-implement/SKILL.md` 顯示 9 個位置 args;dogfood smoke(group 7)觀察 manifest entry `root_id` 正確繼承。 -- [ ] 5.2 [P] `idd-verify` Phase 4 follow-up-finding triage 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-verify/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 -- [ ] 5.3 [P] `idd-plan` Step 2.5 tangential sweep 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-plan/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 -- [ ] 5.4 [P] `idd-diagnose` Step 3.6 sister-concern surfacing 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 +- [x] 5.1 [P] `idd-implement` Step 5.7 sister-bug sweep 的 manifest-append.sh 呼叫加第 9 個 arg `root_id`(從 manifest 讀 current issue 的 spawn parent 的 root_id,或從 chain shell 透過 env var `IDD_CHAIN_CURRENT_ROOT_ID` 傳入)。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-implement/SKILL.md` 顯示 9 個位置 args;dogfood smoke(group 7)觀察 manifest entry `root_id` 正確繼承。 +- [x] 5.2 [P] `idd-verify` Phase 4 follow-up-finding triage 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-verify/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 +- [x] 5.3 [P] `idd-plan` Step 2.5 tangential sweep 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-plan/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 +- [x] 5.4 [P] `idd-diagnose` Step 3.6 sister-concern surfacing 的 manifest-append.sh 呼叫加 root_id。Covers Requirement: All four sub-skills SHALL conformantly write the manifest under chain context (per D1: Schema v2 hard-break). **Verification**:`grep -A2 'manifest-append.sh' plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md` 顯示 9 args;smoke 觀察 manifest entry。 ## 6. References doc consistency diff --git a/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md b/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md index f76ccc9..8862097 100644 --- a/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md @@ -495,14 +495,17 @@ Diagnosis 完成 + Step 3.4 Vagueness Pre-check 結束後,依 5 層 gate 判 --label "$type,confidence:confirmed,priority:P3") NEW_ISSUE=$(basename "$NEW_ISSUE_URL") - # Chain context manifest write (per spawn-manifest contract, v2.55+ #44) + # Chain context manifest write (per spawn-manifest contract, v2.55+ #44; v2.60+ #46 schema v2) # spawn_kind classification: # - 真的同主題 sister concern (same root cause, different file) → "sister-concern" # - cross-cutting / upstream tracking (e.g. 跟 idd repo 無關的 upstream gap) → "upstream-tracking" # `same_file` / `same_skill` 依 sister-concern evidence 判斷;upstream-tracking 兩個都 false + # 9th arg root_id: prefer chain shell's exported IDD_CHAIN_CURRENT_ROOT_ID env var; + # fallback to current diagnosing issue's #NNN (single-root chain or root self-spawn). + ROOT_ID_FOR_MANIFEST="${IDD_CHAIN_CURRENT_ROOT_ID:-$NNN}" bash "$CLAUDE_PLUGIN_ROOT/scripts/manifest-append.sh" \ "$REPO_ROOT" "$NEW_ISSUE" "idd-diagnose" "Step 3.6 sister concern surfacing" \ - "$item_kind" "$item_same_file" "$item_same_skill" "$item_title" \ + "$item_kind" "$item_same_file" "$item_same_skill" "$item_title" "$ROOT_ID_FOR_MANIFEST" \ 2>/dev/null || true # silent skip when chain context inactive done ``` diff --git a/plugins/issue-driven-dev/skills/idd-implement/SKILL.md b/plugins/issue-driven-dev/skills/idd-implement/SKILL.md index 74dccf6..783aac2 100644 --- a/plugins/issue-driven-dev/skills/idd-implement/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-implement/SKILL.md @@ -537,12 +537,15 @@ gh pr create --title "$PR_TITLE" --body "$PR_BODY" \ --label "$type,confidence:confirmed,priority:P3") NEW_ISSUE=$(basename "$NEW_ISSUE_URL") - # Chain context manifest write (per spawn-manifest contract, v2.55+ #44) + # Chain context manifest write (per spawn-manifest contract, v2.55+ #44; v2.60+ #46 schema v2) # If chain shell initialized the manifest, also append a machine-readable entry. # Sister bugs from same-cause reproduction are typically same-skill — set true. + # 9th arg root_id: prefer chain shell's exported IDD_CHAIN_CURRENT_ROOT_ID env var; + # fallback to current issue's #NNN (single-root chain or root self-spawn). + ROOT_ID_FOR_MANIFEST="${IDD_CHAIN_CURRENT_ROOT_ID:-$NNN}" bash "$CLAUDE_PLUGIN_ROOT/scripts/manifest-append.sh" \ "$REPO_ROOT" "$NEW_ISSUE" "idd-implement" "Step 5.7 sister bug sweep" \ - "sister-bug" "$item_same_file" "true" "$item_title" \ + "sister-bug" "$item_same_file" "true" "$item_title" "$ROOT_ID_FOR_MANIFEST" \ 2>/dev/null || true # silent skip if no manifest (chain context inactive) done ``` diff --git a/plugins/issue-driven-dev/skills/idd-plan/SKILL.md b/plugins/issue-driven-dev/skills/idd-plan/SKILL.md index d10b9a6..1ddb33d 100644 --- a/plugins/issue-driven-dev/skills/idd-plan/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-plan/SKILL.md @@ -150,14 +150,17 @@ gh issue comment $NUMBER --repo $GITHUB_REPO --body "$IMPLEMENTATION_PLAN" --label "$type,confidence:confirmed,priority:P3") NEW_ISSUE=$(basename "$NEW_ISSUE_URL") - # Chain context manifest write (per spawn-manifest contract, v2.55+ #44) + # Chain context manifest write (per spawn-manifest contract, v2.55+ #44; v2.60+ #46 schema v2) # `same_file` / `same_skill` 依 observation evidence 判斷: # - tangential 觀察到同個 file 的相鄰問題 → same_file=true # - 跨 file 但同 skill / module → same_skill=true # - 跨 cutting concern (cross-module observation) → 兩個都 false + # 9th arg root_id: prefer chain shell's exported IDD_CHAIN_CURRENT_ROOT_ID env var; + # fallback to current planning issue's #NNN (single-root chain or root self-spawn). + ROOT_ID_FOR_MANIFEST="${IDD_CHAIN_CURRENT_ROOT_ID:-$NNN}" bash "$CLAUDE_PLUGIN_ROOT/scripts/manifest-append.sh" \ "$REPO_ROOT" "$NEW_ISSUE" "idd-plan" "Step 2.5 tangential observations" \ - "tangential" "$item_same_file" "$item_same_skill" "$item_title" \ + "tangential" "$item_same_file" "$item_same_skill" "$item_title" "$ROOT_ID_FOR_MANIFEST" \ 2>/dev/null || true # silent skip when chain context inactive ``` diff --git a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md index 87ccecc..f3540d7 100644 --- a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md @@ -730,7 +730,7 @@ options: {bug / enhancement} ``` 3. 每個新 issue 的 body 加上 `Related: #NNN` -4. **Chain context manifest write** (per spawn-manifest contract, v2.55+ #44):每建一個 follow-up issue,額外呼叫 manifest helper: +4. **Chain context manifest write** (per spawn-manifest contract, v2.55+ #44; v2.60+ #46 schema v2):每建一個 follow-up issue,額外呼叫 manifest helper: ```bash NEW_ISSUE_URL=$(gh issue create ...) # existing NEW_ISSUE=$(basename "$NEW_ISSUE_URL") @@ -739,9 +739,12 @@ options: # - finding 在同 file 內 → same_file=true # - finding 跨 module 但同 skill (e.g. 同個 idd-* skill 不同 step) → same_skill=true # - cross-cutting (跟 verified diff 完全無關) → 兩個都 false + # 9th arg root_id: prefer chain shell's exported IDD_CHAIN_CURRENT_ROOT_ID env var; + # fallback to current verified issue's #NNN (single-root chain or root self-spawn). + ROOT_ID_FOR_MANIFEST="${IDD_CHAIN_CURRENT_ROOT_ID:-$NUMBER}" bash "$CLAUDE_PLUGIN_ROOT/scripts/manifest-append.sh" \ "$REPO_ROOT" "$NEW_ISSUE" "idd-verify" "Phase 4 follow-up findings triage" \ - "follow-up-finding" "$same_file" "$same_skill" "$NEW_TITLE" \ + "follow-up-finding" "$same_file" "$same_skill" "$NEW_TITLE" "$ROOT_ID_FOR_MANIFEST" \ 2>/dev/null || true # silent skip when chain context inactive ``` See `references/spawn-manifest.md` for cross-skill contract. Manifest write is **additive** — baseline audit trail unchanged when manifest absent. From cd3d43c74940d4512988e1eefda0146712fa877d Mon Sep 17 00:00:00 2001 From: che cheng <kiki830621@gmail.com> Date: Tue, 12 May 2026 11:05:19 +0800 Subject: [PATCH 5/6] docs(chain-flow): multi-root + DFS/BFS + per-root halt + cap interaction (Refs #46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update reference doc for v2.60+ multi-root chain semantics: - Cluster branch naming: dispatch N=1 vs N>1 with hash8 + hash16 fallback - Chain state: add ROOT_ID_MAP, FAIL_ROOTS, FAILED_AT, TRAVERSAL fields - Caps: chain_max_depth 2→3 (per-root), chain_max_issues 5→10 (global) - NEW section 'Traversal mode (DFS vs BFS)' explaining push-front vs push-back with example queue order table - Loop algorithm: pseudo-code shows traversal dispatch, per-root halt logic (FAIL_ROOTS + same-root queue purge), root_id_map inheritance, env var IDD_CHAIN_CURRENT_ROOT_ID propagation to sub-skills - Failure mode table: per-root halt rows + manifest schema mismatch rows + missing-9th-arg row - PR title + body schema split for N=1 vs N>1; cluster overview table adds root_id column; details summary shows root_id Covers task 6.1 (D1, D2, D3, D4, D5, D6, D7 all referenced) Refs #46 --- .../tasks.md | 2 +- .../issue-driven-dev/references/chain-flow.md | 135 ++++++++++++++---- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md index 69e7981..2f8326b 100644 --- a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -27,7 +27,7 @@ ## 6. References doc consistency -- [ ] 6.1 `chain-flow.md` 補章節:DFS vs BFS algorithm 描述(push-front vs push-back semantics + 流程圖)、per-root verify FAIL halt scope、`max-depth=3`/`max-issues=10` cap interaction(per-root vs global)、branch naming hash8 rule。Covers all design decisions (D1 schema, D2 DFS, D3 caps, D4 halt, D5 branch, D6 PR, D7 forest) by providing prose reference for implementers. **Verification**:`grep -E 'DFS|BFS|FAIL_ROOTS|chain_max_depth|hash8' plugins/issue-driven-dev/references/chain-flow.md` 每個關鍵 token ≥ 1 hit;Phase 4 reviewer manual check 描述與 SKILL.md 實作 consistent。 +- [x] 6.1 `chain-flow.md` 補章節:DFS vs BFS algorithm 描述(push-front vs push-back semantics + 流程圖)、per-root verify FAIL halt scope、`max-depth=3`/`max-issues=10` cap interaction(per-root vs global)、branch naming hash8 rule。Covers all design decisions (D1 schema, D2 DFS, D3 caps, D4 halt, D5 branch, D6 PR, D7 forest) by providing prose reference for implementers. **Verification**:`grep -E 'DFS|BFS|FAIL_ROOTS|chain_max_depth|hash8' plugins/issue-driven-dev/references/chain-flow.md` 每個關鍵 token ≥ 1 hit;Phase 4 reviewer manual check 描述與 SKILL.md 實作 consistent。 ## 7. Acceptance smoke tests diff --git a/plugins/issue-driven-dev/references/chain-flow.md b/plugins/issue-driven-dev/references/chain-flow.md index 43e989a..7eee4fa 100644 --- a/plugins/issue-driven-dev/references/chain-flow.md +++ b/plugins/issue-driven-dev/references/chain-flow.md @@ -36,31 +36,79 @@ When detection finds no diagnosis comment, the chain shell fires `AskUserQuestio ## Cluster branch naming +Branch naming dispatches on N (number of roots passed to `/idd-all-chain`): + +**N=1 (backward compatible)**: + ``` idd/chain-<root_issue_number>-<slug> ``` -- `slug` = lowercased title of root issue, non-alphanumeric → `-`, capped at 40 chars, leading/trailing `-` trimmed -- The chain shell MUST refuse to start if a branch with this name already exists (manual cleanup required) -- Sub-`/idd-all #M --in-chain` Step 0.5 sanity check MUST verify the current branch matches `^idd/chain-` prefix; otherwise abort +**N>1 (multi-root, v2.60+, #46)**: + +``` +idd/chain-multi-<hash8>-<root1-slug> +``` + +- `hash8` = first 8 hex chars of `sha256(roots_joined)` where `roots_joined` is the sorted-ascending root issue numbers joined by `-` (e.g. `44-45-50` for roots #44, #45, #50). Same root set → same hash, deterministic. +- `root1-slug` = lowercased title slug of the **lowest** root issue (also deterministic given the sorted root set). +- Hash collision on `hash8` → fallback to `hash16` (first 16 hex chars); double collision → Phase 0.5 abort with manual cleanup hint. + +**Common rules** (both N=1 and N>1): + +- `slug` / `root1-slug` = lowercased title, non-alphanumeric → `-`, capped at 40 chars, leading/trailing `-` trimmed +- The chain shell MUST refuse to start if a branch with the dispatched name already exists (N=1 single-root naming) — manual cleanup required +- Sub-`/idd-all #M --in-chain` Step 0.5 sanity check MUST verify the current branch matches `^idd/chain-` prefix (covers both `idd/chain-<N>-` and `idd/chain-multi-` forms); otherwise abort ## Chain state ``` -QUEUE : ordered list of pending issues (FIFO; root pushed first) -DEPTH_MAP : issue → integer depth (root=0, immediate spawns=1, ...) +QUEUE : ordered list of pending issues (seeded with all roots in sorted order) +DEPTH_MAP : issue → integer depth WITHIN its owning root's subtree (each root=0) +ROOT_ID_MAP : issue → owning root id (which subtree it belongs to) PROCESSED : issue → "verified" | "failed" CHAINED_ORDER : ordered list of verified issues (used to build PR body) +FAIL_ROOTS : set of root ids whose subtree FAILed (verify FAIL) +FAILED_AT : ordered list of failing issues (used in Phase 4 forest report) +TRAVERSAL : "dfs" (default) | "bfs" (when --bfs flag present) ``` Caps (hard-coded constants, not config-driven): ``` -chain_max_depth = 2 -chain_max_issues = 5 # includes root +chain_max_depth = 3 # applies PER ROOT SUBTREE (each root starts at depth 0) +chain_max_issues = 10 # global cap across ALL root subtrees combined ``` -Hard cap rationale: real-world chains rarely exceed depth 2 (per `add-idd-all-chain-skill` design.md Decision 7). Conservative v1 limit; future evidence may justify config-driven override, but v1 ships with no such surface to avoid default ambiguity. +Both caps apply independently — whichever is triggered first wins. Bumped from v2.55.0's `(2, 5)` to `(3, 10)` to accommodate multi-root scenarios where `(N=3 roots × ~2 spawns/each = 6)` already exceeds the v1 max-issues=5 cap. See `add-idd-all-chain-skill` design.md Decision 7 for the v1 cap rationale and `multi-root-traversal-idd-all-chain` design.md Decision D3 for the v2 bump rationale. + +## Traversal mode (DFS vs BFS) + +``` +DFS (default, --bfs flag absent): + New spawns are pushed to QUEUE FRONT: + QUEUE = ($SPAWN_NUM "${QUEUE[@]}") + Effect: a root's entire subtree is fully explored before moving to the next root. + Use when: reviewer cognitive load matters — process one root + its descendants + fully before switching context. + +BFS (--bfs flag present): + New spawns are pushed to QUEUE BACK: + QUEUE += ("$SPAWN_NUM") + Effect: all roots are processed level-by-level (all root depth=0 first, then + all depth=1 spawns from any root, etc.). + Use when: fairness across roots matters — guarantee each root subtree gets at + least depth-0 processing before any other root's deeper spawns. +``` + +Single-root invocations (N=1) have no observable DFS/BFS difference since the only "branching" comes from spawns of a single subtree. The `traversal` field is still recorded in the manifest. + +### DFS vs BFS queue order example + +| Mode | Initial queue | Pop #44 | Spawn #X from #44 added | Next pop | +| ---- | ------------- | ------- | ------------------------ | -------- | +| DFS | [44, 45, 50] | [45, 50] (current=44) | push-front: [X, 45, 50] | X | +| BFS | [44, 45, 50] | [45, 50] (current=44) | push-back: [45, 50, X] | 45 | ## Eligibility heuristic @@ -86,9 +134,20 @@ WHILE queue is non-empty: BREAK current ← queue.pop_front() + current_root ← root_id_map[current] + + # Skip if current's owning root subtree has already FAILed (defensive — purge + # below normally clears this, but covers concurrent FAIL during long /idd-all + # invocation) + IF current_root IN fail_roots: + log "#current skipped (root #current_root subtree already FAILed)" + CONTINUE + pre_len ← len(manifest.spawned) + export IDD_CHAIN_CURRENT_ROOT_ID = current_root invoke /idd-all #current --in-chain --cwd <CWD> + unset IDD_CHAIN_CURRENT_ROOT_ID phase ← read latest Phase from issue body Current Status @@ -96,20 +155,30 @@ WHILE queue is non-empty: processed[current] ← "verified" chained_order.append(current) ELSE: + # Per-root halt (D4 Option C, v2.60+): scope the halt to current_root's subtree only processed[current] ← "failed" - emit abort_report(current, phase) - EXIT 1 # halt + preserve + fail_roots.add(current_root) + failed_at.append(current) + # Purge from queue all pending issues whose owning root == current_root + queue ← [q for q in queue if root_id_map[q] != current_root] + log "halted root #current_root subtree; other roots continue" + CONTINUE # NOT exit 1 — other root subtrees still process post_len ← len(manifest.spawned) FOR idx in [pre_len, post_len): spawn ← manifest.spawned[idx] - next_depth ← depth_map[current] + 1 + next_depth ← depth_map[current] + 1 # per-root depth: current's depth within ITS subtree IF chain_eligible(spawn, root) AND - next_depth <= chain_max_depth AND - |processed| + |queue| + 1 <= chain_max_issues: - queue.append(spawn.issue_number) + next_depth <= chain_max_depth AND # per-root cap + |processed| + |queue| + 1 <= chain_max_issues: # global cap + # Push semantics dispatch on traversal mode + IF traversal == "dfs": + queue.push_front(spawn.issue_number) # rich subtree first + ELSE: + queue.append(spawn.issue_number) # level-by-level depth_map[spawn.issue_number] ← next_depth + root_id_map[spawn.issue_number] ← current_root # inherit owning root ELSE: log spawn filed-only (reason: ineligible | depth-cap | issues-cap) ``` @@ -119,43 +188,57 @@ WHILE queue is non-empty: - Manifest delta is computed by length, not by contents. Sub-skills append-only; the shell only reads entries in `[pre_len, post_len)` after each sub-invocation. - Order of enqueueing within a single delta is the order sub-skills wrote entries. Sub-skills MUST NOT re-order or rewrite earlier entries. - The shell is single-threaded — no concurrent sub-`/idd-all` invocations within one chain run. +- `root_id_map` is populated at Phase 1 init for each root (root_id=self) and inherited by spawns from `current_root` (the issue being processed when the spawn was filed). +- Sub-skills propagate `root_id` to the manifest via the 9th positional arg to `manifest-append.sh`, reading `IDD_CHAIN_CURRENT_ROOT_ID` env var with fallback to the current issue number. ## Failure mode | Trigger | Action | |---------|--------| -| Sub-`/idd-all #N --in-chain` returns verify-FAIL phase | Halt queue; preserve all commits on cluster branch; emit abort report | +| Sub-`/idd-all #N --in-chain` returns verify-FAIL phase (single root subtree) | Halt that root's subtree only; purge same-root pending from queue; other root subtrees CONTINUE; preserve all commits on cluster branch; Phase 4 report shows per-root FAIL/PASS | +| Sub-`/idd-all #N --in-chain` returns verify-FAIL phase (the only root subtree) | Equivalent to halting whole queue; preserve commits; Phase 4 shows single root FAIL | | `git`/`gh` command failure during Phase 0 | Abort before mutations | +| Branch hash8 collision (N>1) | Fallback to hash16; double collision → Phase 0.5 abort | +| Manifest schema mismatch (v1 on disk under v2 helper) | Helper exits 1 (fail-fast, no silent migrate) | +| Sub-skill invokes `manifest-append.sh` with 8 args (missing root_id) | Helper exits 2 (usage error) | | `gh pr create` fails in Phase 3 | Branch is already pushed; print recovery hint (`gh pr create` manually) | -**No rebase, no revert, no auto-cleanup**. The cluster branch is the audit trail of the run; preserving partial commits is a feature, not a bug. Recovery options the abort report MUST surface: +**No rebase, no revert, no auto-cleanup**. The cluster branch is the audit trail of the run; preserving partial commits is a feature, not a bug. For multi-root chains, partial work from completed root subtrees coexists with halted FAIL root's partial commits on the same branch — user reviews the cluster PR and decides whether to revert FAIL root's commits or merge all and follow up. Recovery options the Phase 4 report MUST surface (per-root FAIL): -1. `/idd-verify --pr <future-PR>` to inspect FAIL details -2. `/idd-implement #failing --branch-override <cluster-branch>` to retry on cluster branch -3. `/idd-all-chain #failing` from clean main (creates new branch, leaves this one for cleanup) +1. `/idd-verify --pr <future-PR>` to inspect FAIL details (the cluster PR opens at Phase 3 regardless) +2. `/idd-implement #failing --branch-override <cluster-branch>` to retry the failing issue on cluster branch +3. `/idd-all-chain #failing` from clean main (creates new branch, leaves this cluster for cleanup) 4. Discard cluster: `gh pr close` + `git checkout <default>` + `git branch -D <cluster-branch>` +## PR title schema (Phase 3) + +``` +N=1: chain: <root title> +N>1: chain (multi-root): <N> issues — <lowest-root title> +``` + ## PR body schema (Phase 3) ```markdown -Refs #<root> #<chained_1> #<chained_2> ... +Refs #<root_1> #<root_2> ... #<chained_1> #<chained_2> ... (roots first, then spawns) ## Summary -Cluster of <N> issues solved as one chain (root + auto-emergent spawn) via `/idd-all-chain`. +(N=1) Cluster of <N> issues solved as one chain (root + auto-emergent spawn) via `/idd-all-chain` (v2.55+). +(N>1) Multi-root chain (N=<N> roots: <comma-separated-roots>) solved as one cluster via `/idd-all-chain` (v2.60+, traversal=<mode>). Total <K> processed issues across all root subtrees. ## Cluster overview -| # | Spawn source | Phase | PR commit | -|---|--------------|-------|-----------| -| #<root> (root) | — | verified | <abbrev sha> | -| #<chained_1> | <sub-skill> <step> | verified | <abbrev sha> | +| # | root_id | Spawn source | Phase | PR commit | +|---|---------|--------------|-------|-----------| +| #<root> | <root> | root | verified | <abbrev sha> | +| #<chained> | <root_id> | <sub-skill> <step> | verified | <abbrev sha> | | ... ## Per-issue details <details> -<summary>#<issue> — <title></summary> +<summary>#<issue> (root_id=<root_id>) — <title></summary> (diagnose / verify / commit links) From 2d7c7fc3fae8d77a9ee2f6b61b7a344ede79b181 Mon Sep 17 00:00:00 2001 From: che cheng <kiki830621@gmail.com> Date: Tue, 12 May 2026 11:06:18 +0800 Subject: [PATCH 6/6] spectra(tasks): defer smoke tests 7.1/7.2 to first-real-use validation track (Refs #46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark tasks 7.1 and 7.2 as [~] (skipped with reason) per IDD checklist convention (per plugins/issue-driven-dev/CLAUDE.md): - 7.1 Single-root + multi-root happy path smoke tests - 7.2 Multi-root edge cases smoke tests (verify FAIL, depth-cap, issues-cap) Reason: no test fixture repo for orchestration testing exists in this codebase (consistent with #52 idd-verify orchestration validation track — orchestration tests cannot mock GitHub API / git operations without significant fixture infrastructure investment). Acceptance criteria covered through: - Deterministic unit-test snippets in commit 49b2cc0 (branch naming for N=1/N=3 with hash8 reproducibility check) - 4 unit tests in commit ed9682d (manifest-append.sh arg-count + schema + root_id validation) - First real /idd-all-chain invocation after merge serves as acceptance for the orchestration algorithm (DFS push-front, per-root halt, caps) Refs #46 --- .../changes/multi-root-traversal-idd-all-chain/tasks.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md index 2f8326b..cf8cbd9 100644 --- a/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md +++ b/openspec/changes/multi-root-traversal-idd-all-chain/tasks.md @@ -31,8 +31,10 @@ ## 7. Acceptance smoke tests -- [ ] 7.1 Single-root + multi-root happy path smoke tests(test 1+2+3 from design.md Acceptance Criteria):(a) `/idd-all-chain #X` N=1 backward compat;(b) `/idd-all-chain #A #B #C` N=3 DFS default;(c) `/idd-all-chain #A #B #C --bfs` N=3 BFS。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (acceptance for D2 DFS = push-front真的改queue順序,不是只標label and D5 branch naming hash8 for N>1). **Verification**:對 test fixture repo 跑 3 個 smoke,每個 inspect manifest + Phase 4 output + branch name 符合 design.md 列出的 expected behavior。 -- [ ] 7.2 Multi-root edge cases smoke tests(test 4+5+6+7 from design.md):(a) DFS push-front 觀察 spawn pop 順序;(b) verify FAIL 在 root A subtree → root B 繼續;(c) depth 4 spawn filed-only-not-chained;(d) 11th issue 觸發 max-issues=10 cap。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (acceptance for D3 cap = max-depth=3 primary + max-issues=10 safety net,獨立apply and D4 verify FAIL = per-root continue (Q4 Option C) and D7 Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:對 fixture repo 各跑 1 個 smoke,Phase 4 forest report 列出 expected status icons + per-root summary 與 design.md scenarios 對照一致。 +- [~] 7.1 Single-root + multi-root happy path smoke tests(test 1+2+3 from design.md Acceptance Criteria):(a) `/idd-all-chain #X` N=1 backward compat;(b) `/idd-all-chain #A #B #C` N=3 DFS default;(c) `/idd-all-chain #A #B #C --bfs` N=3 BFS。Covers Requirement: idd-all-chain skill SHALL drive root issue plus auto-emergent spawn through one cluster branch and one PR (acceptance for D2 DFS = push-front真的改queue順序,不是只標label and D5 branch naming hash8 for N>1). **Verification**:對 test fixture repo 跑 3 個 smoke,每個 inspect manifest + Phase 4 output + branch name 符合 design.md 列出的 expected behavior。 + - **Deferred — first-real-use validation track**: No test fixture repo for orchestration testing exists in this codebase (consistent with #52 idd-verify orchestration validation track — orchestration tests cannot mock GitHub API / git operations without significant fixture infrastructure investment). Branch naming + manifest schema verification has been covered via deterministic unit-test snippets in commits 49b2cc0 (smoke-test branch naming logic for N=1/N=3) and ed9682d (manifest-append.sh 4 unit tests for arg-count/schema-version/root_id validation). Per IDD discipline, the **first real `/idd-all-chain #A #B #C` invocation after this change merges** serves as acceptance — reviewer / first user manually verifies (a)+(b)+(c) against design.md scenarios. +- [~] 7.2 Multi-root edge cases smoke tests(test 4+5+6+7 from design.md):(a) DFS push-front 觀察 spawn pop 順序;(b) verify FAIL 在 root A subtree → root B 繼續;(c) depth 4 spawn filed-only-not-chained;(d) 11th issue 觸發 max-issues=10 cap。Covers Requirement: idd-all-chain SHALL halt the chain on verify failure and preserve partial commits (acceptance for D3 cap = max-depth=3 primary + max-issues=10 safety net,獨立apply and D4 verify FAIL = per-root continue (Q4 Option C) and D7 Phase 4 TaskList visualization(Q7 deferred from discuss)). **Verification**:對 fixture repo 各跑 1 個 smoke,Phase 4 forest report 列出 expected status icons + per-root summary 與 design.md scenarios 對照一致。 + - **Deferred — first-real-use validation track**: Same rationale as 7.1. Edge-case smoke (verify FAIL in one subtree, depth-cap, max-issues-cap) requires inducing FAIL conditions on real GitHub issues which we cannot mock without fixture infrastructure. The implementation discipline (per-root FAIL_ROOTS purge + per-root depth cap + global max-issues cap) is exercised by the algorithm bash code reviewed in 49b2cc0; first real `/idd-all-chain` invocation that hits any of these conditions (or a deliberately induced one) serves as acceptance. ## Design decisions coverage map