diff --git a/openspec/specs/append-vs-modify-discipline/spec.md b/openspec/specs/append-vs-modify-discipline/spec.md index 4d20d5c..c3fe3ae 100644 --- a/openspec/specs/append-vs-modify-discipline/spec.md +++ b/openspec/specs/append-vs-modify-discipline/spec.md @@ -4,7 +4,7 @@ Defines the action-scoped modify discipline that governs every IDD plugin action that modifies existing artifacts (issue bodies, comments, files, state fields). Every modify-action SHALL declare exactly one scope category from a canonical 7-category enumeration (`state-field-update`, `bounded-section-replace`, `audit-block-append`, `inline-replace-before-publish`, `verbatim-preserve`, `append-only`, `free-rewrite`). Undeclared modify-actions SHALL be refused at runtime regardless of actor identity (default-refuse), and `/idd-edit --replace` SHALL require an explicit `--scope` or `--section` flag. The discipline also defines authoritative-source resolution for checklist gates (Implementation Complete > Current Status > top-level Todo/Tasks/Checklist) with a legacy-scan fallback for pre-discipline issues, plus retroactive category labeling of existing IDD skills. Sourced from change `add-action-scoped-modify-discipline`. -**Implementation status (v2.74.0)**: discipline declared at spec level. Runtime enforcement landed for `bounded-section-replace` (`/idd-update` REPLACE), `state-field-update` (`/idd-clarify` status mutation, `spectra task done`), `audit-block-append` (5 IC_R011 PATCH sites), `inline-replace-before-publish` (`/idd-close` Step 3.5), Path C authoritative-source gate logic across 4 gate sites. **`/idd-edit` runtime enforcement of `--scope` / `--section` (Requirement 4) + user-authored verbatim-preserve guard (Requirement 5) deferred to follow-up issue [#154](https://github.com/PsychQuant/issue-driven-development/issues/154)** after 3 verify iterations (R1/R2/R3 on PR #153) showed bash-incremental impl introduces new bugs each pass. AI/user invocation SHALL apply Requirements 4-5 as discipline (read spec + apply manually) until #154 ships runtime gate. Requirements 4-5 SHALL be re-verified for runtime conformance when #154 closes. +**Implementation status (v2.75.0)**: discipline declared at spec level + runtime enforcement landed for ALL categories. Runtime gates for `bounded-section-replace` (`/idd-update` REPLACE + `/idd-edit --replace --scope`/`--section` per R4), `state-field-update` (`/idd-clarify` status mutation, `spectra task done`), `audit-block-append` (5 IC_R011 PATCH sites), `inline-replace-before-publish` (`/idd-close` Step 3.5), `verbatim-preserve` (`/idd-edit` R5 author-check + override pathway), Path C authoritative-source gate logic across 4 gate sites. **`/idd-edit` Requirements 4 + 5 runtime gates landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154)** through extracted helper `.claude/scripts/idd-edit-helper.sh` (parse-args / validate-target / section-replace subcommands) with 13 unit-test fixtures at `.claude/scripts/tests/idd-edit/` — closes R1/R2/R3 bash-inline parser bug class observed on PR #153. ## Requirements @@ -113,7 +113,7 @@ code: --- ### Requirement: /idd-edit --replace SHALL require scope flag -**Implementation status (v2.74.0)**: discipline declared; runtime enforcement deferred to [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). AI / user invocations SHALL apply this requirement as discipline (include `--scope` / `--section` in invocation patterns) until #154 ships bash runtime gate. Scenarios below describe intended runtime behavior post-#154 — they are NOT currently enforced by `idd-edit/SKILL.md` (which is at pre-#150 baseline). Re-verify for runtime conformance when #154 closes. +**Implementation status (v2.75.0)**: landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). `/idd-edit` Step 1 invokes `.claude/scripts/idd-edit-helper.sh parse-args` which enforces R4 gate (exit code 3 with actionable error message). Tested by fixture `10-replace-no-scope`. SKILL.md `## Runtime gates` section documents the gate matrix. The `/idd-edit` skill SHALL require explicit scope when invoked with `--replace` mode. The skill SHALL accept either `--scope whole-comment` (explicit acknowledgment of full-comment overwrite) OR `--section ` (limit replacement to a named subsection within the comment). Invocations of `--replace` without either flag SHALL be refused. The `--append` and `--prepend-note` modes SHALL NOT require scope flags because their scope is inherently bounded (trailing block / leading errata marker respectively). @@ -159,7 +159,7 @@ code: --- ### Requirement: /idd-edit SHALL refuse modifications to user-authored comments -**Implementation status (v2.74.0)**: discipline declared; runtime enforcement deferred to [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). AI / user invocations SHALL apply this requirement as discipline (include `--override-user-content --reason="..."` when intentionally editing non-OWNER non-bot comments) until #154 ships bash runtime gate + `/idd-comment` errata flow integration. Scenarios below describe intended runtime behavior post-#154 — NOT currently enforced by `idd-edit/SKILL.md` (pre-#150 baseline). Re-verify for runtime conformance when #154 closes. +**Implementation status (v2.75.0)**: landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). `/idd-edit` Step 1.5 invokes `.claude/scripts/idd-edit-helper.sh validate-target` which enforces R5 gate (exit code 4 with actionable error message + bot allowlist via `*[bot]` glob + OWNER passthrough + override pathway). `/idd-comment` errata Template auto-call handles exit 4 with helpful manual-invocation hint (D2 decision: refuse-with-message > auto-override, aligns with IC_R007 user-authored-intent spirit). Tested by fixtures `11-non-owner-no-override` (default OVERRIDE=false) + `12-non-owner-with-override` (override-pair guard) + `13-errata-refuse-message` (override+reason succeeds). The `/idd-edit` skill SHALL refuse modifications targeting comments authored by users whose `author_association` is not `OWNER` and who are not in the known-bot allowlist (`github-actions[bot]`, `dependabot[bot]`, and other repo-configured bots). This protection SHALL apply to all three modes (`--append`, `--prepend-note`, `--replace`). Invocations targeting user-authored comments SHALL be refused unless the caller provides `--override-user-content` flag together with `--reason=""` documenting the explicit decision to modify user content. This requirement is the comment-level instance of the `verbatim-preserve` category, aligned with IC_R007 (verbatim source preservation). diff --git a/plugins/issue-driven-dev/.claude-plugin/plugin.json b/plugins/issue-driven-dev/.claude-plugin/plugin.json index f51314e..a6e3b33 100644 --- a/plugins/issue-driven-dev/.claude-plugin/plugin.json +++ b/plugins/issue-driven-dev/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "issue-driven-dev", - "description": "v2.63.0: #96-backlog Simple cluster \u2014 6 docs/reference follow-ups shipped via cluster-PR #101 (PsychQuant/issue-driven-development #60 #62 #63 #78 #90 #91). These are the Simple-tier subset of an 18-issue `/idd-diagnose` batch (6 Simple / 12 Plan) run over the #96-backlog cleanup; the 12 Plan-tier issues are driven separately. #60: NEW `## Cluster-PR eligibility (when to bundle vs split)` section in `references/batch-and-cluster.md` \u2014 a criteria table (same-file \u2713 / same-skill \u2713 / same-root-issue chain-only \u2713 / same-label \u2717 / same-review-timing \u2717) plus a borderline >50-line review-surface heuristic, with a cross-reference from `idd-implement/SKILL.md`'s Cluster-PR mode paragraph; motivated by PR #58 having bundled unrelated issues #49+#53 under only a shared parent label. #62: `references/usecase-routing.md` decision-tree section gains a bulk-solve note pointing to row 27 \u2014 there is no built-in zero-arg backlog bulk-solve; use per-issue `/idd-all` or `/idd-all-chain`. #63: `usecase-routing.md` row 27 `#44 chain-solve` plain text upgraded to an explicit `[#44 chain-solve](url)` link for raw-markdown-viewer cross-link consistency with the already-linked `#37`/`#46`. #78: `idd-issue/SKILL.md` multi-finding override-flags section gains a \u26a0 CI-caller note \u2014 automated / CI / `/loop` callers expecting the pre-v2.55.0 always-single-issue behavior of `idd-issue source.docx` MUST pass `--no-multi-finding` explicitly (v2.55.0 changed the default to auto-enter multi-finding mode on \u22652 findings); a retroactive behavioral-change notice was added to `CHANGELOG.md` (placed under `[Unreleased]` then rolled into this entry \u2014 no standalone `## [2.55.0]` entry exists). #90: NEW `openspec/CONVENTIONS.md` documenting the `**GitHub-side tracker**: #NN` canonical Spectra-proposal \u2192 GitHub-issue linking convention (collapses `idd-close`'s 3-fallback detection chain to a one-line lookup). R1 placed this at `openspec/LANGUAGE.md`; the 6-AI cluster verify's Devil's Advocate caught (coordinator-confirmed) that `openspec/LANGUAGE.md` is a reserved filename \u2014 `spectra-discuss` reads it as the project's canonical vocabulary file with a vocabulary-drift capture mechanism \u2014 so R2 relocated the convention to `openspec/CONVENTIONS.md` (purpose-built, verified not reserved by any skill). #91: `.claude/skills/spectra-archive/SKILL.md` gains a `Step 0: Bootstrap Stage Task List` section before its `**Steps**` block, with 8 `TaskCreate` entries mapping 1:1 to the skill's existing Steps 1-8, matching the idd-* Bootstrap discipline; the parallel tool-managed command-file surface (`.claude/commands/spectra/archive.md`, wrapped in `` regenerated markers) was intentionally NOT hand-edited \u2014 its Step-0 gap is folded into #93's 4-copy divergence scope. Cluster-PR #101 verified by 6-AI cluster verify: R1 CONDITIONAL PASS \u2014 4/5 Claude reviewers PASS, Devil's Advocate surfaced 2 HIGH blocking findings (#90 reserved-name collision, #91 invocation-surface scope), both coordinator-confirmed via file-existence checks; R2 PASS after #90 relocation + #91 re-scoping. Codex (6th reviewer) hung and never returned \u2014 recorded as an explicit process gap rather than hidden in the aggregate. Squash-merged to main as `0eb419c`. v2.62.0: cluster mode override \u2014 pr-flow.md canonical documentation + idd-implement Step 0.5 bash implementation (PsychQuant/issue-driven-development#96). Resolves a 3-file contradiction in IDD's PR-vs-direct-commit path resolution: `pr-flow.md` canonical resolution-algorithm table had no cluster carve-out while `idd-implement/SKILL.md:49` + `batch-and-cluster.md:133` independently asserted cluster-PR mode forces PR (\"\u4e0d\u63a5\u53d7 --no-pr\"); the three files contradicted and the behavior on `--no-pr` + cluster collision (abort / warn / silent ignore) was never specified. Surfaced during PR #94's cluster work. Option A (user-selected in `/idd-all` session from 3 diagnosis candidates A/B/C): maintain forced PR for cluster mode, but make it explicit + consistent. NEW `pr-flow.md` `### Cluster mode override` subsection \u2014 cluster mode (any IDD skill invoked with \u22652 `#N` args) is a multi-issue mode where all cluster issues share one feature branch + one PR; path resolution is `idd-implement`'s job (the only skill that resolves PR-vs-direct-commit) and for it cluster mode is a precondition that pre-empts the resolution-algorithm table and forces PR path; `idd-verify` / `idd-close` are cluster-aware but operate on the cluster's already-existing PR \u2014 they consume the path decision, they don't make it. Explicit override notice mirrors fork detection (`\u2192 cluster mode (N issues) \u2192 PR path enforced (overriding --no-pr / pr_policy=never)`); fork+cluster co-occurrence prints both notices (the two pre-emptions independently force PR path, no precedence question). `idd-implement` Step 0.5 bash wired with cluster detection: parse `#N` token count in `$@` \u2192 derive `CLUSTER_MODE` \u2192 pre-empt block before the existing flag/fork/policy resolution \u2192 `OVERRIDE_SRC` accumulation composes the actual triggering condition(s) into the notice; Step 0.5 local algorithm summary gains a row 0 noting cluster pre-emption. `batch-and-cluster.md:133` rule statement demoted to a pointer at the new canonical section with the rationale phrase (\"stacked half-isolated changes on default branch\") inlined verbatim in `pr-flow.md` for downstream-grep stability. Verified 6-AI \u00d7 2 rounds: R1 (doc-only `cbe6f5d`) CONDITIONAL PASS \u2014 5/6 reviewers converged on a HIGH doc/code gap (spec described a Phase 0.5 override notice the bash had no capability to emit); R2 (`5351116`) extended PR scope per user opt-in to add the ~24-line bash impl (8-case behavioral dry-run + `bash -n` clean), 6/6 PASS with Devil's Advocate explicitly recommending MERGE; R3 (`04c51cb`) closed DA's one new actionable finding (the subsection originally over-claimed cluster mode \"pre-empts the Resolution algorithm\" for verify/close \u2014 those skills never run path resolution). Step 0.8 auto-close-trap scan clean. Backward compat: single-issue invocation (`idd-implement #19`) byte-equivalent \u2014 the cluster carve-out only fires on \u22652 `#N`. Follow-up #100 tracks 2 non-blocking deferred items (Option A still forces PR on a non-default feature branch where cluster direct-commit is a legitimate workflow \u2014 Option B revisit candidate; cluster-detection glob `\\#[0-9]*` over-counts malformed/duplicate tokens vs the stricter documented `^#\\d+$`). PR #99 squashed as `b7f72ff`. v2.61.0: idd-verify Step 0.8 \u2014 squash-commit-body auto-close trap fix (PsychQuant/issue-driven-development#97). Step 0.8 (added in v2.60.1 by PR #94) extended from a 1-source scan (PR body via `closingIssuesReferences`) to a 2-source scan covering: (1) PR body authoritative parse via `closingIssuesReferences` (kept), and (2) per-commit `messageHeadline` + `messageBody` via `gh pr view --json commits` + trap regex `(^|[^-/[:alnum:]])(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]+#[0-9]+` (case-insensitive via `tolower($0)`). R1/R2/R3 lessons baked into the regex from PR #94's verify history: `(^|[^-/[:alnum:]])` prefix excludes `/idd-close #N` IDD skill invocations and hyphenated tokens; `:?` covers the colon form; `[[:space:]]+` mirrors GitHub's space requirement. Same-repo `#N` form only; cross-repo `owner/repo#N` deferred per Plan D7. The fix addresses the ironic v2.60.1 dogfood failure where PR #94 itself was squash-merged and GitHub auto-closed `#87` two seconds later because one of PR #94's commits had a body that *quoted* the trap pattern as a verify-finding example. R2 (in-PR fix after R1 verify) extended the jq filter from body-only to headline+body after Devil's Advocate + Codex independently confirmed the missed-subject channel with empirical evidence: commit `8ac8206` headline `resolves #N` form auto-closed `#70` (2026-05-11); commit `a82867d` headline `fix #N` form auto-closed `#26` (2026-05-07). R1's body-only filter would have missed both. PR #98 6-AI verified in 2 rounds: R1 CONDITIONAL PASS (5/6, 1 HIGH blocking DA-H1 + Codex Finding 2); R2 6/6 PASS with Devil's Advocate explicitly recommending MERGE. Also adds `### \u5f15\u7528 trap pattern \u4f5c\u53cd\u4f8b\u7684\u5beb\u4f5c\u7d00\u5f8b` subsection under `## Commit Conventions` in `plugins/issue-driven-dev/CLAUDE.md` codifying the writing discipline: code fence is **visual only** (parser is context-blind), literal letter N (capital, no digit) is the **actual suppression** mechanism mirroring the safe pattern in `references/pr-flow.md:127`, cite-via-link is strongest. Single/double quotes do NOT suppress the parser. This is the write-time root fix; Source 2 is defence-in-depth at verify time. Step 0 bootstrap TaskCreate entry renamed `scan_pr_body_trailers` \u2192 `scan_pr_body_and_commits_trailers` reflecting Source 2. Backward compat: Source 2 is additive \u2014 clean PRs see no change in output; PRs with trap pattern in commits (subject or body) now get a warn block with fix-options (rebase + amend with letter N, override squash message via `gh pr merge --body`, or post-hoc `/idd-close`). Dogfood: PR #98's own squash commit (`e0d61e7`) is clean under the new 2-source Step 0.8 \u2014 the discipline added to CLAUDE.md held for both commit message body AND headline. Master verify reports at PR #98 #issuecomment-4483847549 (R2) and #issuecomment-4483788120 (R1). v2.60.1: cluster fix \u2014 PR-body auto-close trap (PsychQuant/issue-driven-development#87 + #74). All 4 IDD skill PR-body templates (idd-implement Step 5.5, idd-all Phase 5, idd-all-chain Step 5.5, pr-flow.md canonical) reworded to drop literal `Closes #${N}` from anti-trailer warnings \u2014 heredoc `${N}` substitution previously turned the cautionary warning string into a real `Closes #` that GitHub auto-close parser matched context-blind (ignoring negation, markdown, quotes), bypassing /idd-close's checklist gate + closing summary. New unified wording: `**Do NOT add a GitHub close trailer** (Closes/Fixes/Resolves) \u2014 IDD discipline requires manual /idd-close after merge to enforce checklist gate + closing summary.` Keywords named but never followed by `#` so GitHub's regex cannot match. NEW idd-verify Step 0.8 preventive gate (PR mode, warn-only): queries `gh pr view --json closingIssuesReferences` \u2014 GitHub's authoritative parse of which issues the PR auto-closes on merge \u2014 covering all trailer forms (Closes #N / Closes: #N colon form / cross-repo / issue-URL) without self-written regex. Warn-only since a PR body may legitimately quote the keywords in prose; gate value is making the risk visible at verify time before merge. Eventual-consistency caveat disclosed (closingIssuesReferences is settled state, settles well within typical verify-after-implement gap). PR #94 6-AI verified in 3 rounds: R1 (initial regex form) caught colon-form gap \u2192 R2 redesigned to closingIssuesReferences eliminating self-parser fragility \u2192 R3 cleanup (deleted orphan regex doc, surfaced gh failures with explicit skip note replacing silent fail-open, scoped overclaim, query `.url` not `.number`). Confirmed prior incidents resolved: #559, che-apple-mail-mcp#99, #73, #56. Two follow-up issues filed: #96 (cluster-PR mode silently forces PR path while direct-commit honours `--no-pr` \u2014 doc contradiction in pr-flow.md canonical algorithm table, design decision pending), #97 (new failure mode discovered at squash-merge of PR #94 itself \u2014 squash commit body inherited commit-message body quoting `Closes: #87` as a verify-finding reference, triggered auto-close of #87 2s after merge; Step 0.8 only scans PR body and doesn't predict squash commit message). Master verify report at PR #94 #issuecomment-4482808720. v2.60.0: idd-all-chain multi-root + DFS/BFS traversal + per-root halt + spawn-manifest schema v2 hard-break (PsychQuant/issue-driven-development#46, multi-root-traversal-idd-all-chain Spectra change). NEW multi-root invocation `/idd-all-chain #A #B #C [--bfs] [--cwd ]` accepts \u22651 root issue (N=1 byte-equivalent backward compat with v2.55.0+). NEW `--bfs` flag selects BFS traversal mode (push-back queue semantics for fairness across roots); default DFS pushes spawns to queue front (rich subtree first per root). NEW spawn manifest schema v2 hard-break: top-level `root_issue: int` \u2192 `root_issues: [int]`, top-level adds `traversal: \"dfs\"|\"bfs\"`, every spawn entry adds `root_id: int` (must match one of root_issues elements). Helper `scripts/manifest-append.sh` bumps `EXPECTED_SCHEMA_VERSION` 1\u21922, accepts 9th positional arg `root_id`, validates root_id \u2208 root_issues array, fail-fast on v1 manifest detection. Cap redesign for multi-root accommodation: per-root `chain_max_depth` 2\u21923 (each root subtree counts depth from 0 independently), global `chain_max_issues` 5\u219210 (union across all root subtrees, applies independently of depth cap). Verify FAIL = per-root halt (D4 Option C): failing issue's `root_id` added to FAIL_ROOTS, all same-root pending issues purged from QUEUE, other root subtrees continue processing, commits preserved; Phase 4 emits per-root PASS/FAIL/SKIPPED summary block. Branch naming dispatches on N: N=1 keeps backward-compat `idd/chain--`, N>1 uses `idd/chain-multi--` where hash8 is first 8 hex of sha256 over sorted-asc root numbers joined by `-` (deterministic per root set); hash8 collision fallback hash16, double collision aborts with manual cleanup hint. PR title dispatches: N=1 `chain: `, N>1 `chain (multi-root): N issues \u2014 `. PR body cluster overview table adds `root_id` column; Refs lists all roots first then chained spawns. NEW Phase 4 forest tree printout: per-root subtree with status icons (\u2713 PASS, \u2717 FAIL, \u2298 filed-but-not-chained), depth labels, spawn-source attribution; per-root PASS/FAIL summary block; filed-only-not-chained list. 4 sub-skills (idd-implement Step 5.7 / idd-verify Phase 4 / idd-plan Step 2.5 / idd-diagnose Step 3.6) propagate root_id via `IDD_CHAIN_CURRENT_ROOT_ID` env var (exported by Phase 2 chain loop before each `/idd-all #M --in-chain` invocation), with defensive `[ -n \"$ROOT_ID_FOR_MANIFEST\" ]` guard preventing silent skip when both env and local fallback variables are unset. NEW `allowed-tools` frontmatter expanded with 11 additional Bash tools (shasum/sed/tr/cut/sort/seq/grep/awk/printf/date/head/tail/wc/basename/comm) for Phase 0.5 branch naming + Phase 4 forest tree rendering. Modified `idd-all-chain` + `idd-spawn-manifest` specs (3 MODIFIED + 1 ADDED requirement each); spec deltas in openspec/changes/multi-root-traversal-idd-all-chain/. Updated `references/spawn-manifest.md` v2 schema doc + `references/chain-flow.md` DFS/BFS algorithm + per-root halt + cap interaction + branch naming hash rule sections + PR title/body dispatch. Backward compat: single-root chain invocation byte-equivalent to v2.55.0 except for the schema bump (v1 manifests on disk become unreadable \u2014 per design, manifest is transient per-chain-session state, hard-break safe). Smoke tests 7.1+7.2 marked `[~]` first-real-use validation track per `## Checklist Conventions` IDD discipline (orchestration tests cannot mock GitHub API + git operations without significant fixture infrastructure, mirroring #52 idd-verify validation pattern). v2.59.0: idd-verify orchestration playbook \u2014 Step 2 spawn restructure + NEW Step 2.5 Recovery Protocol (PsychQuant/issue-driven-development#52, resolves #70 structurally). Step 2 switches from TeamCreate (5 teammates with Read/Grep/Glob/Bash tools, NO Write) to 5 parallel Agent(subagent_type=general-purpose) calls (\u542b Write tool) + 1 Bash codex background, single-message dispatch preserves parallelism. Each reviewer prompt mandatorily contains 3 elements: (1) explicit findings file output path `Write findings to /tmp/verify__findings_.md`, (2) explicit 'DO NOT idle without producing output' rule, (3) retry-context-re-paste hint ('treat later SendMessage with re-pasted prompt as retry signal'). Pre-spawn prompt persistence: coordinator MUST save each role's prompt to /tmp/verify__prompt_.md before invoking Agent \u2014 Step 2.5b retry reads this file for FULL context re-paste (never assumes context survived idle/wake cycle, per #47 incident root cause). Devil's Advocate sequencing: bash polling loop on sibling findings files (max 30 iter \u00d7 5s = 2.5min timeout) replaces TeamCreate wait_for_idle primitive; timeout fallback writes SENTINEL marker `[STAGE 2.5 RECOVERY: DEVILS_ADVOCATE_TIMEOUT_/4]` on first line + body explanation. NEW Step 2.5 Recovery Protocol section between Step 2 spawn and Step 3 merge: (2.5a) file existence check scans 5 findings files; detects DA timeout sentinel via head -1 | grep then rm -f the file + add to MISSING_ROLES so downstream -s checks see role as missing; (2.5b) retry with FULL context re-paste using saved prompt file + 90s polling; (2.5c) second-idle coordinator self-review fallback; (2.5d) explicit 'Process Gaps' section in master report \u2014 no silent engine degradation. Step 3 merge prose source tag swept `[team:...]` \u2192 `[agents:...]`; ASCII architecture tree + \u9435\u5f8b rule updated; CLI alias `team` preserved backward-compat with documented backend as 5 standalone Agent calls. Frontmatter: TeamCreate removed from allowed-tools (no longer used). Side effect: #70 (TeamDelete cleanup gap on idle teammates from #47 verify-pr58 cycle) structurally dissolved \u2014 no team to delete = no cleanup gap. Plan tier D1-D5 + D6 first-real-use validation track: 3 codex verify rounds (R1 3 P1 \u2192 R2 2 P1 \u2192 R3 PASS) under codex-only degraded mode (Anthropic API rate-limit blocked Claude reviewer ensemble throughout session \u2014 dogfooded as Process Gap on first-real-use). Empirical bash smoke validated DA sentinel writer + Step 2.5a head -1 | grep detection + rm -f sequence. v2.58.0: idd-issue Stage 4.5 \u2014 jsonl gitignore pre-flight gate (PsychQuant/issue-driven-development#55). NEW pre-flight gate at idd-issue/SKILL.md between Stage 4 Dispatch and JSONL write: detects `.gitignore` shadowing of `.claude/.idd/issue-runs/.jsonl` via `git check-ignore -v` (D2 spec contract preservation). Source-aware classification via `IS_NESTED_GITIGNORE` flag \u2014 case statement orders absolute path / `.git/info/exclude` / bare `.gitignore` BEFORE `*/.gitignore` so global `core.excludesfile` named `.gitignore` does NOT mis-classify as nested. AskUserQuestion branches: Case A (fixable: root `.gitignore` / `.git/info/exclude` / global) \u2192 3-option Add carve-out / Skip / Abort; Case B (nested `.gitignore`) \u2192 2-option Skip / Abort with complete manual-fix chain hint (root rewrite cannot override per-directory ignore; nested file requires its own 4-line chain with trailing slashes on dir patterns + explicit `!.idd/issue-runs/*` glob, empirically validated). Universal 5-line carve-out block with `!.claude` parent re-include leverages git's last-matching rule to neutralize ANY outer ignore source \u2014 survives multi-source stacked ignores (root + `.git/info/exclude` / root + global / `.git/info/exclude` + global). Idempotent + upgrade-safe via two-part check (marker AND `!.claude` content presence): stale 4-line block (same marker, missing `!.claude`) triggers awk two-state-machine upgrade \u2014 STATE 1 consumes # rationale comments adjacent to marker; STATE 2 consumes only known carve-out literal lines, ENDS skip immediately after final pattern `!.claude/.idd/issue-runs` \u2014 adjacent user content (blank lines, user `# Section` comments, sibling patterns) preserved across all variants. Empty body's grep idiom uses `grep | wc -l | tr -d ' '` (clean integer) instead of `grep -c || echo 0` (which doubled output to `0\\n0` on no-match). Dispatch summary surfaces ignore source + user choice + continuity status (committed / pending exception / \u26a0 local-only with manual export hint / aborted). Ordering invariant: dispatch \u2192 gate \u2192 materialize \u2014 Stage 4 loop accumulates in-memory RUN_LOG_ENTRIES, Stage 4.5 gate fires after loop completes, materialize phase decides jsonl write fate per `JSONL_GITIGNORE_DECISION`. Abort discards in-memory entries BEFORE materialization; already-dispatched GitHub actions NOT rolled back (user-confirmed intent per Stage 3). Env var bypass `IDD_JSONL_GITIGNORE_GATE=false` for CI/unattended with 1-line audit cite. Plan tier D3 evolved through 3 revisions (single-line \u2192 4-line \u2192 universal 5-line) across 7 codex verify rounds (R1: 4 P1 \u2192 R2: 3 new P1 \u2192 R3: 2 new P1 \u2192 R4: 2 new P1 + 1 residual \u2192 R5: 3 new P1 \u2192 R6: 2 new P1 \u2192 R7: PASS), cumulative 16 P1 caught + fixed under codex-only degraded mode (Anthropic API limit blocked Claude reviewer team). Empirical 15/15 smoke validation across all source-classification scenarios (single source / stacked sources / nested / stale upgrade / idempotency / fresh / env bypass / skip-commit / abort). PR #71 squashed as `c342aa2`. Master verify report at PR #71 #issuecomment-4421108494. v2.57.0: idd-close Step 6.5 \u2014 Distribution Sync chain (PsychQuant/issue-driven-development#45). NEW Step 6.5 inserted between Step 6 (auto-update phase=closed) and Step 7 (batch close special rules), surfacing user-facing distribution channel sync (plugin marketplace / MCP binary / CLI binary) at issue close moment. Detection-driven AskUserQuestion 3-option pattern (per IC_R011 canonical): (a) `chain to now` invokes `/plugin-tools:plugin-update ` / `/mcp-tools:mcp-deploy` / `/cli-tools:cli-deploy`; (b) `skip \u2014 manual later` records `### Distribution Sync Pending` audit + manual command; (c) `not applicable` records reason. Detection helpers (inlined in Step 6.5 + canonical contract in references/distribution-detection.md): `is_plugin_marketplace_member` walk-up scan ancestor `.claude-plugin/marketplace.json` parse `plugins[].source` (string `\"./plugins/\"` form) + `has_binary_wrapper` line-agnostic scan `bin/*.sh` for GitHub release URL patterns + `resolve_plugin_name` extract matched plugin name for chain command composition + `infer_distribution_type` orchestrator returning plugin/mcp/cli/plugin+mcp/plugin+cli/n/a. Detection-based silent skip for non-distribution repos (always-on). `IDD_DISTRIBUTION_SYNC_PROMPT=false` env var bypasses prompt for distribution-detected repos (1-line audit). D3 mixed-type v1: explicit ordering binary-deploy first \u2192 plugin-update second (idempotent regardless of plugin-update Phase 1.5 cascade availability per #66 audit). Step 0.5 Bootstrap Task List adds `distribution_sync_chain_detection` entry. Step 4 closing comment ID capture hardened (stdout-only + sed -n + explicit empty-check). NEW reference doc references/distribution-detection.md. Complements `common-release-flow.md` (release-tier trigger) at close-tier window. Verify discipline survived 3 rounds + degraded engine (Anthropic API limit) \u2014 Codex CLI carried + Round 1 had regression + devil's advocate (3 sources convergent), 5 P1 \u2192 0 P1 at Round 3. 2 follow-ups filed: #66 D3 audit (mid-plan tangential), #68 monorepo host disambiguation (round-3 advisory). Backward compat: non-distribution repos see zero behavior change. v2.56.0: idd-issue multi-finding source mode (PsychQuant/issue-driven-development#48, add-multi-finding-source-mode-to-idd-issue Spectra change). Auto-trigger on Step 1 source extracting \u22652 paragraph-level findings from docx/pdf/Telegram/Apple Mail/Apple Notes/pasted-text/md adapters. 4-stage pipeline: Stage 1 Extract verbatim quotes + AI summary; Stage 2 Per-finding picker with AI surface top-3 candidates via gh issue list --search keyword overlap (title\u00d72 + body[:300]\u00d71) + 4-option AskUserQuestion + intent disambiguation [comment/edit body/update status/skip] for picked existing #N + [Other] expands to [New issue/Skip/Merge/Pick free-text]; Stage 3 Batch preview single AskUserQuestion [Execute all/Edit row N/Cancel]; Stage 4 Dispatch with warn-continue (failures log to jsonl actions[i].error + retry_hint, no abort, no rollback). Audit trail dual-track: per-action body footer `> Surfaced via /idd-issue multi-finding mode from ` + structured JSONL at `.claude/.idd/issue-runs/.jsonl` committed to git for cross-machine continuity. Two-way merge via inline sub-prompt (partner picker + combined target picker), JSONL records merged_from / merged_into bidirectionally; three-way+ refused. NEW override flags `--multi-finding` (force mode) / `--no-multi-finding` (force fall-through); mutually exclusive with each other and with `--bundle-mode` (different mental models: bundle = explicit ordered/unordered creation; multi-finding = source-driven mixed routing). NEW capability `idd-issue-multi-finding-source` parallel to existing `idd-issue-bundle` (both extend idd-issue with non-overlapping modes). Cross-reference updates to idd-comment/idd-edit/idd-update SKILL.md adding \"When to use idd-issue multi-finding mode instead\" sections redirecting batch source workflows. Backward compat: single-issue invocations unchanged byte-equivalent; --bundle-mode invocations unchanged; auto-trigger threshold is \u22652 detected findings else fall through. 5 architectural decisions D1-D5 from spectra-discuss session 2026-05-10 + 2 derived D6 trigger detection / D7 mutual exclusion in design.md. v2.55.0: NEW /idd-all-chain skill \u2014 chain-solve mode (PsychQuant/issue-driven-development#44, add-idd-all-chain-skill Spectra change). Drives root issue + auto-emergent spawned issues (sub-skill sister sweeps / verify follow-ups / mid-plan tangentials / sister concerns) through ONE cluster branch + ONE review PR. NEW skill /idd-all-chain #N: thin shell over /idd-all, internally recursive-invokes /idd-all #M --in-chain. Phase 0 creates cluster branch idd/chain-- from default branch + initializes spawn manifest at .claude/.idd/state/chain-spawned-issues.json (schema_version=1, atomic temp-file rename writes). Phase 2 main loop pops queue, invokes sub-/idd-all, reads manifest delta, enqueues chain-eligible spawns (rule: same_file_as_root OR same_skill_as_root OR spawn_kind='sister-bug'). Phase 3 opens cluster PR (title prefix 'chain:', collapsed
per issue, Refs all chained, Pending review checklist forbidding Closes/Fixes/Resolves trailers per IDD discipline). Phase 4 STOPs at verified \u2014 no auto-close, no auto-merge (per-issue /idd-close required). NEW --in-chain flag on /idd-all: single source for chain context, derives 4th mode tuple (direct-commit, unattended). Sub-/idd-all skips Phase 0.5 PR-mode branch creation + skips Phase 5.5 PR open + sub-skills receive UNATTENDED MODE directive. --in-chain mutex with --pr/--no-pr. NEW spawn manifest cross-skill contract: 4 sub-skills (idd-implement Step 5.7 sister bug sweep / idd-verify Phase 4 follow-up findings / idd-plan Step 2.5 tangentials / idd-diagnose Step 3.6 sister concerns) all conformantly write entries with classify spawn_kind + same_file_as_root + same_skill_as_root flags. Helper script scripts/manifest-append.sh implements atomic write + schema_version mismatch abort. Hard caps: chain_max_depth=2, chain_max_issues=5 (incl. root) \u2014 over-cap spawns still file as follow-up issues but not enqueued. Failure mode: any chained verify FAIL halts queue + preserves partial commits on cluster branch (no rebase/revert) + abort report cites 4 recovery paths. MODIFIED capability idd-orchestrator-modes: 4th mode tuple (direct-commit, unattended) added for chain context; existing 3 tuples behavior unchanged. NEW reference docs: references/spawn-manifest.md (schema canonical contract) + references/chain-flow.md (chain shell algorithm canonical contract incl. eligibility rule + caps + failure mode + PR body schema). Backward compat: /idd-all #N without --in-chain flag is byte-equivalent to v2.53.0 baseline. v2.52.0: idd-issue ordered/unordered bundle flags (PsychQuant/issue-driven-development#21). NEW `--parent ` flag PATCHes parent issue's body task list with new child entry, idempotent via `#N` reference scan + fallback `## Children` anchor when no list exists. NEW `--blocked-by [,...]` flag applies three-layer fallback chain: Layer 1 GraphQL `addBlockedByDependency` mutation attempt (graceful failure \u2192 warning + continue, no abort) + Layer 2 unconditional body blockquote `> Blocked by #M` (always readable in any markdown viewer) + Layer 3 parent task list annotation `(blocked by #M)` when `--parent` co-used. NEW `--bundle-mode ` flag orchestrates bundle creation in single invocation: builds 1 epic parent + N children with auto-applied `--parent `, ordered mode adds strict `child[i] blocked by child[i-1]` chain, unordered keeps task list only. Pre-flight gates: cross-repo refuse (parent in different repo than resolved target \u2192 abort + redirect to `groups` mechanism), bundle-mode and group-mode mutual exclusion (different mental models, refuse if both set). Step 3.B inserted between 3.A (single repo) and 3.G (group cross-link), reusing 3.A flow as primitive. Orthogonal with Step 4.5 milestone (bundle children get milestone assignment), Step 4.7 sister sweep (parent epic still subject to sweep, sibling issues NOT added to bundle task list). NEW canonical reference doc references/bundle-flags.md (flag spec + edit algorithm + fallback chain + partial failure + idempotency contract). NEW `## Ordered Bundle Pattern` section after Step 5 in idd-issue SKILL.md (3-mode comparison table + 3 usage scenarios + design rationale for not creating separate /idd-bundle skill). Step 0 Bootstrap Task List adds `resolve_parent_link`, `apply_blocked_by`, `orchestrate_bundle_mode` TaskCreate entries. NEW capability `idd-issue-bundle` in openspec/specs/. No breaking changes \u2014 all flags additive, omitted invocation behavior unchanged. Spectra change `add-bundle-flags-to-idd-issue` in this repo's openspec/changes/. v2.51.0: idd-list shows open PR info per issue + cluster detection (PsychQuant/issue-driven-development#13). NEW Step 2.5 batch fetches all open PRs once via 'gh pr list --state open --limit 100'; NEW Step 3.5 client-side regex-scans PR bodies for '#N' refs and builds reverse issue\u2192PR index plus cluster map (PRs ref'ing 2+ issues). Step 4 Format Output extended: each issue with a PR ref gets a sub-line '\u2514\u2500 PR #N (status, mergeable)'; cluster leaders (lowest issue number in refs) show 'cluster: #X #Y #Z' listing all members; cluster members show '\u2192 see PR #N (cluster member)' redirect. Direct-commit issues (no PR refs) display unchanged from v2.50 \u2014 fully backward compatible. Footer adds second line summarizing 'N issues bundled in M cluster(s); P solo PR(s); Q direct-commit'. Step 5 Suggest Next extended to phase \u00d7 PR state matrix (10+ rows): implemented + draft \u2192 'gh pr ready N \u2192 /idd-verify --pr N'; implemented + ready MERGEABLE \u2192 '/idd-verify --pr N'; verified + ready MERGEABLE \u2192 'gh pr review N \u2192 gh pr merge N \u2192 /idd-close #N'; verified + merged catch-up \u2192 '/idd-close #N'; CONFLICTING \u2192 'gh pr checkout N \u2192 resolve'; cluster member \u2192 'see leader's next action'. Sister concerns filed as future P3 follow-ups: #14 (markdown-aware PR body parser to ignore '#N' inside fenced code blocks; v1 accepts false positive) + #15 (cluster_leader config 'lowest|primary' instead of hardcoded lowest). v2.50.0: Layer V Vagueness Pre-check (PsychQuant/issue-driven-development#12). NEW Step 3.4 in idd-diagnose between Layer 1 disqualifier and Layer 2 Spectra evaluation: AI scores V1 (vague WHAT) + V4 (vague ACCEPTANCE) on Likert 6-point scale (no neutral midpoint), trigger threshold per-axis \u2265 4. Triggered cases fire Hybrid 3-option AskUserQuestion (clarify now / proceed anyway / escalate to Plan) with default option score-driven (V=4 \u2192 proceed, V=5 \u2192 clarify, V=6 \u2192 escalate). 'clarify now' appends Q/A pairs to issue body via gh issue edit then re-runs Layer V; 'proceed anyway' continues to Layer 2/3/P with audit trail recording trigger fact; 'escalate to Plan' force-sets verdict = 'Plan via Layer V' and skips Layer 2/3/P. Layer evaluation order: Layer 1 \u2192 V \u2192 2+3 \u2192 P \u2192 Simple. Routing parsers in idd-implement Step 2.5 + idd-all Phase 3 strip ' via X' suffix to extract canonical tier \u2014 bare 'Plan' / 'Simple' / 'Spectra' verdicts unchanged (backward compat). NEW project rule .claude/rules/attribute-assessment.md codifies meta-principle 'attribute scoring SHALL use Likert scale, not keyword matching' \u2014 applies session-wide via root CLAUDE.md @import, scope beyond Layer V (any future attribute scoring need). MANIFESTO 5-axis bug-fix model expanded to 6-axis adding 'Alignment quality' (TDD \u274c / SDD \u274c / IDD \u2705), evidence = Layer V. idd-all unattended mode auto-applies 'proceed anyway' + audit trail '[Layer V: V1=N V4=M, clarify-default skipped under unattended mode, defaulting to proceed]' (same pattern as Plan tier under unattended). Backward compat: pre-v2.50 diagnoses NOT retroactively re-evaluated; existing Simple / Plan / Spectra / SDD-warranted verdicts remain valid. No --ignore-vagueness flag (option B 'proceed anyway' covers that need). Spectra change add-vagueness-layer-routing in this repo's openspec/changes/. Step 0 Bootstrap Task List adds 'vagueness_precheck' TaskCreate. v2.49.0: references/ic-r011-checkpoint.md v1.1.0 \u2014 Third-Party Skill Alignment section for /spectra-discuss + /spectra-propose (kiki830621/ai_martech_global_scripts#530, sub-issue E of #523 systematic plugin alignment, last sub-issue closing the parent epic). spectra-* skills are published by third-party kaochenlong/spectra-app \u2014 direct SKILL.md modification not in this plugin's commit cycle. Documentation-side alignment: agents/users invoking /spectra-discuss + /spectra-propose with IC_R011 in mind apply the canonical 3-option AskUserQuestion + audit trail manually at deliberation-moment equivalents (discussion convergence / proposal drafting). Per canonical eligibility criteria \u00a76, only the 2 deliberation-moment spectra-* skills (discuss / propose) need alignment; the other 6 (apply / archive / ask / ingest / commit / debug) are mechanical execution and N/A. If spectra-app upstream adopts native IC_R011 checkpoint, this section becomes redundant + can be removed. Strength: SHALL \u2014 discussion / proposal drafting are deliberation moments per canonical eligibility criteria. v2.48.0: idd-issue Step 4.7 \u2014 Linked-Context Sister Sweep (kiki830621/ai_martech_global_scripts#529, sub-issue D of #523 systematic plugin alignment). NEW advisory step between Step 4.5 (auto-milestone) and Step 5 (\u5831\u544a), scanning issue body draft + linked attachments + recent session conversation for sibling-concern markers (also / additionally / related / \u53e6\u5916 / \u9806\u4fbf / BTW). If hits, AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file as sibling issues now' / 'file selected' files via 'gh issue create' as parallel issues (NOT cross-linked into the just-created issue body, since user's primary concern stays focused), each with confidence:confirmed + priority:P3 + source link 'surfaced during /idd-issue #NEW linked-context sister sweep (Step 4.7)'. PATCHes the just-created issue body to add '### Linked-Context Siblings Filed (v2.48.0+ #529)' audit trail per canonical heading conventions. Strength: SHOULD (advisory, non-blocking) per canonical eligibility criteria \u00a76 \u2014 issue creation is light-touch (user is already in filing-active mode, double-prompt risks friction). Empty list = silent no-op default. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Step 0 Bootstrap Task List adds 'linked_context_sister_sweep' TaskCreate. v2.47.0: idd-diagnose Step 3.6 \u2014 Sister Concern Surfacing (kiki830621/ai_martech_global_scripts#528, sub-issue C of #523 systematic plugin alignment). NEW mandatory step between Step 3.5 (Complexity Assessment) and Step 3.7 (Agent Routing), surfacing sister-concern markers in just-posted Diagnosis content (\u4e5f\u6709 / sister / \u540c\u6a23\u7684 / \u53e6\u5916 / likewise affects) + scout session log. AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file all/selected' files via 'gh issue create' with confidence:confirmed + priority:P3 + source link, then PATCHes Diagnosis comment with '### Sister Concerns Filed (mid-diagnose, v2.47.0+ #528)' audit trail per canonical heading conventions. Strength: SHALL (mandatory step) per canonical eligibility criteria \u2014 diagnosis is a deliberation moment where sister concerns naturally surface during Strategy authoring. Empty list legitimate. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Step 0 Bootstrap Task List adds 'sister_concern_surfacing' TaskCreate. v2.46.0: idd-all HITL mode (PsychQuant/issue-driven-development#1). Phase 0.5 mode resolution from existing pr_policy + new --pr/--no-pr flags into (path, interaction) tuple \u2014 PR + unattended (v2.40.0 regression \u2014 /loop friendly) or direct-commit + attended (HITL \u2014 solo/personal repos where PR is ceremony, user is in keyboard, sub-skill AskUserQuestion / EnterPlanMode / Park-Apply prompts fire natively). Two axes from one source (no duplicate config surface). v2.45.0: idd-close Step 3.5 \u2014 Closing Summary Follow-up Keyword Scan (kiki830621/ai_martech_global_scripts#527, sub-issue B of #523 systematic plugin alignment). NEW step between Step 3 (review with user) and Step 4 (gh issue close), scanning drafted closing summary for trigger phrases (follow-up / deferred / future / TODO / later / \u4e4b\u5f8c / \u672a\u4f86 / \u9806\u4fbf / \u6211\u4e4b\u524d\u89c0\u5bdf\u5230 / \u4e4b\u5f8c\u518d / \u6539\u5929). Each match is checked against existing #NNN cross-links via 'gh issue view' \u2014 orphan mentions (no link or stale link) trigger AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file all/selected' files via 'gh issue create' with confidence:confirmed + priority:P3, then PATCHes closing summary inline (mention \u2192 '...(see #NEW)') and adds '### Closing Follow-ups Filed (v2.45.0+ #527)' audit trail. Strength: SHOULD (advisory, non-blocking) per canonical eligibility criteria \u2014 closure is mostly mechanical action; surfacing orphan-mention pattern at decision moment without forcing filing. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Disambiguation note added: this Step 3.5 is the IC_R011 checkpoint; Step 0 supersession (#515 v2.41.0) is gate logic \u2014 orthogonal concerns. Step 0.5 Bootstrap Task List adds 'closing_followup_keyword_scan' TaskCreate. v2.44.0: idd-implement Step 5.7 \u2014 Sister Bug Sweep (kiki830621/ai_martech_global_scripts#526, sub-issue A of #523 systematic plugin alignment). New mandatory step between Step 5.5 (Open PR if PR path) and chain to /idd-verify, surfacing sister bugs discovered during TDD reproduction (Step 3) \u2014 adjacent same-root-cause sibling files like the proven 2026-05-03 #510 \u2192 #518 \u2192 #520 cluster (gen_*.R / fix_wiser_poisson_tables.R / _build.R) where each manual reminder was needed despite same pattern. Cites canonical references/ic-r011-checkpoint.md (#525) for 3-option AskUserQuestion (file all / file selected / skip) + heuristic triggers + audit trail format + AI_LOW_BAR_ISSUE_FILING=false rollback hatch. PATCHes Implementation Complete comment to add `### Sister Bugs Filed (mid-impl, v2.44.0+ #526)` audit trail per canonical heading conventions table. Strength: SHALL (mandatory step) but empty list legitimate. Step 0 Bootstrap Task List adds `sister_bug_sweep` TaskCreate entry. v2.43.0: NEW canonical reference doc references/ic-r011-checkpoint.md (kiki830621/ai_martech_global_scripts#525, sub-issue F of #523 systematic plugin alignment). Standardizes the 3-option AskUserQuestion pattern (file all / file selected / skip), heuristic triggers (verifiable behavior gap / sister bug from reproduction / observed friction / deferred work / out-of-scope user mentions / drift / TODO encounters), default-off exemptions (pure exploration / existing issue / hallucinated / CONSTRAINT / mechanical execution stages), audit trail per-skill heading conventions, rollback escape hatch (env var + repo CLAUDE.md flag), and eligibility criteria (which skills SHALL vs SHOULD vs N/A). Cited from idd-plan Step 2.5 (#524) + idd-close Step 0 supersession (#515) \u2014 both back-link the canonical doc. Sister sub-issues #526-#530 (idd-implement / idd-close closing summary scan / idd-diagnose / idd-issue / spectra-discuss + spectra-propose) will all cite this canonical doc when their Plan tier ships;cross-skill consistency mechanically anchored. v2.42.0: idd-plan Step 2.5 \u2014 mid-plan tangential observations sweep (kiki830621/ai_martech_global_scripts#524 fix). Plan-tier deliberation surfaces tangential discoveries (Phase 1 Explore agents pass-by sister bugs, Phase 2 grep-discovered drift, Phase 3 user-mentioned sub-concerns) that previously fell into the gap between In-scope and Out-of-scope categorization, vanishing into conversation. New mandatory step between Step 2 (Draft Plan) and Step 3 (Confirm post): agent reviews session log, surfaces candidates with IC_R011 default-on heuristic (verifiable behavior gap / sister bug / out-of-scope user-mentioned), AskUserQuestion three-option (file all / file selected / skip), files via 'gh issue create' with confidence:confirmed + priority:P3 + source link to plan issue, then PATCHes plan body to add '### Tangential Observations' audit trail. Empty list = no-op (legitimate). AI_LOW_BAR_ISSUE_FILING=false env var skips per IC_R011 rollback hatch. Codifies IC_R011 spirit at the mid-plan window \u2014 finer gap than #523 broader systematic alignment. v2.41.0: idd-close Step 0 supersession of pre-implementation Strategy / Implementation Plan checkboxes (kiki830621/ai_martech_global_scripts#515 fix). When `## Implementation Complete > ### Checklist` exists and all its items are `- [x]`, that subsection is treated as the canonical state of truth \u2014 `Strategy` / `Implementation Plan` `- [ ]` items are auto-superseded (skipped from gate). Resolves the recurring friction where work was complete but `idd-implement` Step 5 only synced its own Implementation Complete comment (never PATCHed Strategy / Plan comments), leaving 8+ pre-impl `- [ ]` items blocking gate and forcing manual `gh api PATCH` workaround on every full-lifecycle close (#455 / #510 close, 2026-05-03). Defensive properties preserved: incomplete Implementation Complete (any `- [ ]` remaining) falls back to legacy full spec scan; legacy issues without Implementation Complete unchanged. Strategy A from #515 diagnosis (chosen over B sync-at-write and C narrow-gate). v2.40.0: --cwd flag propagated to all sub-skills (idd-diagnose / idd-implement / idd-verify) so cross-repo orchestration via idd-all actually works end-to-end. v2.39.0 added --cwd to idd-all only \u2014 but sub-skills still inherited Claude Code session cwd, so idd-implement would commit to the wrong repo. NEW: shared `references/cross-repo-cwd.md` documents the substitution rule (`git X` \u2192 `git -C $CWD X`, `gh issue/pr/repo X` \u2192 `gh ... X -R $GITHUB_REPO`) once; each sub-skill cites it at the top of Execution. NEW: idd-all Phase 1/2/3a/4 forward `--cwd $CWD` to sub-skill args; Phase 1 (idd-issue) and follow-up-issue creation use `--target $GITHUB_REPO` instead (read-only, no local git needed). Backward compat: omitting --cwd reads session pwd (existing behavior). v2.39.0: idd-all --cwd flag + cross-repo invocation. NEW Step 0.2 Resolve Working Tree resolves target repo from --cwd /path/to/clone (per-invocation override) instead of hardcoded session cwd. All git ops use 'git -C $CWD'; all gh ops use 'gh -R $GITHUB_REPO' (repo derived from origin remote). Solves 'Skill tool inherits Claude Code session-level cwd, can't follow mid-session cd' friction when running idd-all on a repo other than the one your session started in (e.g. thesis work in repo A, want pipeline on dependency repo B). Phase 0.2/0.3 abort messages now include explicit 'pass --cwd /path/to/clone' alternative. Backward compat: omitting --cwd uses session cwd (existing behavior). v2.38.0: idd-route integration \u2014 data-driven agent routing recommendation from observed track record. NEW idd-diagnose Step 3.7: if ~/bin/idd-route is available, calls it with current issue's complexity + estimated scope LOC + extracted signals to get an agent recommendation (codex-gpt-5.5-xhigh / claude-opus-4.7 / sonnet-4.6 / haiku-4.5), injects 'Recommended Agent' section into diagnosis comment. NEW idd-verify Step 5d: records each verify outcome (issue, agent, complexity, scope, round trips, blocking findings, follow-ups) to /.claude/.idd/routing-stats.jsonl + global mirror. NEW idd-close Step 4.5: appends final outcome (merged/abandoned) \u2014 append-only so original in_review record stays for audit. All three are gracefully no-op when idd-route binary missing (command -v check). Companion plugin idd-route ships in same marketplace; binary source at PsychQuant/idd-route-swift. NEW references/agent-routing.md is canonical contract for the IDD \u21c4 idd-route boundary. Built on top of v2.37.0's external-agent / PR mode foundation \u2014 the routing recommendation closes the loop: idd-diagnose suggests agent \u2192 user delegates \u2192 idd-verify records outcome \u2192 next idd-diagnose recommendation gets smarter. Plus marketplace migration: this is the first issue-driven-dev release in the new PsychQuant/issue-driven-development marketplace (formerly lived in psychquant-claude-plugins; full git history preserved via filter-repo). v2.37.0: External-agent / PR mode for idd-verify + use-case routing reference. NEW idd-verify --pr input mode for verifying PRs opened by external agents (Codex via codex exec, Copilot Workspace, remote claw on PsychQuantClaw) \u2014 gh pr diff + gh pr checkout so reviewer agents see file context, auto-restore original branch after verify. Plus --commits N / --since / --branch flags for other input sources. Auto-detect mode (no flag): counts unpushed Refs #N commits since origin/ first; if 0, queries open PRs ref'ing #N and AskUserQuestion to pick between local diff vs PR \u2014 catches the common 'forgot --commits N' case without silently switching modes. Issue\u2194PR correspondence is a hard iron rule: PR mode aborts before invoking 6-AI ensemble if PR body has zero Refs #N (untrackable change violates IDD discipline) or if user-passed issue isn't in PR's Refs set (correspondence broken); discovers superset triggers AskUserQuestion to confirm scope. PR mode flips master comment location: full verify report posts to PR (external agent owners work in PR view, never see issue comments), each ref'd issue gets a 1-line pointer comment back with PASS/FAIL + master comment URL. Capture-master-URL-then-write-pointer SOP enforced (prevents the recurring bug class where pointer URLs accidentally reference earlier diagnosis/implementation comments). NEW references/external-agent-delegation.md is canonical contract: 4-phase delegation impact matrix (diagnose stays, implement may delegate, verify+close return to IDD), hands-off principle (no babysitting external agents; strict verify, opt-in fix takeover), 3 input modes + auto-detect algorithm, issue\u2194PR gate, PR-as-master cross-post, working tree handling, deferred items (--takeover, idd-handoff, force-push detection). NEW references/usecase-routing.md closes discoverability gap: 24-row table mapping common scenarios \u2192 exact skill chain + flags + contract doc (single / batch / cluster-PR / external-agent PR/commits/branch/auto / Plan tier / Spectra-warranted / bundle close / Spectra-bridge / multi-repo monorepo) plus top-of-doc decision tree for users unsure which entry point to start from. Linked from CLAUDE.md (Claude-facing) and README.md (human-facing). Backward compat: single-issue invocation idd-verify #42 without flags still works as v2.36 in common case (no Refs commits, no open PRs \u2192 falls back HEAD~1); cluster-PR mode #34 #36 #38 unchanged; no flag deprecations. v2.36.0: 3-tier Complexity routing (Simple / Plan / Spectra) + new idd-plan skill. SDD-warranted renamed to Spectra (backward-compat alias preserved). Plan tier inserts EnterPlanMode approval gate between diagnosis and TDD execution \u2014 covers 'think before leap, no spec contract needed' (most common case where Simple was too thin and Spectra was overkill). Issue-driven development methodology: issue \u2192 diagnose \u2192 (idd-plan if Plan tier) \u2192 implement \u2192 verify \u2192 close.", - "version": "2.74.0", + "description": "v2.63.0: #96-backlog Simple cluster — 6 docs/reference follow-ups shipped via cluster-PR #101 (PsychQuant/issue-driven-development #60 #62 #63 #78 #90 #91). These are the Simple-tier subset of an 18-issue `/idd-diagnose` batch (6 Simple / 12 Plan) run over the #96-backlog cleanup; the 12 Plan-tier issues are driven separately. #60: NEW `## Cluster-PR eligibility (when to bundle vs split)` section in `references/batch-and-cluster.md` — a criteria table (same-file ✓ / same-skill ✓ / same-root-issue chain-only ✓ / same-label ✗ / same-review-timing ✗) plus a borderline >50-line review-surface heuristic, with a cross-reference from `idd-implement/SKILL.md`'s Cluster-PR mode paragraph; motivated by PR #58 having bundled unrelated issues #49+#53 under only a shared parent label. #62: `references/usecase-routing.md` decision-tree section gains a bulk-solve note pointing to row 27 — there is no built-in zero-arg backlog bulk-solve; use per-issue `/idd-all` or `/idd-all-chain`. #63: `usecase-routing.md` row 27 `#44 chain-solve` plain text upgraded to an explicit `[#44 chain-solve](url)` link for raw-markdown-viewer cross-link consistency with the already-linked `#37`/`#46`. #78: `idd-issue/SKILL.md` multi-finding override-flags section gains a ⚠ CI-caller note — automated / CI / `/loop` callers expecting the pre-v2.55.0 always-single-issue behavior of `idd-issue source.docx` MUST pass `--no-multi-finding` explicitly (v2.55.0 changed the default to auto-enter multi-finding mode on ≥2 findings); a retroactive behavioral-change notice was added to `CHANGELOG.md` (placed under `[Unreleased]` then rolled into this entry — no standalone `## [2.55.0]` entry exists). #90: NEW `openspec/CONVENTIONS.md` documenting the `**GitHub-side tracker**: #NN` canonical Spectra-proposal → GitHub-issue linking convention (collapses `idd-close`'s 3-fallback detection chain to a one-line lookup). R1 placed this at `openspec/LANGUAGE.md`; the 6-AI cluster verify's Devil's Advocate caught (coordinator-confirmed) that `openspec/LANGUAGE.md` is a reserved filename — `spectra-discuss` reads it as the project's canonical vocabulary file with a vocabulary-drift capture mechanism — so R2 relocated the convention to `openspec/CONVENTIONS.md` (purpose-built, verified not reserved by any skill). #91: `.claude/skills/spectra-archive/SKILL.md` gains a `Step 0: Bootstrap Stage Task List` section before its `**Steps**` block, with 8 `TaskCreate` entries mapping 1:1 to the skill's existing Steps 1-8, matching the idd-* Bootstrap discipline; the parallel tool-managed command-file surface (`.claude/commands/spectra/archive.md`, wrapped in `` regenerated markers) was intentionally NOT hand-edited — its Step-0 gap is folded into #93's 4-copy divergence scope. Cluster-PR #101 verified by 6-AI cluster verify: R1 CONDITIONAL PASS — 4/5 Claude reviewers PASS, Devil's Advocate surfaced 2 HIGH blocking findings (#90 reserved-name collision, #91 invocation-surface scope), both coordinator-confirmed via file-existence checks; R2 PASS after #90 relocation + #91 re-scoping. Codex (6th reviewer) hung and never returned — recorded as an explicit process gap rather than hidden in the aggregate. Squash-merged to main as `0eb419c`. v2.62.0: cluster mode override — pr-flow.md canonical documentation + idd-implement Step 0.5 bash implementation (PsychQuant/issue-driven-development#96). Resolves a 3-file contradiction in IDD's PR-vs-direct-commit path resolution: `pr-flow.md` canonical resolution-algorithm table had no cluster carve-out while `idd-implement/SKILL.md:49` + `batch-and-cluster.md:133` independently asserted cluster-PR mode forces PR (\"不接受 --no-pr\"); the three files contradicted and the behavior on `--no-pr` + cluster collision (abort / warn / silent ignore) was never specified. Surfaced during PR #94's cluster work. Option A (user-selected in `/idd-all` session from 3 diagnosis candidates A/B/C): maintain forced PR for cluster mode, but make it explicit + consistent. NEW `pr-flow.md` `### Cluster mode override` subsection — cluster mode (any IDD skill invoked with ≥2 `#N` args) is a multi-issue mode where all cluster issues share one feature branch + one PR; path resolution is `idd-implement`'s job (the only skill that resolves PR-vs-direct-commit) and for it cluster mode is a precondition that pre-empts the resolution-algorithm table and forces PR path; `idd-verify` / `idd-close` are cluster-aware but operate on the cluster's already-existing PR — they consume the path decision, they don't make it. Explicit override notice mirrors fork detection (`→ cluster mode (N issues) → PR path enforced (overriding --no-pr / pr_policy=never)`); fork+cluster co-occurrence prints both notices (the two pre-emptions independently force PR path, no precedence question). `idd-implement` Step 0.5 bash wired with cluster detection: parse `#N` token count in `$@` → derive `CLUSTER_MODE` → pre-empt block before the existing flag/fork/policy resolution → `OVERRIDE_SRC` accumulation composes the actual triggering condition(s) into the notice; Step 0.5 local algorithm summary gains a row 0 noting cluster pre-emption. `batch-and-cluster.md:133` rule statement demoted to a pointer at the new canonical section with the rationale phrase (\"stacked half-isolated changes on default branch\") inlined verbatim in `pr-flow.md` for downstream-grep stability. Verified 6-AI × 2 rounds: R1 (doc-only `cbe6f5d`) CONDITIONAL PASS — 5/6 reviewers converged on a HIGH doc/code gap (spec described a Phase 0.5 override notice the bash had no capability to emit); R2 (`5351116`) extended PR scope per user opt-in to add the ~24-line bash impl (8-case behavioral dry-run + `bash -n` clean), 6/6 PASS with Devil's Advocate explicitly recommending MERGE; R3 (`04c51cb`) closed DA's one new actionable finding (the subsection originally over-claimed cluster mode \"pre-empts the Resolution algorithm\" for verify/close — those skills never run path resolution). Step 0.8 auto-close-trap scan clean. Backward compat: single-issue invocation (`idd-implement #19`) byte-equivalent — the cluster carve-out only fires on ≥2 `#N`. Follow-up #100 tracks 2 non-blocking deferred items (Option A still forces PR on a non-default feature branch where cluster direct-commit is a legitimate workflow — Option B revisit candidate; cluster-detection glob `\\#[0-9]*` over-counts malformed/duplicate tokens vs the stricter documented `^#\\d+$`). PR #99 squashed as `b7f72ff`. v2.61.0: idd-verify Step 0.8 — squash-commit-body auto-close trap fix (PsychQuant/issue-driven-development#97). Step 0.8 (added in v2.60.1 by PR #94) extended from a 1-source scan (PR body via `closingIssuesReferences`) to a 2-source scan covering: (1) PR body authoritative parse via `closingIssuesReferences` (kept), and (2) per-commit `messageHeadline` + `messageBody` via `gh pr view --json commits` + trap regex `(^|[^-/[:alnum:]])(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]+#[0-9]+` (case-insensitive via `tolower($0)`). R1/R2/R3 lessons baked into the regex from PR #94's verify history: `(^|[^-/[:alnum:]])` prefix excludes `/idd-close #N` IDD skill invocations and hyphenated tokens; `:?` covers the colon form; `[[:space:]]+` mirrors GitHub's space requirement. Same-repo `#N` form only; cross-repo `owner/repo#N` deferred per Plan D7. The fix addresses the ironic v2.60.1 dogfood failure where PR #94 itself was squash-merged and GitHub auto-closed `#87` two seconds later because one of PR #94's commits had a body that *quoted* the trap pattern as a verify-finding example. R2 (in-PR fix after R1 verify) extended the jq filter from body-only to headline+body after Devil's Advocate + Codex independently confirmed the missed-subject channel with empirical evidence: commit `8ac8206` headline `resolves #N` form auto-closed `#70` (2026-05-11); commit `a82867d` headline `fix #N` form auto-closed `#26` (2026-05-07). R1's body-only filter would have missed both. PR #98 6-AI verified in 2 rounds: R1 CONDITIONAL PASS (5/6, 1 HIGH blocking DA-H1 + Codex Finding 2); R2 6/6 PASS with Devil's Advocate explicitly recommending MERGE. Also adds `### 引用 trap pattern 作反例的寫作紀律` subsection under `## Commit Conventions` in `plugins/issue-driven-dev/CLAUDE.md` codifying the writing discipline: code fence is **visual only** (parser is context-blind), literal letter N (capital, no digit) is the **actual suppression** mechanism mirroring the safe pattern in `references/pr-flow.md:127`, cite-via-link is strongest. Single/double quotes do NOT suppress the parser. This is the write-time root fix; Source 2 is defence-in-depth at verify time. Step 0 bootstrap TaskCreate entry renamed `scan_pr_body_trailers` → `scan_pr_body_and_commits_trailers` reflecting Source 2. Backward compat: Source 2 is additive — clean PRs see no change in output; PRs with trap pattern in commits (subject or body) now get a warn block with fix-options (rebase + amend with letter N, override squash message via `gh pr merge --body`, or post-hoc `/idd-close`). Dogfood: PR #98's own squash commit (`e0d61e7`) is clean under the new 2-source Step 0.8 — the discipline added to CLAUDE.md held for both commit message body AND headline. Master verify reports at PR #98 #issuecomment-4483847549 (R2) and #issuecomment-4483788120 (R1). v2.60.1: cluster fix — PR-body auto-close trap (PsychQuant/issue-driven-development#87 + #74). All 4 IDD skill PR-body templates (idd-implement Step 5.5, idd-all Phase 5, idd-all-chain Step 5.5, pr-flow.md canonical) reworded to drop literal `Closes #${N}` from anti-trailer warnings — heredoc `${N}` substitution previously turned the cautionary warning string into a real `Closes #` that GitHub auto-close parser matched context-blind (ignoring negation, markdown, quotes), bypassing /idd-close's checklist gate + closing summary. New unified wording: `**Do NOT add a GitHub close trailer** (Closes/Fixes/Resolves) — IDD discipline requires manual /idd-close after merge to enforce checklist gate + closing summary.` Keywords named but never followed by `#` so GitHub's regex cannot match. NEW idd-verify Step 0.8 preventive gate (PR mode, warn-only): queries `gh pr view --json closingIssuesReferences` — GitHub's authoritative parse of which issues the PR auto-closes on merge — covering all trailer forms (Closes #N / Closes: #N colon form / cross-repo / issue-URL) without self-written regex. Warn-only since a PR body may legitimately quote the keywords in prose; gate value is making the risk visible at verify time before merge. Eventual-consistency caveat disclosed (closingIssuesReferences is settled state, settles well within typical verify-after-implement gap). PR #94 6-AI verified in 3 rounds: R1 (initial regex form) caught colon-form gap → R2 redesigned to closingIssuesReferences eliminating self-parser fragility → R3 cleanup (deleted orphan regex doc, surfaced gh failures with explicit skip note replacing silent fail-open, scoped overclaim, query `.url` not `.number`). Confirmed prior incidents resolved: #559, che-apple-mail-mcp#99, #73, #56. Two follow-up issues filed: #96 (cluster-PR mode silently forces PR path while direct-commit honours `--no-pr` — doc contradiction in pr-flow.md canonical algorithm table, design decision pending), #97 (new failure mode discovered at squash-merge of PR #94 itself — squash commit body inherited commit-message body quoting `Closes: #87` as a verify-finding reference, triggered auto-close of #87 2s after merge; Step 0.8 only scans PR body and doesn't predict squash commit message). Master verify report at PR #94 #issuecomment-4482808720. v2.60.0: idd-all-chain multi-root + DFS/BFS traversal + per-root halt + spawn-manifest schema v2 hard-break (PsychQuant/issue-driven-development#46, multi-root-traversal-idd-all-chain Spectra change). NEW multi-root invocation `/idd-all-chain #A #B #C [--bfs] [--cwd ]` accepts ≥1 root issue (N=1 byte-equivalent backward compat with v2.55.0+). NEW `--bfs` flag selects BFS traversal mode (push-back queue semantics for fairness across roots); default DFS pushes spawns to queue front (rich subtree first per root). NEW spawn manifest schema v2 hard-break: top-level `root_issue: int` → `root_issues: [int]`, top-level adds `traversal: \"dfs\"|\"bfs\"`, every spawn entry adds `root_id: int` (must match one of root_issues elements). Helper `scripts/manifest-append.sh` bumps `EXPECTED_SCHEMA_VERSION` 1→2, accepts 9th positional arg `root_id`, validates root_id ∈ root_issues array, fail-fast on v1 manifest detection. Cap redesign for multi-root accommodation: per-root `chain_max_depth` 2→3 (each root subtree counts depth from 0 independently), global `chain_max_issues` 5→10 (union across all root subtrees, applies independently of depth cap). Verify FAIL = per-root halt (D4 Option C): failing issue's `root_id` added to FAIL_ROOTS, all same-root pending issues purged from QUEUE, other root subtrees continue processing, commits preserved; Phase 4 emits per-root PASS/FAIL/SKIPPED summary block. Branch naming dispatches on N: N=1 keeps backward-compat `idd/chain--`, N>1 uses `idd/chain-multi--` where hash8 is first 8 hex of sha256 over sorted-asc root numbers joined by `-` (deterministic per root set); hash8 collision fallback hash16, double collision aborts with manual cleanup hint. PR title dispatches: N=1 `chain: `, N>1 `chain (multi-root): N issues — `. PR body cluster overview table adds `root_id` column; Refs lists all roots first then chained spawns. NEW Phase 4 forest tree printout: per-root subtree with status icons (✓ PASS, ✗ FAIL, ⊘ filed-but-not-chained), depth labels, spawn-source attribution; per-root PASS/FAIL summary block; filed-only-not-chained list. 4 sub-skills (idd-implement Step 5.7 / idd-verify Phase 4 / idd-plan Step 2.5 / idd-diagnose Step 3.6) propagate root_id via `IDD_CHAIN_CURRENT_ROOT_ID` env var (exported by Phase 2 chain loop before each `/idd-all #M --in-chain` invocation), with defensive `[ -n \"$ROOT_ID_FOR_MANIFEST\" ]` guard preventing silent skip when both env and local fallback variables are unset. NEW `allowed-tools` frontmatter expanded with 11 additional Bash tools (shasum/sed/tr/cut/sort/seq/grep/awk/printf/date/head/tail/wc/basename/comm) for Phase 0.5 branch naming + Phase 4 forest tree rendering. Modified `idd-all-chain` + `idd-spawn-manifest` specs (3 MODIFIED + 1 ADDED requirement each); spec deltas in openspec/changes/multi-root-traversal-idd-all-chain/. Updated `references/spawn-manifest.md` v2 schema doc + `references/chain-flow.md` DFS/BFS algorithm + per-root halt + cap interaction + branch naming hash rule sections + PR title/body dispatch. Backward compat: single-root chain invocation byte-equivalent to v2.55.0 except for the schema bump (v1 manifests on disk become unreadable — per design, manifest is transient per-chain-session state, hard-break safe). Smoke tests 7.1+7.2 marked `[~]` first-real-use validation track per `## Checklist Conventions` IDD discipline (orchestration tests cannot mock GitHub API + git operations without significant fixture infrastructure, mirroring #52 idd-verify validation pattern). v2.59.0: idd-verify orchestration playbook — Step 2 spawn restructure + NEW Step 2.5 Recovery Protocol (PsychQuant/issue-driven-development#52, resolves #70 structurally). Step 2 switches from TeamCreate (5 teammates with Read/Grep/Glob/Bash tools, NO Write) to 5 parallel Agent(subagent_type=general-purpose) calls (含 Write tool) + 1 Bash codex background, single-message dispatch preserves parallelism. Each reviewer prompt mandatorily contains 3 elements: (1) explicit findings file output path `Write findings to /tmp/verify__findings_.md`, (2) explicit 'DO NOT idle without producing output' rule, (3) retry-context-re-paste hint ('treat later SendMessage with re-pasted prompt as retry signal'). Pre-spawn prompt persistence: coordinator MUST save each role's prompt to /tmp/verify__prompt_.md before invoking Agent — Step 2.5b retry reads this file for FULL context re-paste (never assumes context survived idle/wake cycle, per #47 incident root cause). Devil's Advocate sequencing: bash polling loop on sibling findings files (max 30 iter × 5s = 2.5min timeout) replaces TeamCreate wait_for_idle primitive; timeout fallback writes SENTINEL marker `[STAGE 2.5 RECOVERY: DEVILS_ADVOCATE_TIMEOUT_/4]` on first line + body explanation. NEW Step 2.5 Recovery Protocol section between Step 2 spawn and Step 3 merge: (2.5a) file existence check scans 5 findings files; detects DA timeout sentinel via head -1 | grep then rm -f the file + add to MISSING_ROLES so downstream -s checks see role as missing; (2.5b) retry with FULL context re-paste using saved prompt file + 90s polling; (2.5c) second-idle coordinator self-review fallback; (2.5d) explicit 'Process Gaps' section in master report — no silent engine degradation. Step 3 merge prose source tag swept `[team:...]` → `[agents:...]`; ASCII architecture tree + 鐵律 rule updated; CLI alias `team` preserved backward-compat with documented backend as 5 standalone Agent calls. Frontmatter: TeamCreate removed from allowed-tools (no longer used). Side effect: #70 (TeamDelete cleanup gap on idle teammates from #47 verify-pr58 cycle) structurally dissolved — no team to delete = no cleanup gap. Plan tier D1-D5 + D6 first-real-use validation track: 3 codex verify rounds (R1 3 P1 → R2 2 P1 → R3 PASS) under codex-only degraded mode (Anthropic API rate-limit blocked Claude reviewer ensemble throughout session — dogfooded as Process Gap on first-real-use). Empirical bash smoke validated DA sentinel writer + Step 2.5a head -1 | grep detection + rm -f sequence. v2.58.0: idd-issue Stage 4.5 — jsonl gitignore pre-flight gate (PsychQuant/issue-driven-development#55). NEW pre-flight gate at idd-issue/SKILL.md between Stage 4 Dispatch and JSONL write: detects `.gitignore` shadowing of `.claude/.idd/issue-runs/.jsonl` via `git check-ignore -v` (D2 spec contract preservation). Source-aware classification via `IS_NESTED_GITIGNORE` flag — case statement orders absolute path / `.git/info/exclude` / bare `.gitignore` BEFORE `*/.gitignore` so global `core.excludesfile` named `.gitignore` does NOT mis-classify as nested. AskUserQuestion branches: Case A (fixable: root `.gitignore` / `.git/info/exclude` / global) → 3-option Add carve-out / Skip / Abort; Case B (nested `.gitignore`) → 2-option Skip / Abort with complete manual-fix chain hint (root rewrite cannot override per-directory ignore; nested file requires its own 4-line chain with trailing slashes on dir patterns + explicit `!.idd/issue-runs/*` glob, empirically validated). Universal 5-line carve-out block with `!.claude` parent re-include leverages git's last-matching rule to neutralize ANY outer ignore source — survives multi-source stacked ignores (root + `.git/info/exclude` / root + global / `.git/info/exclude` + global). Idempotent + upgrade-safe via two-part check (marker AND `!.claude` content presence): stale 4-line block (same marker, missing `!.claude`) triggers awk two-state-machine upgrade — STATE 1 consumes # rationale comments adjacent to marker; STATE 2 consumes only known carve-out literal lines, ENDS skip immediately after final pattern `!.claude/.idd/issue-runs` — adjacent user content (blank lines, user `# Section` comments, sibling patterns) preserved across all variants. Empty body's grep idiom uses `grep | wc -l | tr -d ' '` (clean integer) instead of `grep -c || echo 0` (which doubled output to `0\\n0` on no-match). Dispatch summary surfaces ignore source + user choice + continuity status (committed / pending exception / ⚠ local-only with manual export hint / aborted). Ordering invariant: dispatch → gate → materialize — Stage 4 loop accumulates in-memory RUN_LOG_ENTRIES, Stage 4.5 gate fires after loop completes, materialize phase decides jsonl write fate per `JSONL_GITIGNORE_DECISION`. Abort discards in-memory entries BEFORE materialization; already-dispatched GitHub actions NOT rolled back (user-confirmed intent per Stage 3). Env var bypass `IDD_JSONL_GITIGNORE_GATE=false` for CI/unattended with 1-line audit cite. Plan tier D3 evolved through 3 revisions (single-line → 4-line → universal 5-line) across 7 codex verify rounds (R1: 4 P1 → R2: 3 new P1 → R3: 2 new P1 → R4: 2 new P1 + 1 residual → R5: 3 new P1 → R6: 2 new P1 → R7: PASS), cumulative 16 P1 caught + fixed under codex-only degraded mode (Anthropic API limit blocked Claude reviewer team). Empirical 15/15 smoke validation across all source-classification scenarios (single source / stacked sources / nested / stale upgrade / idempotency / fresh / env bypass / skip-commit / abort). PR #71 squashed as `c342aa2`. Master verify report at PR #71 #issuecomment-4421108494. v2.57.0: idd-close Step 6.5 — Distribution Sync chain (PsychQuant/issue-driven-development#45). NEW Step 6.5 inserted between Step 6 (auto-update phase=closed) and Step 7 (batch close special rules), surfacing user-facing distribution channel sync (plugin marketplace / MCP binary / CLI binary) at issue close moment. Detection-driven AskUserQuestion 3-option pattern (per IC_R011 canonical): (a) `chain to now` invokes `/plugin-tools:plugin-update ` / `/mcp-tools:mcp-deploy` / `/cli-tools:cli-deploy`; (b) `skip — manual later` records `### Distribution Sync Pending` audit + manual command; (c) `not applicable` records reason. Detection helpers (inlined in Step 6.5 + canonical contract in references/distribution-detection.md): `is_plugin_marketplace_member` walk-up scan ancestor `.claude-plugin/marketplace.json` parse `plugins[].source` (string `\"./plugins/\"` form) + `has_binary_wrapper` line-agnostic scan `bin/*.sh` for GitHub release URL patterns + `resolve_plugin_name` extract matched plugin name for chain command composition + `infer_distribution_type` orchestrator returning plugin/mcp/cli/plugin+mcp/plugin+cli/n/a. Detection-based silent skip for non-distribution repos (always-on). `IDD_DISTRIBUTION_SYNC_PROMPT=false` env var bypasses prompt for distribution-detected repos (1-line audit). D3 mixed-type v1: explicit ordering binary-deploy first → plugin-update second (idempotent regardless of plugin-update Phase 1.5 cascade availability per #66 audit). Step 0.5 Bootstrap Task List adds `distribution_sync_chain_detection` entry. Step 4 closing comment ID capture hardened (stdout-only + sed -n + explicit empty-check). NEW reference doc references/distribution-detection.md. Complements `common-release-flow.md` (release-tier trigger) at close-tier window. Verify discipline survived 3 rounds + degraded engine (Anthropic API limit) — Codex CLI carried + Round 1 had regression + devil's advocate (3 sources convergent), 5 P1 → 0 P1 at Round 3. 2 follow-ups filed: #66 D3 audit (mid-plan tangential), #68 monorepo host disambiguation (round-3 advisory). Backward compat: non-distribution repos see zero behavior change. v2.56.0: idd-issue multi-finding source mode (PsychQuant/issue-driven-development#48, add-multi-finding-source-mode-to-idd-issue Spectra change). Auto-trigger on Step 1 source extracting ≥2 paragraph-level findings from docx/pdf/Telegram/Apple Mail/Apple Notes/pasted-text/md adapters. 4-stage pipeline: Stage 1 Extract verbatim quotes + AI summary; Stage 2 Per-finding picker with AI surface top-3 candidates via gh issue list --search keyword overlap (title×2 + body[:300]×1) + 4-option AskUserQuestion + intent disambiguation [comment/edit body/update status/skip] for picked existing #N + [Other] expands to [New issue/Skip/Merge/Pick free-text]; Stage 3 Batch preview single AskUserQuestion [Execute all/Edit row N/Cancel]; Stage 4 Dispatch with warn-continue (failures log to jsonl actions[i].error + retry_hint, no abort, no rollback). Audit trail dual-track: per-action body footer `> Surfaced via /idd-issue multi-finding mode from ` + structured JSONL at `.claude/.idd/issue-runs/.jsonl` committed to git for cross-machine continuity. Two-way merge via inline sub-prompt (partner picker + combined target picker), JSONL records merged_from / merged_into bidirectionally; three-way+ refused. NEW override flags `--multi-finding` (force mode) / `--no-multi-finding` (force fall-through); mutually exclusive with each other and with `--bundle-mode` (different mental models: bundle = explicit ordered/unordered creation; multi-finding = source-driven mixed routing). NEW capability `idd-issue-multi-finding-source` parallel to existing `idd-issue-bundle` (both extend idd-issue with non-overlapping modes). Cross-reference updates to idd-comment/idd-edit/idd-update SKILL.md adding \"When to use idd-issue multi-finding mode instead\" sections redirecting batch source workflows. Backward compat: single-issue invocations unchanged byte-equivalent; --bundle-mode invocations unchanged; auto-trigger threshold is ≥2 detected findings else fall through. 5 architectural decisions D1-D5 from spectra-discuss session 2026-05-10 + 2 derived D6 trigger detection / D7 mutual exclusion in design.md. v2.55.0: NEW /idd-all-chain skill — chain-solve mode (PsychQuant/issue-driven-development#44, add-idd-all-chain-skill Spectra change). Drives root issue + auto-emergent spawned issues (sub-skill sister sweeps / verify follow-ups / mid-plan tangentials / sister concerns) through ONE cluster branch + ONE review PR. NEW skill /idd-all-chain #N: thin shell over /idd-all, internally recursive-invokes /idd-all #M --in-chain. Phase 0 creates cluster branch idd/chain-- from default branch + initializes spawn manifest at .claude/.idd/state/chain-spawned-issues.json (schema_version=1, atomic temp-file rename writes). Phase 2 main loop pops queue, invokes sub-/idd-all, reads manifest delta, enqueues chain-eligible spawns (rule: same_file_as_root OR same_skill_as_root OR spawn_kind='sister-bug'). Phase 3 opens cluster PR (title prefix 'chain:', collapsed
per issue, Refs all chained, Pending review checklist forbidding Closes/Fixes/Resolves trailers per IDD discipline). Phase 4 STOPs at verified — no auto-close, no auto-merge (per-issue /idd-close required). NEW --in-chain flag on /idd-all: single source for chain context, derives 4th mode tuple (direct-commit, unattended). Sub-/idd-all skips Phase 0.5 PR-mode branch creation + skips Phase 5.5 PR open + sub-skills receive UNATTENDED MODE directive. --in-chain mutex with --pr/--no-pr. NEW spawn manifest cross-skill contract: 4 sub-skills (idd-implement Step 5.7 sister bug sweep / idd-verify Phase 4 follow-up findings / idd-plan Step 2.5 tangentials / idd-diagnose Step 3.6 sister concerns) all conformantly write entries with classify spawn_kind + same_file_as_root + same_skill_as_root flags. Helper script scripts/manifest-append.sh implements atomic write + schema_version mismatch abort. Hard caps: chain_max_depth=2, chain_max_issues=5 (incl. root) — over-cap spawns still file as follow-up issues but not enqueued. Failure mode: any chained verify FAIL halts queue + preserves partial commits on cluster branch (no rebase/revert) + abort report cites 4 recovery paths. MODIFIED capability idd-orchestrator-modes: 4th mode tuple (direct-commit, unattended) added for chain context; existing 3 tuples behavior unchanged. NEW reference docs: references/spawn-manifest.md (schema canonical contract) + references/chain-flow.md (chain shell algorithm canonical contract incl. eligibility rule + caps + failure mode + PR body schema). Backward compat: /idd-all #N without --in-chain flag is byte-equivalent to v2.53.0 baseline. v2.52.0: idd-issue ordered/unordered bundle flags (PsychQuant/issue-driven-development#21). NEW `--parent ` flag PATCHes parent issue's body task list with new child entry, idempotent via `#N` reference scan + fallback `## Children` anchor when no list exists. NEW `--blocked-by [,...]` flag applies three-layer fallback chain: Layer 1 GraphQL `addBlockedByDependency` mutation attempt (graceful failure → warning + continue, no abort) + Layer 2 unconditional body blockquote `> Blocked by #M` (always readable in any markdown viewer) + Layer 3 parent task list annotation `(blocked by #M)` when `--parent` co-used. NEW `--bundle-mode ` flag orchestrates bundle creation in single invocation: builds 1 epic parent + N children with auto-applied `--parent `, ordered mode adds strict `child[i] blocked by child[i-1]` chain, unordered keeps task list only. Pre-flight gates: cross-repo refuse (parent in different repo than resolved target → abort + redirect to `groups` mechanism), bundle-mode and group-mode mutual exclusion (different mental models, refuse if both set). Step 3.B inserted between 3.A (single repo) and 3.G (group cross-link), reusing 3.A flow as primitive. Orthogonal with Step 4.5 milestone (bundle children get milestone assignment), Step 4.7 sister sweep (parent epic still subject to sweep, sibling issues NOT added to bundle task list). NEW canonical reference doc references/bundle-flags.md (flag spec + edit algorithm + fallback chain + partial failure + idempotency contract). NEW `## Ordered Bundle Pattern` section after Step 5 in idd-issue SKILL.md (3-mode comparison table + 3 usage scenarios + design rationale for not creating separate /idd-bundle skill). Step 0 Bootstrap Task List adds `resolve_parent_link`, `apply_blocked_by`, `orchestrate_bundle_mode` TaskCreate entries. NEW capability `idd-issue-bundle` in openspec/specs/. No breaking changes — all flags additive, omitted invocation behavior unchanged. Spectra change `add-bundle-flags-to-idd-issue` in this repo's openspec/changes/. v2.51.0: idd-list shows open PR info per issue + cluster detection (PsychQuant/issue-driven-development#13). NEW Step 2.5 batch fetches all open PRs once via 'gh pr list --state open --limit 100'; NEW Step 3.5 client-side regex-scans PR bodies for '#N' refs and builds reverse issue→PR index plus cluster map (PRs ref'ing 2+ issues). Step 4 Format Output extended: each issue with a PR ref gets a sub-line '└─ PR #N (status, mergeable)'; cluster leaders (lowest issue number in refs) show 'cluster: #X #Y #Z' listing all members; cluster members show '→ see PR #N (cluster member)' redirect. Direct-commit issues (no PR refs) display unchanged from v2.50 — fully backward compatible. Footer adds second line summarizing 'N issues bundled in M cluster(s); P solo PR(s); Q direct-commit'. Step 5 Suggest Next extended to phase × PR state matrix (10+ rows): implemented + draft → 'gh pr ready N → /idd-verify --pr N'; implemented + ready MERGEABLE → '/idd-verify --pr N'; verified + ready MERGEABLE → 'gh pr review N → gh pr merge N → /idd-close #N'; verified + merged catch-up → '/idd-close #N'; CONFLICTING → 'gh pr checkout N → resolve'; cluster member → 'see leader's next action'. Sister concerns filed as future P3 follow-ups: #14 (markdown-aware PR body parser to ignore '#N' inside fenced code blocks; v1 accepts false positive) + #15 (cluster_leader config 'lowest|primary' instead of hardcoded lowest). v2.50.0: Layer V Vagueness Pre-check (PsychQuant/issue-driven-development#12). NEW Step 3.4 in idd-diagnose between Layer 1 disqualifier and Layer 2 Spectra evaluation: AI scores V1 (vague WHAT) + V4 (vague ACCEPTANCE) on Likert 6-point scale (no neutral midpoint), trigger threshold per-axis ≥ 4. Triggered cases fire Hybrid 3-option AskUserQuestion (clarify now / proceed anyway / escalate to Plan) with default option score-driven (V=4 → proceed, V=5 → clarify, V=6 → escalate). 'clarify now' appends Q/A pairs to issue body via gh issue edit then re-runs Layer V; 'proceed anyway' continues to Layer 2/3/P with audit trail recording trigger fact; 'escalate to Plan' force-sets verdict = 'Plan via Layer V' and skips Layer 2/3/P. Layer evaluation order: Layer 1 → V → 2+3 → P → Simple. Routing parsers in idd-implement Step 2.5 + idd-all Phase 3 strip ' via X' suffix to extract canonical tier — bare 'Plan' / 'Simple' / 'Spectra' verdicts unchanged (backward compat). NEW project rule .claude/rules/attribute-assessment.md codifies meta-principle 'attribute scoring SHALL use Likert scale, not keyword matching' — applies session-wide via root CLAUDE.md @import, scope beyond Layer V (any future attribute scoring need). MANIFESTO 5-axis bug-fix model expanded to 6-axis adding 'Alignment quality' (TDD ❌ / SDD ❌ / IDD ✅), evidence = Layer V. idd-all unattended mode auto-applies 'proceed anyway' + audit trail '[Layer V: V1=N V4=M, clarify-default skipped under unattended mode, defaulting to proceed]' (same pattern as Plan tier under unattended). Backward compat: pre-v2.50 diagnoses NOT retroactively re-evaluated; existing Simple / Plan / Spectra / SDD-warranted verdicts remain valid. No --ignore-vagueness flag (option B 'proceed anyway' covers that need). Spectra change add-vagueness-layer-routing in this repo's openspec/changes/. Step 0 Bootstrap Task List adds 'vagueness_precheck' TaskCreate. v2.49.0: references/ic-r011-checkpoint.md v1.1.0 — Third-Party Skill Alignment section for /spectra-discuss + /spectra-propose (kiki830621/ai_martech_global_scripts#530, sub-issue E of #523 systematic plugin alignment, last sub-issue closing the parent epic). spectra-* skills are published by third-party kaochenlong/spectra-app — direct SKILL.md modification not in this plugin's commit cycle. Documentation-side alignment: agents/users invoking /spectra-discuss + /spectra-propose with IC_R011 in mind apply the canonical 3-option AskUserQuestion + audit trail manually at deliberation-moment equivalents (discussion convergence / proposal drafting). Per canonical eligibility criteria §6, only the 2 deliberation-moment spectra-* skills (discuss / propose) need alignment; the other 6 (apply / archive / ask / ingest / commit / debug) are mechanical execution and N/A. If spectra-app upstream adopts native IC_R011 checkpoint, this section becomes redundant + can be removed. Strength: SHALL — discussion / proposal drafting are deliberation moments per canonical eligibility criteria. v2.48.0: idd-issue Step 4.7 — Linked-Context Sister Sweep (kiki830621/ai_martech_global_scripts#529, sub-issue D of #523 systematic plugin alignment). NEW advisory step between Step 4.5 (auto-milestone) and Step 5 (報告), scanning issue body draft + linked attachments + recent session conversation for sibling-concern markers (also / additionally / related / 另外 / 順便 / BTW). If hits, AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file as sibling issues now' / 'file selected' files via 'gh issue create' as parallel issues (NOT cross-linked into the just-created issue body, since user's primary concern stays focused), each with confidence:confirmed + priority:P3 + source link 'surfaced during /idd-issue #NEW linked-context sister sweep (Step 4.7)'. PATCHes the just-created issue body to add '### Linked-Context Siblings Filed (v2.48.0+ #529)' audit trail per canonical heading conventions. Strength: SHOULD (advisory, non-blocking) per canonical eligibility criteria §6 — issue creation is light-touch (user is already in filing-active mode, double-prompt risks friction). Empty list = silent no-op default. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Step 0 Bootstrap Task List adds 'linked_context_sister_sweep' TaskCreate. v2.47.0: idd-diagnose Step 3.6 — Sister Concern Surfacing (kiki830621/ai_martech_global_scripts#528, sub-issue C of #523 systematic plugin alignment). NEW mandatory step between Step 3.5 (Complexity Assessment) and Step 3.7 (Agent Routing), surfacing sister-concern markers in just-posted Diagnosis content (也有 / sister / 同樣的 / 另外 / likewise affects) + scout session log. AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file all/selected' files via 'gh issue create' with confidence:confirmed + priority:P3 + source link, then PATCHes Diagnosis comment with '### Sister Concerns Filed (mid-diagnose, v2.47.0+ #528)' audit trail per canonical heading conventions. Strength: SHALL (mandatory step) per canonical eligibility criteria — diagnosis is a deliberation moment where sister concerns naturally surface during Strategy authoring. Empty list legitimate. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Step 0 Bootstrap Task List adds 'sister_concern_surfacing' TaskCreate. v2.46.0: idd-all HITL mode (PsychQuant/issue-driven-development#1). Phase 0.5 mode resolution from existing pr_policy + new --pr/--no-pr flags into (path, interaction) tuple — PR + unattended (v2.40.0 regression — /loop friendly) or direct-commit + attended (HITL — solo/personal repos where PR is ceremony, user is in keyboard, sub-skill AskUserQuestion / EnterPlanMode / Park-Apply prompts fire natively). Two axes from one source (no duplicate config surface). v2.45.0: idd-close Step 3.5 — Closing Summary Follow-up Keyword Scan (kiki830621/ai_martech_global_scripts#527, sub-issue B of #523 systematic plugin alignment). NEW step between Step 3 (review with user) and Step 4 (gh issue close), scanning drafted closing summary for trigger phrases (follow-up / deferred / future / TODO / later / 之後 / 未來 / 順便 / 我之前觀察到 / 之後再 / 改天). Each match is checked against existing #NNN cross-links via 'gh issue view' — orphan mentions (no link or stale link) trigger AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md (#525). 'file all/selected' files via 'gh issue create' with confidence:confirmed + priority:P3, then PATCHes closing summary inline (mention → '...(see #NEW)') and adds '### Closing Follow-ups Filed (v2.45.0+ #527)' audit trail. Strength: SHOULD (advisory, non-blocking) per canonical eligibility criteria — closure is mostly mechanical action; surfacing orphan-mention pattern at decision moment without forcing filing. AI_LOW_BAR_ISSUE_FILING=false env var skips silently per IC_R011 rollback hatch. Disambiguation note added: this Step 3.5 is the IC_R011 checkpoint; Step 0 supersession (#515 v2.41.0) is gate logic — orthogonal concerns. Step 0.5 Bootstrap Task List adds 'closing_followup_keyword_scan' TaskCreate. v2.44.0: idd-implement Step 5.7 — Sister Bug Sweep (kiki830621/ai_martech_global_scripts#526, sub-issue A of #523 systematic plugin alignment). New mandatory step between Step 5.5 (Open PR if PR path) and chain to /idd-verify, surfacing sister bugs discovered during TDD reproduction (Step 3) — adjacent same-root-cause sibling files like the proven 2026-05-03 #510 → #518 → #520 cluster (gen_*.R / fix_wiser_poisson_tables.R / _build.R) where each manual reminder was needed despite same pattern. Cites canonical references/ic-r011-checkpoint.md (#525) for 3-option AskUserQuestion (file all / file selected / skip) + heuristic triggers + audit trail format + AI_LOW_BAR_ISSUE_FILING=false rollback hatch. PATCHes Implementation Complete comment to add `### Sister Bugs Filed (mid-impl, v2.44.0+ #526)` audit trail per canonical heading conventions table. Strength: SHALL (mandatory step) but empty list legitimate. Step 0 Bootstrap Task List adds `sister_bug_sweep` TaskCreate entry. v2.43.0: NEW canonical reference doc references/ic-r011-checkpoint.md (kiki830621/ai_martech_global_scripts#525, sub-issue F of #523 systematic plugin alignment). Standardizes the 3-option AskUserQuestion pattern (file all / file selected / skip), heuristic triggers (verifiable behavior gap / sister bug from reproduction / observed friction / deferred work / out-of-scope user mentions / drift / TODO encounters), default-off exemptions (pure exploration / existing issue / hallucinated / CONSTRAINT / mechanical execution stages), audit trail per-skill heading conventions, rollback escape hatch (env var + repo CLAUDE.md flag), and eligibility criteria (which skills SHALL vs SHOULD vs N/A). Cited from idd-plan Step 2.5 (#524) + idd-close Step 0 supersession (#515) — both back-link the canonical doc. Sister sub-issues #526-#530 (idd-implement / idd-close closing summary scan / idd-diagnose / idd-issue / spectra-discuss + spectra-propose) will all cite this canonical doc when their Plan tier ships;cross-skill consistency mechanically anchored. v2.42.0: idd-plan Step 2.5 — mid-plan tangential observations sweep (kiki830621/ai_martech_global_scripts#524 fix). Plan-tier deliberation surfaces tangential discoveries (Phase 1 Explore agents pass-by sister bugs, Phase 2 grep-discovered drift, Phase 3 user-mentioned sub-concerns) that previously fell into the gap between In-scope and Out-of-scope categorization, vanishing into conversation. New mandatory step between Step 2 (Draft Plan) and Step 3 (Confirm post): agent reviews session log, surfaces candidates with IC_R011 default-on heuristic (verifiable behavior gap / sister bug / out-of-scope user-mentioned), AskUserQuestion three-option (file all / file selected / skip), files via 'gh issue create' with confidence:confirmed + priority:P3 + source link to plan issue, then PATCHes plan body to add '### Tangential Observations' audit trail. Empty list = no-op (legitimate). AI_LOW_BAR_ISSUE_FILING=false env var skips per IC_R011 rollback hatch. Codifies IC_R011 spirit at the mid-plan window — finer gap than #523 broader systematic alignment. v2.41.0: idd-close Step 0 supersession of pre-implementation Strategy / Implementation Plan checkboxes (kiki830621/ai_martech_global_scripts#515 fix). When `## Implementation Complete > ### Checklist` exists and all its items are `- [x]`, that subsection is treated as the canonical state of truth — `Strategy` / `Implementation Plan` `- [ ]` items are auto-superseded (skipped from gate). Resolves the recurring friction where work was complete but `idd-implement` Step 5 only synced its own Implementation Complete comment (never PATCHed Strategy / Plan comments), leaving 8+ pre-impl `- [ ]` items blocking gate and forcing manual `gh api PATCH` workaround on every full-lifecycle close (#455 / #510 close, 2026-05-03). Defensive properties preserved: incomplete Implementation Complete (any `- [ ]` remaining) falls back to legacy full spec scan; legacy issues without Implementation Complete unchanged. Strategy A from #515 diagnosis (chosen over B sync-at-write and C narrow-gate). v2.40.0: --cwd flag propagated to all sub-skills (idd-diagnose / idd-implement / idd-verify) so cross-repo orchestration via idd-all actually works end-to-end. v2.39.0 added --cwd to idd-all only — but sub-skills still inherited Claude Code session cwd, so idd-implement would commit to the wrong repo. NEW: shared `references/cross-repo-cwd.md` documents the substitution rule (`git X` → `git -C $CWD X`, `gh issue/pr/repo X` → `gh ... X -R $GITHUB_REPO`) once; each sub-skill cites it at the top of Execution. NEW: idd-all Phase 1/2/3a/4 forward `--cwd $CWD` to sub-skill args; Phase 1 (idd-issue) and follow-up-issue creation use `--target $GITHUB_REPO` instead (read-only, no local git needed). Backward compat: omitting --cwd reads session pwd (existing behavior). v2.39.0: idd-all --cwd flag + cross-repo invocation. NEW Step 0.2 Resolve Working Tree resolves target repo from --cwd /path/to/clone (per-invocation override) instead of hardcoded session cwd. All git ops use 'git -C $CWD'; all gh ops use 'gh -R $GITHUB_REPO' (repo derived from origin remote). Solves 'Skill tool inherits Claude Code session-level cwd, can't follow mid-session cd' friction when running idd-all on a repo other than the one your session started in (e.g. thesis work in repo A, want pipeline on dependency repo B). Phase 0.2/0.3 abort messages now include explicit 'pass --cwd /path/to/clone' alternative. Backward compat: omitting --cwd uses session cwd (existing behavior). v2.38.0: idd-route integration — data-driven agent routing recommendation from observed track record. NEW idd-diagnose Step 3.7: if ~/bin/idd-route is available, calls it with current issue's complexity + estimated scope LOC + extracted signals to get an agent recommendation (codex-gpt-5.5-xhigh / claude-opus-4.7 / sonnet-4.6 / haiku-4.5), injects 'Recommended Agent' section into diagnosis comment. NEW idd-verify Step 5d: records each verify outcome (issue, agent, complexity, scope, round trips, blocking findings, follow-ups) to /.claude/.idd/routing-stats.jsonl + global mirror. NEW idd-close Step 4.5: appends final outcome (merged/abandoned) — append-only so original in_review record stays for audit. All three are gracefully no-op when idd-route binary missing (command -v check). Companion plugin idd-route ships in same marketplace; binary source at PsychQuant/idd-route-swift. NEW references/agent-routing.md is canonical contract for the IDD ⇄ idd-route boundary. Built on top of v2.37.0's external-agent / PR mode foundation — the routing recommendation closes the loop: idd-diagnose suggests agent → user delegates → idd-verify records outcome → next idd-diagnose recommendation gets smarter. Plus marketplace migration: this is the first issue-driven-dev release in the new PsychQuant/issue-driven-development marketplace (formerly lived in psychquant-claude-plugins; full git history preserved via filter-repo). v2.37.0: External-agent / PR mode for idd-verify + use-case routing reference. NEW idd-verify --pr input mode for verifying PRs opened by external agents (Codex via codex exec, Copilot Workspace, remote claw on PsychQuantClaw) — gh pr diff + gh pr checkout so reviewer agents see file context, auto-restore original branch after verify. Plus --commits N / --since / --branch flags for other input sources. Auto-detect mode (no flag): counts unpushed Refs #N commits since origin/ first; if 0, queries open PRs ref'ing #N and AskUserQuestion to pick between local diff vs PR — catches the common 'forgot --commits N' case without silently switching modes. Issue↔PR correspondence is a hard iron rule: PR mode aborts before invoking 6-AI ensemble if PR body has zero Refs #N (untrackable change violates IDD discipline) or if user-passed issue isn't in PR's Refs set (correspondence broken); discovers superset triggers AskUserQuestion to confirm scope. PR mode flips master comment location: full verify report posts to PR (external agent owners work in PR view, never see issue comments), each ref'd issue gets a 1-line pointer comment back with PASS/FAIL + master comment URL. Capture-master-URL-then-write-pointer SOP enforced (prevents the recurring bug class where pointer URLs accidentally reference earlier diagnosis/implementation comments). NEW references/external-agent-delegation.md is canonical contract: 4-phase delegation impact matrix (diagnose stays, implement may delegate, verify+close return to IDD), hands-off principle (no babysitting external agents; strict verify, opt-in fix takeover), 3 input modes + auto-detect algorithm, issue↔PR gate, PR-as-master cross-post, working tree handling, deferred items (--takeover, idd-handoff, force-push detection). NEW references/usecase-routing.md closes discoverability gap: 24-row table mapping common scenarios → exact skill chain + flags + contract doc (single / batch / cluster-PR / external-agent PR/commits/branch/auto / Plan tier / Spectra-warranted / bundle close / Spectra-bridge / multi-repo monorepo) plus top-of-doc decision tree for users unsure which entry point to start from. Linked from CLAUDE.md (Claude-facing) and README.md (human-facing). Backward compat: single-issue invocation idd-verify #42 without flags still works as v2.36 in common case (no Refs commits, no open PRs → falls back HEAD~1); cluster-PR mode #34 #36 #38 unchanged; no flag deprecations. v2.36.0: 3-tier Complexity routing (Simple / Plan / Spectra) + new idd-plan skill. SDD-warranted renamed to Spectra (backward-compat alias preserved). Plan tier inserts EnterPlanMode approval gate between diagnosis and TDD execution — covers 'think before leap, no spec contract needed' (most common case where Simple was too thin and Spectra was overkill). Issue-driven development methodology: issue → diagnose → (idd-plan if Plan tier) → implement → verify → close.", + "version": "2.75.0", "author": { "name": "Che Cheng" }, diff --git a/plugins/issue-driven-dev/CHANGELOG.md b/plugins/issue-driven-dev/CHANGELOG.md index c0ebfd1..834a6a5 100644 --- a/plugins/issue-driven-dev/CHANGELOG.md +++ b/plugins/issue-driven-dev/CHANGELOG.md @@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.75.0] - 2026-05-25 + +### Added + +- **`plugins/issue-driven-dev/scripts/idd-edit-helper.sh`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): NEW extracted runtime helper with 3 subcommands. `parse-args` does positional shift over 7 space-form flags with explicit missing-value guards (`[ -z "${2:-}" ]` + `[[ "$2" == --* ]]`) + eq-form support + body-file readability pre-check + emits eval-friendly KEY=quoted-value via printf %q (preserves newlines). `validate-target` enforces R5 via single gh API call (author_association + login fetch) with `*[bot]` allowlist + OWNER passthrough + override pathway. `section-replace` uses awk-getline pattern (BSD/gnu safe — closes R3 C3 BSD awk -v multi-line newline reject). 5 distinct exit codes (0/1/2/3/4/5). + +- **`plugins/issue-driven-dev/scripts/tests/idd-edit/{test.sh,fixtures/01-19/}`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): NEW test infrastructure per `.claude/scripts/tests/spectra-archive-post-ic/` precedent. **19 fixtures** cover R1/R2/R3 regression set + Round 1 verify finding additions: parser robustness (eq form / space form / missing value / next-flag-eats-value / unreadable file / multi-line body / single-line body / subsections / no-closing-heading), R4 gate (no scope/section refuse + invalid scope value refuse), R5 override pair guard (default=false / requires --reason / succeeds with both), validate-target via IDD_EDIT_HELPER_GH_MOCK env var (OWNER passthrough / bot allowlist / non-OWNER refuse / non-OWNER with override), section-replace CRLF input handling. All 19 GREEN. + +- **`emit-audit-marker` helper subcommand** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154) Round 1 fix C3+M1): centralizes HTML-comment-escape so attacker-controlled `$REASON` / `$SECTION_FLAG` cannot forge audit trail. Strips `-->` tokens (→ `-\>`), newlines, control chars. Used by all 3 modes for both `edit` markers and `override` markers. Closes R5 forensic gap where override marker only emitted in `--replace` branch. + +- **`IDD_EDIT_HELPER_GH_MOCK` env var** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154) Round 1 fix H3): test hook for `validate-target` subcommand. When set, reads mock JSON `{"login": ..., "assoc": ...}` instead of calling `gh api`. Unblocks unit-test coverage of bot allowlist / OWNER passthrough / refuse paths without live API. Used by fixtures 15-18. + +- **`comment_id` numeric validation** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154) Round 1 fix C2): SKILL.md Step 1 enforces `[[ "$COMMENT_ID" =~ ^[0-9]+$ ]]` before substitution into `gh api repos/.../comments/$comment_id` URL or `/tmp/idd-edit-repl-$COMMENT_ID.md` filename. Closes Round 1 verify finding C2 path traversal via Step 0.7 PR↔issue correspondence input. + +- **`--body-file` path-traversal documentation** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154) Round 1 fix H5): SKILL.md 鐵律 section explicitly notes `--body-file=/etc/passwd` would be read + pushed to public PATCH; user/caller validates path; restrict-to-subtree is future enhancement. + +- **`/idd-edit` R4 runtime gate** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): `--replace` without `--scope whole-comment` OR `--section ` → refuse with exit 3 + actionable error message + spec Requirement 4 citation. Closes #150 Requirement 4 runtime enforcement deferral. Tested by fixture 10. + +- **`/idd-edit` R5 runtime gate** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): non-OWNER non-bot comment without `--override-user-content --reason="..."` → refuse with exit 4 + helpful manual-invocation message + spec Requirement 5 citation. Bot allowlist matches `*[bot]` glob (github-actions[bot], dependabot[bot], etc.). Override appends `` audit marker to comment body. Closes #150 Requirement 5 runtime enforcement deferral. + +- **`/idd-comment` errata flow R5 integration** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): errata Template SPECIAL BEHAVIOUR sets `IDD_CALLER=idd-comment-errata` env var when auto-calling `/idd-edit --prepend-note`. On R5 refuse (exit 4), displays helpful message with exact manual invocation pattern (`--override-user-content --reason='errata clarification per IDD discipline'`). Per D2 decision (refuse-with-message > auto-override, aligns with IC_R007 user-authored-intent spirit). + +### Refactored + +- **`/idd-edit` SKILL.md Step 1** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): replaced inline `if [[ "$ARG" == comment:* ]]; then ... elif [[ "$ARG" == \#* ]]; then ...` pseudocode with `bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh parse-args "$@"` invocation + eval-import. AI no longer generates parser bash inline — uses tested helper. Closes R1/R2/R3 root cause: AI bash generation inconsistency. + +- **`/idd-edit` SKILL.md Step 4 `--replace` mode** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): replaced inline `awk -v new_content="$BODY"` (R3 C3 BSD-broken) with `bash ... section-replace` invocation that uses awk-getline reading from file. Whole-comment path stays inline (no awk needed). Override audit marker appended when applicable. + +- **`/idd-edit` SKILL.md frontmatter `argument-hint`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): updated to show new flag syntax — mode flags grouped with R4-required `--scope`/`--section` for `--replace` + new `--body-file`/`--reason`/`--override-user-content` flags. + +- **`/idd-edit` SKILL.md `## 使用範例`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): 3 existing examples updated with new flag syntax; 2 new examples added (section-replace + errata override flow). + +- **`/idd-edit` SKILL.md `## Batch mode`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): added per-target R4/R5 enforcement note + cross-link to [#158](https://github.com/PsychQuant/issue-driven-development/issues/158) for full batch+R5 semantics design. + +- **`openspec/specs/append-vs-modify-discipline/spec.md`** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): Purpose / Requirement 4 / Requirement 5 preambles updated from "deferred to #154" to "landed via #154" + specific helper subcommand + tested-by fixture references. Removed "Re-verify for runtime conformance when #154 closes" cleanup tags. + +### BREAKING (runtime) + +- **`/idd-edit --replace` without `--scope`/`--section` now refuses** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): existing AI invocations of `/idd-edit comment:NNN --replace --body "..."` will get exit 3 + R4 message. Must add `--scope whole-comment` (full overwrite) or `--section "### Heading"` (named subsection). Discipline already declared in v2.73.0/v2.74.0 spec; v2.75.0 makes it runtime-enforced. + +- **`/idd-edit` modifying non-OWNER non-bot comment now refuses** ([#154](https://github.com/PsychQuant/issue-driven-development/issues/154)): existing AI invocations targeting external collaborator / non-OWNER comments will get exit 4 + R5 message. Must add `--override-user-content --reason="..."` for explicit consent. `/idd-comment` errata flow auto-call gracefully handles this with helpful message. + +### Round 1 verify fixes (all 11 pre-merge findings addressed) + +- **C1**: Helper + test fixtures moved from `.claude/scripts/` to `plugins/issue-driven-dev/scripts/` — matches `process-attachments.sh` / `manifest-append.sh` precedent. `$CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh` now resolves correctly in production install. +- **C2**: `comment_id` numeric validation (described above under Added). +- **C3+M1**: `emit-audit-marker` helper subcommand (described above under Added) — centralized HTML-escape, marker emission in all 3 modes. +- **B1**: SKILL.md Step 4 `--append` mode uses `$BODY_INPUT` (helper-exported) instead of undefined `$APPEND_BODY`. +- **B2**: SKILL.md Step 6 PATCH + Step 7 verify use `$REPO` (helper-exported, respects `--repo` flag + walk-up config) instead of undefined `$GITHUB_REPO`. +- **H1**: helper `parse-args` R4 gate validates `--scope` value MUST be `whole-comment` (no other valid scopes today). Invalid value → exit 3 with hint. +- **H2**: SKILL.md Step 1 splits `parse-args` stdout/stderr via temp file. `eval` only sees `printf %q` quoted assignments; stderr never reaches eval (closes shell-injection via `cat`-on-directory `$()` POC). +- **H3**: `validate-target` test coverage via `IDD_EDIT_HELPER_GH_MOCK` env var (described above under Added) — 4 new fixtures. +- **H4**: `section-replace` heading-level counter rewritten via awk char-by-char (no `wc -c` trailing-newline off-by-one). CRLF input strip via `tr -d '\r'` on both input + replacement. Fixture 08 strengthened with exact-stdout match. New fixture 19 verifies CRLF. +- **H5**: `--body-file` path-traversal risk documented in SKILL.md 鐵律. +- **D1**: PR body + commit body Closes/auto-close trailer cleaned per `CLAUDE.md` Commit Conventions §「引用 trap pattern 作反例的寫作紀律」(#97). +- **M3**: Bot allowlist dead-code redundant patterns cleaned up. +- **M6**: `validate-target` guards against null `login`/`assoc` from malformed gh API response. + +### Filed during implementation + +- **[#155](https://github.com/PsychQuant/issue-driven-development/issues/155)** — sister concern: bash vs alternative layer for strict flag parsing (parking-lot P3; helper extraction proves bash + thin SKILL.md orchestration works) +- **[#156](https://github.com/PsychQuant/issue-driven-development/issues/156)** — sister concern: IDD plugin test framework (P2; partially pre-empted by reusing `.claude/scripts/tests/` fixture-dir precedent for #154 — same pattern now applies to 2 sites) +- **[#157](https://github.com/PsychQuant/issue-driven-development/issues/157)** — tangential: spec.md `` blocks lack auto-updater (parking-lot P3) +- **[#158](https://github.com/PsychQuant/issue-driven-development/issues/158)** — tangential: `/idd-edit` batch mode + R5 interaction semantics decision (P2; single-target enforcement shipped, batch interaction follow-up) +- **[#160](https://github.com/PsychQuant/issue-driven-development/issues/160)** — sister bug: spectra-archive-post-ic/test.sh likely has same `grep -qF --` bug (parking-lot P3) +- **[#161](https://github.com/PsychQuant/issue-driven-development/issues/161)** — sister concern: IDD_CALLER env var registry codification (parking-lot P3) + ## [2.74.0] - 2026-05-25 ### Added diff --git a/plugins/issue-driven-dev/scripts/idd-edit-helper.sh b/plugins/issue-driven-dev/scripts/idd-edit-helper.sh new file mode 100755 index 0000000..9301e6b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/idd-edit-helper.sh @@ -0,0 +1,508 @@ +#!/usr/bin/env bash +# idd-edit-helper.sh — Robust runtime support for /idd-edit skill. +# +# Extracted from plugins/issue-driven-dev/skills/idd-edit/SKILL.md per #154 +# (proper proposal after R1/R2/R3 bash-incremental failure on PR #153). Pattern +# follows .claude/scripts/spectra-archive-post-ic.sh precedent. +# +# Subcommands: +# parse-args — Parse /idd-edit flags + emit shell exports +# (eval-friendly KEY="VALUE" lines on stdout) +# validate-target — Fetch comment author_association/login, +# enforce R5 (non-OWNER refuse) unless override +# section-replace +# — awk-getline pattern replacing named section +# with body-file content. BSD/gnu awk safe. +# +# Exit codes: +# 0 — success +# 1 — generic error +# 2 — usage error (missing/invalid args) +# 3 — R4 gate refused (--replace missing --scope/--section) +# 4 — R5 gate refused (non-OWNER, no --override-user-content) +# 5 — --body-file unreadable +# +# Each subcommand prints diagnostics to stderr; stdout reserved for +# machine-parseable output (eval lines / extracted text / etc.). + +set -uo pipefail + +SUBCMD="${1:-}" +shift || true + +# ── R4/R5 helpful messages (constants) ── +R4_MSG='Refuse: --replace requires --scope whole-comment OR --section (action-scoped discipline per plugins/issue-driven-dev/rules/append-vs-modify.md spec Requirement 4)' +R5_MSG_TEMPLATE='Refuse: comment %s was authored by %s (author_association=%s, non-OWNER non-bot) and is verbatim-preserve per IC_R007; pass --override-user-content --reason="..." to explicitly modify user content (spec Requirement 5)' + +# ─────────────────────────────────────────────────────────────────────── +# Subcommand: parse-args +# ─────────────────────────────────────────────────────────────────────── +# Parses /idd-edit flags. Emits shell-eval'able assignments on stdout. +# Caller does: eval "$(./idd-edit-helper.sh parse-args "$@")" +# +# Recognized flags (all support both --flag=value and --flag value forms): +# --append / --replace / --prepend-note (mode flag, mutually exclusive) +# --scope= (R4 explicit scope) +# --section= (R4 named subsection) +# --reason= +# --body= (single-line body) +# --body-file= (multi-line body via file) +# --repo= +# --cwd= +# --last (idiomatic: take last comment) +# --override-user-content (R5 bypass, must pair with --reason) +# +# Positional: comment: | # | comment: comment:... (batch) +# +# Output (stdout): one assignment per line, shell-quoted values +# MODE='--replace' +# SCOPE_FLAG='whole-comment' +# SECTION_FLAG='### Foo' +# BODY_INPUT='multi-line content with newlines preserved' +# TARGETS=('comment:123' 'comment:456') +# ... +parse_args_subcmd() { + local mode="" scope_flag="" section_flag="" reason="" body_input="" + local body_file="" repo="" cwd="" last="false" override="false" + local -a targets=() + + while [ $# -gt 0 ]; do + local arg="$1" + case "$arg" in + # Mode flags (mutually exclusive) + --append|--replace|--prepend-note) + if [ -n "$mode" ] && [ "$mode" != "$arg" ]; then + echo "ERROR: conflicting mode flags: $mode and $arg" >&2 + return 2 + fi + mode="$arg" + shift + ;; + + # Bare boolean flags + --last) + last="true" + shift + ;; + --override-user-content) + override="true" + shift + ;; + + # Eq-form flags (--flag=value): SAFE — no value-eating risk + --scope=*) scope_flag="${arg#--scope=}"; shift ;; + --section=*) section_flag="${arg#--section=}"; shift ;; + --reason=*) reason="${arg#--reason=}"; shift ;; + --body=*) body_input="${arg#--body=}"; shift ;; + --body-file=*) + body_file="${arg#--body-file=}" + # R3 H1 guard: readability pre-check (silent overwrite was R3 bug) + if [ ! -r "$body_file" ]; then + echo "ERROR: --body-file not readable: $body_file" >&2 + return 5 + fi + body_input="$(cat "$body_file")" + shift + ;; + --repo=*) repo="${arg#--repo=}"; shift ;; + --cwd=*) cwd="${arg#--cwd=}"; shift ;; + + # Space-form flags (--flag value): GUARDED — close R2 + R3 bugs + # R2 B3-NEW-1/2: literal capture when next arg looks like a flag + # R3 C1/C2: infinite loop on shift 2 when $#=1; flag-eats-flag + --scope|--section|--reason|--body|--body-file|--repo|--cwd) + # R3 C1 guard: missing-value when flag is last arg + if [ $# -lt 2 ]; then + echo "ERROR: ${arg} requires value (no argument follows)" >&2 + return 2 + fi + local next="$2" + # R3 C2 guard: next arg looks like a flag → user forgot value + if [[ "$next" == --* ]]; then + echo "ERROR: ${arg} value cannot start with '--' (got: ${next}). Did you forget the value?" >&2 + return 2 + fi + case "$arg" in + --scope) scope_flag="$next" ;; + --section) section_flag="$next" ;; + --reason) reason="$next" ;; + --body) body_input="$next" ;; + --body-file) + body_file="$next" + if [ ! -r "$body_file" ]; then + echo "ERROR: --body-file not readable: $body_file" >&2 + return 5 + fi + body_input="$(cat "$body_file")" + ;; + --repo) repo="$next" ;; + --cwd) cwd="$next" ;; + esac + shift 2 + ;; + + # Positional: comment: or # + comment:*|\#*) + targets+=("$arg") + shift + ;; + + *) + echo "ERROR: unknown argument: $arg" >&2 + return 2 + ;; + esac + done + + # ── R4 gate: --replace requires --scope=whole-comment OR --section ── + # Tightened by #154 verify Round 1 H1: previously only checked non-empty, + # `--scope typo` passed but downstream had no matching branch → undefined NEW_BODY. + if [ "$mode" = "--replace" ]; then + if [ -z "$scope_flag" ] && [ -z "$section_flag" ]; then + echo "$R4_MSG" >&2 + return 3 + fi + # If --scope provided, value MUST be exactly "whole-comment" (no other valid scopes today) + if [ -n "$scope_flag" ] && [ "$scope_flag" != "whole-comment" ]; then + echo "Refuse: --scope value must be 'whole-comment' (got: '$scope_flag'). Use --section for named subsection scope." >&2 + return 3 + fi + fi + + # ── R5-pair guard: --override-user-content requires --reason ── + if [ "$override" = "true" ] && [ -z "$reason" ]; then + echo 'ERROR: --override-user-content requires --reason="" (spec Requirement 5 audit)' >&2 + return 2 + fi + + # ── Emit eval-friendly output ── + # printf %q produces shell-safe quoted form preserving newlines + printf 'MODE=%q\n' "$mode" + printf 'SCOPE_FLAG=%q\n' "$scope_flag" + printf 'SECTION_FLAG=%q\n' "$section_flag" + printf 'REASON=%q\n' "$reason" + printf 'BODY_INPUT=%q\n' "$body_input" + printf 'BODY_FILE=%q\n' "$body_file" + printf 'REPO=%q\n' "$repo" + printf 'CWD=%q\n' "$cwd" + printf 'LAST=%q\n' "$last" + printf 'OVERRIDE_USER_CONTENT=%q\n' "$override" + # Arrays via space-separated quoted form (caller: eval "TARGETS=( $TARGETS )") + local targets_quoted="" + for t in "${targets[@]+"${targets[@]}"}"; do + targets_quoted+=" $(printf '%q' "$t")" + done + printf 'TARGETS=(%s )\n' "$targets_quoted" +} + +# ─────────────────────────────────────────────────────────────────────── +# Subcommand: validate-target +# ─────────────────────────────────────────────────────────────────────── +# Enforces R5: fetch comment's author_association + login, refuse if non-OWNER +# and non-bot unless --override-user-content was passed. +# +# Usage: validate-target [--print-author] +# override-flag: "true" / "false" (from parse-args OVERRIDE_USER_CONTENT) +# +# Exit codes: +# 0 — proceed (OWNER, bot, or override active) +# 4 — R5 refuse +validate_target_subcmd() { + local comment_id="${1:-}" + local repo="${2:-}" + local override="${3:-false}" + + if [ -z "$comment_id" ] || [ -z "$repo" ]; then + echo "ERROR: validate-target requires " >&2 + return 2 + fi + + # Fetch author info (single API call, two fields). + # Test hook: when IDD_EDIT_HELPER_GH_MOCK is set, read mock JSON from that + # path instead of calling gh api. Format expected: + # {"login": "alice", "assoc": "OWNER"} + # Closes #154 verify Round 1 H3 — adds validate-target unit-test surface. + local author_data + if [ -n "${IDD_EDIT_HELPER_GH_MOCK:-}" ]; then + if [ ! -r "$IDD_EDIT_HELPER_GH_MOCK" ]; then + echo "ERROR: IDD_EDIT_HELPER_GH_MOCK file not readable: $IDD_EDIT_HELPER_GH_MOCK" >&2 + return 1 + fi + author_data=$(cat "$IDD_EDIT_HELPER_GH_MOCK") + else + author_data=$(gh api "repos/$repo/issues/comments/$comment_id" \ + --jq '{login: .user.login, assoc: .author_association}' 2>&1) || { + echo "ERROR: gh api fetch failed for comment $comment_id: $author_data" >&2 + return 1 + } + fi + + local author_login author_assoc + author_login=$(echo "$author_data" | jq -r '.login // ""') + author_assoc=$(echo "$author_data" | jq -r '.assoc // ""') + + # M6 guard: refuse if either field is null (malformed gh API response) + if [ "$author_login" = "" ] || [ "$author_assoc" = "" ]; then + echo "ERROR: gh api response missing required fields (login/author_association) for comment $comment_id" >&2 + return 1 + fi + + # Known-bot allowlist: any *[bot] suffix matches (github-actions[bot], + # dependabot[bot], renovate[bot], custom org bots, etc.). M3 cleanup + # of dead-code redundant patterns from Round 1. + # NOTE: non-[bot]-suffixed bot accounts (e.g. "myorg-bot", "coderabbitai") + # currently bypass this allowlist → fall through to OWNER/override path. + # Document this limitation; future enhancement could read explicit allowlist + # from config (out of scope for #154). + case "$author_login" in + *\[bot\]) + echo "✓ Bot author: $author_login (skip R5 gate)" >&2 + return 0 + ;; + esac + + # OWNER passes + if [ "$author_assoc" = "OWNER" ]; then + echo "✓ OWNER author: $author_login (skip R5 gate)" >&2 + return 0 + fi + + # Non-OWNER non-bot: require override + if [ "$override" = "true" ]; then + echo "⚠ Override active: editing $author_login ($author_assoc) content" >&2 + return 0 + fi + + # Refuse with R5 message + # shellcheck disable=SC2059 + printf "$R5_MSG_TEMPLATE\n" "$comment_id" "$author_login" "$author_assoc" >&2 + return 4 +} + +# ─────────────────────────────────────────────────────────────────────── +# Subcommand: section-replace +# ─────────────────────────────────────────────────────────────────────── +# Replace a named section within a markdown file using awk-getline pattern. +# This closes R3 C3 (BSD awk -v cannot handle newline in value) by reading +# replacement body via getline from a file. +# +# Usage: section-replace +# input-file: original markdown +# heading-line: exact section heading (e.g. "### Sister Concerns Filed") +# Section ends at next heading of same OR higher level (e.g. +# "### Foo" ends at next "###" or "##" or "#"). EOF also ends. +# replacement-file: file containing replacement body (preserved newlines) +# +# Output: modified markdown to stdout +# Exit: 0 on success, 1 if heading not found in input +section_replace_subcmd() { + local input_file="${1:-}" + local heading_line="${2:-}" + local replacement_file="${3:-}" + + if [ -z "$input_file" ] || [ -z "$heading_line" ] || [ -z "$replacement_file" ]; then + echo "ERROR: section-replace requires " >&2 + return 2 + fi + if [ ! -r "$input_file" ]; then + echo "ERROR: input-file not readable: $input_file" >&2 + return 5 + fi + if [ ! -r "$replacement_file" ]; then + echo "ERROR: replacement-file not readable: $replacement_file" >&2 + return 5 + fi + + # Strip CRLF from input + replacement (closes #154 verify Round 1 H4 part 2: + # CRLF input broke grep -Fxq exact match: `## Foo\r` != `## Foo`). + local clean_input="/tmp/idd-edit-clean-input-$$" + local clean_repl="/tmp/idd-edit-clean-repl-$$" + tr -d '\r' < "$input_file" > "$clean_input" + tr -d '\r' < "$replacement_file" > "$clean_repl" + + # Verify heading exists in cleaned input + if ! grep -Fxq "$heading_line" "$clean_input"; then + echo "ERROR: heading not found in input: $heading_line" >&2 + rm -f "$clean_input" "$clean_repl" + return 1 + fi + + # Determine heading level (count leading #s). + # Closes #154 verify Round 1 H4 part 1: previous `wc -c` included trailing + # newline → off-by-one (`## Foo` got level=3 instead of 2). Use awk char-by-char. + local heading_level + heading_level=$(printf '%s' "$heading_line" | awk '{ + n = 0 + for (i = 1; i <= length($0); i++) { + if (substr($0, i, 1) == "#") n++ + else break + } + print n + }') + + # Sanity: 1-6 + if [ "$heading_level" -lt 1 ] || [ "$heading_level" -gt 6 ]; then + echo "ERROR: invalid heading level ($heading_level) for: $heading_line" >&2 + rm -f "$clean_input" "$clean_repl" + return 2 + fi + + # awk pattern: + # When heading line matches, enter "skipping" mode, getline-print replacement + # "Skipping" ends at next heading of level <= heading_level + # Outside skipping → print line as-is + awk -v target="$heading_line" \ + -v level="$heading_level" \ + -v repl_file="$clean_repl" ' + BEGIN { skipping = 0; replaced = 0 } + # Detect heading line by exact match + $0 == target && replaced == 0 { + # Print the heading itself + print $0 + # Print replacement body via getline + while ((getline new_line < repl_file) > 0) { + print new_line + } + close(repl_file) + skipping = 1 + replaced = 1 + next + } + # If we are skipping, check for next heading of same/higher level + skipping == 1 { + # Match next heading: ^#{1,level} followed by space + if (match($0, "^#{1," level "}[[:space:]]")) { + skipping = 0 + print $0 + next + } + # Still in old section → skip + next + } + # Normal line outside skipping + { print $0 } + ' "$clean_input" + local awk_exit=$? + + rm -f "$clean_input" "$clean_repl" + return $awk_exit +} + +# ─────────────────────────────────────────────────────────────────────── +# Subcommand: emit-audit-marker +# ─────────────────────────────────────────────────────────────────────── +# Emit a single-line HTML-comment audit marker with `-->` tokens stripped +# from value content. Centralizes escaping so all 3 modes (--replace / +# --append / --prepend-note) and the override pathway can share one +# safe code path. +# +# Closes #154 verify finding C3: +# `` +# interpolated $REASON raw → `--reason='legit --> ` collapsed to `--\\>` (visual) so the HTML +# comment cannot be terminated early by attacker-controlled input. +emit_audit_marker_subcmd() { + local kind="${1:-}" + shift || true + + if [ -z "$kind" ]; then + echo "ERROR: emit-audit-marker requires arg" >&2 + return 2 + fi + + case "$kind" in + edit|override) ;; + *) + echo "ERROR: emit-audit-marker kind must be 'edit' or 'override' (got: $kind)" >&2 + return 2 + ;; + esac + + local marker=" tokens (collapse to --\> visual placeholder) + val="${val//-->/-\\>}" + # Strip newlines (markers must be single-line) + val="${val//$'\n'/ }" + # Strip control characters (anything < 0x20 except space) + val=$(printf '%s' "$val" | tr -d '\000-\010\013\014\016-\037') + marker="$marker $key=\"$val\"" + [ "$key" = "date" ] && has_date="true" + ;; + *) + echo "ERROR: emit-audit-marker arg must be key=value (got: $kv)" >&2 + return 2 + ;; + esac + done + + # Auto-add date if not provided + if [ "$has_date" = "false" ]; then + marker="$marker date=\"$(date +%Y-%m-%d)\"" + fi + + marker="$marker -->" + printf '%s\n' "$marker" +} + +# ─────────────────────────────────────────────────────────────────────── +# Dispatch +# ─────────────────────────────────────────────────────────────────────── +case "$SUBCMD" in + parse-args) + parse_args_subcmd "$@" + ;; + validate-target) + validate_target_subcmd "$@" + ;; + section-replace) + section_replace_subcmd "$@" + ;; + emit-audit-marker) + emit_audit_marker_subcmd "$@" + ;; + -h|--help|help|"") + cat <&2 +idd-edit-helper.sh — Runtime support for /idd-edit skill. + +Subcommands: + parse-args + Parse /idd-edit flags, emit shell-eval'able assignments to stdout. + + validate-target + Enforce R5: refuse non-OWNER non-bot unless override flag is "true". + + section-replace + Replace named markdown section via awk-getline (BSD/gnu safe). + + emit-audit-marker ... + Emit HTML-comment audit marker with --> tokens stripped from values. + Closes #154 verify finding C3 — REASON HTML-comment-out injection. + Kinds: edit / override. + +Exit codes: + 0=success, 1=generic, 2=usage, 3=R4-refuse, 4=R5-refuse, 5=unreadable-file +EOF + [ "$SUBCMD" = "" ] && exit 2 || exit 0 + ;; + *) + echo "ERROR: unknown subcommand: $SUBCMD" >&2 + echo "Run with --help for usage." >&2 + exit 2 + ;; +esac diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/args.txt new file mode 100644 index 0000000..88fb48d --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/args.txt @@ -0,0 +1,4 @@ +comment:123 +--replace +--scope=whole-comment +--body=hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_stdout_contains.txt new file mode 100644 index 0000000..0417bc7 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/expected_stdout_contains.txt @@ -0,0 +1,3 @@ +SCOPE_FLAG=whole-comment +MODE=--replace +BODY_INPUT=hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/01-scope-eq/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/args.txt new file mode 100644 index 0000000..bf27f6c --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/args.txt @@ -0,0 +1,5 @@ +comment:123 +--replace +--scope +whole-comment +--body=hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_stdout_contains.txt new file mode 100644 index 0000000..1c88293 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/expected_stdout_contains.txt @@ -0,0 +1,2 @@ +SCOPE_FLAG=whole-comment +MODE=--replace diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/02-scope-space/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/args.txt new file mode 100644 index 0000000..eda5bd5 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/args.txt @@ -0,0 +1,3 @@ +comment:123 +--replace +--scope diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_exit.txt new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_exit.txt @@ -0,0 +1 @@ +2 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_stderr_contains.txt new file mode 100644 index 0000000..a5b2755 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/expected_stderr_contains.txt @@ -0,0 +1 @@ +--scope requires value diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/03-scope-last-arg/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/args.txt new file mode 100644 index 0000000..c042e92 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/args.txt @@ -0,0 +1,5 @@ +comment:123 +--replace +--scope +--body +hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_exit.txt new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_exit.txt @@ -0,0 +1 @@ +2 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_stderr_contains.txt new file mode 100644 index 0000000..b3c3531 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/expected_stderr_contains.txt @@ -0,0 +1 @@ +--scope value cannot start with '--' diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/04-scope-eats-next-flag/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/args.txt new file mode 100644 index 0000000..e437cb8 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/args.txt @@ -0,0 +1,4 @@ +comment:123 +--replace +--scope=whole-comment +--body-file=/tmp/idd-edit-nonexistent-xyz-99999.md diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_exit.txt new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_exit.txt @@ -0,0 +1 @@ +5 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_stderr_contains.txt new file mode 100644 index 0000000..3a42e74 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/expected_stderr_contains.txt @@ -0,0 +1 @@ +--body-file not readable diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/05-body-file-missing/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/args.txt new file mode 100644 index 0000000..27c464d --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/args.txt @@ -0,0 +1,4 @@ +comment:123 +--replace +--scope=whole-comment +--body-file=__FIXTURE_PATH__/multiline_input.md diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_stdout_contains.txt new file mode 100644 index 0000000..84fc5ca --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/expected_stdout_contains.txt @@ -0,0 +1,3 @@ +line 1 +line 2 +backtick diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/multiline_input.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/multiline_input.md new file mode 100644 index 0000000..79215e4 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/multiline_input.md @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 with `backtick` diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/06-multi-line-body/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/args.txt new file mode 100644 index 0000000..0ec4906 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/args.txt @@ -0,0 +1,3 @@ +comment:123 +--append +--body=hello world diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_stdout_contains.txt new file mode 100644 index 0000000..1a1a45c --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/expected_stdout_contains.txt @@ -0,0 +1,2 @@ +BODY_INPUT=hello\ world +MODE=--append diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/07-single-line-body/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/args.txt new file mode 100644 index 0000000..f07be67 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/args.txt @@ -0,0 +1,3 @@ +__FIXTURE_PATH__/input.md +## Foo +__FIXTURE_PATH__/replacement.md diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout.txt new file mode 100644 index 0000000..901ca2c --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout.txt @@ -0,0 +1,7 @@ +# Doc + +## Foo +REPLACED FOO +new line 2 +## Qux +qux content (should NOT be replaced) diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout_contains.txt new file mode 100644 index 0000000..d5f1dfb --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/expected_stdout_contains.txt @@ -0,0 +1,5 @@ +## Foo +REPLACED FOO +new line 2 +## Qux +qux content (should NOT be replaced) diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/input.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/input.md new file mode 100644 index 0000000..cb92eff --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/input.md @@ -0,0 +1,13 @@ +# Doc + +## Foo +foo content + +### Bar +bar content under foo + +### Baz +baz content under foo + +## Qux +qux content (should NOT be replaced) diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/replacement.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/replacement.md new file mode 100644 index 0000000..e4fe559 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/replacement.md @@ -0,0 +1,2 @@ +REPLACED FOO +new line 2 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/subcmd.txt new file mode 100644 index 0000000..b0f7b20 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/08-section-with-subs/subcmd.txt @@ -0,0 +1 @@ +section-replace diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/args.txt new file mode 100644 index 0000000..f07be67 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/args.txt @@ -0,0 +1,3 @@ +__FIXTURE_PATH__/input.md +## Foo +__FIXTURE_PATH__/replacement.md diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_stdout_contains.txt new file mode 100644 index 0000000..7c8cefb --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/expected_stdout_contains.txt @@ -0,0 +1,2 @@ +## Foo +REPLACED TO EOF diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/input.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/input.md new file mode 100644 index 0000000..3c9a9eb --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/input.md @@ -0,0 +1,4 @@ +# Doc + +## Foo +some content diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/replacement.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/replacement.md new file mode 100644 index 0000000..c2e62a7 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/replacement.md @@ -0,0 +1 @@ +REPLACED TO EOF diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/subcmd.txt new file mode 100644 index 0000000..b0f7b20 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/09-section-no-closing-heading/subcmd.txt @@ -0,0 +1 @@ +section-replace diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/args.txt new file mode 100644 index 0000000..ba25e7a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/args.txt @@ -0,0 +1,3 @@ +comment:123 +--replace +--body=hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_exit.txt new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_exit.txt @@ -0,0 +1 @@ +3 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_stderr_contains.txt new file mode 100644 index 0000000..ff56c1c --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/expected_stderr_contains.txt @@ -0,0 +1,2 @@ +--replace requires --scope whole-comment OR --section +Requirement 4 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/10-replace-no-scope/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/args.txt new file mode 100644 index 0000000..23b27e6 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/args.txt @@ -0,0 +1,3 @@ +comment:123 +--append +--body=test diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_stdout_contains.txt new file mode 100644 index 0000000..76021dd --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/expected_stdout_contains.txt @@ -0,0 +1 @@ +OVERRIDE_USER_CONTENT=false diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/11-non-owner-no-override/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/args.txt new file mode 100644 index 0000000..053616a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/args.txt @@ -0,0 +1,4 @@ +comment:123 +--append +--body=test +--override-user-content diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_exit.txt new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_exit.txt @@ -0,0 +1 @@ +2 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_stderr_contains.txt new file mode 100644 index 0000000..c27a8f9 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/expected_stderr_contains.txt @@ -0,0 +1 @@ +--override-user-content requires --reason diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/12-non-owner-with-override/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/args.txt new file mode 100644 index 0000000..578ef8d --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/args.txt @@ -0,0 +1,4 @@ +comment:123 +--prepend-note +--reason=errata clarification per IDD discipline +--override-user-content diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_stdout_contains.txt new file mode 100644 index 0000000..5b7c37f --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/expected_stdout_contains.txt @@ -0,0 +1,2 @@ +OVERRIDE_USER_CONTENT=true +REASON=errata\ clarification\ per\ IDD\ discipline diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/13-errata-refuse-message/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/args.txt new file mode 100644 index 0000000..386ca5a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/args.txt @@ -0,0 +1,4 @@ +comment:123 +--replace +--scope=typo +--body=hello diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_exit.txt new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_exit.txt @@ -0,0 +1 @@ +3 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_stderr_contains.txt new file mode 100644 index 0000000..86e1b0e --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/expected_stderr_contains.txt @@ -0,0 +1,2 @@ +--scope value must be 'whole-comment' +typo diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/subcmd.txt new file mode 100644 index 0000000..6595370 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/14-replace-scope-invalid/subcmd.txt @@ -0,0 +1 @@ +parse-args diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/args.txt new file mode 100644 index 0000000..718139e --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/args.txt @@ -0,0 +1,3 @@ +123 +PsychQuant/issue-driven-development +false diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_stderr_contains.txt new file mode 100644 index 0000000..05dc13f --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/expected_stderr_contains.txt @@ -0,0 +1 @@ +OWNER author: alice diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/mock_owner.json b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/mock_owner.json new file mode 100644 index 0000000..7f583a5 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/mock_owner.json @@ -0,0 +1 @@ +{"login":"alice","assoc":"OWNER"} diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/subcmd.txt new file mode 100644 index 0000000..99522a0 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/15-validate-owner-passes/subcmd.txt @@ -0,0 +1 @@ +validate-target diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/args.txt new file mode 100644 index 0000000..4cf2e53 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/args.txt @@ -0,0 +1,3 @@ +456 +PsychQuant/issue-driven-development +false diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_stderr_contains.txt new file mode 100644 index 0000000..6e38ba7 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/expected_stderr_contains.txt @@ -0,0 +1 @@ +Bot author: github-actions[bot] diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/mock_bot.json b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/mock_bot.json new file mode 100644 index 0000000..5d1c5e2 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/mock_bot.json @@ -0,0 +1 @@ +{"login":"github-actions[bot]","assoc":"NONE"} diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/subcmd.txt new file mode 100644 index 0000000..99522a0 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/16-validate-bot-passes/subcmd.txt @@ -0,0 +1 @@ +validate-target diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/args.txt new file mode 100644 index 0000000..01a39c6 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/args.txt @@ -0,0 +1,3 @@ +789 +PsychQuant/issue-driven-development +false diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_exit.txt new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_exit.txt @@ -0,0 +1 @@ +4 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_stderr_contains.txt new file mode 100644 index 0000000..1e30258 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/expected_stderr_contains.txt @@ -0,0 +1,3 @@ +external-contributor +verbatim-preserve per IC_R007 +--override-user-content diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/mock_user.json b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/mock_user.json new file mode 100644 index 0000000..2d9fd2b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/mock_user.json @@ -0,0 +1 @@ +{"login":"external-contributor","assoc":"CONTRIBUTOR"} diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/subcmd.txt new file mode 100644 index 0000000..99522a0 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/17-validate-non-owner-refuses/subcmd.txt @@ -0,0 +1 @@ +validate-target diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/args.txt new file mode 100644 index 0000000..4a6193a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/args.txt @@ -0,0 +1,3 @@ +789 +PsychQuant/issue-driven-development +true diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_stderr_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_stderr_contains.txt new file mode 100644 index 0000000..4cf9056 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/expected_stderr_contains.txt @@ -0,0 +1,2 @@ +Override active +external-contributor diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/mock_user.json b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/mock_user.json new file mode 100644 index 0000000..2d9fd2b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/mock_user.json @@ -0,0 +1 @@ +{"login":"external-contributor","assoc":"CONTRIBUTOR"} diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/subcmd.txt new file mode 100644 index 0000000..99522a0 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/18-validate-non-owner-with-override/subcmd.txt @@ -0,0 +1 @@ +validate-target diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/args.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/args.txt new file mode 100644 index 0000000..f07be67 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/args.txt @@ -0,0 +1,3 @@ +__FIXTURE_PATH__/input.md +## Foo +__FIXTURE_PATH__/replacement.md diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_exit.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_exit.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_exit.txt @@ -0,0 +1 @@ +0 diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_stdout_contains.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_stdout_contains.txt new file mode 100644 index 0000000..36a630b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/expected_stdout_contains.txt @@ -0,0 +1,3 @@ +## Foo +REPLACED +## Bar diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/input.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/input.md new file mode 100644 index 0000000..83faf50 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/input.md @@ -0,0 +1,4 @@ +## Foo +old content +## Bar +bar content diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/replacement.md b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/replacement.md new file mode 100644 index 0000000..82a618b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/replacement.md @@ -0,0 +1 @@ +REPLACED diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/subcmd.txt b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/subcmd.txt new file mode 100644 index 0000000..b0f7b20 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/fixtures/19-section-replace-crlf/subcmd.txt @@ -0,0 +1 @@ +section-replace diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit/test.sh b/plugins/issue-driven-dev/scripts/tests/idd-edit/test.sh new file mode 100755 index 0000000..3857389 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit/test.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Test runner for .claude/scripts/idd-edit-helper.sh +# +# Each fixture in fixtures// provides: +# - subcmd.txt — subcommand to run (parse-args / validate-target / section-replace) +# - args.txt — args to pass (one per line; supports __FIXTURE_PATH__ placeholder) +# - expected_exit.txt — expected exit code +# - expected_stderr_contains.txt (optional) — substrings that MUST appear in stderr +# - expected_stdout.txt (optional) — exact stdout match (only for section-replace) +# - expected_stdout_contains.txt (optional) — substrings that MUST appear in stdout +# +# Exit 0 if all tests pass; 1 otherwise. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# Path: plugins/issue-driven-dev/scripts/tests/idd-edit/ → 5 levels deep +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +TARGET_SCRIPT="$REPO_ROOT/plugins/issue-driven-dev/scripts/idd-edit-helper.sh" +FIXTURES_DIR="$SCRIPT_DIR/fixtures" + +if [ ! -x "$TARGET_SCRIPT" ]; then + echo "ERROR: target script not executable: $TARGET_SCRIPT" >&2 + exit 1 +fi + +PASS=0 +FAIL=0 +FAILED_TESTS=() + +for fixture in "$FIXTURES_DIR"/*/; do + name=$(basename "$fixture") + fixture_abs=$(cd "$fixture" && pwd) + + if [ ! -f "$fixture/subcmd.txt" ] || [ ! -f "$fixture/args.txt" ] || [ ! -f "$fixture/expected_exit.txt" ]; then + echo "SKIP $name (missing subcmd.txt/args.txt/expected_exit.txt)" + continue + fi + + subcmd=$(cat "$fixture/subcmd.txt") + expected_exit=$(cat "$fixture/expected_exit.txt" | tr -d '[:space:]') + + # Build args array — read one line per arg, substitute placeholder + args=() + while IFS= read -r line || [ -n "$line" ]; do + line="${line//__FIXTURE_PATH__/$fixture_abs}" + args+=("$line") + done < "$fixture/args.txt" + + # Auto-discover mock_*.json file for validate-target fixtures (closes #154 H3). + # When fixture dir contains a mock_*.json, run target with + # IDD_EDIT_HELPER_GH_MOCK pointing at it (helper reads mock instead of gh api). + # Use shell glob (with nullglob via set + array) rather than `ls | head` pipe + # (pipefail makes ls-no-match abort the script). + mock_env="" + for cand in "$fixture"/mock_*.json; do + if [ -f "$cand" ]; then + mock_env="IDD_EDIT_HELPER_GH_MOCK=$cand" + break + fi + done + + # Run target script, capture stdout + stderr + exit. + # Script-level: `set -uo pipefail` only (NO `set -e`) — fixtures testing + # refuse paths expect non-zero exit codes; $? captures regardless. + actual_stdout=$(env $mock_env "$TARGET_SCRIPT" "$subcmd" "${args[@]+"${args[@]}"}" 2>"/tmp/idd-edit-test-stderr-$$") + actual_exit=$? + actual_stderr=$(cat "/tmp/idd-edit-test-stderr-$$") + rm -f "/tmp/idd-edit-test-stderr-$$" + + # Assertions + local_pass=true + failure_reasons=() + + # Exit code + if [ "$actual_exit" != "$expected_exit" ]; then + local_pass=false + failure_reasons+=("exit code: expected=$expected_exit actual=$actual_exit") + fi + + # Stderr contains (use -- to handle needles starting with --) + if [ -f "$fixture/expected_stderr_contains.txt" ]; then + while IFS= read -r needle || [ -n "$needle" ]; do + [ -z "$needle" ] && continue + if ! echo "$actual_stderr" | grep -qF -- "$needle"; then + local_pass=false + failure_reasons+=("stderr missing: $needle") + fi + done < "$fixture/expected_stderr_contains.txt" + fi + + # Stdout exact + if [ -f "$fixture/expected_stdout.txt" ]; then + expected_stdout=$(cat "$fixture/expected_stdout.txt") + if [ "$actual_stdout" != "$expected_stdout" ]; then + local_pass=false + failure_reasons+=("stdout mismatch (see diff below)") + fi + fi + + # Stdout contains (use -- to handle needles starting with --) + if [ -f "$fixture/expected_stdout_contains.txt" ]; then + while IFS= read -r needle || [ -n "$needle" ]; do + [ -z "$needle" ] && continue + if ! echo "$actual_stdout" | grep -qF -- "$needle"; then + local_pass=false + failure_reasons+=("stdout missing: $needle") + fi + done < "$fixture/expected_stdout_contains.txt" + fi + + if [ "$local_pass" = "true" ]; then + echo "PASS $name" + PASS=$((PASS + 1)) + else + echo "FAIL $name" + for r in "${failure_reasons[@]}"; do + echo " → $r" + done + # Diff for visibility on stdout mismatch + if [ -f "$fixture/expected_stdout.txt" ] && [ "$actual_stdout" != "$(cat "$fixture/expected_stdout.txt")" ]; then + echo " expected stdout:" + sed 's/^/ /' "$fixture/expected_stdout.txt" + echo " actual stdout:" + echo "$actual_stdout" | sed 's/^/ /' + fi + echo " stderr:" + echo "$actual_stderr" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + FAILED_TESTS+=("$name") + fi +done + +echo "" +echo "================================" +echo "Results: $PASS passed, $FAIL failed" +if [ "$FAIL" -gt 0 ]; then + echo "Failed tests:" + for t in "${FAILED_TESTS[@]}"; do + echo " - $t" + done + exit 1 +fi +exit 0 diff --git a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md index 03dc8d9..76edee6 100644 --- a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md @@ -281,12 +281,34 @@ This notice concerns [comment {target-comment}](https://github.com/{repo}/issues 建立 errata comment 後,**自動呼叫 idd-edit** 在 target comment 頂部加警示(prepend-note mode): ```bash -# 自動執行: -/idd-edit comment:{target-comment} --prepend-note --reason="See errata at " +# 自動執行(OWNER comment 或 bot comment): +IDD_CALLER=idd-comment-errata \ + /idd-edit comment:{target-comment} --prepend-note --reason="See errata at " ``` 這讓 target comment 讀者看到「⚠️ 本 comment 已有 errata(下方)」的警示。 +**v2.75.0+ (#154) R5 行為**:當 target comment 是 user-authored(非 OWNER 非 bot)時,`/idd-edit` 會 refuse with exit code 4 + 印 helpful message(per [#154](https://github.com/PsychQuant/issue-driven-development/issues/154) D2 decision — 不 auto-override per IC_R007 user-authored-intent spirit)。 處理: + +```bash +# Auto-call 偵測 exit code 4 → idd-comment 顯示 hint: +if [ $EDIT_EXIT -eq 4 ]; then + cat <&2 + +Errata target comment $TARGET_COMMENT is user-authored. +Per IDD R5 discipline (rules/append-vs-modify.md), modifying user content requires explicit consent. + +Manually run: + /idd-edit comment:$TARGET_COMMENT --prepend-note \\ + --override-user-content \\ + --reason='errata clarification per IDD discipline — see new errata at $NEW_ERRATA_URL' +EOF + # errata comment 本身已成功 post — 只 prepend-note marker 需 user 顯式 consent +fi +``` + +完整 R4/R5 規範 + override pathway 見 [`/idd-edit` SKILL.md](../idd-edit/SKILL.md) `## Runtime gates (#154, v2.75.0+)`。 + ### Step 3.5: Verify mentions (v2.32.0+) Post 前最後一道防線: diff --git a/plugins/issue-driven-dev/skills/idd-edit/SKILL.md b/plugins/issue-driven-dev/skills/idd-edit/SKILL.md index 5a09b05..14e0a3b 100644 --- a/plugins/issue-driven-dev/skills/idd-edit/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-edit/SKILL.md @@ -6,7 +6,7 @@ description: | 支援 batch mode(v2.34.0+):多個 comment 套同一段 edit(如 `comment:NNN comment:MMM --replace --body '...'`),每個 comment 仍 per-confirm。 Use when: 補既有 comment 說明(如圖片下方解釋)、修 typo、標示「此 comment 已被後續 errata 修正」。 防止的失敗:手動 `gh api PATCH` 字串 escape 錯誤、誤覆蓋未 backup 的原內容。 -argument-hint: "comment:[ comment:...]|#issue --last [--append|--replace|--prepend-note] [--body=\"...\"] (multi-comment = batch)" +argument-hint: "comment:[ comment:...]|#issue --last (--append|--replace [--scope whole-comment|--section ]|--prepend-note) [--body=... | --body-file=...] [--reason=...] [--override-user-content --reason='...']" allowed-tools: - Bash(gh:*) - Read @@ -23,7 +23,12 @@ allowed-tools: ## Batch mode(v2.34.0+) -`idd-edit comment:NNN comment:MMM --replace --body '...'` 把同一段內容套到多個 comment。Edit 是破壞性動作,batch 把破壞範圍放大 N 倍 — preview + per-comment confirm 仍照舊(不允許 `--yes-to-all`),但每個 confirm 後就推進,不需要 N 次重打命令。 +`idd-edit comment:NNN comment:MMM --replace --scope whole-comment --body '...'` 把同一段內容套到多個 comment。Edit 是破壞性動作,batch 把破壞範圍放大 N 倍 — preview + per-comment confirm 仍照舊(不允許 `--yes-to-all`),但每個 confirm 後就推進,不需要 N 次重打命令。 + +**v2.75.0+ R4/R5 在 batch mode 下 per-target 評估**: +- R4 (`--replace` 必要 scope) 一次套用全 batch +- R5 (author check) **per-target**:N 個 targets 若 mixed OWNER + non-OWNER,目前 single-target 邏輯 refuse 第一個 non-OWNER target 並印 hint;`--override-user-content` 套用所有 targets(全 batch 啟用 override) +- 完整 batch + R5 semantics (per-comment refuse vs transactional abort vs pre-flight scan) 設計 deferred to **[#158](https://github.com/PsychQuant/issue-driven-development/issues/158)** follow-up 完整契約見 [batch-and-cluster.md](../../references/batch-and-cluster.md)。罕見場景:跨 issue 的 typo 統一修、補同一段 errata note、把多個 stale comment 統一標 deprecated。 @@ -47,11 +52,31 @@ idd-issue source.docx # auto-trigger when source contains ≥2 findings ## 三種 Edit Mode -| Mode | 動作 | 原 body | 適用 | -|------|------|--------|------| -| `--append` | 在末尾加 `---\n**Edit YYYY-MM-DD**: {reason}\n\n{body}` | 保留 | 補充 / 更正(保留歷史) | -| `--replace` | 完全替換 body | 寫入 backup 檔 | 大幅改寫(如補圖說明) | -| `--prepend-note` | 在最上方加 `> ⚠️ {reason}\n\n---\n\n` | 保留 | 標示「此 comment 已過時」(errata flow 用) | +| Mode | 動作 | 原 body | 適用 | 強制 flags | +|------|------|--------|------|-----------| +| `--append` | 在末尾加 `---\n**Edit YYYY-MM-DD**: {reason}\n\n{body}` | 保留 | 補充 / 更正(保留歷史) | `--reason` | +| `--replace` | 完全替換 body 或指定 section | 寫入 backup 檔 | 大幅改寫(如補圖說明) | `--reason` + **`--scope whole-comment` OR `--section `**(R4 enforced)| +| `--prepend-note` | 在最上方加 `> ⚠️ {reason}\n\n---\n\n` | 保留 | 標示「此 comment 已過時」(errata flow 用) | `--reason` | + +## 動作分類 (per [`#150` action-scoped modify discipline](../../rules/append-vs-modify.md)) + +| Mode | Category | Scope contract | +|------|----------|---------------| +| `--append` | `audit-block-append` | trailing block,inherently bounded — no `--scope` needed | +| `--replace --scope whole-comment` | `bounded-section-replace` | scope = whole comment(explicit acknowledgment of full overwrite)| +| `--replace --section ` | `bounded-section-replace` | scope = named markdown subsection only | +| `--prepend-note` | `audit-block-append` | leading errata marker,inherently bounded — no `--scope` needed | + +## Runtime gates (#154, v2.75.0+) + +`/idd-edit` enforces 2 SHALL requirements at runtime via `.claude/scripts/idd-edit-helper.sh` (extracted parser + validator per #154 to avoid R1/R2/R3-style bash inline bugs): + +| Requirement | Gate | Refuse code | +|-------------|------|-------------| +| **R4** (spec) | `--replace` without `--scope`/`--section` → refuse | exit 3 | +| **R5** (spec) | Comment author non-OWNER non-bot,無 `--override-user-content --reason="..."` → refuse | exit 4 | + +Override pathway:`--override-user-content --reason=""` 顯式同意修改 user content。 Audit marker `` 自動 append。 ## Configuration @@ -95,24 +120,80 @@ TaskCreate(name="verify_and_report", description="re-fetch comment 比對寫入 --- -### Step 1: Parse arguments + resolve target +### Step 1: Parse arguments via helper (R4 gate enforced) + +**v2.75.0+ (#154)**: Parser extracted to `.claude/scripts/idd-edit-helper.sh parse-args` per [`#154`](https://github.com/PsychQuant/issue-driven-development/issues/154) (R1/R2/R3 bash-inline failure on PR #153 → 3-iteration verify cycle showed AI-generated inline parsers introduce bugs each pass). Helper provides positional shift + missing-value guards + eq-form support + body-file readability check + R4 gate refuse + R5 override-reason pair guard, all unit-tested via `.claude/scripts/tests/idd-edit/` 13 fixtures. + +```bash +# Parse + R4 gate (refuse if --replace lacks --scope/--section) +# CRITICAL: split stdout (eval-safe assignments) from stderr (diagnostic text). +# Closes #154 verify Round 1 H2 — previously used `2>&1` which mixed +# stderr (potentially containing $() from cat-on-directory failure etc.) +# into the eval input, defeating printf %q quoting safety. +PARSE_ERR_FILE="/tmp/idd-edit-parse-err-$$" +PARSE_OUT=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh parse-args "$@" 2>"$PARSE_ERR_FILE") +PARSE_EXIT=$? +PARSE_ERR=$(cat "$PARSE_ERR_FILE") +rm -f "$PARSE_ERR_FILE" + +case $PARSE_EXIT in + 0) eval "$PARSE_OUT" ;; # imports MODE/SCOPE_FLAG/SECTION_FLAG/BODY_INPUT/etc. + 3) echo "$PARSE_ERR" >&2; exit 3 ;; # R4 refuse — actionable message + *) echo "$PARSE_ERR" >&2; exit $PARSE_EXIT ;; +esac + +# Resolve target from TARGETS array +for target in "${TARGETS[@]}"; do + case "$target" in + comment:*) + COMMENT_ID="${target#comment:}" + ;; + \#*) + ISSUE_NUMBER="${target#\#}" + # Validate issue number is numeric before substitution into gh api URL + [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid issue number: $ISSUE_NUMBER" >&2; exit 2; } + if [ "$LAST" = "true" ]; then + COMMENT_ID=$(gh api repos/$REPO/issues/$ISSUE_NUMBER/comments --jq '.[-1].id') + else + gh api repos/$REPO/issues/$ISSUE_NUMBER/comments \ + --jq '.[] | "\(.id) | \(.created_at) | \(.body | .[0:80])"' + # Use AskUserQuestion to select + fi + ;; + esac + + # R4/R5 security gate: COMMENT_ID MUST be numeric before any URL / filename substitution. + # Closes #154 verify finding C2 — unsanitized comment_id flows into: + # - gh api repos/.../comments/$COMMENT_ID (REST path traversal) + # - /tmp/idd-edit-repl-${COMMENT_ID}.md (arbitrary local file write via embedded `/` + `..`) + [[ "$COMMENT_ID" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid comment ID (must be numeric): $COMMENT_ID" >&2; exit 2; } + + # ... continue to Step 1.5 + Step 2 for each COMMENT_ID +done +``` + +### Step 1.5: Validate target (R5 author gate) + +**v2.75.0+ (#154)**: R5 gate refuses modifications to user-authored comments (non-OWNER non-bot) unless `--override-user-content --reason="..."` provided。 Helper does single `gh api` call,checks `author_association` + `user.login`,applies bot allowlist (`*[bot]` pattern + `OWNER` passthrough)。 ```bash -# 解析 target -if [[ "$ARG" == comment:* ]]; then - COMMENT_ID=${ARG#comment:} -elif [[ "$ARG" == \#* ]]; then - ISSUE_NUMBER=${ARG#\#} - if [[ "$LAST" == "true" ]]; then - # 取最後一個 comment id - COMMENT_ID=$(gh api repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/comments --jq '.[-1].id') - else - # 列出供使用者選 - gh api repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/comments \ - --jq '.[] | "\(.id) | \(.created_at) | \(.body | .[0:80])"' - # 用 AskUserQuestion 選 +bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh \ + validate-target "$COMMENT_ID" "$REPO" "$OVERRIDE_USER_CONTENT" +VALIDATE_EXIT=$? +case $VALIDATE_EXIT in + 0) ;; # proceed (OWNER, bot, or override active) + 4) + # R5 refuse — actionable message already on stderr + # If called from /idd-comment errata flow: print additional helpful hint + if [ "${IDD_CALLER:-}" = "idd-comment-errata" ]; then + echo "" >&2 + echo "Hint: errata target was user-authored; manually run:" >&2 + echo " /idd-edit comment:$COMMENT_ID --prepend-note --override-user-content --reason='errata clarification per IDD discipline'" >&2 fi -fi + exit 4 + ;; + *) exit $VALIDATE_EXIT ;; +esac ``` ### Step 2: Fetch current body + backup @@ -121,7 +202,7 @@ fi mkdir -p /tmp/idd-edit-backup BACKUP_FILE="/tmp/idd-edit-backup/comment-${COMMENT_ID}-$(date +%s).md" -gh api repos/$GITHUB_REPO/issues/comments/$COMMENT_ID --jq '.body' > "$BACKUP_FILE" +gh api repos/$REPO/issues/comments/$COMMENT_ID --jq '.body' > "$BACKUP_FILE" echo "✓ Backup: $BACKUP_FILE" ``` @@ -136,40 +217,85 @@ echo "✓ Backup: $BACKUP_FILE" ### Step 4: Build new body per mode +> **v2.75.0+ (#154)**: audit markers are built via `bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker` — centralizes HTML-comment-escape (`-->` stripping) so attacker-controlled `$REASON` / `$SECTION_FLAG` cannot forge audit trail (closes #154 verify finding C3). + #### Mode: `--append` ```bash +EDIT_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker edit mode=append) NEW_BODY="$(cat $BACKUP_FILE) --- **Edit $(date +%Y-%m-%d)**: $REASON -$APPEND_BODY +$BODY_INPUT + +$EDIT_MARKER" -" +# Append R5 override audit marker if applicable +if [ "$OVERRIDE_USER_CONTENT" = "true" ]; then + OVERRIDE_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker override mode=append reason="$REASON") + NEW_BODY="$NEW_BODY + +$OVERRIDE_MARKER" +fi ``` #### Mode: `--replace` +R4 gate already enforced in Step 1 — `SCOPE_FLAG` or `SECTION_FLAG` is guaranteed non-empty here. + ```bash -NEW_BODY="$REPLACE_BODY +if [ "$SCOPE_FLAG" = "whole-comment" ]; then + # Whole-comment replacement + EDIT_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker edit mode=replace scope=whole-comment backup="$BACKUP_FILE") + NEW_BODY="$BODY_INPUT + +$EDIT_MARKER" +elif [ -n "$SECTION_FLAG" ]; then + # Named section replacement via getline pattern (closes R3 C3 BSD awk newline reject) + REPL_FILE="/tmp/idd-edit-repl-${COMMENT_ID}.md" + echo "$BODY_INPUT" > "$REPL_FILE" + NEW_BODY=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh \ + section-replace "$BACKUP_FILE" "$SECTION_FLAG" "$REPL_FILE") + rm -f "$REPL_FILE" + EDIT_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker edit mode=replace section="$SECTION_FLAG" backup="$BACKUP_FILE") + NEW_BODY="$NEW_BODY + +$EDIT_MARKER" +fi + +# Append R5 override audit marker if applicable +if [ "$OVERRIDE_USER_CONTENT" = "true" ]; then + OVERRIDE_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker override mode=replace reason="$REASON") + NEW_BODY="$NEW_BODY -" +$OVERRIDE_MARKER" +fi ``` -**警告**:`--replace` 完全覆蓋原 body。必顯示 diff preview,使用者確認後才 PATCH。 +**警告**:`--replace` 是 `bounded-section-replace` 動作(per [#150 rule](../../rules/append-vs-modify.md))。必顯示 diff preview,使用者確認後才 PATCH。 R4 強制 `--scope`/`--section` 避免「忘記講動作範圍 = 全 comment overwrite」silent footgun。 #### Mode: `--prepend-note` ```bash +EDIT_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker edit mode=prepend-note) NEW_BODY="> ⚠️ **Edit $(date +%Y-%m-%d)**: $REASON --- $(cat $BACKUP_FILE) -" +$EDIT_MARKER" + +# Append R5 override audit marker if applicable +if [ "$OVERRIDE_USER_CONTENT" = "true" ]; then + OVERRIDE_MARKER=$(bash $CLAUDE_PLUGIN_ROOT/scripts/idd-edit-helper.sh emit-audit-marker override mode=prepend-note reason="$REASON") + NEW_BODY="$NEW_BODY + +$OVERRIDE_MARKER" +fi ``` ### Step 5: Preview + confirm @@ -192,7 +318,7 @@ Confirm edit? (y/n) TMP_BODY_FILE="/tmp/idd-edit-new-${COMMENT_ID}.md" echo "$NEW_BODY" > "$TMP_BODY_FILE" -gh api repos/$GITHUB_REPO/issues/comments/$COMMENT_ID \ +gh api repos/$REPO/issues/comments/$COMMENT_ID \ -X PATCH \ -F body=@"$TMP_BODY_FILE" @@ -203,9 +329,9 @@ rm "$TMP_BODY_FILE" ```bash # Re-fetch 確認 -UPDATED=$(gh api repos/$GITHUB_REPO/issues/comments/$COMMENT_ID --jq '.body' | head -5) +UPDATED=$(gh api repos/$REPO/issues/comments/$COMMENT_ID --jq '.body' | head -5) echo "✓ Comment updated" -echo " URL: $(gh api repos/$GITHUB_REPO/issues/comments/$COMMENT_ID --jq '.html_url')" +echo " URL: $(gh api repos/$REPO/issues/comments/$COMMENT_ID --jq '.html_url')" echo " Backup: $BACKUP_FILE" echo " First 5 lines of new body: $UPDATED" ``` @@ -222,15 +348,16 @@ echo " First 5 lines of new body: $UPDATED" ## 使用範例 -### 補既有 comment 的圖片說明(剛才 #13 的痛點) +### 補既有 comment 的圖片說明(自己發的 comment,whole-comment scope) ``` /idd-edit comment:4241327867 --replace \ + --scope whole-comment \ --body-file=/tmp/new-implementation-summary.md \ --reason="依新 skill 規則補圖下方資料/統計/結論說明" ``` -### 修 typo +### 修 typo(單 section replace) ``` /idd-edit #18 --last --append \ @@ -238,13 +365,34 @@ echo " First 5 lines of new body: $UPDATED" --reason="p-value 計算誤差" ``` -### 標記 comment 已過時(errata flow) +### 修 Diagnosis comment 內某 ### 區段(自己發的 comment) + +``` +/idd-edit comment:4530594011 --replace \ + --section="### Strategy" \ + --body-file=/tmp/new-strategy.md \ + --reason="重新拆 Block A → B 依賴順序" +``` + +### 標記 comment 已過時(errata flow,自己發的 comment) ``` /idd-edit comment:4241327867 --prepend-note \ --reason="See errata at https://github.com/.../issuecomment-4241609713 — Holm 校正後結論不同" ``` +### Errata 修別人發的 comment(需顯式 override) + +R5 強制非 OWNER 非 bot comment 必須 explicit consent。 `/idd-comment --type=errata` auto-call 在 R5 refuse 時印 helpful message,指引你手動加 flag: + +``` +/idd-edit comment:9999999 --prepend-note \ + --override-user-content \ + --reason="errata clarification per IDD discipline — see new errata at " +``` + +Audit marker `` 自動 append 到 body 留 audit trail。 + ## 鐵律 - **原 body 必 backup**:存到 `/tmp/idd-edit-backup/` 保留 7 天(或 session 結束) @@ -253,6 +401,7 @@ echo " First 5 lines of new body: $UPDATED" - **Metadata marker 不覆蓋**:每次 edit 加新 marker,保留 history - **`--replace` 預設 confirm = NO**:破壞性動作不自動 yes - **Log 每次 edit**:顯示 URL 讓使用者能立即 verify +- **`--body-file` path 由 user 負責**(v2.75.0+ #154 H5):helper 不限制路徑,`--body-file=/etc/passwd` 之類 absolute path 會被讀取並進入 PATCH body → public GitHub comment。 Preview gate 是最後一道防線。 未來增強:限制到 repo subtree 或 user-home(out of scope for #154)。 Programmatic caller(`/idd-comment` errata)若接受 user-supplied `--body-file` 必須先 validate path ## 與 idd-comment 的配合 @@ -274,8 +423,8 @@ Target comment 頂部加警示「⚠️ See errata below」 # 列出所有 backup ls -la /tmp/idd-edit-backup/ -# 回復某次 edit -gh api repos/$GITHUB_REPO/issues/comments/ \ +# 回復某次 edit (set REPO=owner/repo first) +gh api repos/$REPO/issues/comments/ \ -X PATCH \ -F body=@/tmp/idd-edit-backup/comment--.md ```