mandrel 2.36.0 → 2.38.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 (225) hide show
  1. package/.agents/README.md +16 -16
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/configuration.md +41 -16
  5. package/.agents/docs/execution-reference.md +3 -3
  6. package/.agents/docs/quality-gates.md +2 -2
  7. package/.agents/docs/workflows.md +6 -6
  8. package/.agents/instructions.md +5 -5
  9. package/.agents/rules/changelog-style.md +3 -3
  10. package/.agents/rules/ci-remediation.md +1 -1
  11. package/.agents/rules/git-conventions-reference.md +4 -4
  12. package/.agents/rules/testing-standards.md +56 -0
  13. package/.agents/schemas/agentrc.schema.json +9 -9
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/boot-sweep.js +7 -1
  20. package/.agents/scripts/check-context-budget.js +195 -103
  21. package/.agents/scripts/check-cyclomatic.js +112 -42
  22. package/.agents/scripts/check-doc-links.js +32 -4
  23. package/.agents/scripts/check-generated-validator.js +202 -0
  24. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  25. package/.agents/scripts/check-schema-references.js +2 -28
  26. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  27. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  28. package/.agents/scripts/deliver-light.js +10 -10
  29. package/.agents/scripts/deliver-recover.js +1 -1
  30. package/.agents/scripts/drain-pending-cleanup.js +19 -2
  31. package/.agents/scripts/evidence-gate.js +1 -1
  32. package/.agents/scripts/generate-workflows-doc.js +1 -1
  33. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  34. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  35. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  37. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  38. package/.agents/scripts/lib/baselines/components.js +32 -2
  39. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  40. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  41. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  42. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  43. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  44. package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
  45. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  46. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  50. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  51. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  52. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  53. package/.agents/scripts/lib/cli-args.js +1 -3
  54. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  55. package/.agents/scripts/lib/close-validation/process.js +1 -1
  56. package/.agents/scripts/lib/command-header.js +1 -1
  57. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  65. package/.agents/scripts/lib/config-settings-schema.js +49 -9
  66. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  67. package/.agents/scripts/lib/cpu-pool.js +90 -10
  68. package/.agents/scripts/lib/crap-utils.js +6 -2
  69. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  70. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  71. package/.agents/scripts/lib/dependency-version.js +86 -0
  72. package/.agents/scripts/lib/duplicate-search.js +2 -2
  73. package/.agents/scripts/lib/errors/index.js +1 -1
  74. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  75. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
  76. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
  77. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  78. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  79. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  80. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  81. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  82. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  83. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  84. package/.agents/scripts/lib/label-constants.js +2 -2
  85. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  86. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  87. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  88. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  89. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  91. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  92. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  94. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  95. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  96. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  97. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  98. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  99. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  100. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  101. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  102. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  103. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  104. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  105. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  106. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
  107. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  108. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  109. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  110. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  111. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  112. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  113. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  114. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  115. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  116. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  117. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  118. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  119. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  120. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  121. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  128. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  129. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  131. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  132. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  134. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  135. package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
  136. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  137. package/.agents/scripts/lib/preflight-runner.js +3 -3
  138. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  139. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  140. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  141. package/.agents/scripts/lib/signals/schema.js +1 -1
  142. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  143. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  144. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  145. package/.agents/scripts/lib/story-adjacency.js +2 -2
  146. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  147. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  148. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  149. package/.agents/scripts/lib/test-tiers.js +135 -18
  150. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  151. package/.agents/scripts/lib/transpile.js +16 -3
  152. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  153. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  154. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  156. package/.agents/scripts/lib/workflow-closure.js +1 -1
  157. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
  158. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  159. package/.agents/scripts/lint-issue-body.js +3 -3
  160. package/.agents/scripts/plan-context.js +4 -4
  161. package/.agents/scripts/plan-critics.js +2 -2
  162. package/.agents/scripts/plan-persist.js +2 -2
  163. package/.agents/scripts/plan-run-epilogue.js +2 -2
  164. package/.agents/scripts/pr-watch-with-update.js +305 -137
  165. package/.agents/scripts/providers/github/board-add.js +1 -1
  166. package/.agents/scripts/providers/github/errors.js +1 -1
  167. package/.agents/scripts/providers/github/tickets.js +114 -21
  168. package/.agents/scripts/quality-preview.js +162 -70
  169. package/.agents/scripts/resolve-stories.js +3 -3
  170. package/.agents/scripts/resync-status-column.js +1 -1
  171. package/.agents/scripts/run-lint.js +1 -1
  172. package/.agents/scripts/run-test-profile.js +8 -5
  173. package/.agents/scripts/run-tests.js +79 -14
  174. package/.agents/scripts/single-story-close.js +2 -4
  175. package/.agents/scripts/single-story-init.js +10 -6
  176. package/.agents/scripts/stories-wave-tick.js +8 -8
  177. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  178. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  179. package/.agents/skills/skills.index.json +1 -1
  180. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  181. package/.agents/workflows/audit-data-model.md +1 -1
  182. package/.agents/workflows/audit-documentation.md +2 -2
  183. package/.agents/workflows/audit-to-stories.md +11 -11
  184. package/.agents/workflows/git-deliver.md +4 -4
  185. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  186. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  187. package/.agents/workflows/helpers/code-review.md +4 -4
  188. package/.agents/workflows/helpers/deliver-light.md +26 -26
  189. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  190. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  191. package/.agents/workflows/helpers/deliver-story.md +6 -7
  192. package/.agents/workflows/helpers/diagnose.md +2 -2
  193. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  194. package/.agents/workflows/helpers/plan-reference.md +9 -9
  195. package/.agents/workflows/helpers/qa-core.md +8 -8
  196. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  197. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  198. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  199. package/.agents/workflows/memory-consolidate.md +4 -4
  200. package/.agents/workflows/prototype.md +3 -3
  201. package/.agents/workflows/qa-assist.md +12 -12
  202. package/.agents/workflows/qa-explore.md +6 -6
  203. package/.agents/workflows/qa-run.md +1 -1
  204. package/README.md +7 -7
  205. package/docs/CHANGELOG.md +43 -0
  206. package/lib/cli/doctor.js +1 -1
  207. package/lib/cli/init.js +1 -1
  208. package/lib/cli/migrate.js +100 -94
  209. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  210. package/lib/migrations/index.js +32 -33
  211. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  212. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  213. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  214. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  215. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  216. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  217. package/package.json +7 -3
  218. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  219. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  220. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  221. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  222. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  223. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  224. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  225. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -3,7 +3,7 @@
3
3
  This rule governs the shape of per-release entries in the project CHANGELOG
4
4
  (typically `docs/CHANGELOG.md` or `CHANGELOG.md`). It applies whenever a
5
5
  release entry is authored or edited — most commonly inside Story #N's
6
- docs sweep before `/deliver` opens the release PR.
6
+ docs sweep before `/mandrel-deliver` opens the release PR.
7
7
 
8
8
  The contract is **guidance-tier**: no automated gate fails a close when an
9
9
  entry drifts off-template. It still binds every author.
@@ -152,12 +152,12 @@ worktree cleanup.
152
152
  - **Shared-store worktrees.** Per-story worktrees link a shared
153
153
  `node_modules` store, so parallel waves no longer duplicate installs
154
154
  or leave residue that blocks reap.
155
- - **`/deliver` auto-invokes pre-merge gates** (code review, retro)
155
+ - **`/mandrel-deliver` auto-invokes pre-merge gates** (code review, retro)
156
156
  inline. `--skip-code-review` is available as an override.
157
157
  - **Closure sweep covers Epic Health tickets** in addition to PRD and
158
158
  Tech Spec tickets.
159
159
  - **Stale-lock sweep** on the shared `.git/` directory runs at
160
- `/deliver` start, clearing lock files left behind by interrupted
160
+ `/mandrel-deliver` start, clearing lock files left behind by interrupted
161
161
  operations.
162
162
  ```
163
163
 
@@ -31,7 +31,7 @@ exactly one of two ways, and no others:
31
31
  delivery's scope — a pre-existing flaky test, a runner/infra weakness, a
32
32
  framework-level environment gap. Open the issue with the `meta::framework-gap`
33
33
  label (see [`git-conventions.md`](git-conventions.md)) carrying **the run
34
- link and the failure signature** so a later `/plan` Phase 0 sweep can act on
34
+ link and the failure signature** so a later `/mandrel-plan` Phase 0 sweep can act on
35
35
  it. Remediate this delivery only if the pre-existing defect is genuinely
36
36
  blocking it.
37
37
 
@@ -95,7 +95,7 @@ signature is worth naming:
95
95
  **Invariant (stated in the core): the delivering flow owns tidying the local
96
96
  checkout — reaping its own merged refs and fast-forwarding the base branch.
97
97
  `/git-cleanup` is a recovery tool, not a routine chore.** The outcome every
98
- delivering flow (`/deliver`, `/git-deliver`) guarantees, with the mechanics
98
+ delivering flow (`/mandrel-deliver`, `/git-deliver`) guarantees, with the mechanics
99
99
  owned by `boot-sweep.js` / `git-cleanup.js`:
100
100
 
101
101
  - **`main` is fast-forwarded** by the flow itself in its cleanup phase, so the
@@ -104,7 +104,7 @@ owned by `boot-sweep.js` / `git-cleanup.js`:
104
104
  - **Merged local refs are reaped** at the next workflow boot's protected sweep
105
105
  (`boot-sweep.js`) — every local branch whose PR is already merged, skipping
106
106
  any candidate with unpushed work, a dirty worktree, or a still-open parent
107
- ticket. `/plan` and `/git-deliver` widen the sweep's `--include` scope beyond
107
+ ticket. `/mandrel-plan` and `/git-deliver` widen the sweep's `--include` scope beyond
108
108
  the default `story-*` to their own branch namespaces at their boot call site.
109
109
  - **Content-merged branches are report-only.** A branch detected only via the
110
110
  weaker content-equivalence signal (`detectedBy: 'content-merged'` — content
@@ -149,13 +149,13 @@ different hazards:
149
149
  - **Recognition signature**: close aborts naming stray files in the main
150
150
  checkout that intersect the Story's diff. **Resolution**: relocate those
151
151
  edits under the worktree, restore the main checkout
152
- (`git -C <main-repo> checkout -- <files>`), then re-run `/deliver <storyId>`.
152
+ (`git -C <main-repo> checkout -- <files>`), then re-run `/mandrel-deliver <storyId>`.
153
153
  Never `git reset --hard` or `git checkout --force` to clear the way.
154
154
 
155
155
  ## Meta Labels (Retrospective Signal Routing)
156
156
 
157
157
  Two `meta::*` labels route retrospective signals into durable substrates so
158
- the `/plan` Phase 0 fetcher (see
158
+ the `/mandrel-plan` Phase 0 fetcher (see
159
159
  [`prior-feedback-fetcher.js`](../scripts/lib/feedback-loop/prior-feedback-fetcher.js))
160
160
  can surface open feedback issues to the planner. Both labels live in
161
161
  [`label-constants.js`](../scripts/lib/label-constants.js) under the
@@ -79,6 +79,13 @@ of the pyramid.
79
79
  - **Location.** `tests/features/**/*.feature` with step definitions in
80
80
  `tests/steps/**` (or the project's equivalent). The companion skill is
81
81
  [`stack/qa/playwright-bdd`](../skills/stack/qa/playwright-bdd/SKILL.md).
82
+ - **Run tier.** This tier MUST NOT ride inside the default suite. It is slow
83
+ by construction — real installs, real browsers, real stacks — and the
84
+ default suite is what a pre-push hook and every local iteration pay for.
85
+ Give it its own runner tier and its own CI job, so its cost is charged to
86
+ the surface whose signal it is. In this repository that is `tests/e2e/**`,
87
+ the `e2e` tier (`npm run test:e2e`), and the per-PR `e2e` job; the coverage
88
+ run still measures those files, so nothing leaves the measured surface.
82
89
 
83
90
  ## Assertion Placement Rule {#assertion-placement}
84
91
 
@@ -236,3 +243,52 @@ Hypothesis keeps a failure DB, proptest writes `proptest-regressions/` — pin o
236
243
  commit whichever the stack provides. Once shrinking surfaces a minimal
237
244
  counterexample, **add it as an example-based regression test** alongside the
238
245
  property: the property guards the domain, the pinned example guards the bug.
246
+
247
+ ## The suite's child-process budget
248
+
249
+ `npm test` forks one process per test file, and the spawns those leaves make
250
+ dominate the suite's system time. Two instruments and one rule keep that
251
+ budget visible and honest (Story #5121).
252
+
253
+ ### Measure it with `npm run test:census`
254
+
255
+ ```bash
256
+ npm run test:census # writes temp/census.json
257
+ ```
258
+
259
+ `tests/fixtures/spawn-census.cjs` is a `--require` preload that counts every
260
+ `child_process` call per binary, aggregates across all ~700 processes, and
261
+ reports `nodeInSuite` (node children spawned *by test files*, excluding the
262
+ runner's own fan-out), `git`, `gh`, `npm`, and any standalone
263
+ `git config user.*` spawns. Read the numbers from the census rather than
264
+ re-deriving them; two audits hand-rolled this measurement and lost it both
265
+ times with the gitignored temp tree.
266
+
267
+ The script interpolates `$PWD` deliberately. A **relative** `--require` path is
268
+ inherited by children that run with a different `cwd`, where it fails to
269
+ resolve and kills the child before it runs a line — measured as 15 spurious
270
+ failures in one file.
271
+
272
+ ### Build a fixture repo once, then copy it
273
+
274
+ A multi-commit fixture repo rebuilt in `beforeEach` is the costly shape. Build
275
+ it once in `before()` and hand each test an `fs` copy via
276
+ `copyGitRepo(pristine)` from `tests/fixtures/git-fixture.js`: each test still
277
+ gets a private directory it may freely mutate, for **no subprocess at all**.
278
+ One file went from 92 `git` spawns to 27 this way.
279
+
280
+ `copyGitRepo` is safe only for a locally-`git init`ed repo, whose
281
+ `.git/config` holds no absolute paths. Do **not** copy a **clone** (its
282
+ `remote.origin.url` is absolute, so the copy would fetch from the original) or a
283
+ linked worktree (its `gitdir:` / `commondir` pointers would dangle).
284
+
285
+ ### Never trade coverage for a spawn count
286
+
287
+ Most of the suite's remaining `git` spawns are integration tests exercising
288
+ real git against git-manipulating production code — the spawn **is** the
289
+ subject under test, and so is a CLI's exit code in an exit-code contract test.
290
+ Those are not fixture waste and must not be converted to in-process calls or
291
+ mocks to make a number smaller. Hoist shared setup; leave the assertions alone.
292
+ A spawn census also records argv, not what the binary resolved to: a
293
+ `gh pr view 4890` line may well be a fake `gh` the test put on `PATH`, so
294
+ verify resolution before calling a spawn a network call.
@@ -298,7 +298,7 @@
298
298
  },
299
299
  "planning": {
300
300
  "type": "object",
301
- "description": "Inputs to `/plan`: risk escalation heuristics, ceremony-lite routing, and the cross-Story conflict-finding severity gates.",
301
+ "description": "Inputs to `/mandrel-plan`: risk escalation heuristics, ceremony-lite routing, and the cross-Story conflict-finding severity gates.",
302
302
  "properties": {
303
303
  "riskHeuristics": {
304
304
  "oneOf": [
@@ -440,7 +440,7 @@
440
440
  },
441
441
  "delivery": {
442
442
  "type": "object",
443
- "description": "Everything `/deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness, signals, quality gates, merge/CI watch, review ceremony, and the feedback loop.",
443
+ "description": "Everything `/mandrel-deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness, signals, quality gates, merge/CI watch, review ceremony, and the feedback loop.",
444
444
  "properties": {
445
445
  "execution": {
446
446
  "type": "object",
@@ -521,12 +521,12 @@
521
521
  },
522
522
  "deliverRunner": {
523
523
  "type": "object",
524
- "description": "Bounded-concurrency knob for the /deliver fan-out.",
524
+ "description": "Bounded-concurrency knob for the /mandrel-deliver fan-out.",
525
525
  "properties": {
526
526
  "concurrencyCap": {
527
527
  "type": "integer",
528
528
  "minimum": 1,
529
- "description": "Maximum ready Stories dispatched by /deliver at once. Default 3. Moderate by design — keeps host-quota consumption predictable while allowing a small ready-set fan-out. Set 1 for strictly sequential delivery; raise further on hosts with adequate parallel-agent quota. See deliver.md for the sequencing model and throughput tradeoff.",
529
+ "description": "Maximum ready Stories dispatched by /mandrel-deliver at once. Default 3. Moderate by design — keeps host-quota consumption predictable while allowing a small ready-set fan-out. Set 1 for strictly sequential delivery; raise further on hosts with adequate parallel-agent quota. See deliver.md for the sequencing model and throughput tradeoff.",
530
530
  "default": 3
531
531
  },
532
532
  "footprintGuard": {
@@ -1628,7 +1628,7 @@
1628
1628
  },
1629
1629
  "navigability": {
1630
1630
  "type": "object",
1631
- "description": "Navigability lens + journey-suite config (Epic #4131, F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and /deliver's per-Story ceremony (journey suite). Opt-in: absent or empty routeGlobs degrades to a silent no-op.",
1631
+ "description": "Navigability lens + journey-suite config (Epic #4131, F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and /mandrel-deliver's per-Story ceremony (journey suite). Opt-in: absent or empty routeGlobs degrades to a silent no-op.",
1632
1632
  "properties": {
1633
1633
  "routeGlobs": {
1634
1634
  "type": "array",
@@ -1648,7 +1648,7 @@
1648
1648
  },
1649
1649
  "journeySuite": {
1650
1650
  "type": "string",
1651
- "description": "Path or command for the per-persona journey suite /deliver's per-Story ceremony runs."
1651
+ "description": "Path or command for the per-persona journey suite /mandrel-deliver's per-Story ceremony runs."
1652
1652
  }
1653
1653
  },
1654
1654
  "additionalProperties": false
@@ -1692,7 +1692,7 @@
1692
1692
  },
1693
1693
  "codeReview": {
1694
1694
  "type": "object",
1695
- "description": "Review-provider chain plus bounded-retry knobs for the /deliver code-review ceremony.",
1695
+ "description": "Review-provider chain plus bounded-retry knobs for the /mandrel-deliver code-review ceremony.",
1696
1696
  "properties": {
1697
1697
  "providers": {
1698
1698
  "type": "array",
@@ -1780,7 +1780,7 @@
1780
1780
  "maxFixAttempts": {
1781
1781
  "type": "integer",
1782
1782
  "minimum": 0,
1783
- "description": "Maximum auto-fix retry attempts per finding in /deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.",
1783
+ "description": "Maximum auto-fix retry attempts per finding in /mandrel-deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.",
1784
1784
  "default": 3
1785
1785
  },
1786
1786
  "maxFixScopeFiles": {
@@ -1792,7 +1792,7 @@
1792
1792
  "autoFixSeverity": {
1793
1793
  "type": "string",
1794
1794
  "enum": ["high", "medium"],
1795
- "description": "Severity threshold for on-branch remediation in /deliver Phase 5 (code-review). `medium` (default) routes 🔴/🟠/🟡 findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while 🟢 suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover — no back-compat flag.",
1795
+ "description": "Severity threshold for on-branch remediation in /mandrel-deliver Phase 5 (code-review). `medium` (default) routes 🔴/🟠/🟡 findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while 🟢 suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover — no back-compat flag.",
1796
1796
  "default": "medium"
1797
1797
  }
1798
1798
  },
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "status": {
23
23
  "type": "string",
24
- "description": "landed — the PR merged, the Story is agent::done, and the post-land tail was attempted. pending — a bounded wait expired with the PR still in flight; NO label was mutated and no merge.unlanded event was emitted, so the run is resumable via nextCommand. blocked — a classified hard block; the Story carries agent::blocked and blocked.blockClass names the class. failed — a phase crashed; phase names which one. escalated — the /deliver-light suitability gate refused an over-scope prompt under --yes; nothing was created and the session ENDS here, nextCommand naming the /plan invocation that owns the work instead.",
24
+ "description": "landed — the PR merged, the Story is agent::done, and the post-land tail was attempted. pending — a bounded wait expired with the PR still in flight; NO label was mutated and no merge.unlanded event was emitted, so the run is resumable via nextCommand. blocked — a classified hard block; the Story carries agent::blocked and blocked.blockClass names the class. failed — a phase crashed; phase names which one. escalated — the /deliver-light suitability gate refused an over-scope prompt under --yes; nothing was created and the session ENDS here, nextCommand naming the /mandrel-plan invocation that owns the work instead.",
25
25
  "enum": ["landed", "pending", "blocked", "failed", "escalated"]
26
26
  },
27
27
  "phase": {
@@ -173,7 +173,7 @@
173
173
  },
174
174
  "nextCommand": {
175
175
  "type": ["string", "null"],
176
- "description": "The single command that advances this work from where it stopped, or null when status === \"landed\" and nothing remains. Shares its vocabulary with deliver-recover.js so recovery and normal resumption speak one language. For status escalated it is the /plan invocation the operator runs in a FRESH session — the one case where the command is a slash command rather than a script, because the work needs planning, not resumption.",
176
+ "description": "The single command that advances this work from where it stopped, or null when status === \"landed\" and nothing remains. Shares its vocabulary with deliver-recover.js so recovery and normal resumption speak one language. For status escalated it is the /mandrel-plan invocation the operator runs in a FRESH session — the one case where the command is a slash command rather than a script, because the work needs planning, not resumption.",
177
177
  "minLength": 1
178
178
  },
179
179
  "elapsedSeconds": { "type": "number", "minimum": 0 },
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "validation-evidence",
4
4
  "title": "Validation Evidence",
5
- "description": "Per-scope record of which validation gates have passed against which commit SHA. Written by lib/validation-evidence.js under the per-Epic temp tree at temp/run-<id>/validation-evidence.json (Epic-scoped) or temp/run-<id>/stories/story-<storyId>/validation-evidence.json (Story-scoped). Standalone Stories (no parent Epic) use a storyId-anchored keyspace at temp/standalone/stories/story-<storyId>/validation-evidence.json. All paths are gitignored via temp/. Consumed by close-validation, epic-code-review, the acceptance self-eval critic, and /deliver Phase 3 (close-validation) to skip identical re-runs against an already-validated tree. The `storyId` field carries the scope id and equals the epic id for Epic-scoped records.",
5
+ "description": "Per-scope record of which validation gates have passed against which commit SHA. Written by lib/validation-evidence.js under the per-Epic temp tree at temp/run-<id>/validation-evidence.json (Epic-scoped) or temp/run-<id>/stories/story-<storyId>/validation-evidence.json (Story-scoped). Standalone Stories (no parent Epic) use a storyId-anchored keyspace at temp/standalone/stories/story-<storyId>/validation-evidence.json. All paths are gitignored via temp/. Consumed by close-validation, epic-code-review, the acceptance self-eval critic, and /mandrel-deliver Phase 3 (close-validation) to skip identical re-runs against an already-validated tree. The `storyId` field carries the scope id and equals the epic id for Epic-scoped records.",
6
6
  "type": "object",
7
7
  "required": ["storyId", "schemaVersion", "records"],
8
8
  "properties": {
@@ -20,7 +20,7 @@
20
20
  * and the resolved, undisableable round cap
21
21
  * (`delivery.acceptanceEval.maxRounds`, clamped to `[1, ceiling]`).
22
22
  * 3. Emit one per-criterion `acceptance-eval` signal into the retro /
23
- * feedback substrate so the retro and `/plan` Phase 0 feedback
23
+ * feedback substrate so the retro and `/mandrel-plan` Phase 0 feedback
24
24
  * fetch can see which acceptance items needed rework and the round
25
25
  * count.
26
26
  * 4. Print a single JSON envelope and exit:
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Story #5007 retired the second step. `migrateBaselinesLayout` relocated
22
22
  * per-Epic ratchet snapshots into `temp/epic/<id>/baselines/` on the contract
23
- * that `/deliver` reaps that namespace on merge — a mechanism the Story-only
23
+ * that `/mandrel-deliver` reaps that namespace on merge — a mechanism the Story-only
24
24
  * v2 model deleted, so the migration moved dead data into a namespace no code
25
25
  * path writes, reads, or reaps. Its one residual hygiene value (getting the
26
26
  * committed `baselines/epic/` tree out of version control) survives as
@@ -13,7 +13,7 @@
13
13
  * stdout when --json is set).
14
14
  *
15
15
  * --emit-plan-seed --plan <plan.json> --out <path>
16
- * Read the plan envelope from disk, render the `/plan --seed`
16
+ * Read the plan envelope from disk, render the `/mandrel-plan --seed`
17
17
  * seed markdown, persist to --out.
18
18
  *
19
19
  * --emit-stories --plan <plan.json>
@@ -844,7 +844,7 @@ runAsCli(import.meta.url, main, {
844
844
  flags: [
845
845
  ['--scan', 'Print the grouped, deduplicated plan as JSON.'],
846
846
  ['--auto', 'Run the full scan → file pipeline and print the summary.'],
847
- ['--emit-plan-seed', 'Emit a /plan --seed-file document.'],
847
+ ['--emit-plan-seed', 'Emit a /mandrel-plan --seed-file document.'],
848
848
  ['--emit-stories', 'Emit the Story drafts as JSON.'],
849
849
  [
850
850
  '--wire-edges',
@@ -45,6 +45,7 @@ import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
45
45
  import { Logger } from './lib/Logger.js';
46
46
  import { createProvider } from './lib/provider-factory.js';
47
47
  import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
48
+ import { resolveSweepLockPath } from './lib/single-story-sweep/sweep-lock.js';
48
49
  import { sweepMergedBranches } from './lib/single-story-sweep.js';
49
50
  import { sweepTempRetention } from './lib/temp-retention.js';
50
51
 
@@ -141,8 +142,13 @@ export async function runBootSweep({
141
142
  excludeGlobs.push(current);
142
143
  }
143
144
 
145
+ // Story #5112 — one critical section, one lock. `single-story-init.js`
146
+ // reaps the same merged `story-*` branches through the same engine; when
147
+ // the two surfaces held differently named lockfiles they could run
148
+ // concurrently, each deleting branches the other had already planned.
149
+ // Both now resolve the path through `resolveSweepLockPath`.
144
150
  const tempRoot = config?.project?.paths?.tempRoot ?? 'temp';
145
- const lockPath = path.resolve(root, tempRoot, 'boot-sweep.lock');
151
+ const lockPath = resolveSweepLockPath({ cwd: root, tempRoot });
146
152
  const lockTimeoutMs =
147
153
  config.delivery?.worktreeIsolation?.sweepLockMs ?? 60_000;
148
154
 
@@ -481,6 +481,179 @@ export function renderReachable(tierMap, baseline) {
481
481
  * @returns {Promise<number>} 0 = clean / within tolerance / shrink-only / no-op;
482
482
  * 1 = a gated tier grew beyond tolerance
483
483
  */
484
+ /**
485
+ * Write a fresh budget, preserving the recorded tolerance so `--update` never
486
+ * silently widens the gate it is refreshing.
487
+ *
488
+ * @param {object} params
489
+ * @returns {0}
490
+ */
491
+ function writeUpdatedBaseline({ tierMap, resolvedBaselinePath, json, stdout }) {
492
+ const existing = loadBaseline(resolvedBaselinePath);
493
+ const tolerance = Number.isFinite(existing?.toleranceBytes)
494
+ ? existing.toleranceBytes
495
+ : DEFAULT_TOLERANCE_BYTES;
496
+ const envelope = buildBaseline(tierMap, tolerance);
497
+ fs.mkdirSync(path.dirname(resolvedBaselinePath), { recursive: true });
498
+ fs.writeFileSync(
499
+ resolvedBaselinePath,
500
+ `${JSON.stringify(envelope, null, 2)}\n`,
501
+ );
502
+ if (!json) {
503
+ stdout.write(
504
+ `[context-budget] wrote baseline ${resolvedBaselinePath} (tolerance ${tolerance} bytes)\n`,
505
+ );
506
+ } else {
507
+ stdout.write(
508
+ `${JSON.stringify({ kind: 'context-budget-update', baselinePath: resolvedBaselinePath, envelope }, null, 2)}\n`,
509
+ );
510
+ }
511
+ return 0;
512
+ }
513
+
514
+ /**
515
+ * An absent budget is a no-op, not a failure: a consumer that has never
516
+ * recorded one has nothing to regress against.
517
+ *
518
+ * @param {object} params
519
+ * @returns {0}
520
+ */
521
+ function reportMissingBaseline({
522
+ tierMap,
523
+ resolvedBaselinePath,
524
+ json,
525
+ stdout,
526
+ stderr,
527
+ }) {
528
+ if (json) {
529
+ stdout.write(
530
+ `${JSON.stringify({ kind: 'context-budget-report', baselinePath: resolvedBaselinePath, tiers: tierMap.tiers, grown: [], shrunk: [], absent: [], skipped: GATED_TIERS, exitCode: 0, noBaseline: true }, null, 2)}\n`,
531
+ );
532
+ } else {
533
+ stderr.write(
534
+ `[context-budget] ⚠ budget not found at ${resolvedBaselinePath} — skipping (no-op)\n`,
535
+ );
536
+ }
537
+ return 0;
538
+ }
539
+
540
+ /**
541
+ * Score the tree against the recorded budget. Pure — every verdict the two
542
+ * renderers below present is decided here, so they cannot disagree about what
543
+ * failed or drift apart in which fields they surface.
544
+ *
545
+ * @param {{ tierMap: object, baseline: object }} params
546
+ * @returns {{ diff: object, ceiling: number, bootOverflow: object[], bootDrift: object[], permissiveDrift: object[], exitCode: 0 | 1 }}
547
+ */
548
+ function evaluateBudget({ tierMap, baseline }) {
549
+ const diff = diffBudget(tierMap, baseline);
550
+ const ceiling = Number.isFinite(baseline?.agentBoot?.ceilingBytes)
551
+ ? baseline.agentBoot.ceilingBytes
552
+ : AGENT_BOOT_CEILING_BYTES;
553
+ const bootOverflow = agentBootOverflow(tierMap, ceiling);
554
+ const bootDrift = agentBootDrift(tierMap, baseline, ceiling);
555
+ const permissiveDrift = bootDrift.filter((d) => d.direction === 'permissive');
556
+ const exitCode =
557
+ budgetFailureCount(diff) > 0 ||
558
+ bootOverflow.length > 0 ||
559
+ permissiveDrift.length > 0
560
+ ? 1
561
+ : 0;
562
+ return { diff, ceiling, bootOverflow, bootDrift, permissiveDrift, exitCode };
563
+ }
564
+
565
+ /**
566
+ * @param {object} params
567
+ * @returns {void}
568
+ */
569
+ function renderJsonReport({
570
+ tierMap,
571
+ baseline,
572
+ resolvedBaselinePath,
573
+ report,
574
+ stdout,
575
+ }) {
576
+ const { diff, ceiling, bootOverflow, bootDrift, exitCode } = report;
577
+ const envelope = {
578
+ kind: 'context-budget-report',
579
+ baselinePath: resolvedBaselinePath,
580
+ toleranceBytes: Number.isFinite(baseline.toleranceBytes)
581
+ ? baseline.toleranceBytes
582
+ : 0,
583
+ current: Object.fromEntries(
584
+ GATED_TIERS.map((t) => [t, tierTotalBytes(tierMap.tiers[t] ?? [])]),
585
+ ),
586
+ grown: diff.grown,
587
+ shrunk: diff.shrunk,
588
+ absent: diff.absent,
589
+ skipped: diff.skipped,
590
+ agentBootCeilingBytes: ceiling,
591
+ agentBootOverflow: bootOverflow,
592
+ agentBootDrift: bootDrift,
593
+ workflowReachableBytes: tierMap.workflowClosure?.reachableTotalBytes ?? 0,
594
+ exitCode,
595
+ };
596
+ stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
597
+ }
598
+
599
+ /**
600
+ * Each failing condition gets its own remediation line: they are fixed
601
+ * differently (trim a role def vs refresh the budget), so a single generic
602
+ * message would leave the author guessing which applies.
603
+ *
604
+ * @param {object} params
605
+ * @returns {void}
606
+ */
607
+ function renderFailureDiagnostics({ report, stderr }) {
608
+ const { diff, ceiling, bootOverflow, permissiveDrift } = report;
609
+ if (permissiveDrift.length > 0) {
610
+ stderr.write(
611
+ `[context-budget] ❌ a recorded agentBoot row understates the file it describes, so it overstates the headroom an author would size an edit against — refresh it with \`node .agents/scripts/check-context-budget.js --update\` (the ceiling is unchanged)\n`,
612
+ );
613
+ }
614
+ if (bootOverflow.length > 0) {
615
+ stderr.write(
616
+ `[context-budget] ❌ a role-agent boot context exceeds the ${ceiling}-byte per-agent ceiling — trim the role def (the ceiling is a hard cap, not a starve target)\n`,
617
+ );
618
+ }
619
+ if (diff.grown.length > 0) {
620
+ stderr.write(
621
+ `[context-budget] ❌ a documentation tier grew beyond tolerance — refresh the budget consciously with \`node .agents/scripts/check-context-budget.js --update\` once the growth is intentional\n`,
622
+ );
623
+ }
624
+ if (diff.shrunk.length > 0) {
625
+ stderr.write(
626
+ `[context-budget] ❌ a documentation tier came in under its recorded total — the ratchet is holding slack the tree no longer spends, so the next growth would be absorbed silently. Lock the gain in with \`node .agents/scripts/check-context-budget.js --update\`\n`,
627
+ );
628
+ }
629
+ if (diff.absent.length > 0) {
630
+ stderr.write(
631
+ `[context-budget] ❌ a recorded row names a path the measured tier no longer contains — its bytes inflate the recorded total against nothing. Refresh with \`node .agents/scripts/check-context-budget.js --update\`\n`,
632
+ );
633
+ }
634
+ }
635
+
636
+ /**
637
+ * @param {object} params
638
+ * @returns {void}
639
+ */
640
+ function renderTextReport({ tierMap, baseline, report, stdout, stderr }) {
641
+ const { diff, bootOverflow, bootDrift, exitCode } = report;
642
+ stdout.write(`\n--- context-budget preview ---\n`);
643
+ stdout.write(`${renderDiff(diff)}\n`);
644
+ const reachable = renderReachable(tierMap, baseline);
645
+ if (reachable) stdout.write(`${reachable}\n`);
646
+ for (const o of bootOverflow) {
647
+ stdout.write(
648
+ `+ agentBoot: ${o.path} is ${o.bytes} bytes, over the ${o.ceiling}-byte per-agent ceiling\n`,
649
+ );
650
+ }
651
+ for (const line of renderBootDrift(bootDrift)) {
652
+ stdout.write(`${line}\n`);
653
+ }
654
+ if (exitCode === 1) renderFailureDiagnostics({ report, stderr });
655
+ }
656
+
484
657
  export async function runCli({
485
658
  argv = process.argv.slice(2),
486
659
  cwd = process.cwd(),
@@ -498,120 +671,39 @@ export async function runCli({
498
671
  const tierMap = resolveDocTiers(resolvedConfig, { root });
499
672
 
500
673
  if (update) {
501
- const existing = loadBaseline(resolvedBaselinePath);
502
- const tolerance = Number.isFinite(existing?.toleranceBytes)
503
- ? existing.toleranceBytes
504
- : DEFAULT_TOLERANCE_BYTES;
505
- const envelope = buildBaseline(tierMap, tolerance);
506
- fs.mkdirSync(path.dirname(resolvedBaselinePath), { recursive: true });
507
- fs.writeFileSync(
674
+ return writeUpdatedBaseline({
675
+ tierMap,
508
676
  resolvedBaselinePath,
509
- `${JSON.stringify(envelope, null, 2)}\n`,
510
- );
511
- if (!json) {
512
- stdout.write(
513
- `[context-budget] wrote baseline ${resolvedBaselinePath} (tolerance ${tolerance} bytes)\n`,
514
- );
515
- } else {
516
- stdout.write(
517
- `${JSON.stringify({ kind: 'context-budget-update', baselinePath: resolvedBaselinePath, envelope }, null, 2)}\n`,
518
- );
519
- }
520
- return 0;
677
+ json,
678
+ stdout,
679
+ });
521
680
  }
522
681
 
523
682
  const baseline = loadBaseline(resolvedBaselinePath);
524
683
  if (!baseline) {
525
- if (json) {
526
- stdout.write(
527
- `${JSON.stringify({ kind: 'context-budget-report', baselinePath: resolvedBaselinePath, tiers: tierMap.tiers, grown: [], shrunk: [], absent: [], skipped: GATED_TIERS, exitCode: 0, noBaseline: true }, null, 2)}\n`,
528
- );
529
- } else {
530
- stderr.write(
531
- `[context-budget] ⚠ budget not found at ${resolvedBaselinePath} — skipping (no-op)\n`,
532
- );
533
- }
534
- return 0;
684
+ return reportMissingBaseline({
685
+ tierMap,
686
+ resolvedBaselinePath,
687
+ json,
688
+ stdout,
689
+ stderr,
690
+ });
535
691
  }
536
692
 
537
- const diff = diffBudget(tierMap, baseline);
538
- const ceiling = Number.isFinite(baseline?.agentBoot?.ceilingBytes)
539
- ? baseline.agentBoot.ceilingBytes
540
- : AGENT_BOOT_CEILING_BYTES;
541
- const bootOverflow = agentBootOverflow(tierMap, ceiling);
542
- const bootDrift = agentBootDrift(tierMap, baseline, ceiling);
543
- const permissiveDrift = bootDrift.filter((d) => d.direction === 'permissive');
544
- const exitCode =
545
- budgetFailureCount(diff) > 0 ||
546
- bootOverflow.length > 0 ||
547
- permissiveDrift.length > 0
548
- ? 1
549
- : 0;
550
-
693
+ const report = evaluateBudget({ tierMap, baseline });
551
694
  if (json) {
552
- const envelope = {
553
- kind: 'context-budget-report',
554
- baselinePath: resolvedBaselinePath,
555
- toleranceBytes: Number.isFinite(baseline.toleranceBytes)
556
- ? baseline.toleranceBytes
557
- : 0,
558
- current: Object.fromEntries(
559
- GATED_TIERS.map((t) => [t, tierTotalBytes(tierMap.tiers[t] ?? [])]),
560
- ),
561
- grown: diff.grown,
562
- shrunk: diff.shrunk,
563
- absent: diff.absent,
564
- skipped: diff.skipped,
565
- agentBootCeilingBytes: ceiling,
566
- agentBootOverflow: bootOverflow,
567
- agentBootDrift: bootDrift,
568
- workflowReachableBytes: tierMap.workflowClosure?.reachableTotalBytes ?? 0,
569
- exitCode,
570
- };
571
- stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
695
+ renderJsonReport({
696
+ tierMap,
697
+ baseline,
698
+ resolvedBaselinePath,
699
+ report,
700
+ stdout,
701
+ });
572
702
  } else {
573
- stdout.write(`\n--- context-budget preview ---\n`);
574
- stdout.write(`${renderDiff(diff)}\n`);
575
- const reachable = renderReachable(tierMap, baseline);
576
- if (reachable) stdout.write(`${reachable}\n`);
577
- for (const o of bootOverflow) {
578
- stdout.write(
579
- `+ agentBoot: ${o.path} is ${o.bytes} bytes, over the ${o.ceiling}-byte per-agent ceiling\n`,
580
- );
581
- }
582
- for (const line of renderBootDrift(bootDrift)) {
583
- stdout.write(`${line}\n`);
584
- }
585
- if (exitCode === 1) {
586
- if (permissiveDrift.length > 0) {
587
- stderr.write(
588
- `[context-budget] ❌ a recorded agentBoot row understates the file it describes, so it overstates the headroom an author would size an edit against — refresh it with \`node .agents/scripts/check-context-budget.js --update\` (the ceiling is unchanged)\n`,
589
- );
590
- }
591
- if (bootOverflow.length > 0) {
592
- stderr.write(
593
- `[context-budget] ❌ a role-agent boot context exceeds the ${ceiling}-byte per-agent ceiling — trim the role def (the ceiling is a hard cap, not a starve target)\n`,
594
- );
595
- }
596
- if (diff.grown.length > 0) {
597
- stderr.write(
598
- `[context-budget] ❌ a documentation tier grew beyond tolerance — refresh the budget consciously with \`node .agents/scripts/check-context-budget.js --update\` once the growth is intentional\n`,
599
- );
600
- }
601
- if (diff.shrunk.length > 0) {
602
- stderr.write(
603
- `[context-budget] ❌ a documentation tier came in under its recorded total — the ratchet is holding slack the tree no longer spends, so the next growth would be absorbed silently. Lock the gain in with \`node .agents/scripts/check-context-budget.js --update\`\n`,
604
- );
605
- }
606
- if (diff.absent.length > 0) {
607
- stderr.write(
608
- `[context-budget] ❌ a recorded row names a path the measured tier no longer contains — its bytes inflate the recorded total against nothing. Refresh with \`node .agents/scripts/check-context-budget.js --update\`\n`,
609
- );
610
- }
611
- }
703
+ renderTextReport({ tierMap, baseline, report, stdout, stderr });
612
704
  }
613
705
 
614
- return exitCode;
706
+ return report.exitCode;
615
707
  }
616
708
 
617
709
  async function main() {