mandrel 2.39.0 → 2.41.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 (76) hide show
  1. package/.agents/README.md +8 -5
  2. package/.agents/agents/auditor.md +5 -0
  3. package/.agents/docs/SDLC.md +21 -12
  4. package/.agents/instructions.md +22 -22
  5. package/.agents/rules/api-conventions.md +43 -7
  6. package/.agents/rules/ci-remediation.md +3 -14
  7. package/.agents/rules/gherkin-standards.md +21 -6
  8. package/.agents/rules/git-conventions.md +6 -5
  9. package/.agents/rules/security-baseline.md +6 -7
  10. package/.agents/rules/testing-standards.md +75 -198
  11. package/.agents/scripts/audit-to-stories.js +510 -66
  12. package/.agents/scripts/install-matrix-assert.js +2 -2
  13. package/.agents/scripts/lib/audit-to-stories/epic-grouping-directive.js +45 -0
  14. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +290 -0
  15. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +94 -3
  16. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +10 -0
  17. package/.agents/scripts/lib/label-constants.js +18 -0
  18. package/.agents/scripts/lib/label-taxonomy.js +18 -5
  19. package/.agents/scripts/lib/orchestration/dependency-candidates.js +139 -0
  20. package/.agents/scripts/lib/orchestration/epic-candidates.js +159 -0
  21. package/.agents/scripts/lib/orchestration/epic-checklist.js +103 -0
  22. package/.agents/scripts/lib/orchestration/epic-container.js +202 -0
  23. package/.agents/scripts/lib/orchestration/epic-expansion.js +148 -0
  24. package/.agents/scripts/lib/orchestration/plan-context.js +97 -36
  25. package/.agents/scripts/lib/orchestration/plan-persist/cross-plan-links.js +80 -0
  26. package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +192 -0
  27. package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +324 -0
  28. package/.agents/scripts/lib/orchestration/plan-persist/external-deps.js +164 -0
  29. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +30 -0
  30. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +26 -5
  31. package/.agents/scripts/lib/orchestration/run-epilogue.js +130 -1
  32. package/.agents/scripts/lib/orchestration/ticket-validator.js +11 -1
  33. package/.agents/scripts/plan-persist.js +98 -1
  34. package/.agents/scripts/providers/github/sub-issue-add.js +218 -0
  35. package/.agents/scripts/resolve-stories.js +42 -2
  36. package/.agents/skills/core/browser-testing-with-devtools/SKILL.md +5 -10
  37. package/.agents/skills/core/browser-testing-with-devtools/reference.md +7 -46
  38. package/.agents/skills/core/code-review-and-quality/SKILL.md +0 -5
  39. package/.agents/skills/core/documentation-and-adrs/SKILL.md +0 -3
  40. package/.agents/skills/core/gates-and-baselines/SKILL.md +10 -137
  41. package/.agents/skills/core/gates-and-baselines/reference.md +103 -0
  42. package/.agents/skills/core/idea-refinement/SKILL.md +2 -18
  43. package/.agents/skills/core/security-and-hardening/SKILL.md +2 -4
  44. package/.agents/skills/core/security-and-hardening/reference.md +0 -70
  45. package/.agents/skills/skills.index.json +10 -70
  46. package/.agents/skills/stack/qa/gherkin-authoring/SKILL.md +4 -10
  47. package/.agents/skills/stack/qa/gherkin-authoring/reference.md +9 -113
  48. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +29 -154
  49. package/.agents/skills/stack/qa/qa-harness/SKILL.md +157 -98
  50. package/.agents/templates/docs/audit-sweep-runbook.md +169 -0
  51. package/.agents/workflows/audit-to-stories.md +85 -7
  52. package/.agents/workflows/git-cleanup.md +3 -2
  53. package/.agents/workflows/git-deliver.md +3 -2
  54. package/.agents/workflows/helpers/audit-lens-core.md +24 -4
  55. package/.agents/workflows/helpers/deliver-reference.md +8 -0
  56. package/.agents/workflows/helpers/plan-reference.md +109 -1
  57. package/.agents/workflows/mandrel-deliver.md +47 -43
  58. package/.agents/workflows/mandrel-plan.md +61 -55
  59. package/.agents/workflows/qa-assist.md +20 -17
  60. package/.agents/workflows/qa-explore.md +30 -29
  61. package/.agents/workflows/qa-run.md +2 -1
  62. package/docs/CHANGELOG.md +30 -0
  63. package/package.json +1 -1
  64. package/.agents/rules/changelog-style.md +0 -180
  65. package/.agents/rules/shell-conventions.md +0 -61
  66. package/.agents/scripts/lib/qa/coverage-verdict.js +0 -214
  67. package/.agents/skills/core/api-and-interface-design/SKILL.md +0 -55
  68. package/.agents/skills/core/api-and-interface-design/reference.md +0 -76
  69. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +0 -45
  70. package/.agents/skills/core/debugging-and-error-recovery/reference.md +0 -56
  71. package/.agents/skills/core/git-workflow-and-versioning/SKILL.md +0 -54
  72. package/.agents/skills/core/idea-refinement/refinement-criteria.md +0 -155
  73. package/.agents/skills/core/idea-refinement/scripts/idea-refine.sh +0 -15
  74. package/.agents/skills/core/qa-coverage-mapping/SKILL.md +0 -105
  75. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +0 -152
  76. package/.agents/skills/stack/qa/vitest/SKILL.md +0 -22
@@ -44,10 +44,12 @@ They remain read-only emitters of audit reports.
44
44
  ## Phase 1 — Discover & parse
45
45
 
46
46
  Run the CLI in `--scan` mode against the resolved glob. It parses every
47
- `### Finding` block, normalises the fields (`Severity` / `Impact` are
47
+ finding block, normalises the fields (`Severity` / `Impact` are
48
48
  both recognised; `Dimension` / `Category` likewise), and extracts file
49
- paths mentioned in the body. It then stamps each finding with a stable
50
- sha1 fingerprint via the shared
49
+ paths mentioned in the body. A `###` heading that carries no severity axis and
50
+ holds `####` blocks is read as a **grouping header**: its `####` children are
51
+ the findings, and the header itself never becomes one. It then stamps each
52
+ finding with a stable sha1 fingerprint via the shared
51
53
  [`lib/findings/route-finding.js`](../scripts/lib/findings/route-finding.js)
52
54
  helper (`fingerprintFinding`) — the single dedup/route implementation
53
55
  shared with `qa-explore`. The workflow carries **no** separate inline
@@ -64,6 +66,19 @@ The emitted plan envelope carries `findings`, `groups`, `edges`,
64
66
  `classifications`, and `summary`. Subsequent phases consume the file
65
67
  rather than re-parsing the reports.
66
68
 
69
+ **The tally cross-check is automatic.** Every report declares
70
+ `Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>` in its
71
+ Executive Summary; the scan compares that line with what it parsed and carries
72
+ each disagreement on `summary.reportFailures[]` as
73
+ `{ sourceReport, kind, reported, parsed }`. The kinds are `missing-tally` (no
74
+ line), `tally-mismatch` (line and parse disagree), and `unresolved-severity` (a
75
+ finding whose severity did not resolve — dropped from grouping, never filed as
76
+ an `unknown` group). They print to stderr before `--scan` returns its plan, so
77
+ a mis-parsed report is never read as a clean audit: re-run the lens rather than
78
+ file from it. Over older reports predating the mandate,
79
+ `--scan --allow-missing-tally` downgrades **only** `missing-tally` to a
80
+ warning.
81
+
67
82
  ## Phase 2 — HITL: severity gate
68
83
 
69
84
  Read the plan envelope's `summary.tally`. Present the operator with the
@@ -117,6 +132,11 @@ Ask:
117
132
  > default-single policy.
118
133
  > - **Individual standalone Stories** — opens one GitHub Issue per
119
134
  > group directly (no plan ceremony).
135
+ >
136
+ > Either way, if the sweep proposes **more than 2** Stories they are grouped
137
+ > under a **container Epic** by default — a title, a one-paragraph goal and a
138
+ > child checklist, carrying nothing a child does not already carry. Say so if
139
+ > you would rather file them flat.
120
140
 
121
141
  **STOP** until the operator picks.
122
142
 
@@ -133,7 +153,14 @@ node .agents/scripts/audit-to-stories.js --emit-plan-seed \
133
153
  The seed renders the canonical one-pager sections — Problem Statement,
134
154
  Recommended Direction, Key Assumptions (with links to every source
135
155
  report), MVP Scope (the M proposed Stories), Key Files (so `/mandrel-plan`'s
136
- authoring step has concrete anchors), Not Doing.
156
+ authoring step has concrete anchors), Grouping, Not Doing.
157
+
158
+ **Grouping is the container-Epic directive.** Above 2 proposed Stories the
159
+ seed instructs `/mandrel-plan` to group them under one Epic — a sweep is the
160
+ clearest case for a container, since every Story shares a provenance and an
161
+ operator usually delivers them together. It is a directive in the text, not an
162
+ automatic write: Phase 4 above is where an operator declines it. Below the
163
+ threshold the section says so and asks for nothing.
137
164
 
138
165
  Chain into the existing planning entrypoint:
139
166
 
@@ -215,6 +242,14 @@ its footprint guard ignores the shared provenance footers, so an unwired cohort
215
242
  is genuinely unordered and `/mandrel-deliver` will co-dispatch Stories the edges say
216
243
  must follow one another.
217
244
 
245
+ **Preconditions.** The pass writes through the configured provider, so it needs
246
+ `github.owner` **and** `github.repo` in `.agentrc.json` plus working `gh` auth
247
+ (`GH_TOKEN`/`gh auth status`) — the same two things Phase 1's dedup needs. When
248
+ either is missing the command refuses and names which one; fix that and re-run
249
+ the exact command above. Do not transcribe the footers by hand: `/mandrel-deliver`
250
+ reads them, but the native `blocked_by` relations only exist if this pass wrote
251
+ them.
252
+
218
253
  ## Phase 6 — Idempotency (folded into Phase 1 scan)
219
254
 
220
255
  The `--scan` step routes each group's findings through the shared
@@ -289,6 +324,8 @@ summarising the run:
289
324
 
290
325
  When the single-plan path ran, link the Story (or plan-run) the chained
291
326
  `/mandrel-plan` opened. When the Standalone-Stories path ran, list every Issue URL.
327
+ Either way, name the container Epic if one was created — it is the single id
328
+ that delivers the whole sweep (`/mandrel-deliver <epicId>`).
292
329
 
293
330
  ## Constraints
294
331
 
@@ -330,18 +367,59 @@ writing their `temp/audits/audit-*-results.md` reports, then (2) invokes the
330
367
  CLI's **`--auto` mode** over those results:
331
368
 
332
369
  ```bash
333
- node .agents/scripts/audit-to-stories.js --auto [--dry-run] \
370
+ node .agents/scripts/audit-to-stories.js --auto [--dry-run] [--ledger-commit] \
334
371
  [--glob "temp/audits/audit-*-results.md"] [--severity <floor>]
335
372
  ```
336
373
 
374
+ The routine shape is **lenses full-scope → dry-run → live with a ledger PR**:
375
+
376
+ 1. Run the `audit-*` lenses with no `--paths` and no change-set filter. A
377
+ sweep scoped to a change set re-reports the same recent files every cycle
378
+ and never reaches the untouched code where findings accumulate.
379
+ 2. `--auto --dry-run` for the first cycles — zero writes, summary only. Read
380
+ `totals.create` and raise the severity floor until it is a batch the team
381
+ would actually take on.
382
+ 3. `--auto --ledger-commit` once the tallies stop surprising you.
383
+
337
384
  `--auto` runs with **no interactive gates**: it resolves the severity floor
338
385
  from `delivery.auditToStories.severityFloor` (default `high`, overridable with
339
386
  `--severity`), applies the two-stage dedup, reconciles the cross-run ledger,
340
387
  and prints a run-summary JSON (create / skip-open / skip-reoccurring /
341
388
  suppressed-by-ledger tallies, plus the re-detected open Issue numbers an
342
389
  operator may want a "re-detected" comment on). `--dry-run` performs zero GitHub
343
- writes and skips the ledger write, emitting only the summary. The host
344
- scheduler owns the cadence; this workflow owns the routing.
390
+ writes and skips the ledger write, emitting only the summary.
391
+
392
+ `--auto` **fails closed on any `summary.reportFailures[]` entry** (Phase 1): an
393
+ unattended sweep has no operator to read a warning, so a missing or mismatched
394
+ `Severity tally:` line — or a finding whose severity did not resolve — exits
395
+ non-zero having opened no Issue and written no ledger. `--allow-missing-tally`
396
+ is a `--scan` affordance that `--auto` ignores. A red sweep means the report is
397
+ untrustworthy: re-run the lens. The host scheduler owns the cadence; this
398
+ workflow owns the routing.
399
+
400
+ ### The ledger is consumer state — commit it
401
+
402
+ `baselines/audit-ledger.json` is **committed consumer state, not scratch
403
+ output**. A scheduled sweep normally runs on an ephemeral checkout, so unless
404
+ the reconciled ledger is committed back it dies with the clone: every later
405
+ sweep starts amnesiac, re-proposing findings already filed and re-surfacing
406
+ findings a human already rejected.
407
+
408
+ `--ledger-commit` closes that loop. After the summary prints — and only when
409
+ the ledger changed — it creates `chore/audit-ledger-<YYYY-MM-DD>` from HEAD,
410
+ commits **only** the ledger file, pushes it, and opens a PR against
411
+ `project.baseBranch`. **Auto-merge is never requested**: a human glance at the
412
+ `accepted-risk` / `regressed` flips before it lands is the point. A git or `gh`
413
+ failure is fatal and names its step, but only after the summary is printed, so
414
+ a broken remote never costs the operator the run's findings. `--dry-run` skips
415
+ the tail. Without the flag, a changed ledger on a checkout that cannot persist
416
+ it — no `origin`, or HEAD off the base branch — sets `ledger.unpersisted: true`
417
+ in the summary and warns on stderr naming the file.
418
+
419
+ The full sweep procedure — tally cross-check, the ledger PR, the
420
+ enrich-before-deliver step and the label convention — ships as a
421
+ consumer-copyable template at
422
+ [`templates/docs/audit-sweep-runbook.md`](../templates/docs/audit-sweep-runbook.md).
345
423
 
346
424
  ## See also
347
425
 
@@ -17,8 +17,9 @@ Reach for it when the automated hygiene left an unusual state behind.
17
17
  > **When to run**: after a session that landed several PRs, or before starting a
18
18
  > new Story, to put the local checkout into a known tidy state.
19
19
  >
20
- > **Persona**: `devops-engineer` · **Skills**:
21
- > `core/git-workflow-and-versioning`
20
+ > **Persona**: `devops-engineer` git guidance comes from the always-on
21
+ > [`rules/git-conventions.md`](../rules/git-conventions.md), which needs no
22
+ > skill citation.
22
23
 
23
24
  The enumeration + reap logic lives in
24
25
  [`git-cleanup.js`](../scripts/git-cleanup.js) — it computes the candidate list,
@@ -19,8 +19,9 @@ explicitly; the interactive choice prompt fires **only** when the detected
19
19
  state is genuinely ambiguous, so the common path stays non-interactive and
20
20
  scriptable.
21
21
 
22
- > **Persona**: `devops-engineer` · **Skills**:
23
- > `core/git-workflow-and-versioning`
22
+ > **Persona**: `devops-engineer` git guidance comes from the always-on
23
+ > [`rules/git-conventions.md`](../rules/git-conventions.md), which needs no
24
+ > skill citation.
24
25
 
25
26
  ---
26
27
 
@@ -51,7 +51,8 @@ path). The report MUST include every section its lens template mandates — writ
51
51
  `_No findings._` rather than omitting a section — and always an
52
52
  `## Executive Summary` and a `## Detailed Findings` section. The Executive
53
53
  Summary carries the self-cross-check `kept <k> / dropped <d>` line
54
- ([below](#self-cross-check)).
54
+ ([below](#self-cross-check)) and, beside it, the machine-readable severity
55
+ tally ([below](#severity-tally)).
55
56
 
56
57
  Every finding under `## Detailed Findings` uses the shared 7-field skeleton
57
58
  below. A lens may **add** fields (e.g. a WCAG success criterion, a CWE ID, a
@@ -64,9 +65,11 @@ drops or renames a shared field.
64
65
  ```markdown
65
66
  ## Executive Summary
66
67
 
67
- [The lens's headline read plus the self-cross-check `kept <k> / dropped <d>`
68
- line. A lens may mandate additional report sections between here and the
69
- findings — its own body names them.]
68
+ [The lens's headline read, the self-cross-check `kept <k> / dropped <d>` line,
69
+ and the severity tally line below. A lens may mandate additional report
70
+ sections between here and the findings — its own body names them.]
71
+
72
+ Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>
70
73
 
71
74
  ## Detailed Findings
72
75
 
@@ -188,6 +191,23 @@ that rests on one of them:
188
191
  A lens that keeps every finding still records `dropped 0` — the line's absence
189
192
  is itself a defect (it means the pass did not run).
190
193
 
194
+ ## Severity tally (mandatory, machine-readable) {#severity-tally}
195
+
196
+ The Executive Summary MUST also carry exactly one tally line, counting the
197
+ findings you kept:
198
+
199
+ ```text
200
+ Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>
201
+ ```
202
+
203
+ Write every bucket, zeros included, and never count `Info` — the scale already
204
+ excludes it from scheduled work. This line is the report's own checksum:
205
+ `audit-to-stories --scan` cross-checks it against the findings its parser
206
+ extracted and names any disagreement as a **report failure**
207
+ (`summary.reportFailures[]`), and `--auto` refuses to file anything from a
208
+ report whose line is missing or wrong. A parse that silently drops findings is
209
+ otherwise indistinguishable from a clean audit.
210
+
191
211
  ## Execution strategy {#execution-strategy}
192
212
 
193
213
  A lens is a self-contained, read-only unit of work — exactly the shape a
@@ -292,6 +292,14 @@ This executes, in order:
292
292
  (files issues when auto-file is on; posts `follow-ups`).
293
293
  - `sibling-coherence` — Spec/Acceptance coherence check across sibling bodies
294
294
  (`plan-run-sibling-coherence`).
295
+ - `epic-close` — closes a container Epic once **every** child Story is
296
+ `agent::done`, as `completed`. This is the only completion cascade v2 has:
297
+ it closes the container and nothing else — no child status roll-up, no label
298
+ inheritance, no reopening. Because linkage is parent→child only, the parent
299
+ is found by scanning open `type::epic` issues, and only an Epic containing
300
+ one of *this run's* Stories is considered, so an unrelated container is
301
+ never swept. An Epic with an outstanding child is reported `pending` and
302
+ left open.
295
303
 
296
304
  A single-Story run skips the epilogue — follow-ups are captured on merge
297
305
  confirm instead (`captureStoryFollowUps`).
@@ -234,7 +234,7 @@ Each `stories.json` entry: `slug` (`^[a-z0-9][a-z0-9-]*$`), `type: "story"`,
234
234
  `title`, `body` (`goal`, optional `spec`, `changes[{path, assumption}]` —
235
235
  `creates|refactors-existing|deletes`, `non_goals`, `reason_to_exist`),
236
236
  top-level `acceptance[]`, `verify[]` (`… (unit|contract|e2e|validate)`), and
237
- `depends_on[]` (N>1 only).
237
+ `depends_on[]` (a sibling slug, or `#<id>` for an existing open Story).
238
238
 
239
239
  Nothing in that shape inventories the repo for the author. `changes[]` arrives
240
240
  pre-resolved against the working tree, and Phase 8's
@@ -414,6 +414,114 @@ That is the whole point of running it first: a dry run that comes back clean
414
414
  has already paid for every deterministic refusal, so the real persist has
415
415
  nothing left to discover except network failure.
416
416
 
417
+ ## The container Epic (Gate #3)
418
+
419
+ Gate #3 has two branches, in this order: **adopt** an Epic that already exists,
420
+ else **create** a new one.
421
+
422
+ ### Adopting an open Epic (any N)
423
+
424
+ `epicCandidates[]` in the plan-context envelope lists **every open `type::epic`
425
+ issue**, each `{ id, title, url, score, childIds }`, ranked by token overlap
426
+ between the seed and the Epic's title, `## Goal` and its children's titles. The
427
+ list is deliberately **complete rather than thresholded**: a low score is
428
+ evidence for the operator to weigh, and hiding a candidate is exactly how a
429
+ plan opens its second container for one body of work.
430
+
431
+ This branch fires at **any N, N=1 included** — "add this to the Epic we started
432
+ last week" is the single-Story case, and refusing it below three Stories would
433
+ leave the common follow-up plan with nowhere to file itself. The three-Story
434
+ threshold governs **creation** only, where it still holds: at two Stories a
435
+ pair of ids is as easy to carry as one container id.
436
+
437
+ On a yes, pass `--epic <id>`. Persist then:
438
+
439
+ 1. resolves the id **before the first create** (dry run included) — it must be
440
+ **open** and carry `type::epic`, or the run hard-errors having written
441
+ nothing;
442
+ 2. after the Stories exist, appends one `- [ ] #N` row per new Story to the
443
+ Epic's checklist via `appendEpicChildIds`, which is idempotent and preserves
444
+ existing rows' **checked state**, their order, and the fingerprint marker;
445
+ 3. mirrors a native sub-issue edge per child.
446
+
447
+ The refusal posture is the **opposite** of creation's, deliberately. Creation
448
+ degrades (an unensurable label just skips the container) because the operator
449
+ never named one. Adoption cannot: the operator named a specific id, so silently
450
+ not adopting it would leave them believing their Stories were filed somewhere
451
+ they were not. Hence a hard error, raised while nothing has been written and
452
+ the fix is free. Once the Stories are live the posture flips back — a failed
453
+ checklist write or sub-issue edge only warns.
454
+
455
+ **Only open Epics are adoptable.** A closed Epic is a finished body of work;
456
+ joining one would reopen a container the epilogue deliberately closed and
457
+ re-scope a completed plan. Open a new container, or reopen the old one by hand.
458
+
459
+ `--epic` and `--epic-title`/`--epic-goal` are **mutually exclusive** — a run
460
+ either joins a container or opens one — and supplying both is a usage error
461
+ raised before any I/O.
462
+
463
+ ### Creating a new container (N>2)
464
+
465
+ Above two Stories, `/mandrel-plan` offers to group them under one `type::epic`
466
+ container. Confirmed, persist opens it **after** the Stories — its body embeds
467
+ their issue numbers and its sub-issue edges need their database ids — and
468
+ links every created Story both ways it can: a `- [ ] #N` body checklist and a
469
+ native GitHub sub-issue edge. Both are written because each survives what the
470
+ other does not; the delivery-side reader unions them.
471
+
472
+ What the Epic must never carry: an `agent::*` label (that absence keeps it out
473
+ of the bare `/mandrel-deliver` ready list and outside the `type::story`-scoped
474
+ body lint), a `## Spec`, an `acceptance[]` / `verify[]`, or any path, finding
475
+ or rationale a child does not already hold. It is a container; unique content
476
+ here is content no delivering agent reads.
477
+
478
+ What the **Stories** never gain is an `Epic: #N` footer. Linkage is
479
+ parent→child only, which is exactly why every existing refusal of that footer
480
+ still stands and each Story stays independently deliverable (ADR
481
+ `20260905-5139`).
482
+
483
+ Degradation is deliberate: an unensurable `type::epic` label skips the Epic
484
+ entirely (an unlabelled container is not a container), while a failed
485
+ sub-issue edge only warns — the checklist still lists every child. Either way
486
+ the Stories are untouched and deliver by id. A resumed persist adopts an
487
+ existing Epic carrying the same fingerprint, which is keyed on the title **and
488
+ the exact child set**, so a run grouping different Stories never adopts the
489
+ wrong container.
490
+
491
+ ## Cross-plan `depends_on` (`#<id>`)
492
+
493
+ A `depends_on[]` entry is read **lexically**: `some-slug` is a sibling inside
494
+ this plan, `#<id>` an **external** blocker already live on the tracker. The
495
+ second form is what lets a plan authored today wait on a Story an earlier plan
496
+ opened and never had to name.
497
+
498
+ `dependencyCandidates[]` in the envelope is the advisory prompt for it: open
499
+ `type::story` issues whose declared `changes[]` footprint intersects the seed's
500
+ `complexitySignals.predictedPaths`, each carrying the `overlappingPaths[]` that
501
+ matched. Overlap is computed on **declared footprints** and not on prose, using
502
+ the same `storyFootprint` the wave runner uses to withhold colliding Stories at
503
+ dispatch — so the planner sees the collision the runtime would later enforce,
504
+ one layer earlier and while it is still cheap to order around. A seed naming no
505
+ paths short-circuits to `[]` with no provider call at all.
506
+
507
+ It stays advisory: two Stories can touch a shared barrel file with no real
508
+ ordering between them, and only the operator knows.
509
+
510
+ External refs are **excluded from sibling ordering and cycle detection** —
511
+ a Story already open is not scheduled by this run, so it has no position in the
512
+ topological sort and cannot close a cycle back into a Story that does not exist
513
+ yet. They are validated **before any create** (dry run included): each must
514
+ resolve to an **open `type::story`**, and a closed issue, a container Epic, a
515
+ missing id or a non-Story hard-errors with every bad ref named in one pass. The
516
+ strictness is the point — a blocker that can never be satisfied reads to the
517
+ delivery engine as a permanent wedge rather than as an error worth reporting.
518
+
519
+ Persist renders the entry unchanged as a `blocked by #<id>` footer line and
520
+ mirrors the native `blocked_by` edge, which is the same pair of surfaces a
521
+ sibling edge produces. `/mandrel-deliver` therefore gates on it with no engine
522
+ change: `resolve-stories.js` has always resolved foreign blockers from live
523
+ state.
524
+
417
525
  ## Ready means fully persisted
418
526
 
419
527
  `agent::ready` is the **terminal** step, not part of the creating POST.
@@ -7,8 +7,8 @@ description:
7
7
 
8
8
  # /mandrel-deliver
9
9
 
10
- > **Lean spine.** Happy path + gate list. Sequencing, dispatch mechanics,
11
- > intent phrases, ceremony and the epilogue live in on-demand
10
+ > **Lean spine.** Happy path + gate list. Sequencing, dispatch mechanics, intent
11
+ > phrases, ceremony and the epilogue live in on-demand
12
12
  > [`helpers/deliver-reference.md`](helpers/deliver-reference.md) ("reference"
13
13
  > below); the unplanned path in
14
14
  > [`helpers/deliver-light.md`](helpers/deliver-light.md). Every delivery reads
@@ -21,9 +21,8 @@ close-and-land tail; Stories are implemented via
21
21
  [`helpers/deliver-story.md`](helpers/deliver-story.md).
22
22
 
23
23
  The dependency graph is **discovered, not declared** — `resolve-stories.js`
24
- reads it from live state, so you can deliver Stories **across plan runs and
25
- over time**. `plan-run::<id>` is filter metadata, never a resolution input;
26
- `route::lite` a body-derived hint only.
24
+ reads it from live state, so you can deliver Stories **across plan runs and over
25
+ time**. `plan-run::<id>` is filter metadata, never a resolution input.
27
26
 
28
27
  ## Inputs
29
28
 
@@ -36,25 +35,28 @@ you read:
36
35
  | `/mandrel-deliver 4712` | ids | One Story via `helpers/deliver-story.md`, **inline in this session** — no `story-worker` spawn. |
37
36
  | `/mandrel-deliver 4712 4713 …` | ids | Resolve the set, sequence by the discovered graph via `stories-wave-tick.js`, dispatch sub-agents. |
38
37
  | `/mandrel-deliver 4712 - 4716` | ids | A **range** — every id in the inclusive span. |
38
+ | `/mandrel-deliver 4700` (a `type::epic`) | ids | The Epic's **open** child Stories. Mixes with Story ids. |
39
39
  | `/mandrel-deliver add a --json flag to doctor` | prompt | Unplanned work: gate, author a receipt Story, land it — [`helpers/deliver-light.md`](helpers/deliver-light.md). |
40
40
 
41
41
  **The discriminator is lexical and total.** An argument matching `^#?\d+$` is an
42
42
  id, and `^#?\d+\s*[-–—]\s*#?\d+$` an inclusive **range** — pass one on as a
43
- single unspaced token, never hand-expanded (reference § Ranges). Either shape
43
+ single unspaced token, never hand-expanded (reference). Either shape
44
44
  means ids; anything else means a prompt. A **mixed** invocation (ids *and*
45
- prose) is a **hard error** — refuse it and ask which was meant. A ticket not
46
- `type::story`, or carrying an `Epic: #N` footer, is a hard error too.
45
+ prose) is a **hard error** — refuse it and ask which was meant. A ticket that
46
+ is neither `type::story` nor `type::epic`, or that carries an `Epic: #N`
47
+ footer, is a hard error too — container Epics link parent→child only, so that
48
+ footer stays refused.
47
49
 
48
50
  ## Saying what you want
49
51
 
50
- No flags to remember: state intent — *"…but I'll merge it myself"*, *"…one at
51
- a time"* — and announce what you read. Phrasings and the flags they fill in:
52
+ No flags to remember: state intent — *"…but I'll merge it myself"*, *"…one at a
53
+ time"* — and announce what you read. Phrasings and the flags they fill in:
52
54
  reference § Intent phrases.
53
55
 
54
56
  `--yes` is **runner-set, never operator-typed**: cron, `/loop` and headless
55
57
  dispatch set it to mean *nobody is at the keyboard*, which fails the unplanned
56
- path's over-scope stop closed to an envelope instead of a question. Never offer
57
- it to an operator or add it to an attended run.
58
+ path's over-scope stop closed to an envelope instead of a question. Never add it
59
+ to an attended run.
58
60
 
59
61
  ## Procedure
60
62
 
@@ -66,9 +68,11 @@ it to an operator or add it to an attended run.
66
68
  `node .agents/scripts/resolve-stories.js --ids <id,id,...>`. It validates the
67
69
  set and shows what will run: read `stories[]`, `dag[]` and `done[]` to
68
70
  present the order in step 2, but do **not** thread them into step 3 — the
69
- tick re-resolves the graph every beat. It hard-errors (exit 1) on an id that
70
- is not a Story, carries an `Epic: #N` footer, or whose edges cannot be
71
- read a missing gate would co-dispatch against an unlanded blocker.
71
+ tick re-resolves the graph every beat. An Epic id expands to its open child
72
+ Stories first **announce it**. It hard-errors (exit 1) on an id that is
73
+ neither a Story nor an Epic, on an `Epic: #N` footer, on an Epic with no
74
+ open children, or on edges it cannot read — a missing gate would co-dispatch
75
+ against an unlanded blocker.
72
76
 
73
77
  2. **Confirm (N>1).** Present the order; wait unless `--yes`.
74
78
 
@@ -87,62 +91,62 @@ it to an operator or add it to an attended run.
87
91
  Each beat re-probes live state to derive done / in-flight itself; you never
88
92
  compute them. `--dispatched` is the one thing you must supply — the
89
93
  append-only list of every id you spawned this run. Cross-run de-confliction
90
- via the assignee lease is automatic (reference §§ Sequencing edge cases,
91
- Dispatch mechanics). Branch on the exit code:
94
+ via the assignee lease is automatic. Branch on the exit code:
92
95
  - **0** — dispatch each `ready` id (already capped and overlap-free); an
93
96
  empty `ready` with work in flight means "waiting", so keep looping;
94
97
  `epilogueDue: true` means every Story is done — step 4.
95
98
  - **2 / 3 / 4** — `cycleError` / `wedged` / `blocked`: stop the loop and
96
- route per reference § Sequencing edge cases. **4** is the protocol's HITL
97
- pause ([`instructions.md` § 1.J](../instructions.md)) — surface it and
98
- wait for the operator; never poll.
99
+ route per reference. **4** is the protocol's HITL pause
100
+ ([`instructions.md` § 1.J](../instructions.md)) — surface it and wait for
101
+ the operator; never poll.
99
102
 
100
103
  4. **Close each hand-off** (§ Closing what the workers hand back), then, with
101
104
  every Story landed, run the **per-run epilogue (N>1)**:
102
- `node .agents/scripts/plan-run-epilogue.js --stories 101,102`. N=1 skips it
103
- ([reference § Per-run epilogue](helpers/deliver-reference.md)).
105
+ `node .agents/scripts/plan-run-epilogue.js --stories 101,102`, which also
106
+ closes a container Epic whose children all landed. N=1 skips it
107
+ ([reference](helpers/deliver-reference.md)).
104
108
 
105
109
  5. **Correct what the change invalidated.** If a memory you recalled this
106
- session is now wrong — a trap this landed, a budget it moved — fix that
107
- entry now, while both the old belief and the new fact are in context, and
108
- say so when you report. No memory substrate → skip silently. Sweeping the
109
- whole pool is [`/memory-consolidate`](memory-consolidate.md), not this step.
110
+ session is now wrong — a trap this landed, a budget it moved — fix that entry
111
+ now, while both the old belief and the new fact are in context, and say so
112
+ when you report. No memory substrate → skip silently. Sweeping the whole pool
113
+ is [`/memory-consolidate`](memory-consolidate.md), not this step.
110
114
 
111
115
  ## Closing what the workers hand back {#tail}
112
116
 
113
117
  **The tail is the orchestrator's, not the worker's.** A dispatched
114
118
  `story-worker` stops at a pushed branch and returns a hand-off; **you** run
115
119
  [`helpers/deliver-story.md`](helpers/deliver-story.md) Step 3
116
- (`single-story-close.js`) for it, foreground, and relay the envelope.
120
+ (`single-story-close.js`) for it, foreground, and relay its envelope.
117
121
 
118
- **Serialize the tail.** Implementation runs in parallel; closing does not. Close
119
- one Story at a time — closes contend on the base branch, the merge queue and
120
- the checkout. A worker handing back mid-close waits its turn.
122
+ **Serialize the tail.** Implementation runs in parallel; closing does not.
123
+ Close one Story at a time — closes contend on the base branch, the merge queue
124
+ and the checkout. A worker handing back mid-close waits its turn.
121
125
 
122
- **A worker returning no terminal envelope is the expected shape**, not a failure
123
- to answer with a re-dispatch: only close mints one. Close the pushed branch, or
124
- probe read-only with `node .agents/scripts/deliver-recover.js --story <storyId>`
125
- and resume the worker or close it names.
126
+ **A worker returning no terminal envelope is expected**, not a failure to answer
127
+ with a re-dispatch: only close mints one. Close the pushed branch, or probe
128
+ read-only with `node .agents/scripts/deliver-recover.js --story <storyId>` and
129
+ resume what it names.
126
130
 
127
131
  **Reading the outcome.** Each close ends the Story in one schema-validated
128
132
  envelope — `landed` | `pending` | `blocked` | `failed`; statuses, exits and
129
- fields are digest § 5. `pending` is **not** a failure — `nextCommand` resumes
130
- it; run that, do not re-dispatch.
133
+ fields are digest § 5. `pending` is **not** a failure — run its `nextCommand`.
131
134
 
132
135
  **Branch model (authoritative).** `story-<id>` → PR → `main` (squash +
133
136
  required checks), per digest § 2; dependent Stories land sequentially. Ceremony
134
- depth (profiles + derived level via `ceremony-routing.js`, review depth reading
135
- it): reference § Ceremony.
137
+ depth (profiles + the derived level via `ceremony-routing.js`, which review
138
+ depth reads): reference § Ceremony.
136
139
 
137
140
  ## Constraints
138
141
 
139
142
  - **Land or block — never a silent local build** (digest § 2). Attended delivers
140
143
  default to close-and-land (`delivery.routing.closeAndLand: true`); rest at
141
- `agent::closing` only when a human owns it.
142
- - **`/mandrel-deliver` never plans.** Planned tickets come from [`/mandrel-plan`](mandrel-plan.md), and
143
- an over-scope prompt **escalates and ends** — never invoke `/mandrel-plan` in this
144
- session to rescue it ([`helpers/deliver-light.md`](helpers/deliver-light.md)
145
- § Escalation is terminal).
144
+ `agent::closing` only when a human owns the merge.
145
+ - **`/mandrel-deliver` never plans.** Planned tickets come from
146
+ [`/mandrel-plan`](mandrel-plan.md), and an over-scope prompt **escalates and
147
+ ends** — never invoke `/mandrel-plan` in this session to rescue it
148
+ ([`helpers/deliver-light.md`](helpers/deliver-light.md) § Escalation is
149
+ terminal).
146
150
 
147
151
  ## See also
148
152