@zigrivers/scaffold 3.32.0 → 3.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +42 -19
  2. package/content/guides/knowledge-freshness/.diagrams/diagram-0.svg +1 -1
  3. package/content/guides/knowledge-freshness/.diagrams/manifest.json +1 -1
  4. package/content/guides/knowledge-freshness/index.html +9 -5
  5. package/content/guides/knowledge-freshness/index.md +5 -4
  6. package/content/guides/multi-agent/index.html +16 -15
  7. package/content/guides/multi-agent/index.md +16 -15
  8. package/content/guides/pipeline/index.html +2 -2
  9. package/content/guides/pipeline/index.md +2 -2
  10. package/content/knowledge/execution/worktree-management.md +4 -4
  11. package/content/knowledge/mcp-server/mcp-authentication.md +100 -0
  12. package/content/knowledge/mcp-server/mcp-deployment-patterns.md +119 -0
  13. package/content/knowledge/mcp-server/mcp-error-handling.md +131 -0
  14. package/content/knowledge/mcp-server/mcp-observability.md +125 -0
  15. package/content/knowledge/mcp-server/mcp-prompt-primitives.md +119 -0
  16. package/content/knowledge/mcp-server/mcp-protocol-fundamentals.md +130 -0
  17. package/content/knowledge/mcp-server/mcp-resource-design.md +111 -0
  18. package/content/knowledge/mcp-server/mcp-sdk-selection.md +136 -0
  19. package/content/knowledge/mcp-server/mcp-testing-strategies.md +127 -0
  20. package/content/knowledge/mcp-server/mcp-tool-design.md +125 -0
  21. package/content/knowledge/mcp-server/mcp-transport-patterns.md +122 -0
  22. package/content/knowledge/mcp-server/mcp-versioning.md +115 -0
  23. package/content/methodology/custom-defaults.yml +2 -0
  24. package/content/methodology/deep.yml +2 -0
  25. package/content/methodology/mcp-server-overlay.yml +88 -0
  26. package/content/methodology/mvp.yml +2 -0
  27. package/content/pipeline/build/multi-agent-resume.md +107 -11
  28. package/content/pipeline/build/multi-agent-start.md +104 -11
  29. package/content/pipeline/build/single-agent-resume.md +74 -8
  30. package/content/pipeline/build/single-agent-start.md +69 -12
  31. package/content/pipeline/environment/git-workflow.md +8 -2
  32. package/content/pipeline/finalization/materialize-plan-to-beads.md +473 -0
  33. package/content/pipeline/foundation/beads.md +6 -0
  34. package/content/pipeline/planning/implementation-plan-review.md +25 -0
  35. package/content/pipeline/planning/implementation-plan.md +75 -1
  36. package/content/pipeline/specification/mcp-tool-resource-contract.md +77 -0
  37. package/dist/cli/commands/adopt.d.ts.map +1 -1
  38. package/dist/cli/commands/adopt.js +33 -1
  39. package/dist/cli/commands/adopt.js.map +1 -1
  40. package/dist/cli/commands/init.d.ts +6 -0
  41. package/dist/cli/commands/init.d.ts.map +1 -1
  42. package/dist/cli/commands/init.js +46 -3
  43. package/dist/cli/commands/init.js.map +1 -1
  44. package/dist/cli/init-flag-families.d.ts +6 -1
  45. package/dist/cli/init-flag-families.d.ts.map +1 -1
  46. package/dist/cli/init-flag-families.js +59 -2
  47. package/dist/cli/init-flag-families.js.map +1 -1
  48. package/dist/cli/init-flag-families.test.js +86 -1
  49. package/dist/cli/init-flag-families.test.js.map +1 -1
  50. package/dist/config/schema.d.ts +2310 -126
  51. package/dist/config/schema.d.ts.map +1 -1
  52. package/dist/config/schema.js +26 -1
  53. package/dist/config/schema.js.map +1 -1
  54. package/dist/config/schema.test.js +75 -2
  55. package/dist/config/schema.test.js.map +1 -1
  56. package/dist/config/validators/index.d.ts.map +1 -1
  57. package/dist/config/validators/index.js +2 -0
  58. package/dist/config/validators/index.js.map +1 -1
  59. package/dist/config/validators/mcp-server.d.ts +4 -0
  60. package/dist/config/validators/mcp-server.d.ts.map +1 -0
  61. package/dist/config/validators/mcp-server.js +37 -0
  62. package/dist/config/validators/mcp-server.js.map +1 -0
  63. package/dist/config/validators/mcp-server.test.d.ts +2 -0
  64. package/dist/config/validators/mcp-server.test.d.ts.map +1 -0
  65. package/dist/config/validators/mcp-server.test.js +47 -0
  66. package/dist/config/validators/mcp-server.test.js.map +1 -0
  67. package/dist/core/assembly/materialize-plan-to-beads-assembly.test.d.ts +2 -0
  68. package/dist/core/assembly/materialize-plan-to-beads-assembly.test.d.ts.map +1 -0
  69. package/dist/core/assembly/materialize-plan-to-beads-assembly.test.js +75 -0
  70. package/dist/core/assembly/materialize-plan-to-beads-assembly.test.js.map +1 -0
  71. package/dist/e2e/project-type-overlays.test.js +83 -0
  72. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  73. package/dist/project/adopt.d.ts.map +1 -1
  74. package/dist/project/adopt.js +3 -1
  75. package/dist/project/adopt.js.map +1 -1
  76. package/dist/project/detectors/coverage.test.js +1 -0
  77. package/dist/project/detectors/coverage.test.js.map +1 -1
  78. package/dist/project/detectors/disambiguate.d.ts.map +1 -1
  79. package/dist/project/detectors/disambiguate.js +6 -1
  80. package/dist/project/detectors/disambiguate.js.map +1 -1
  81. package/dist/project/detectors/disambiguate.test.js +18 -0
  82. package/dist/project/detectors/disambiguate.test.js.map +1 -1
  83. package/dist/project/detectors/index.d.ts.map +1 -1
  84. package/dist/project/detectors/index.js +2 -1
  85. package/dist/project/detectors/index.js.map +1 -1
  86. package/dist/project/detectors/mcp-server.d.ts +4 -0
  87. package/dist/project/detectors/mcp-server.d.ts.map +1 -0
  88. package/dist/project/detectors/mcp-server.js +91 -0
  89. package/dist/project/detectors/mcp-server.js.map +1 -0
  90. package/dist/project/detectors/mcp-server.test.d.ts +2 -0
  91. package/dist/project/detectors/mcp-server.test.d.ts.map +1 -0
  92. package/dist/project/detectors/mcp-server.test.js +115 -0
  93. package/dist/project/detectors/mcp-server.test.js.map +1 -0
  94. package/dist/project/detectors/types.d.ts +6 -2
  95. package/dist/project/detectors/types.d.ts.map +1 -1
  96. package/dist/project/detectors/types.js.map +1 -1
  97. package/dist/project/gitignore.d.ts.map +1 -1
  98. package/dist/project/gitignore.js +4 -0
  99. package/dist/project/gitignore.js.map +1 -1
  100. package/dist/project/gitignore.test.js +1 -0
  101. package/dist/project/gitignore.test.js.map +1 -1
  102. package/dist/types/config.d.ts +8 -1
  103. package/dist/types/config.d.ts.map +1 -1
  104. package/dist/wizard/copy/core.d.ts.map +1 -1
  105. package/dist/wizard/copy/core.js +4 -0
  106. package/dist/wizard/copy/core.js.map +1 -1
  107. package/dist/wizard/copy/index.d.ts.map +1 -1
  108. package/dist/wizard/copy/index.js +2 -0
  109. package/dist/wizard/copy/index.js.map +1 -1
  110. package/dist/wizard/copy/mcp-server.d.ts +3 -0
  111. package/dist/wizard/copy/mcp-server.d.ts.map +1 -0
  112. package/dist/wizard/copy/mcp-server.js +40 -0
  113. package/dist/wizard/copy/mcp-server.js.map +1 -0
  114. package/dist/wizard/copy/types.d.ts +5 -1
  115. package/dist/wizard/copy/types.d.ts.map +1 -1
  116. package/dist/wizard/flags.d.ts +9 -1
  117. package/dist/wizard/flags.d.ts.map +1 -1
  118. package/dist/wizard/questions.d.ts +4 -2
  119. package/dist/wizard/questions.d.ts.map +1 -1
  120. package/dist/wizard/questions.js +37 -0
  121. package/dist/wizard/questions.js.map +1 -1
  122. package/dist/wizard/questions.test.js +107 -0
  123. package/dist/wizard/questions.test.js.map +1 -1
  124. package/dist/wizard/wizard.d.ts +3 -2
  125. package/dist/wizard/wizard.d.ts.map +1 -1
  126. package/dist/wizard/wizard.js +3 -1
  127. package/dist/wizard/wizard.js.map +1 -1
  128. package/package.json +1 -1
@@ -98,18 +98,75 @@ Before writing any code, verify the environment is ready:
98
98
 
99
99
  ### Beads Detection
100
100
 
101
- Check if `.beads/` directory exists.
102
-
103
- **If Beads is configured:**
104
- - Atomically claim the next ready task: `TASK=$(bd ready --claim --json | jq -r '.id')`
105
- - This sets `assignee=$BEADS_ACTOR` (or your git user.name) and `status=in_progress` in a single round-trip — no race window vs other agents.
106
- - If you need a specific task by ID instead, use `bd update <id> --claim`.
107
- - If `bd ready --claim` returns no task, you're done exit the loop.
108
- - Implement following the TDD workflow below
109
- - After PR is merged, run `bd close <id>`
110
- - Repeat (`bd ready --claim --json`) until no tasks remain
111
-
112
- **Without Beads:**
101
+ The implementation plan is materialized into Beads issues by
102
+ `/scaffold:materialize-plan-to-beads` before the build phase. This block is the
103
+ **defensive preflight** that guarantees the tracker is populated and current
104
+ before any work is claimed it never claims against an empty or stale tracker.
105
+
106
+ **Step 1 compute `beads_usable`.** `beads_usable` is true only when **all**
107
+ hold: `.beads/` exists, `bd` is on `PATH`, `bd version` parses to **≥ 1.0.5**
108
+ (using a macOS/BSD-safe numeric compare split major/minor/patch and compare
109
+ numerically, never rely on GNU `sort -V`), and `jq` is on `PATH`. Never write
110
+ `[ -d .beads ] && bd …` as a whole command it returns exit 1 when `.beads/` is
111
+ absent and breaks callers under `set -e`; use an `if`.
112
+
113
+ **Step 2 — route on the decision table** (this is what prevents the "empty
114
+ tracker looks done" bug):
115
+
116
+ | Condition | Action |
117
+ |---|---|
118
+ | `.beads/` **absent** | Non-Beads project → drive the loop from the markdown playbook/plan (see "Markdown fallback" below). Do **not** call `bd`. |
119
+ | `.beads/` present but `beads_usable` is false (`bd`/`jq` missing or `bd` < 1.0.5) | **Fail closed.** Stop and tell the user to install/upgrade `bd` (≥ v1.0.5) and `jq`. Do **not** markdown-fall-back — Beads may already hold execution state, and a markdown re-run would re-execute completed work. |
120
+ | `beads_usable`, but the plan has **no** stable task IDs **and** Beads holds no plan-derived issues and no non-bootstrap claimed/closed work | Genuinely legacy plan → markdown loop, and emit "re-run planning to assign stable task IDs". Do **not** claim. |
121
+ | `beads_usable`, plan has no stable IDs **but** Beads already holds plausible build work (claimed/closed non-bootstrap issues) | **Fail closed** — markdown would bypass existing execution state. Require re-running planning + materialization. |
122
+ | `beads_usable`, contract **partially present or malformed** (some IDs present, or plan-derived issues already exist, but the contract doesn't fully parse) | **Fail closed.** Do **not** markdown-fall-back (would bypass existing plan-derived issues and diverge). Require planning to be re-run/fixed. |
123
+ | `beads_usable` **and a valid stable-ID contract** | **Always materialize first, then claim** (see Step 3). |
124
+
125
+ **Step 3 — `beads_usable` + valid contract → materialize, then claim:**
126
+
127
+ 1. **Always invoke the canonical materializer:** `/scaffold:materialize-plan-to-beads`.
128
+ Run it unconditionally — do **not** gate it on a count or ID-set comparison
129
+ (every such gate misses content-only edits, partial imports, or stale deps).
130
+ The materializer is idempotent and a cheap no-op when already in sync; it is
131
+ the single source of the four-pass reconcile logic — this prompt **invokes**
132
+ it, it does not duplicate it. If the materializer returns non-zero (mid-run
133
+ failure), **fail closed** — stop and surface the error; do **not** claim and
134
+ do **not** markdown-fall-back past existing Beads state.
135
+ 2. **Run the scoped claim loop.** Atomically claim the next ready **plan** task:
136
+ `TASK=$(bd ready --claim --has-metadata-key plan_task_id --json | jq -r '.id')`
137
+ - Scoping to `plan_task_id` keeps the loop from ever claiming the bootstrap
138
+ "initialize Beads" bead or a manually-created issue.
139
+ - This sets `assignee=$BEADS_ACTOR` (or your git user.name) and
140
+ `status=in_progress` in a single round-trip — no race window.
141
+ - If you need a specific task by ID instead, use `bd update <id> --claim`.
142
+ 3. Implement following the TDD workflow below.
143
+ 4. After the PR is merged, run `bd close <id>`.
144
+ 5. Repeat the scoped claim (`bd ready --claim --has-metadata-key plan_task_id --json`)
145
+ until it returns no ready task, then run the **completion check**.
146
+
147
+ **Completion check (empty `bd ready` ≠ done).** An empty scoped-ready result does
148
+ **not** mean the build is finished — every remaining task could be blocked. On an
149
+ empty result, fetch all plan-derived tasks
150
+ (`bd list --all --limit 0 --has-metadata-key plan_task_id --json`) and classify
151
+ the remaining non-`closed` tasks:
152
+
153
+ - **All plan tasks `closed`** → genuinely **done**; exit gracefully.
154
+ - Otherwise classify **each** remaining non-`closed` task independently — do
155
+ **not** short-circuit on "any task is `in_progress`". Resolve blocker statuses
156
+ from an **unfiltered** `bd list --all --limit 0 --json` (manual blockers carry
157
+ no `plan_task_id`):
158
+ - **advancing** — the task is itself `in_progress`, **or** at least one of its
159
+ **transitive** blockers (walk the chain; bound the walk and reuse
160
+ `bd dep cycles` as a guard) is `in_progress`.
161
+ - **stalled** — not `in_progress` and **no** transitive blocker is
162
+ `in_progress` (open-but-unready behind inactive blockers, manually `blocked`,
163
+ or `deferred`).
164
+ - **All remaining tasks advancing** → exit gracefully.
165
+ - **Any task stalled** → **stop and report the stalled subset**, grouped by why
166
+ (open dependency, manual `blocked`, `deferred`), so the user can unblock them.
167
+
168
+ **Markdown fallback** (only when `.beads/` is **absent**, or for a genuinely
169
+ legacy plan per the table — never past existing Beads state):
113
170
  1. Read `docs/implementation-playbook.md` as the primary task execution reference.
114
171
  Fall back to `docs/implementation-plan.md` when no playbook is present.
115
172
  2. Pick the first uncompleted task that has no unfinished dependencies.
@@ -26,7 +26,11 @@ parallel agents, CI pipeline, branch protection, and conflict prevention rules.
26
26
  - docs/git-workflow.md — branching strategy, commit standards, rebase strategy,
27
27
  PR workflow (8 sub-steps), task closure, agent crash recovery, branch protection,
28
28
  conflict prevention, and worktree documentation
29
- - scripts/setup-agent-worktree.sh — permanent worktree creation script
29
+ - scripts/setup-agent-worktree.sh — permanent worktree creation script. Create
30
+ worktrees project-local at `<repo>/.worktrees/<agent-slug>` (a single,
31
+ consistent location — never as repo siblings like `../<repo>-<agent>`). The
32
+ script must ensure `.worktrees/` is gitignored before creating the worktree so
33
+ the worktree's checkout is never accidentally committed.
30
34
  - .github/workflows/ci.yml — CI workflow with lint and test jobs
31
35
  - .github/pull_request_template.md — PR template with task ID format
32
36
  - CLAUDE.md updated with Committing/PR Workflow, Task Closure, Parallel Sessions,
@@ -37,7 +41,9 @@ parallel agents, CI pipeline, branch protection, and conflict prevention rules.
37
41
  - (mvp) Commit format is consistent (Beads: [bd-<id>] type(scope): desc. Non-Beads: type(scope): desc)
38
42
  - (deep) PR workflow includes all 8 sub-steps (commit, AI review, rebase, push, create,
39
43
  auto-merge with --delete-branch, watch CI, confirm merge)
40
- - (deep) Worktree script creates permanent worktrees with workspace branches
44
+ - (deep) Worktree script creates permanent worktrees with workspace branches at
45
+ the project-local path `<repo>/.worktrees/<agent-slug>` and ensures
46
+ `.worktrees/` is gitignored
41
47
  - (deep) If Beads: BEADS_ACTOR environment variable documented for agent identity
42
48
  - (deep) CI workflow job name matches branch protection context
43
49
  - (mvp) Branch cleanup documented for both single-agent and worktree-agent variants
@@ -0,0 +1,473 @@
1
+ ---
2
+ name: materialize-plan-to-beads
3
+ description: Materialize the implementation plan into Beads issues before the build phase
4
+ summary: "When Beads is enabled, converts docs/implementation-plan.md into Beads issues — creating, updating, and reconciling tasks/stories/epics and their dependencies idempotently — so the build phase has a populated tracker to claim from."
5
+ phase: "finalization"
6
+ order: 1440
7
+ dependencies: [implementation-playbook]
8
+ outputs: []
9
+ conditional: "if-needed"
10
+ stateless: true
11
+ category: pipeline
12
+ knowledge-base: [task-tracking]
13
+ ---
14
+
15
+ ## Purpose
16
+ Materialize the frozen implementation plan into Beads (`bd`) issues so the build
17
+ phase has a populated tracker to claim work from. Reads
18
+ `docs/implementation-plan.md` and creates, updates, and reconciles the
19
+ corresponding epics, stories, tasks, and dependency edges in Beads.
20
+
21
+ ## Inputs
22
+ - **`docs/implementation-plan.md`** (required) — the frozen plan emitting the
23
+ Plan Output Contract: stable task IDs (`T-001`…), stable container IDs
24
+ (`S-001`/`E-001`, deep only), and per-task / per-container metadata blocks
25
+ (`id`, `title`, `priority`, `wave`, `risk`, `story`/`epic` parent refs,
26
+ `depends_on`, `acceptance_criteria`). This is the design source of truth for
27
+ *what* the tasks are.
28
+ - **`docs/implementation-playbook.md`** (optional but expected) — wave ordering
29
+ and per-task context used to enrich issue descriptions and to compute the
30
+ wave-biased default priority.
31
+ - **`.beads/`** (required for materialization) — the Beads database initialized
32
+ by the foundation `beads.md` step. Its presence is the signal that this
33
+ project tracks work in Beads; its absence routes to the markdown fallback (see
34
+ Instructions → degradation split).
35
+ - **Scaffold state** (`.scaffold/config.yml` / methodology preset) — supplies
36
+ the methodology/depth that governs whether containers, waves, and risk
37
+ metadata are materialized.
38
+
39
+ ## Expected Outputs
40
+ - Beads issues for every plan task (and, deep only, every story/epic container),
41
+ joined to the plan by metadata keys `plan_task_id` / `plan_story_id` /
42
+ `plan_epic_id`, with `--external-ref "plan:<id>"` stamped for human
43
+ traceability.
44
+ - All plan dependency edges materialized via `bd dep add`, with the
45
+ materializer-owned blocker set recorded per issue in `plan_deps`.
46
+ - A reconciled tracker: not-started issues updated to match the plan, started
47
+ (`in_progress`/`closed`) issues preserved, removed-from-plan not-started issues
48
+ retired, and stale duplicates collapsed to one canonical issue per join key.
49
+ - A deterministic one-line **summary report** (`materialize: …`).
50
+ - On success, a **run-stamped materialization-complete signal** that build
51
+ workers wait on before claiming.
52
+ - This step writes **no markdown documents** (`outputs: []`); its product is the
53
+ state of the Beads database plus the printed summary.
54
+
55
+ ## Quality Criteria
56
+ - (mvp) Every plan task carrying a stable ID maps to exactly one not-started
57
+ Beads issue after a run (joined on `plan_task_id`).
58
+ - (mvp) The reconcile is idempotent — a second run over an unchanged plan
59
+ creates nothing and updates nothing (`materialize: 0 created, 0 updated, …`).
60
+ - (mvp) Only plan-derived issues are claimable: the build claim loop is scoped by
61
+ `--has-metadata-key plan_task_id`, so the bootstrap bead and manual issues are
62
+ never claimed.
63
+ - (mvp) Started issues (`in_progress`/`closed`) are never mutated except the two
64
+ narrow metadata-only exceptions (join-key cleanup, `ac_warn_hash`).
65
+ - (mvp) `bd dep cycles` reports no cycle after a run.
66
+ - (deep) Story/epic containers are created top-down (epics, then stories) with
67
+ correct `--parent` links; a depth-4 story with no epic parent is valid.
68
+ - (deep) Dependency edges reconcile to the plan: missing plan edges added, stale
69
+ `plan_deps`-owned edges removed, manual/external edges preserved.
70
+ - (deep) Tasks removed from the plan are retired when not-started; completed
71
+ (`closed`) issues stay linked for revert-safety.
72
+
73
+ ## Methodology Scaling
74
+ Structure is a function of **methodology/depth only** — read it from scaffold
75
+ state, never probe `bd` for type availability. Both `-t story` and `-t epic` are
76
+ usable directly on the supported `bd` (≥ v1.0.5); mvp stays flat as a deliberate
77
+ *simplicity* choice, not because the type is unavailable. **Dependencies are
78
+ materialized at every depth** (the plan is a DAG even at mvp); depth scales only
79
+ the container hierarchy and `wave`/`risk` metadata.
80
+
81
+ (Note: the **`mvp` methodology preset disables this step entirely** — it doesn't
82
+ use Beads. The "mvp" rules below therefore apply to a **custom** low-depth build
83
+ that has Beads enabled, not to the mvp preset.)
84
+
85
+ - **mvp** → flat `-t task` issues **plus dependencies**. No story/epic
86
+ containers, no `--parent`; skip Pass 0b. Priority defaults to `-p 2`.
87
+ - **deep** → full container hierarchy + `wave`/`risk` metadata + wave-biased
88
+ priority. At depth 4, tasks are parented to **stories** (`-t story`) and
89
+ stories have no epic parent; at depth 5, the full **epic → story → task**
90
+ hierarchy with stories carrying an epic `--parent`.
91
+ - **custom:depth(1-5)** → dials between the two:
92
+ - Depth 1: flat `-t task` issues + dependencies; skip Pass 0b. Priority `-p 2`.
93
+ - Depth 2: flat `-t task` issues + dependencies; skip Pass 0b (sizing detail
94
+ differs upstream only). Priority `-p 2`.
95
+ - Depth 3: flat `-t task` issues + dependencies; skip Pass 0b. Wave-biased
96
+ priority if the plan assigns waves.
97
+ - Depth 4: tasks under `-t story` parents + `wave` metadata; a missing epic ref
98
+ on a story is valid, not a dangling ref. Pass 0b creates stories.
99
+ - Depth 5: full epic → story → task hierarchy + `risk` metadata + full
100
+ traceability; stories carry an epic `--parent`.
101
+
102
+ **Priority is wave-biased** so `bd ready` surfaces work in playbook order:
103
+
104
+ 1. **Explicit plan priority wins**: `P0`→`-p 0`, `P1`→`-p 1`, `P2`→`-p 2`,
105
+ `P3`→`-p 3`.
106
+ 2. **Otherwise bias by wave**: Wave 1 → `-p 1`, Wave 2 → `-p 2`, Wave 3+ →
107
+ `-p 3` (clamp at 3). Dependencies still gate readiness; the bias only orders
108
+ *among* ready tasks.
109
+ 3. **No priority and no waves** (mvp) → default `-p 2`, rely on dependencies.
110
+
111
+ **Dependencies are always materialized**, at **every** depth (including mvp). The
112
+ plan is a DAG at every depth, and without the `bd dep add` edges the scoped
113
+ `bd ready --claim` would expose dependent tasks out of order. Depth governs only
114
+ **hierarchy** (story/epic parents), **wave**, and **risk** — never *whether*
115
+ dependencies exist.
116
+
117
+ ## Mode Detection
118
+ This step is **idempotent and re-runnable**: running it again reconciles the
119
+ current plan against existing Beads issues rather than duplicating them. There is
120
+ no separate "fresh" vs. "update" code path — the same four passes
121
+ (duplicate-guard → container upsert → task upsert → dependency reconcile → stale
122
+ reconcile) cover both. Detect which case you are in by the join-key fetch:
123
+
124
+ - **No plan-derived issues exist yet** (`bd list --all --limit 0
125
+ --has-metadata-key plan_task_id --json` returns `[]`) → effectively a fresh
126
+ import: every pass falls through to "create".
127
+ - **Plan-derived issues already exist** → reconciliation: each pass looks up by
128
+ join key and creates / updates / retires as needed.
129
+
130
+ Because the build preflight invokes this step **unconditionally** before every
131
+ claim loop, design every pass to be a cheap **lookup-and-reconcile no-op when
132
+ already in sync** (a second run over an unchanged plan reports `0 created,
133
+ 0 updated`).
134
+
135
+ ## Update Mode Specifics
136
+ Reconcile plan changes into Beads **one-way (plan → Beads)** without clobbering
137
+ started work:
138
+
139
+ - **Preserve started state.** Never mutate the content fields, execution status,
140
+ claims, or assignees of issues in a **started** status (`in_progress` or
141
+ `closed`). Freely update the content fields
142
+ (title/description/priority/parent/wave/risk) of **not-started**
143
+ (`open`/`blocked`/`deferred`) issues to match the plan.
144
+ - **Two narrow metadata-only exceptions** to "never touch started issues" — each
145
+ touches a single metadata tag, is reported, and changes no content field,
146
+ status, claim, or assignee:
147
+ 1. **Join-key cleanup (Pass 0a)** — a started *non-canonical duplicate* may have
148
+ its own join key `--unset-metadata`'d to restore the one-key invariant.
149
+ 2. **AC-drift bookkeeping (Pass 1)** — an `in_progress` issue whose plan AC
150
+ changed may get `--set-metadata ac_warn_hash=…` so the warning comment posts
151
+ once per distinct change, not every run.
152
+ - **Triggers handled:** new task IDs added (created), content-only edits
153
+ (title/AC/priority/parent/deps changed with no ID change → not-started issues
154
+ updated, edges reconciled), dependency add/remove, and tasks/containers removed
155
+ from the plan (retired if not-started, flagged if `in_progress`, left linked if
156
+ `closed`).
157
+ - A dependency-blocked task stays stored `open` — blockers affect *computed*
158
+ readiness, not stored status; a stored `blocked`/`deferred` is an explicit
159
+ signal that descriptive-field updates neither set nor clear.
160
+
161
+ ## Instructions
162
+
163
+ Execute the steps below in order. All `bd` commands use the verified v1.0.5
164
+ surface — do **not** invent flags (in particular, there is **no `bd list
165
+ --external-ref` filter**; join by metadata keys only).
166
+
167
+ ### Gate on `beads_usable` (version + tooling), then split degradation
168
+
169
+ Compute a single shared `beads_usable` check. It is true **only** when *all* of:
170
+
171
+ 1. `.beads/` exists.
172
+ 2. `bd` is on PATH (`command -v bd`).
173
+ 3. `bd version` parses to **≥ 1.0.5**, using a **macOS/BSD-portable** numeric
174
+ compare (do **not** depend on GNU `sort -V`). Split major/minor/patch and
175
+ compare numerically.
176
+ 4. `jq` is on PATH (`command -v jq`).
177
+
178
+ ```bash
179
+ beads_usable() {
180
+ [ -d .beads ] || return 1
181
+ command -v bd >/dev/null 2>&1 || return 1
182
+ command -v jq >/dev/null 2>&1 || return 1
183
+ # Portable >= 1.0.5 compare — no `sort -V` (absent on macOS/BSD).
184
+ local ver have_major have_minor have_patch
185
+ ver=$(bd version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
186
+ [ -n "$ver" ] || return 1
187
+ IFS=. read -r have_major have_minor have_patch <<EOF
188
+ $ver
189
+ EOF
190
+ # Compare against floor 1.0.5
191
+ if [ "$have_major" -gt 1 ]; then return 0; fi
192
+ if [ "$have_major" -lt 1 ]; then return 1; fi
193
+ if [ "$have_minor" -gt 0 ]; then return 0; fi
194
+ if [ "$have_minor" -lt 0 ]; then return 1; fi
195
+ [ "$have_patch" -ge 5 ]
196
+ }
197
+ ```
198
+
199
+ > **Never** write `[ -d .beads ] && bd …` as a whole command — it returns exit 1
200
+ > when `.beads/` is absent and breaks callers under `set -e`. Always branch on
201
+ > the function's return code with an explicit `if`.
202
+
203
+ **Degradation splits on whether `.beads/` exists** — markdown is safe *only* when
204
+ there is no Beads state to diverge from:
205
+
206
+ - **`.beads/` absent** → genuinely a non-Beads project. Stop materializing and
207
+ let the build drive from the markdown plan/playbook. (Nothing to do here; this
208
+ prompt only runs when Beads is enabled.)
209
+ - **`.beads/` present but `bd`/`jq` missing or too old** (`beads_usable` false)
210
+ → **FAIL CLOSED.** Do **not** markdown-fallback — the tracker may already hold
211
+ execution state (claimed/closed tasks) and the markdown loop would re-run
212
+ completed work. Stop and tell the user to install/upgrade `bd` (≥ v1.0.5) and
213
+ `jq`.
214
+
215
+ Once `beads_usable` is true, resolve **methodology/depth** from scaffold state
216
+ (see Methodology Scaling) and parse the plan's task / container / dependency
217
+ blocks per the Plan Output Contract. Then run the passes below.
218
+
219
+ ### The Retire convention
220
+
221
+ Whenever a pass removes an issue from the plan-derived set (Pass 0a duplicate
222
+ close, Pass 3 stale close), do it in **this exact order**, against the issue's
223
+ **own** join key (`plan_task_id` for a task, `plan_story_id` for a story,
224
+ `plan_epic_id` for an epic — resolved from the issue's type, never hardcoded):
225
+
226
+ 1. `bd label add <id> <stale-label>` — `stale:duplicate` or
227
+ `stale:removed-from-plan`. The **label is applied first** and is what marks the
228
+ issue as *retired-by-the-materializer* (vs. closed as completed work).
229
+ 2. `bd close <id> --reason "…"` — skipped if already `closed`.
230
+ 3. **then** `bd update <id> --unset-metadata <join-key>`.
231
+
232
+ The key is unset **last** so a failure at any step is **resumable**: the issue
233
+ keeps its join key (still found next run) and carries the `stale:*` label
234
+ (recognized as retire-pending, not completed work). Each run re-applies whatever
235
+ steps are missing for any join-keyed issue bearing a `stale:*` label. Because the
236
+ `stale:*` label distinguishes a retired close from a genuine completion, two later
237
+ rules key off it: (a) Pass 0a canonical selection **excludes any `stale:*`-labelled
238
+ issue**; and (b) Pass 3 leaves a completed `closed` issue linked **only when it
239
+ carries no `stale:*` label**.
240
+
241
+ ### Pass 0a — Duplicate guard (always)
242
+
243
+ Before any upsert, fetch the plan-derived issues once and group by each join key:
244
+
245
+ ```bash
246
+ bd list --all --limit 0 --has-metadata-key plan_task_id --json # tasks
247
+ bd list --all --limit 0 --has-metadata-key plan_story_id --json # stories (deep)
248
+ bd list --all --limit 0 --has-metadata-key plan_epic_id --json # epics (depth 5)
249
+ ```
250
+
251
+ For any join key held by **more than one** issue (failed/manual/concurrent prior
252
+ import), restore the **exactly-one-issue-per-key invariant**:
253
+
254
+ 1. **Pick the canonical issue.** First **exclude any issue already bearing a
255
+ `stale:*` label** (retire-pending leftovers can never win). Among the rest,
256
+ the ordering is total:
257
+ - if **exactly one** duplicate is `in_progress` → it is canonical (active work
258
+ wins over any `closed` or not-started copy);
259
+ - else if any is `closed` → canonical is the **oldest** `closed` one;
260
+ - else the **oldest not-started** issue (lowest `created_at`, ties by `id`).
261
+
262
+ The **only** unorderable case is **two or more `in_progress` duplicates** for
263
+ the same plan ID — there is no safe way to pick which active effort to detach,
264
+ so **FAIL CLOSED** and report. (An `in_progress` + `closed` mix is *not* a
265
+ conflict: `in_progress` wins.)
266
+ 2. **Not-started non-canonical duplicates** → retire them per the **Retire
267
+ convention** (`stale:duplicate` label → close → unset the issue's own
268
+ `<join-key>`), so they leave the plan-derived set and are never re-detected.
269
+ 3. **Started non-canonical duplicates** → do **not** close or mutate fields; only
270
+ `bd update <id> --unset-metadata <join-key>` to restore key uniqueness, and
271
+ **report** them for human review.
272
+
273
+ After Pass 0a, exactly one canonical issue retains each `plan_*_id`, so every
274
+ later bulk fetch, map, dep reconcile, stale pass, and the scoped claim loop can
275
+ rely on the one-key invariant.
276
+
277
+ ### Pass 0b — Container upsert (deep only), top-down
278
+
279
+ Skip entirely at mvp / depth 1–3. **Bulk-fetch containers once** (two queries),
280
+ build in-memory `plan_epic_id → id` and `plan_story_id → id` maps, then process
281
+ **epics first, then stories** so each child can reference its parent's resolved
282
+ Beads ID via `--parent`. Containers carry the **same fields as tasks** (title,
283
+ wave-biased `-p`, description/AC, and — for stories — `--parent`).
284
+
285
+ ```bash
286
+ EPIC_MAP_JSON=$(bd list --all --limit 0 --has-metadata-key plan_epic_id --json \
287
+ | jq 'map({ (.metadata.plan_epic_id): .id }) | add // {}')
288
+ STORY_MAP_JSON=$(bd list --all --limit 0 --has-metadata-key plan_story_id --json \
289
+ | jq 'map({ (.metadata.plan_story_id): .id }) | add // {}')
290
+
291
+ # Epic E-001 (depth 5): look up in the epic map; create if absent.
292
+ ID=$(printf '%s' "$EPIC_MAP_JSON" | jq -r '."E-001" // empty')
293
+ [ -z "$ID" ] && ID=$(bd create "<epic title>" -t epic -p <prio> \
294
+ --description "<epic body>" \
295
+ --metadata '{"plan_epic_id":"E-001","wave":"<n>","risk":"<type>"}' \
296
+ --external-ref "plan:E-001" --json | jq -r '.id')
297
+
298
+ # Story S-001: same lookup-or-create against the story map; wire --parent only
299
+ # when an epic ref is present (depth 4 stories have NO epic parent).
300
+ ID=$(printf '%s' "$STORY_MAP_JSON" | jq -r '."S-001" // empty')
301
+ [ -z "$ID" ] && ID=$(bd create "<story title>" -t story -p <prio> \
302
+ ${EPIC_PARENT_ID:+--parent "$EPIC_PARENT_ID"} \
303
+ --description "<story body>" \
304
+ --metadata '{"plan_story_id":"S-001","wave":"<n>","risk":"<type>"}' \
305
+ --external-ref "plan:S-001" --json | jq -r '.id')
306
+ ```
307
+
308
+ Container upsert obeys the **same not-started-vs-started rules as Pass 1**: a
309
+ not-started (`open`/`blocked`/`deferred`) epic/story is updated to match the plan
310
+ (`bd update <id> --title … -d … -p … --parent … --set-metadata wave=… --set-metadata
311
+ risk=…`); a started (`in_progress`/`closed`) container is left untouched.
312
+ Reparenting a not-started story uses `bd update <id> --parent <new>`.
313
+
314
+ ### Pass 1 — Task upsert
315
+
316
+ **Fetch existing plan-derived task issues once**, build an in-memory
317
+ `plan_task_id → issue` map (one `bd` process for the whole plan, not one per
318
+ task), then for each plan task (parent IDs already resolved by Pass 0b):
319
+
320
+ ```bash
321
+ TASK_MAP_JSON=$(bd list --all --limit 0 --has-metadata-key plan_task_id --json \
322
+ | jq 'map({ (.metadata.plan_task_id): . }) | add // {}')
323
+ EXISTING=$(printf '%s' "$TASK_MAP_JSON" | jq -r '."T-001" // empty')
324
+ ```
325
+
326
+ 1. **Create if absent:**
327
+ ```bash
328
+ bd create "<title>" -t task -p <prio> \
329
+ ${PARENT_ID:+--parent "$PARENT_ID"} \
330
+ --metadata '{"plan_task_id":"T-001","wave":"<n>","risk":"<type>"}' \
331
+ --external-ref "plan:T-001" \
332
+ --description "<body incl. acceptance criteria + traceability>"
333
+ ```
334
+ 2. **If present, branch on stored status** (not-started vs. started):
335
+ - **`open` / `blocked` / `deferred`** → **update fields to match the plan,
336
+ including wave/risk metadata** so nothing drifts:
337
+ ```bash
338
+ bd update <id> --title "<title>" -d "<body>" -p <prio> \
339
+ ${PARENT_ID:+--parent "$PARENT_ID"} \
340
+ --set-metadata wave=<n> --set-metadata risk=<type>
341
+ ```
342
+ These are not-started states; mutating their fields never changes execution
343
+ status or readiness, and never unblocks/disturbs a stored `blocked`/`deferred`
344
+ signal.
345
+ - **`in_progress`** → **do not mutate fields.** If the plan's AC/description
346
+ changed, post a warning **idempotently**: compute a hash of the current plan
347
+ AC text; if it differs from the issue's `ac_warn_hash` metadata, post one
348
+ `bd comment <id> "Plan AC changed since this task was started: …"` then record
349
+ the new hash with the targeted `bd update <id> --set-metadata
350
+ ac_warn_hash=<hash>`. Re-runs with the same AC post nothing.
351
+ - **`closed`** → leave **entirely untouched** (do not recreate, do not edit).
352
+
353
+ ### Pass 2 — Dependency reconcile
354
+
355
+ Run after all issues exist (forward references resolved). **Read existing edges
356
+ from a single bulk fetch**, not per task — `bd list --all --limit 0 --json`
357
+ carries an inline `dependencies` array (`depends_on_id`, `type`) on each issue.
358
+ Reconcile in-memory against the plan's `depends_on` lists; only the mutating
359
+ calls need per-edge invocations.
360
+
361
+ **Materializer-owned edges are tracked explicitly** in the `plan_deps` metadata
362
+ key (a sorted CSV of blocker task IDs, e.g. `"T-003,T-007"`) — *not* inferred
363
+ from endpoint types. "Both endpoints are plan-derived" is **not** proof of
364
+ ownership: an agent may add an execution blocker during the build, and deleting
365
+ it just because it is absent from the markdown plan would corrupt Beads-owned
366
+ state.
367
+
368
+ For each **`open` / `blocked` / `deferred`** dependent (never mutate
369
+ `in_progress`/`closed`), reconcile against the plan's `depends_on` list:
370
+
371
+ - **Add** plan edges not already present: `bd dep add <blocked-id> <blocker-id>`
372
+ (re-adding is a no-op).
373
+ - **Remove** an edge **only** when it is **in the prior `plan_deps`** (materializer
374
+ created it) **and absent from the current plan**: `bd dep remove <blocked-id>
375
+ <blocker-id>`. Edges **not** in `plan_deps` (manual/external blockers) are
376
+ preserved untouched; a manual edge that collides with a plan edge is kept and
377
+ noted in the summary, not deleted.
378
+ - **Rewrite `plan_deps` authoritatively** at the end of each issue's reconcile:
379
+
380
+ > `plan_deps' = (prior plan_deps ∩ current-plan deps) ∪ (edges this run added)`
381
+
382
+ Keep still-valid owned edges, add the ones just created, and **explicitly
383
+ exclude any pre-existing edge that was *not* already in `plan_deps`** — even if
384
+ it collides with a current plan dep (that edge stays manual-owned so a later
385
+ plan removal never deletes it). Write with `bd update <id> --set-metadata
386
+ plan_deps=<csv>` (or `bd update <id> --unset-metadata plan_deps` when empty).
387
+
388
+ A per-issue read, if ever needed, is `bd dep list <id> --direction down --json`
389
+ (verified: `down` = what `<id>` depends on, i.e. its blockers; `up` returns
390
+ dependents and is wrong here). No manual status reconciliation is needed after
391
+ add/remove — readiness is computed from open blockers.
392
+
393
+ **Detect cycles after applying:** `bd dep cycles` — surface any cycle as an error
394
+ (the plan DAG is validated upstream, but a manual edit could reintroduce one).
395
+
396
+ ### Pass 3 — Stale reconcile (tasks/containers removed from the plan)
397
+
398
+ List every plan-derived issue (the three `--has-metadata-key` queries) and diff
399
+ IDs against the current plan:
400
+
401
+ - ID no longer in the plan and issue **not started** (`open`/`blocked`/
402
+ `deferred`) → **retire it** per the Retire convention
403
+ (`stale:removed-from-plan` label → `bd close <id> --reason "Removed from
404
+ implementation plan (was <id>)"` → unset the issue's own `<join-key>`), so it
405
+ drops out of future queries and can never be misread as "started/dropped".
406
+ - Such an issue **`in_progress`** → **do not auto-close.** Report it in the
407
+ summary for human attention (it may be mid-flight work the plan dropped by
408
+ mistake). Leave its join key intact so it stays visible until a human decides.
409
+ - Such an issue **`closed` as genuinely-completed work** (no `stale:*` label) →
410
+ **leave it entirely untouched** (do **not** unset its join key). Plan IDs are
411
+ never reused, so a later revert restoring the task must let Pass 1 find this
412
+ issue by `plan_task_id` and recognize it as already `closed` — not recreate an
413
+ `open` issue and force redone work. (A `closed` issue that **does** carry
414
+ `stale:removed-from-plan` is a retire-pending leftover — the Retire convention
415
+ finishes unsetting its key.)
416
+
417
+ ### Summary report (deterministic)
418
+
419
+ Print exactly one line:
420
+
421
+ ```
422
+ materialize: C created, U updated, K unchanged,
423
+ S skipped (in_progress/closed — started, not mutated),
424
+ D deps added, R deps removed,
425
+ X stale closed, W stale flagged for review
426
+ ```
427
+
428
+ where each letter is the count accumulated across the passes. A re-run over an
429
+ unchanged plan prints `0 created, 0 updated` (proof of idempotency).
430
+
431
+ ### Run-stamped materialization-complete signal
432
+
433
+ On **success**, set a durable **materialization-complete signal** that build
434
+ workers wait on before their first claim. The signal **must be run-specific** —
435
+ carry a `run_id` or the current plan hash (e.g. `materialized_at=<run_id>` /
436
+ `plan_hash=<sha>` as metadata on the project merge-slot / bootstrap bead, or a
437
+ workspace marker file). **Clear/overwrite any prior signal *before* acquiring the
438
+ merge-slot lock**, so a stale signal from a previous pipeline run (or a pre-update
439
+ plan) can never let workers race ahead of a fresh re-materialization.
440
+
441
+ In the sequential finalization path there is no concurrency, but still set the
442
+ run-stamped signal so the multi-agent build preflight (which gates workers on a
443
+ signal matching **this run's** id/hash) is satisfied without re-running the
444
+ materializer. The multi-agent orchestrator runs this whole procedure **once per
445
+ wave under the merge-slot lock** (acquire via a real acquisition loop, verify
446
+ ownership with `bd merge-slot check --json`, run the idempotent materializer, set
447
+ the signal, then `bd merge-slot release` via a `trap … EXIT INT TERM`); workers
448
+ never materialize — they only wait for the signal, then claim.
449
+
450
+ ## After This Step
451
+
452
+ The implementation plan is now **materialized into Beads** — every plan task
453
+ (and, on deep builds, every story/epic and dependency edge) exists as a `bd`
454
+ issue joined to the plan by `plan_task_id` / `plan_story_id` / `plan_epic_id`.
455
+ Print the summary line and tell the user:
456
+
457
+ ---
458
+ **Plan materialized into Beads.** The tracker is populated and ready for the
459
+ build phase.
460
+
461
+ **Summary:** [paste the `materialize: …` line]
462
+
463
+ **Next:**
464
+ - **Single agent:** run `/scaffold:single-agent-start` to begin the scoped claim
465
+ loop (`bd ready --claim --has-metadata-key plan_task_id`).
466
+ - **Multiple agents in parallel:** run `/scaffold:multi-agent-start` — the
467
+ orchestrator re-runs this materializer once under the merge-slot lock before
468
+ fan-out, then workers wait for the completion signal and claim.
469
+
470
+ **Re-running:** this step is idempotent — after any plan update, re-run
471
+ `/scaffold:materialize-plan-to-beads` to reconcile new/changed/removed tasks and
472
+ dependencies into Beads without clobbering started work.
473
+ ---
@@ -178,6 +178,12 @@ Enable when: project uses Beads task tracking methodology (user selects Beads du
178
178
  setup), or user explicitly enables structured task management. Skip when: user prefers
179
179
  GitHub Issues, Linear, or another task tracker, or explicitly declines Beads setup.
180
180
 
181
+ Note: this step only initializes the tracker — it does **not** create your
182
+ implementation tasks. Beads stays empty of plan tasks until the finalization step
183
+ `/scaffold:materialize-plan-to-beads` converts `docs/implementation-plan.md` into
184
+ Beads issues just before the build phase. An empty `bd ready` right after this
185
+ step is therefore expected, not a problem.
186
+
181
187
  ## Mode Detection
182
188
  Update mode if `.beads/` contains a populated database (look for `.beads/embeddeddolt/`
183
189
  in the default embedded-Dolt layout, `.beads/dolt/` in server mode, or any `.beads/*.db`
@@ -46,8 +46,33 @@ and produce a structured coverage matrix and review summary.
46
46
  - (deep) Every task has verb-first description, >= 1 input file reference, >= 1 acceptance criterion, and defined output artifact
47
47
  - (mvp) Every task complies with agent executability rules (3-file, 150-line, single-concern, decision-free, test co-location)
48
48
  - (mvp) Tasks exceeding limits have explicit `<!-- agent-size-exception -->` justification
49
+ - (mvp) Plan Output Contract holds: every task and container has an ID; IDs are unique and stable; all parent and `depends_on` refs resolve; the dependency graph is acyclic; field blocks parse
49
50
  - (depth 4+) Independent model reviews completed and reconciled
50
51
 
52
+ ## Plan Output Contract Validation
53
+
54
+ The plan is consumed by an automated materializer that needs stable join keys
55
+ and a parseable structure (see the **Plan Output Contract** section in
56
+ `implementation-plan.md`). This review MUST verify that contract holds, rejecting
57
+ any plan that violates it:
58
+
59
+ - **ID presence** — every task has a `T-NNN` ID, and (deep) every story has an
60
+ `S-NNN` ID and every epic an `E-NNN` ID. Flag any item missing an ID.
61
+ - **Uniqueness and stability** — task, story, and epic IDs are each **unique**
62
+ and **stable** (no two items share an ID; no ID changed for an item that
63
+ already existed in a prior plan revision; IDs are never reused for a different
64
+ task/container).
65
+ - **Referential integrity (no dangling refs)** — every `story` / `epic` parent
66
+ reference **and every `depends_on` reference** **resolves** to a declared ID in
67
+ the plan. Report any **dangling** ref (a parent or `depends_on` entry that does
68
+ not resolve to a declared ID) as a blocking finding. A story with no `epic`
69
+ parent is valid, not dangling.
70
+ - **Acyclicity** — the dependency graph formed by all `depends_on` edges is a
71
+ **DAG**: there are **no cycles**. Walk the graph and report any cycle.
72
+ - **Parseability** — every per-task and per-container fenced metadata block
73
+ parses cleanly under the canonical serialization (well-formed key/value block
74
+ with the required fields present).
75
+
51
76
  ## Methodology Scaling
52
77
  - **deep**: Full multi-pass review with multi-model validation. AC coverage
53
78
  matrix. Independent Codex/Gemini dispatches. Detailed reconciliation report.