mandrel 2.54.0 → 2.56.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 (134) hide show
  1. package/.agents/agents/story-worker.md +24 -23
  2. package/.agents/audit-checklists/accessibility.md +0 -3
  3. package/.agents/audit-checklists/mobile.md +0 -4
  4. package/.agents/docs/agentrc-reference.json +8 -2
  5. package/.agents/docs/configuration.md +5 -0
  6. package/.agents/rules/ci-remediation.md +39 -21
  7. package/.agents/schemas/agentrc.schema.json +34 -1
  8. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
  10. package/.agents/scripts/audit-to-stories.js +374 -76
  11. package/.agents/scripts/check-audit-attribution.js +119 -62
  12. package/.agents/scripts/check-test-portability.js +512 -0
  13. package/.agents/scripts/coverage-capture.js +17 -10
  14. package/.agents/scripts/evidence-gate.js +31 -4
  15. package/.agents/scripts/file-ci-gap.js +306 -0
  16. package/.agents/scripts/generate-workflows-doc.js +65 -14
  17. package/.agents/scripts/git-cleanup.js +4 -0
  18. package/.agents/scripts/lib/ITicketingProvider.js +78 -0
  19. package/.agents/scripts/lib/audit-advisories.js +195 -0
  20. package/.agents/scripts/lib/audit-attribution.js +22 -0
  21. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  22. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
  23. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  24. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  25. package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
  26. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  27. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
  28. package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
  29. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  30. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
  31. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  32. package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
  33. package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
  34. package/.agents/scripts/lib/cli-args.js +26 -0
  35. package/.agents/scripts/lib/close-validation/gates.js +113 -7
  36. package/.agents/scripts/lib/close-validation/process.js +7 -3
  37. package/.agents/scripts/lib/close-validation/runner.js +62 -11
  38. package/.agents/scripts/lib/config/ci.js +28 -9
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
  40. package/.agents/scripts/lib/config-settings-schema.js +52 -1
  41. package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
  42. package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
  43. package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
  44. package/.agents/scripts/lib/coverage-capture.js +77 -3
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +42 -2
  50. package/.agents/scripts/lib/full-suite-lock.js +232 -6
  51. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  52. package/.agents/scripts/lib/git/sync-from-base.js +130 -13
  53. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  54. package/.agents/scripts/lib/label-constants.js +6 -1
  55. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  56. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
  57. package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
  58. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  59. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  60. package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
  61. package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
  62. package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
  63. package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
  64. package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
  65. package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
  66. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
  67. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
  68. package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
  69. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
  70. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
  71. package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
  72. package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
  73. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  74. package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
  75. package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
  76. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +39 -3
  77. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
  78. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
  79. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
  80. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
  81. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
  82. package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
  83. package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
  84. package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
  85. package/.agents/scripts/lib/orchestration/run-epilogue.js +63 -42
  86. package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
  87. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
  88. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
  89. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
  90. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
  91. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
  92. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
  93. package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
  94. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
  95. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  96. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
  97. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
  98. package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
  99. package/.agents/scripts/lib/pinned-override-notes.js +41 -53
  100. package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
  101. package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
  102. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
  103. package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
  104. package/.agents/scripts/lib/test-temp.js +167 -30
  105. package/.agents/scripts/lib/validation-evidence.js +37 -0
  106. package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
  107. package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
  108. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  109. package/.agents/scripts/merge-baseline.js +175 -21
  110. package/.agents/scripts/pr-watch-with-update.js +3 -2
  111. package/.agents/scripts/providers/github/errors.js +22 -1
  112. package/.agents/scripts/providers/github/issues.js +106 -1
  113. package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
  114. package/.agents/scripts/providers/github.js +6 -0
  115. package/.agents/scripts/resolve-stories.js +44 -34
  116. package/.agents/scripts/single-story-close.js +5 -0
  117. package/.agents/scripts/stories-wave-tick.js +37 -13
  118. package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
  119. package/.agents/workflows/audit-accessibility.md +16 -31
  120. package/.agents/workflows/audit-mobile.md +20 -37
  121. package/.agents/workflows/audit-to-stories.md +63 -27
  122. package/.agents/workflows/git-cleanup.md +17 -3
  123. package/.agents/workflows/helpers/audit-lens-core.md +45 -0
  124. package/.agents/workflows/helpers/deliver-digest.md +7 -6
  125. package/.agents/workflows/helpers/deliver-reference.md +35 -14
  126. package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
  127. package/.agents/workflows/helpers/deliver-story.md +15 -12
  128. package/.agents/workflows/helpers/plan-reference.md +30 -0
  129. package/.agents/workflows/mandrel-plan.md +10 -13
  130. package/.agents/workflows/memory-consolidate.md +14 -9
  131. package/docs/CHANGELOG.md +37 -0
  132. package/lib/cli/registry.js +64 -21
  133. package/lib/cli/sync.js +27 -2
  134. package/package.json +7 -4
@@ -49,6 +49,13 @@ issue state rather than against anything you hand it. That is why there is no
49
49
  batch label to pass and why a blocker that landed in an unrelated run is simply
50
50
  seen as done.
51
51
 
52
+ **A Story with no `agent::*` label is refused.** The audit sweep files Stories
53
+ deliberately without one — their bodies are audit prose, not a scoped change
54
+ with verifiable acceptance criteria — so resolving one means dispatching a
55
+ worker at an unenriched body, after taking its lease. Route it through
56
+ `/mandrel-plan` first, which applies `agent::ready` at the end of planning.
57
+ `--allow-unlabelled` is the deliberate escape hatch.
58
+
52
59
  **The non-zero exit codes.** **2** — `cycleError`: the graph is
53
60
  self-referential; fix `depends_on`, do not retry. **3** — `wedged`: nothing
54
61
  dispatchable and nothing in flight, with the undone Stories and their unmet
@@ -292,10 +299,10 @@ This executes, in order:
292
299
  (files issues when auto-file is on; posts `follow-ups`).
293
300
  - `sibling-coherence` — Spec/Acceptance coherence check across sibling bodies
294
301
  (`plan-run-sibling-coherence`).
295
- - `epic-close` — **reports** which container Epics this run closed and which
296
- are still pending. It derives nothing itself: every step here and the
297
- per-Story land tail alike delegate to `epic-rollup.js`, so one rule decides
298
- a container's state.
302
+ - `epic-close` — **reports** which of the run's container Epics its land tails
303
+ left closed and which are still open. **Read-only** — it derives nothing:
304
+ every child state change is already a rollup edge, so the container was
305
+ derived from a complete child set by the last Story's own land tail.
299
306
 
300
307
  A single-Story run skips the epilogue — follow-ups are captured on merge
301
308
  confirm instead (`captureStoryFollowUps`).
@@ -304,9 +311,12 @@ confirm instead (`captureStoryFollowUps`).
304
311
 
305
312
  A container Epic is never delivered, so nothing used to write to it during
306
313
  the run it was the subject of. `epic-rollup.js` derives its state from its
307
- children at both per-Story lifecycle edges — the `agent::executing` flip in
308
- `single-story-init.js` and the post-land tail (reported as the tail's
309
- `epicRollup` step) — which is why it holds at **N=1**, where no epilogue runs.
314
+ children at **every edge that changes a child's state** — the
315
+ `agent::executing` flip in `single-story-init.js`, the post-land tail
316
+ (reported as the tail's `epicRollup` step), and `plan-persist`'s supersede
317
+ close (reported as `supersede.epicRollup`) — which is why it holds at **N=1**,
318
+ where no epilogue runs, and why a cohort superseded by a re-plan no longer
319
+ strands its container open above finished work.
310
320
 
311
321
  - **Status** follows the children's composition (`deriveParentState` mapped
312
322
  onto the board's three options): any **open** child executing or blocked →
@@ -319,13 +329,24 @@ children at both per-Story lifecycle edges — the `agent::executing` flip in
319
329
  ready list.
320
330
  - **Owner** — `github.operatorHandle` is added to the Epic while any child is
321
331
  in flight, through the additive assignees endpoint, and is never removed.
322
- - **Closure** is one-way: every child landed closes the container as
323
- `completed`; a reopened child moves Status back to `In Progress` and does
324
- **not** reopen it.
325
- - The parent lookup scans open `type::epic` issues, because linkage is
326
- parent→child only, and reads children as the body checklist **union** the
327
- native sub-issue edges — the same reader `/mandrel-deliver`'s expansion
328
- uses, so an Epic can never be expandable but unclosable.
332
+ - **Closure** is one-way: a container whose children are all finished closes,
333
+ as `completed` when at least one child landed and as `not_planned` when none
334
+ did (a cohort superseded by a re-plan is finished, but nothing merged). A
335
+ reopened child moves Status back to `In Progress` and does **not** reopen the
336
+ issue — which is why the lookup reads `state: 'all'`, since an open-only
337
+ listing cannot see the container it would have to correct.
338
+ - The parent lookup resolves the native parent edge in **one** call
339
+ (`getParentIssue`), because linkage is parent→child only; it falls back to a
340
+ `type::epic` scan for a child linked by checklist alone. Children are read as
341
+ the body checklist **union** the native sub-issue edges — literally the same
342
+ reader `/mandrel-deliver`'s expansion uses, so an Epic can never be
343
+ expandable but unclosable — bounded at 5 concurrent reads.
344
+ - A checklist row citing an id that resolves to nothing is **dropped with a
345
+ warning** when the native read succeeded: hand-edited prose can cite a
346
+ deleted or mistyped issue, and no re-run will make it resolve. An
347
+ unresolvable *native* edge still fails the read. An Epic-typed child is
348
+ refused by name (`epic-typed-child`) and neither blocks nor advances the
349
+ parent.
329
350
  - Every step is best-effort and never throws: a stale container costs
330
351
  tidiness, not a landed Story's envelope.
331
352
 
@@ -240,9 +240,10 @@ discovers them only after the whole close pipeline has run, at several times
240
240
  the cost of one full-suite run in the worktree.
241
241
 
242
242
  **Run it once, last, so close can credit it.** The run belongs **after** the
243
- self-eval loop's last fix commit and immediately **before** the hand-off push,
244
- so its stamp describes the tree that is pushed; redraft rounds run scoped
245
- tests. Close skips a gate that already passed at the current HEAD, but a bare
243
+ self-eval loop's last fix commit and **after** the hand-off push — the credit
244
+ is keyed on the tree, not on push state, so pushing first keeps the stamp and
245
+ buys the ordering Step 2.5 needs (the capture is backgrounded, and its
246
+ completion ends the turn); redraft rounds run scoped tests. Close skips a gate that already passed at the current HEAD, but a bare
246
247
  `npm test` deposits no such record — the suite then runs twice per delivery,
247
248
  once here and once in the close gate chain. Pick the invocation by the same
248
249
  predicate `close-validation/gates.js` uses to choose its test gate:
@@ -262,7 +263,9 @@ The credit expires the moment it stops describing the tree: evidence is keyed
262
263
  on HEAD, the capture stamp on a content digest of `crap.targetDirs`. A
263
264
  self-eval fix — or any commit — invalidates it and close re-runs the suite for
264
265
  real, so this never trades away the gate. That keying is exactly why the run
265
- comes last.
266
+ comes last, and why the push before it is free. Close's own base-sync can
267
+ spend the stamp too when it lands base commits; it now says so out loud rather
268
+ than silently re-running the suite.
266
269
 
267
270
  **`verify[]` reuses the same stamp.** A `verify[]` entry that is itself a
268
271
  full-suite command is reported **credited** against that stamp rather than
@@ -679,13 +682,28 @@ When the watch exits, branch on the exit code:
679
682
 
680
683
  **Triage authority.** How to classify and remediate a red (or repeatedly slow)
681
684
  check — the root-cause-only decision tree for infra/transient and flaky failures
682
- (reproduce → check `main` → bisect env vs code → fix in-scope or file a
683
- `meta::framework-gap` issue), the never-rerun / never-quarantine prohibitions,
684
- and the escalation criteria (three-strikes, the 30-minute wall-clock timebox,
685
- and the clearly-environmental fast path) — is defined once in
685
+ (reproduce → check `main` → bisect env vs code → fix in-scope, or reach an
686
+ Option-2 verdict and file the intake issue), the never-rerun / never-quarantine
687
+ prohibitions, and the escalation criteria (three-strikes, the 30-minute
688
+ wall-clock timebox, and the clearly-environmental fast path) — is defined once in
686
689
  [`.agents/rules/ci-remediation.md`](../../rules/ci-remediation.md). Read it
687
690
  before remediating a red check.
688
691
 
692
+ **Filing an out-of-scope root cause is one command, never a hand-run `gh issue
693
+ create`:**
694
+
695
+ ```bash
696
+ node <agentRoot>/scripts/file-ci-gap.js --story <storyId> \
697
+ --verdict <pre-existing|capacity|unreproducible-tier> \
698
+ --owner <consumer|framework|platform> --evidence "<proof reading>" [--block]
699
+ ```
700
+
701
+ It reads the digest, routes the filing to the repo that owns the fault, updates
702
+ the existing ticket when the signature is a repeat, posts the `friction`
703
+ comment, and with `--block` flips the Story. What it files is an **intake**
704
+ issue, not a Story — `/mandrel-plan <issue number>` graduates it on the next
705
+ planning pass, so the delivery never waits on planning.
706
+
689
707
  ### The auto-merge wait is an internally-blocking step
690
708
 
691
709
  This is the single most important contract of this workflow, and the seam
@@ -93,18 +93,21 @@ are reference § Step 2. Hard gates always run in Step 3 — the derived level
93
93
  never disables them; do **not** pre-run the chain here — Step 2.5's credited
94
94
  suite run is the sole exception.
95
95
 
96
- ### Step 2.5 — The creditable full-suite run, then push and hand off
97
-
98
- Run the full suite **once**, after the self-eval loop's last fix commit and
99
- immediately **before** the push, in the shape close credits (**digest § 5**):
100
- the credit is keyed on the tree, so any later commit invalidates it, and a bare
101
- `npm test` deposits none. Red → fix, commit, re-run. An inline run makes the
102
- same run before Step 3.
103
-
104
- Then (sub-agent dispatch only) push `story-<storyId>` to `origin`, confirm the
105
- remote ref moved, and return the hand-off — Story id, `workCwd`, branch, pushed
106
- head SHA, self-eval verdict, `verify[]` evidence — then stop. Do not open the
107
- PR; do not compose a terminal envelope.
96
+ ### Step 2.5 — Push, then the creditable full-suite run, then hand off
97
+
98
+ **Push first.** After the self-eval loop's last fix commit, push
99
+ `story-<storyId>` to `origin`, confirming the remote ref moved: the capture
100
+ below is backgrounded, so *its* completion ends the turn.
101
+
102
+ Then run the full suite **once**, after the push, in the shape close credits
103
+ (**digest § 5**): the credit is keyed on the tree, not on push state, so only
104
+ a *later* commit invalidates it; a bare `npm test` deposits none. Red →
105
+ fix, commit, push, re-capture.
106
+
107
+ Then (sub-agent dispatch only) return the hand-off — Story id, `workCwd`,
108
+ branch, pushed head SHA, self-eval verdict, `verify[]` evidence — and stop.
109
+ Do not open the PR or compose a terminal envelope. An inline run captures
110
+ before Step 3.
108
111
 
109
112
  ## Step 3 — Close and land (`single-story-close.js`)
110
113
 
@@ -92,6 +92,36 @@ The marker keeps the operator's undelegated decisions findable after the
92
92
  fact: reviewing a `--yes` plan means scanning its decisions-made-by-default,
93
93
  not re-deriving which assumptions were really the agent's to make.
94
94
 
95
+ ## Gate #1 → the memory-pool advisory (`memoryPoolAdvisory`)
96
+
97
+ On a truthy `memoryPoolAdvisory.recommend`, name
98
+ [`/memory-consolidate`](../memory-consolidate.md) at Gate #1, quoting its
99
+ `reasons[]`. Purely advisory: a stale pool degrades recall, it does not make
100
+ the plan wrong, so it never blocks and never reroutes.
101
+
102
+ ## Gate #1 → graduating a CI-gap intake filing (`intake`)
103
+
104
+ The envelope's `priorFeedback` arrays carry the open `meta::*` feedback issues.
105
+ A row flagged `intake: true` is a **CI-gap intake filing** — written by
106
+ [`file-ci-gap.js`](../../scripts/file-ci-gap.js) when a delivery reached an
107
+ Option-2 verdict in [`ci-remediation.md`](../../rules/ci-remediation.md) and the
108
+ root cause was outside its scope. It carries evidence (failure signature, run
109
+ link, occurrence history, ownership routing) but **no `## Spec`, no
110
+ `acceptance[]` / `verify[]` and no `agent::*` label**, so `/mandrel-deliver`
111
+ cannot take it: it is intake awaiting graduation, by design. Delivery files it
112
+ and moves on rather than blocking on a planning pass nobody is present for.
113
+
114
+ Graduating one is exactly **tickets mode**: `/mandrel-plan <issue number>`
115
+ rewrites it into a Story, `supersedes[]` claims it, and persist closes it.
116
+
117
+ At Gate #1, in **ask** and **seed** mode, name any open intake rows and offer
118
+ that instead of the seed in front of you — a filing that keeps recurring
119
+ (its `## Occurrences` table is the count) is usually the better next Story than
120
+ whatever prompted this run. It is **advisory**: never reroute automatically, and
121
+ skip the offer entirely under `--yes`, where nobody is at the keyboard to take
122
+ it. A `platformGaps[]` row is the same shape with a different owner — the
123
+ Story it graduates into may well be a config or runbook change rather than code.
124
+
95
125
  ## Gate #1 → the light path (in-session handoff)
96
126
 
97
127
  On a confirmed `deliverLightSuggestion`, `/mandrel-plan` routes into
@@ -25,8 +25,7 @@ mode from what the operator typed, announce it, act**:
25
25
 
26
26
  **Resolving a bare id.** Read live state rather than asking: `agent::done` can
27
27
  only be amended, an open unplanned issue only planned. **Announce the
28
- derivation** — "4712 is `agent::done` → amending". Ask **only** for an open
29
- Story already at `agent::ready`.
28
+ derivation**. Ask **only** for an open Story already at `agent::ready`.
30
29
 
31
30
  ## Saying what you want
32
31
 
@@ -59,10 +58,9 @@ and derives source ids from its `sourceTickets[]`; it also writes
59
58
 
60
59
  The envelope carries docs context, the story-author prompt, `sourceTickets[]`,
61
60
  `duplicates[]` (open **Stories**, never Epics), `epicCandidates[]` +
62
- `dependencyCandidates[]` (Gate #3; path collisions) and advisory
63
- `complexitySignals` (**no routing authority**). A trivial scope can claim the
64
- lite route at persist — shape-validated, failing closed to `full`
65
- ([ref](helpers/plan-reference.md)).
61
+ `dependencyCandidates[]` (Gate #3; path collisions), `priorFeedback` and
62
+ advisory `complexitySignals` (**no routing authority**). A trivial scope claims
63
+ the lite route at persist, failing closed to `full`.
66
64
 
67
65
  **Triage each unknown by resolver** ([ref](helpers/plan-reference.md)): an
68
66
  **AFK** unknown (research settles it) is resolved before authoring, never
@@ -70,12 +68,9 @@ assumed; a **HITL** unknown goes to Gate #1. Under `--yes` do not ask free-form
70
68
  operator questions — AFK unknowns are still researched; only HITL unknowns land
71
69
  in Key Assumptions, each a decision-made-by-default.
72
70
 
73
- **Gate #1** — STOP to confirm the sharpened plan intent and any
74
- duplicate-candidate review. Under `--yes`, auto-proceed.
75
-
76
- On a truthy `memoryPoolAdvisory.recommend`, name
77
- [`/memory-consolidate`](memory-consolidate.md) quoting its `reasons[]`;
78
- advisory.
71
+ **Gate #1** — STOP to confirm the sharpened plan intent, any
72
+ duplicate-candidate review, and any `intake` row worth graduating
73
+ ([ref](helpers/plan-reference.md)). Under `--yes`, auto-proceed.
79
74
 
80
75
  On a truthy `deliverLightSuggestion.suggested`, offer — advisory, never an
81
76
  automatic reroute — to deliver the seed instead; on confirm route **in this
@@ -93,7 +88,9 @@ persist parses either, serializes canonical markdown and syncs top-level
93
88
 
94
89
  **Grounding = your reads + Phase 8.** Nothing inventories the repo: read each
95
90
  file you cite; persist hard-errors on any `{path, assumption}` absent from the
96
- tree. Fields: [ref](helpers/plan-reference.md).
91
+ tree — a `refactors-existing` on a path the base branch **deleted or renamed**
92
+ included. One rescue: a **never-tracked** one normalises to `creates`. Fields:
93
+ [ref](helpers/plan-reference.md).
97
94
 
98
95
  Artifacts under `temp/plan-<slug>/`: `stories.json` (**length 1 by default**;
99
96
  over-budget Specs fail closed — split or tighten, never under `docs/`); optional
@@ -100,19 +100,24 @@ Then write the receipt to `.consolidation-stamp.json` in the pool root:
100
100
  count the directory, never the plan. It is the baseline the next run measures
101
101
  growth against, so a wrong number silently mis-arms the nudge.
102
102
 
103
- The `/mandrel-plan` Phase 0 advisory re-arms on exactly two conditions: the
104
- stamp aging past `planning.memoryPool.staleAfterDays` (30), or
105
- `planning.memoryPool.growthDelta` (25) entries written since that count. Pool
106
- size alone never triggers it — a pass that keeps every entry still quiets the
107
- nudge. A stamp with no `entryCount` leaves growth unmeasured, and only the age
108
- arm can speak until the next pass writes one.
103
+ The `/mandrel-plan` Phase 0 advisory re-arms on exactly three conditions: the
104
+ stamp aging past `planning.memoryPool.staleAfterDays` (30),
105
+ `planning.memoryPool.growthDelta` (25) entries written since that count, or
106
+ `MEMORY.md` exceeding `planning.memoryPool.indexByteCeiling` (24576) bytes.
107
+ Pool size alone never triggers it — a pass that keeps every entry still quiets
108
+ the first two arms. A stamp with no `entryCount` leaves growth unmeasured, and
109
+ only the age and index arms can speak until the next pass writes one; a stamp
110
+ dated in the future reads as no stamp at all.
109
111
 
110
112
  Write it **only** after Gate #2 — the stamp asserts an operator reviewed the
111
113
  pass, so writing it early makes it a lie.
112
114
 
113
- Close with counts: entries read, corrected, merged, pruned, and the new total.
114
- Then the forecast the operator would otherwise derive by hand: when the
115
- advisory next fires, and which arm reaches it first.
115
+ Close with counts: entries read, corrected, merged, pruned, the new total, and
116
+ **the rewritten `MEMORY.md`'s size in bytes beside that count** — the index is
117
+ truncated at the byte ceiling, so a pass that pruned entries but left the
118
+ index over the cap has not fixed the loss, and the number is the only way the
119
+ operator can see that. Then the forecast the operator would otherwise derive
120
+ by hand: when the advisory next fires, and which arm reaches it first.
116
121
 
117
122
  ## Constraints
118
123
 
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,43 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.56.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.55.0...mandrel-v2.56.0) (2026-09-11)
19
+
20
+
21
+ ### Added
22
+
23
+ * audit-to-stories: dedup against a host-supplied issue index, so a sweep on a gh-less host stops re-filing what it already filed ([#5301](https://github.com/dsj1984/mandrel/issues/5301)) ([#5304](https://github.com/dsj1984/mandrel/issues/5304)) ([1b8abf0](https://github.com/dsj1984/mandrel/commit/1b8abf072a05db65e54a63cb8ec6cf6f06bc80ab))
24
+ * audit-to-stories: make Phase 5a filings visible to the next sweep — record the ledger from plan-persist and stamp the audit labels the corpus is listed by ([#5307](https://github.com/dsj1984/mandrel/issues/5307)) ([#5308](https://github.com/dsj1984/mandrel/issues/5308)) ([094ba8a](https://github.com/dsj1984/mandrel/commit/094ba8ae6e2640ba3ac7cb5a4aeb9309aa72e42d))
25
+ * audit-to-stories: record filed Issues in the cross-run ledger, so its suppression branch stops being unreachable ([#5305](https://github.com/dsj1984/mandrel/issues/5305)) ([#5306](https://github.com/dsj1984/mandrel/issues/5306)) ([d5e36ab](https://github.com/dsj1984/mandrel/commit/d5e36ab796f6946bf93d7161efa1470cad8a0a2b))
26
+ * cI-gap intake: script-backed, repo-routed, deduped meta filings that /mandrel-plan can graduate ([#5300](https://github.com/dsj1984/mandrel/issues/5300)) ([#5302](https://github.com/dsj1984/mandrel/issues/5302)) ([37bc287](https://github.com/dsj1984/mandrel/commit/37bc2875ed8c98298afd90c72ece4f318e2c233d))
27
+
28
+ ## [2.55.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.54.0...mandrel-v2.55.0) (2026-09-11)
29
+
30
+
31
+ ### Added
32
+
33
+ * a backgrounded capture ends the worker's turn on an unpushed branch, and close's base-sync silently spends the credit that capture was earning ([#5267](https://github.com/dsj1984/mandrel/issues/5267)) ([#5273](https://github.com/dsj1984/mandrel/issues/5273)) ([8d2bed1](https://github.com/dsj1984/mandrel/commit/8d2bed1e2132bc8b8901b58a33231b2bda5fd61c))
34
+ * epic rollup: every child state change is an edge, the parent is resolved in one call, and provider reads carry one declared shape ([#5280](https://github.com/dsj1984/mandrel/issues/5280)) ([#5294](https://github.com/dsj1984/mandrel/issues/5294)) ([c2c9b96](https://github.com/dsj1984/mandrel/commit/c2c9b9654497404c20105bc3b7edcdbd45fb307e))
35
+ * full-suite credit: a deposit path exists, the lock is liveness-aware, and stamps and evidence are keyed on content ([#5278](https://github.com/dsj1984/mandrel/issues/5278)) ([#5296](https://github.com/dsj1984/mandrel/issues/5296)) ([96c3240](https://github.com/dsj1984/mandrel/commit/96c32405025768de96d2e12680a54ed326787345))
36
+ * memory advisory: measure the index against the harness byte cap, reject future-dated stamps, tighten the skill-id schema, and correct the rename cutover text ([#5285](https://github.com/dsj1984/mandrel/issues/5285)) ([#5289](https://github.com/dsj1984/mandrel/issues/5289)) ([22389a1](https://github.com/dsj1984/mandrel/commit/22389a106cb8e3ce42b0fde288ee473b88e49b99))
37
+ * tests: a portability lint catches the Windows-only shapes before they land, and a vanished temp root is recorded instead of healed ([#5284](https://github.com/dsj1984/mandrel/issues/5284)) ([#5295](https://github.com/dsj1984/mandrel/issues/5295)) ([3358eef](https://github.com/dsj1984/mandrel/commit/3358eefeb0e5f36446b84a4f995e4b90641f64dc))
38
+
39
+
40
+ ### Fixed
41
+
42
+ * a test fixture that loses its temp root mid-run fails with an unreadable ENOENT, and every later makeTempDir in that process fails with it ([#5274](https://github.com/dsj1984/mandrel/issues/5274)) ([#5275](https://github.com/dsj1984/mandrel/issues/5275)) ([5ac8699](https://github.com/dsj1984/mandrel/commit/5ac86995b92537b33bb209492cf9c3d9a064f792))
43
+ * audit sweep: empty groups parse, the ledger commit is re-runnable from the remote base, SCA attribution is per advisory, and label-less audit Stories cannot be dispatched ([#5281](https://github.com/dsj1984/mandrel/issues/5281)) ([#5290](https://github.com/dsj1984/mandrel/issues/5290)) ([e229b0f](https://github.com/dsj1984/mandrel/commit/e229b0fb07e37e251b713a35646105cc54e791c9))
44
+ * baselines: the merge driver is installed wherever base-sync runs, the refresh ack is scoped to the tagged commit's diff, and the land-time write-back cannot launder a regression ([#5277](https://github.com/dsj1984/mandrel/issues/5277)) ([#5292](https://github.com/dsj1984/mandrel/issues/5292)) ([9354e93](https://github.com/dsj1984/mandrel/commit/9354e93f7e74f263ffaf64c6e72056784a9b74d4))
45
+ * close result: the merged flag is derived from what the run observed, failed closes report only registered gates, and advisory blocks carry their own remedy ([#5279](https://github.com/dsj1984/mandrel/issues/5279)) ([#5293](https://github.com/dsj1984/mandrel/issues/5293)) ([93414ac](https://github.com/dsj1984/mandrel/commit/93414ac756bd3f3300f869d46c28f680883edce4))
46
+ * close's result note claims a merge it never confirmed, and a timed-out advisory scan blocks with the same class and remedies as a real violation ([#5266](https://github.com/dsj1984/mandrel/issues/5266)) ([#5271](https://github.com/dsj1984/mandrel/issues/5271)) ([585a2c3](https://github.com/dsj1984/mandrel/commit/585a2c340d96b972028d2d2f469e2f6b287f7ac2))
47
+ * **deps:** override smol-toml past the GHSA-7w5x-hrqm-74c2 DoS advisory (refs [#5264](https://github.com/dsj1984/mandrel/issues/5264)) ([#5268](https://github.com/dsj1984/mandrel/issues/5268)) ([8db060c](https://github.com/dsj1984/mandrel/commit/8db060ca23a5242c1d13e4a049647be86e853504))
48
+ * git-cleanup: weak-signal remote deletes need an explicit opt-in under --yes, and the bulk PR index short-circuits its per-branch fallback ([#5283](https://github.com/dsj1984/mandrel/issues/5283)) ([#5288](https://github.com/dsj1984/mandrel/issues/5288)) ([8346932](https://github.com/dsj1984/mandrel/commit/83469327b63488ea41a2bd9fb53a37bc2f57d1c0))
49
+ * plan-persist reports two things the tree contradicts: a refactors-existing path deleted weeks ago, and a wave table promising parallelism the dispatch guard refuses ([#5265](https://github.com/dsj1984/mandrel/issues/5265)) ([#5270](https://github.com/dsj1984/mandrel/issues/5270)) ([1b5d1ff](https://github.com/dsj1984/mandrel/commit/1b5d1ffdd6e8821c833d320e93381f7804cb61c8))
50
+ * scoped lint: a degraded surface no longer discards the other surface's findings ([#5282](https://github.com/dsj1984/mandrel/issues/5282)) ([#5287](https://github.com/dsj1984/mandrel/issues/5287)) ([75005ee](https://github.com/dsj1984/mandrel/commit/75005ee925d463d46f5cb24cb3788647e08a37b2))
51
+ * **tests:** a RegExp built from a raw path can never match on Windows (refs [#5274](https://github.com/dsj1984/mandrel/issues/5274)) ([#5276](https://github.com/dsj1984/mandrel/issues/5276)) ([8d5efb2](https://github.com/dsj1984/mandrel/commit/8d5efb256d936f7dd955eff55c68d70d8abbfb9c))
52
+ * **tests:** close the two Windows-only breaks Epic [#5286](https://github.com/dsj1984/mandrel/issues/5286) left on main, and the class behind them ([#5297](https://github.com/dsj1984/mandrel/issues/5297)) ([ec419c7](https://github.com/dsj1984/mandrel/commit/ec419c7b7462f4e6e0adf76805c22735ac2e9673))
53
+ * **tests:** prove the workflows-doc drift gate on a fixture root, not the live checkout ([#5291](https://github.com/dsj1984/mandrel/issues/5291)) ([e44aa99](https://github.com/dsj1984/mandrel/commit/e44aa991027859c5d94d9dc875e80d41e1572c00))
54
+
18
55
  ## [2.54.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.53.0...mandrel-v2.54.0) (2026-09-09)
19
56
 
20
57
 
@@ -28,7 +28,8 @@ import { fileURLToPath } from 'node:url';
28
28
  import {
29
29
  BASELINE_MERGE_DRIVER_CONFIG_KEY,
30
30
  BASELINE_MERGE_DRIVER_REMEDY,
31
- declaresBaselineMergeDriver,
31
+ parseBaselineMergeDriverCommand,
32
+ probeBaselineMergeDriver,
32
33
  } from '../../.agents/scripts/lib/bootstrap/baseline-merge-driver.js';
33
34
  import {
34
35
  REQUIRED_NODE_CEILING_MAJOR,
@@ -59,8 +60,8 @@ import {
59
60
  * @param {string[]} args
60
61
  * @returns {{ status: number|null, stdout: string, stderr: string, error?: NodeJS.ErrnoException }}
61
62
  */
62
- function spawn(cmd, args) {
63
- const r = spawnSync(cmd, args, { encoding: 'utf8' });
63
+ function spawn(cmd, args, opts = {}) {
64
+ const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
64
65
  return {
65
66
  status: r.status,
66
67
  stdout: typeof r.stdout === 'string' ? r.stdout : '',
@@ -1101,34 +1102,76 @@ function runVersionCurrent({ cachePath, installedVersion, fsImpl = fs } = {}) {
1101
1102
  */
1102
1103
  export function runMergeDriver({ cwd, fsImpl = fs, runner = spawn } = {}) {
1103
1104
  const projectRoot = (cwd ?? (() => process.cwd()))();
1104
- const attributesPath = path.join(projectRoot, '.gitattributes');
1105
-
1106
- let attributes = '';
1107
- try {
1108
- attributes = fsImpl.readFileSync(attributesPath, 'utf8');
1109
- } catch {
1110
- attributes = '';
1111
- }
1112
- if (!declaresBaselineMergeDriver(attributes)) {
1105
+ // Both git calls run from `projectRoot`: the config key is per-clone, and
1106
+ // reading it from wherever `mandrel doctor` was typed answers about a
1107
+ // different repository (or none).
1108
+ const { declared, command } = probeBaselineMergeDriver({
1109
+ projectRoot,
1110
+ fsImpl,
1111
+ runGit: (args) => runner('git', args, { cwd: projectRoot }),
1112
+ });
1113
+ if (!declared) {
1113
1114
  return {
1114
1115
  ok: true,
1115
1116
  detail:
1116
1117
  'skipped — .gitattributes does not route baselines/*.json through the mandrel merge driver',
1117
1118
  };
1118
1119
  }
1119
-
1120
- const configured = runner('git', [
1121
- 'config',
1122
- '--get',
1123
- BASELINE_MERGE_DRIVER_CONFIG_KEY,
1124
- ]);
1125
- if (configured.status === 0 && configured.stdout.trim() !== '') {
1126
- return { ok: true, detail: configured.stdout.trim() };
1120
+ if (command === '') {
1121
+ return {
1122
+ ok: false,
1123
+ detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset — baselines/*.json will fall back to git's text merge, which conflicts on the generatedAt stamp and can splice rows neither branch scored`,
1124
+ remedy: BASELINE_MERGE_DRIVER_REMEDY,
1125
+ };
1127
1126
  }
1127
+ return probeConfiguredDriver(command, runner, projectRoot);
1128
+ }
1128
1129
 
1130
+ /**
1131
+ * Does the configured driver command actually RUN? (Story #5277)
1132
+ *
1133
+ * A set-but-broken key is the failure mode a presence check cannot see, and it
1134
+ * is the common one: the command names an absolute node binary, and an nvm or
1135
+ * volta version bump moves that binary out from under it months after the
1136
+ * driver was installed. Git's behaviour then is the same silence a missing key
1137
+ * produces — the driver exits non-zero, git leaves the file conflicted, and the
1138
+ * operator reads it as "baselines conflict again" rather than as a broken
1139
+ * registration. So the check executes what git would execute.
1140
+ *
1141
+ * `--help` is the probe because the driver's real invocation mutates `%A` in
1142
+ * place; the shipped CLI answers `--help` with exit 0 and touches nothing.
1143
+ *
1144
+ * The command is tokenised, never handed to a shell
1145
+ * ({@link parseBaselineMergeDriverCommand}) — it is a config value, and running
1146
+ * it through `shell: true` would make any write to that key arbitrary command
1147
+ * execution (security-baseline § Output & Rendering).
1148
+ *
1149
+ * The probe runs from `projectRoot`, because the command's script path is
1150
+ * relative to the worktree root — which is where git invokes a merge driver
1151
+ * from, and is not necessarily where `mandrel doctor` was typed.
1152
+ *
1153
+ * @param {string} command
1154
+ * @param {typeof spawn} runner
1155
+ * @param {string} projectRoot
1156
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
1157
+ */
1158
+ function probeConfiguredDriver(command, runner, projectRoot) {
1159
+ const parsed = parseBaselineMergeDriverCommand(command);
1160
+ if (!parsed) {
1161
+ return {
1162
+ ok: false,
1163
+ detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is set to ${JSON.stringify(command)}, which has no runnable command in it`,
1164
+ remedy: BASELINE_MERGE_DRIVER_REMEDY,
1165
+ };
1166
+ }
1167
+ const probe = runner(parsed.file, [...parsed.args, '--help'], {
1168
+ cwd: projectRoot,
1169
+ });
1170
+ if (probe.status === 0) return { ok: true, detail: command };
1171
+ const why = probe.error?.message ?? `exit ${probe.status}`;
1129
1172
  return {
1130
1173
  ok: false,
1131
- detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset — baselines/*.json will fall back to git's text merge, which conflicts on the generatedAt stamp and can splice rows neither branch scored`,
1174
+ detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is set to ${JSON.stringify(command)} but running it failed (${why}) — git would fall back to text-merging baselines/*.json exactly as if the key were unset`,
1132
1175
  remedy: BASELINE_MERGE_DRIVER_REMEDY,
1133
1176
  };
1134
1177
  }
package/lib/cli/sync.js CHANGED
@@ -38,14 +38,24 @@
38
38
  * {@link readVersionMarker} for how callers consume it.
39
39
  *
40
40
  * Security (Tech Spec #3459 "Postinstall safety"):
41
- * - Does nothing beyond a local file copy: no network, no shell, no writes
42
- * outside `./.agents/`.
41
+ * - No network, and no shell: every child process is spawned with argv
42
+ * tokens and `shell: false`.
43
43
  * - Logs only paths and counts, never file contents or environment values.
44
+ * - One write outside `./.agents/` (Story #5277): the per-clone
45
+ * `merge.mandrel-baseline.driver` git config, and only when the project's
46
+ * own tracked `.gitattributes` already declares the attribute that needs
47
+ * it. That half of the registration cannot ship with the repository — git
48
+ * will not execute a command chosen by whoever wrote it — so every fresh
49
+ * clone silently text-merges generated baselines until something installs
50
+ * it locally, and `sync` is the one command every consumer runs. It never
51
+ * creates or edits `.gitattributes`; a project that has not opted into the
52
+ * quality surface is left untouched.
44
53
  *
45
54
  * Injectable seams (used by lib/cli/__tests__/sync.test.js):
46
55
  * - `resolvePackageRoot` — replaces real `mandrel` resolution
47
56
  * - `fs` — replaces the node:fs surface used here
48
57
  * - `cwd` — replaces process.cwd()
58
+ * - `ensureMergeDriver` — replaces the git-config merge-driver install
49
59
  * - `write` — replaces process.stdout.write
50
60
  * - `writeErr` — replaces process.stderr.write
51
61
  * - `exit` — replaces process.exit
@@ -55,6 +65,7 @@ import nodeFs from 'node:fs';
55
65
  import { createRequire } from 'node:module';
56
66
  import path from 'node:path';
57
67
 
68
+ import { ensureBaselineMergeDriver } from '../../.agents/scripts/lib/bootstrap/baseline-merge-driver.js';
58
69
  import { LEDGER_RELATIVE_PATH } from '../../.agents/scripts/lib/bootstrap/install-ledger.js';
59
70
 
60
71
  export const PACKAGE_NAME = 'mandrel';
@@ -337,6 +348,7 @@ function listDestFiles(dir, fsImpl, prefix = '') {
337
348
  * write?: (s: string) => void,
338
349
  * writeErr?: (s: string) => void,
339
350
  * exit?: (code: number) => void,
351
+ * ensureMergeDriver?: typeof ensureBaselineMergeDriver,
340
352
  * }} [opts]
341
353
  * @returns {{ copied: number, planned: number, pruned: number, dryRun: boolean }}
342
354
  * Summary (also returned in dry-run / error paths for testability).
@@ -344,6 +356,7 @@ function listDestFiles(dir, fsImpl, prefix = '') {
344
356
  export function runSync({
345
357
  argv = [],
346
358
  resolvePackageRoot = defaultResolvePackageRoot,
359
+ ensureMergeDriver = ensureBaselineMergeDriver,
347
360
  fs = nodeFs,
348
361
  cwd = () => process.cwd(),
349
362
  write = (s) => process.stdout.write(s),
@@ -435,6 +448,18 @@ export function runSync({
435
448
  `${packageVersion}\n`,
436
449
  );
437
450
 
451
+ // Baseline merge driver (Story #5277) — config half only; see the module
452
+ // preamble's security note. Never allowed to fail the sync: a consumer
453
+ // without git, or with a read-only config, still gets their `.agents/` tree.
454
+ const driver = ensureMergeDriver({
455
+ projectRoot,
456
+ configOnly: true,
457
+ fsImpl: fs,
458
+ });
459
+ if (driver?.action === 'updated') {
460
+ write('✅ Registered the baselines/*.json merge driver for this clone\n');
461
+ }
462
+
438
463
  if (staleFiles.length > 0) {
439
464
  write(
440
465
  `✅ Installed ${payloadFiles.length} file(s) into ./.agents/ (pruned ${staleFiles.length} stale file(s))\n`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.54.0",
3
+ "version": "2.56.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -32,6 +32,7 @@
32
32
  "baseline:agents-loc": "node baselines/agents-loc-baseline.mjs",
33
33
  "baselines:scope": "node .agents/scripts/check-baseline-scope.js",
34
34
  "baselines:prune": "node .agents/scripts/prune-baseline-orphans.js",
35
+ "baselines:merge-driver": "node .agents/scripts/merge-baseline.js --install",
35
36
  "lint:md": "markdownlint-cli2 \".agents/**/*.md\" \"*.md\" \"!node_modules/**\" \"!.worktrees/**\"",
36
37
  "lint": "node .agents/scripts/run-lint.js && node .agents/scripts/check-generated-validator.js --check && node .agents/scripts/check-pinned-override-notes.js && npm run docs:check",
37
38
  "docs:gen": "node .agents/scripts/generate-config-docs.js && node .agents/scripts/generate-workflows-doc.js && node .agents/scripts/generate-lens-checklists.js",
@@ -63,7 +64,7 @@
63
64
  "quality:watch": "node .agents/scripts/quality-watch.js",
64
65
  "sync:commands": "node bin/mandrel.js sync-commands",
65
66
  "sync:agents": "node .agents/scripts/sync-claude-agents.js",
66
- "prepare": "husky && npm run sync:commands && npm run sync:agents",
67
+ "prepare": "husky && npm run sync:commands && npm run sync:agents && npm run baselines:merge-driver",
67
68
  "postinstall": "node bin/postinstall.js mandrel sync"
68
69
  },
69
70
  "repository": {
@@ -130,10 +131,12 @@
130
131
  "//": {
131
132
  "peerDependencies.@cucumber/gherkin": "OPTIONAL peer, mirroring the `typescript` precedent, and deliberately NOT a runtime dependency. `check-gherkin-corpus.js` is the only consumer and it is opt-in behind `qa.gherkinLint`, so a consumer with no BDD tier must gain nothing from an upgrade. The devDependency alongside it is what lets this repository's own suite drive the real parser. The gate resolves it through a require path anchored at the consumer project — `.agents/` reaches a consumer by plain file copy, so a bare specifier would resolve against the consumer's module chain, which under a non-hoisting linker need not hold it.",
132
133
  "overrides.js-yaml": "COUPLED to devDependencies.markdownlint-cli2 — do not bump either alone, and do NOT drop this override. It is load-bearing: markdownlint-cli2 0.22.x pulls js-yaml 4.1.1, which carries GHSA-52cp-r559-cp3m (high) and GHSA-h67p-54hq-rp68 (moderate); removing the override was measured to reintroduce both (1 high + 1 moderate), while with it in place `npm audit` is clean. An npm override also wins over a transitive package's own pin, so this tree-wide ^4.3.2 is imposed on every js-yaml consumer regardless of what they declare — and markdownlint-cli2 0.23.x declares an exact js-yaml 5.2.1. A dry-run bump confirmed the trap: markdownlint-cli2 0.23.1 resolves against js-yaml 4.3.0, two majors off what it declares, silently. The only safe move is to raise this override and bump markdownlint-cli2 in ONE reviewed commit (first confirming cosmiconfig, under @commitlint/cli, tolerates the same major). renovate.json excludes markdownlint-cli2 from devDependency auto-merge so that pair cannot drift apart unattended. The floor has since been raised twice for advisories against the pinned range itself (most recently to ^4.3.2 for GHSA-2883-xcg3-v3hh, whose fix is 4.3.2) — raising it is safe and does NOT touch the coupling above; check-pinned-override-notes.js now fails the build if this sentence and the pin disagree.",
133
- "dependencies.js-yaml": "States the SAME range as overrides.js-yaml above. The two are deliberate duplicates — npm has no way to reference the direct range from the overrides block — so they MUST move in lockstep; changing one without the other silently splits the direct and transitive resolutions."
134
+ "dependencies.js-yaml": "States the SAME range as overrides.js-yaml above. The two are deliberate duplicates — npm has no way to reference the direct range from the overrides block — so they MUST move in lockstep; changing one without the other silently splits the direct and transitive resolutions.",
135
+ "overrides.smol-toml": "Load-bearing: markdownlint-cli2 0.22.1 declares an EXACT smol-toml 1.6.1, which carries GHSA-7w5x-hrqm-74c2 (high — denial of service via malformed TOML). The advisory’s vulnerable range is everything <= 1.7.0, so no lockfile-only resolution reaches a patched version and this tree-wide ^1.7.1 pin is what clears it. npm’s own `audit fix --force` instead proposes markdownlint-cli2 0.21.0 — a DOWNGRADE presented as a breaking change; do not take it. An npm override wins over a transitive package’s own exact pin, so markdownlint-cli2 runs against a minor it never declared: `npm run lint` was confirmed green under the forced version and is what must be re-run if this pin moves. knip declares ^1.6.1 and is satisfied natively. smol-toml is NOT a direct dependency, so unlike overrides.js-yaml there is no lockstep duplicate range to keep in sync."
134
136
  },
135
137
  "overrides": {
136
138
  "js-yaml": "^4.3.2",
137
- "markdown-it": "^14.2.0"
139
+ "markdown-it": "^14.2.0",
140
+ "smol-toml": "^1.7.1"
138
141
  }
139
142
  }