mandrel 2.13.0 → 2.15.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 (71) hide show
  1. package/.agents/audit-checklists/navigability.md +1 -1
  2. package/.agents/docs/workflows.md +4 -5
  3. package/.agents/schemas/story-deliver-terminal.schema.json +60 -6
  4. package/.agents/scripts/acceptance-eval.js +18 -1
  5. package/.agents/scripts/agents-bootstrap-github.js +22 -1
  6. package/.agents/scripts/apply-quality-bootstrap.js +6 -0
  7. package/.agents/scripts/audit-labels-bootstrap.js +15 -1
  8. package/.agents/scripts/audit-to-stories.js +26 -1
  9. package/.agents/scripts/boot-sweep.js +4 -1
  10. package/.agents/scripts/bootstrap.js +1 -0
  11. package/.agents/scripts/check-arch-cycles.js +20 -0
  12. package/.agents/scripts/check-baselines.js +8 -2
  13. package/.agents/scripts/check-context-budget.js +40 -5
  14. package/.agents/scripts/check-dead-exports.js +21 -0
  15. package/.agents/scripts/check-doc-links.js +12 -1
  16. package/.agents/scripts/check-lifecycle-doc-drift.js +9 -0
  17. package/.agents/scripts/check-workflow-citations.js +332 -0
  18. package/.agents/scripts/deliver-light.js +71 -9
  19. package/.agents/scripts/deliver-recover.js +4 -1
  20. package/.agents/scripts/diagnose-friction.js +17 -1
  21. package/.agents/scripts/diagnose.js +20 -14
  22. package/.agents/scripts/drain-pending-cleanup.js +20 -1
  23. package/.agents/scripts/evidence-gate.js +20 -1
  24. package/.agents/scripts/generate-config-docs.js +14 -1
  25. package/.agents/scripts/generate-lifecycle-docs.js +14 -1
  26. package/.agents/scripts/generate-workflows-doc.js +14 -1
  27. package/.agents/scripts/git-cleanup.js +32 -1
  28. package/.agents/scripts/lib/cli-usage.js +174 -0
  29. package/.agents/scripts/lib/cli-utils.js +12 -0
  30. package/.agents/scripts/lib/doc-tiers.js +53 -10
  31. package/.agents/scripts/lib/orchestration/plan-context.js +16 -11
  32. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +122 -6
  33. package/.agents/scripts/lib/workflow-closure.js +431 -0
  34. package/.agents/scripts/mandrel-update-preflight.js +9 -0
  35. package/.agents/scripts/nav-registry-diff.js +13 -0
  36. package/.agents/scripts/plan-context.js +17 -1
  37. package/.agents/scripts/plan-critics.js +10 -0
  38. package/.agents/scripts/plan-persist.js +33 -1
  39. package/.agents/scripts/plan-run-epilogue.js +12 -1
  40. package/.agents/scripts/quality-preview.js +17 -1
  41. package/.agents/scripts/resolve-doc-tiers.js +13 -0
  42. package/.agents/scripts/resolve-stories.js +1 -0
  43. package/.agents/scripts/resync-status-column.js +4 -1
  44. package/.agents/scripts/signals-view.js +11 -0
  45. package/.agents/scripts/single-story-close.js +24 -0
  46. package/.agents/scripts/single-story-confirm-merge.js +16 -0
  47. package/.agents/scripts/single-story-init.js +21 -1
  48. package/.agents/scripts/stories-wave-tick.js +1 -0
  49. package/.agents/scripts/sync-agentrc.js +16 -4
  50. package/.agents/scripts/update-ticket-state.js +23 -2
  51. package/.agents/workflows/audit-navigability.md +2 -2
  52. package/.agents/workflows/audit-to-stories.md +1 -1
  53. package/.agents/workflows/deliver.md +80 -81
  54. package/.agents/workflows/git-cleanup.md +9 -14
  55. package/.agents/workflows/helpers/acceptance-self-eval.md +14 -13
  56. package/.agents/workflows/helpers/audit-lens-core.md +2 -2
  57. package/.agents/workflows/helpers/code-review.md +11 -11
  58. package/.agents/workflows/helpers/deliver-digest.md +7 -8
  59. package/.agents/workflows/helpers/deliver-light.md +196 -0
  60. package/.agents/workflows/helpers/deliver-reference.md +46 -14
  61. package/.agents/workflows/helpers/deliver-story-reference.md +55 -63
  62. package/.agents/workflows/helpers/deliver-story.md +22 -22
  63. package/.agents/workflows/helpers/mandrel-sync-config.md +2 -2
  64. package/.agents/workflows/helpers/parallel-tooling.md +1 -2
  65. package/.agents/workflows/helpers/plan-reference.md +72 -14
  66. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -4
  67. package/.agents/workflows/mandrel-update.md +6 -6
  68. package/.agents/workflows/plan.md +86 -83
  69. package/docs/CHANGELOG.md +22 -0
  70. package/package.json +2 -1
  71. package/.agents/workflows/deliver-light.md +0 -117
@@ -6,7 +6,7 @@
6
6
 
7
7
  # Navigability Audit — authoring checklist
8
8
 
9
- > Audit the whole route tree against the consumer's nav-registry SSOT — every route has a persona nav door and no nav href is dead. A deliberately-global lens (Epic #4131, F2/F3) exempt from the cross-epic-leak guard and routed onto route-adding change sets.
9
+ > Audit the whole route tree against the consumer's nav-registry SSOT — every route has a persona nav door and no nav href is dead. A deliberately-global lens exempt from the cross-epic-leak guard and routed onto route-adding change sets.
10
10
 
11
11
  Self-check your change against this lens's concerns before you ship:
12
12
 
@@ -32,7 +32,7 @@ by `node .agents/scripts/generate-workflows-doc.js`; `npm run docs:check`
32
32
  fails when it drifts from the on-disk workflow set. To change a command’s
33
33
  description, edit the workflow file’s front-matter and regenerate.
34
34
 
35
- ## Commands (25)
35
+ ## Commands (24)
36
36
 
37
37
  | Command | Description |
38
38
  | --- | --- |
@@ -43,7 +43,7 @@ description, edit the workflow file’s front-matter and regenerate.
43
43
  | `/audit-dependencies` | Audit `package.json` for unused, outdated, and major-version-stale dependencies; surface Node-engine drift and propose upgrade batches. |
44
44
  | `/audit-devops` | Audit CI/CD workflows, container images, infrastructure-as-code, and deployment pipelines; surface failure modes and hardening gaps. |
45
45
  | `/audit-documentation` | Audit the repository's main documentation for staleness, semantic drift, and completeness; emit a structured High/Medium/Low findings report. |
46
- | `/audit-navigability` | Audit the whole route tree against the consumer's nav-registry SSOT — every route has a persona nav door and no nav href is dead. A deliberately-global lens (Epic #4131, F2/F3) exempt from the cross-epic-leak guard and routed onto route-adding change sets. |
46
+ | `/audit-navigability` | Audit the whole route tree against the consumer's nav-registry SSOT — every route has a persona nav door and no nav href is dead. A deliberately-global lens exempt from the cross-epic-leak guard and routed onto route-adding change sets. |
47
47
  | `/audit-performance` | Audit performance by measuring first — profile hot paths, I/O, memory, and payload against the repo's own numbers — and audit interleaving/partial-failure correctness (TOCTOU, unawaited promises, non-atomic writes) as a first-class dimension. |
48
48
  | `/audit-privacy` | Audit logs, telemetry, and persistence paths for PII leakage and retention violations; surface secrets exposure and consent gaps. |
49
49
  | `/audit-quality` | Audit test coverage gaps, flaky tests, missing assertions, and test-pyramid balance; recommend a remediation batch. |
@@ -52,11 +52,10 @@ description, edit the workflow file’s front-matter and regenerate.
52
52
  | `/audit-sre` | "Audit production-readiness for a release candidate: SLOs, observability, runbooks, error budgets, and rollback paths." |
53
53
  | `/audit-to-stories` | Convert findings produced by the audit-\* workflows into actionable GitHub Stories. Reads temp/audits/audit-\*-results.md, groups findings cross-audit, deduplicates against existing Issues by fingerprint, and either chains into /plan --seed-file or opens standalone Stories. |
54
54
  | `/audit-ux-ui` | Audit UX/UI consistency and design system adherence |
55
- | `/deliver` | Unified delivery entry point. Takes a list of Story ids, resolves their dependency graph from live state, and delivers each via the single deliver-story engine — story-<id> → PR → main. |
56
- | `/deliver-light` | Single-session delivery for genuinely small work. Judges a prompt's predicted footprint, authors a receipt Story, then lands it through the same single-story-init / single-story-close engine — every close gate unchanged. |
55
+ | `/deliver` | Unified delivery entry point. Takes Story ids or a plain-language prompt, derives which path the work belongs on, and lands it via the single deliver-story engine — story-<id> → PR → main. |
57
56
  | `/git-cleanup` | Tidy the local checkout in four phases: fast-forward `main`, prune stale remote-tracking refs, sweep merged branches (squash-aware), and triage `git stash` entries — each step gated by operator confirmation. |
58
57
  | `/git-deliver` | Single ad-hoc delivery command for working-tree changes. Detects the git setup and escalates to the right terminal step — commit only, commit + push, or commit + push + open a PR with native auto-merge — picking the default from observable state and letting flags pin any level explicitly. Replaces the retired git-commit-all, git-push, and git-pr-all trio. |
59
- | `/mandrel-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `npx mandrel update` (resolve newest published version → install → re-materialize `.agents/` → migrate → doctor → surface changelog) as the single mechanical step, then walks the operator through the judgment wraparound the CLI deliberately leaves unowned: reconcile `.agentrc.json`, install the Epic #1386 quality-gate surface, refresh the harness permission allowlist, reconcile the consumer's `AGENTS.md` / runbooks against the surfaced changelog, and stage + commit the staged lockfile bump. |
58
+ | `/mandrel-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `npx mandrel update` (resolve newest published version → install → re-materialize `.agents/` → migrate → doctor → surface changelog) as the single mechanical step, then walks the operator through the judgment wraparound the CLI deliberately leaves unowned: reconcile `.agentrc.json`, install the stabilized quality-gate surface, refresh the harness permission allowlist, reconcile the consumer's `AGENTS.md` / runbooks against the surfaced changelog, and stage + commit the staged lockfile bump. |
60
59
  | `/plan` | Unified planning entry point. Interrogate → author → persist. Emits one Story by default; splits into N>1 only under the default-single split policy. |
61
60
  | `/qa-assist` | Human-led QA assist loop — set up, then ride a rolling multi-observation intake session. The operator reports observations in any order; the agent enriches each (repro + root-cause file:line + coverage verdict for bugs; analysis + options + recommendation for enhancements), asks clarifying questions only when ambiguous, and appends a redacted ledger item — recording, never planning — to a persistent, resumable session under temp/qa/. Only when the operator says they are done does it review the full ledger and hand off to /plan. |
62
61
  | `/qa-explore` | Agent-led exploratory-QA loop — the agent Plans a surface with an explicit static-vs-drive method choice, drives it (browser MCP or static), and captures ledger items read-only, then Triages — a bounded per-surface session, HITL-gated at every phase transition, routed through the shared dedup/coverage/classification/missing-test/redaction/session core under temp/qa/ |
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/dsj1984/mandrel/blob/main/.agents/schemas/story-deliver-terminal.schema.json",
4
4
  "title": "story-deliver-terminal",
5
- "description": "The single terminal envelope every Story close-and-land invocation emits (Story #4543). Before this schema, the delivery tail had two divergent prose return contracts — one in .agents/workflows/helpers/deliver-story.md, a different one in .agents/agents/story-worker.md — and neither was validated by anything, so a caller could not tell a landed Story from a parked one without re-probing GitHub. This is the SSOT both now reference rather than restate. status is exactly one of landed | pending | blocked | failed; phase names where the run ended; tail carries per-step booleans so a partial-tail degradation is visible without failing an otherwise-landed merge; nextCommand names the single command that resumes or remediates the run, drawn from the same vocabulary deliver-recover.js prints.",
5
+ "description": "The single terminal envelope a Story delivery invocation emits (Story #4543). Before this schema, the delivery tail had two divergent prose return contracts — one in .agents/workflows/helpers/deliver-story.md, a different one in .agents/agents/story-worker.md — and neither was validated by anything, so a caller could not tell a landed Story from a parked one without re-probing GitHub. This is the SSOT both now reference rather than restate. status is exactly one of landed | pending | blocked | failed | escalated; phase names where the run ended; tail carries per-step booleans so a partial-tail degradation is visible without failing an otherwise-landed merge; nextCommand names the single command that resumes or remediates the run, drawn from the same vocabulary deliver-recover.js prints. escalated (Story #4746) is the one status emitted BEFORE a Story exists — /deliver-light's suitability gate refusing an over-scope prompt — which is why storyId is null exactly there and non-null everywhere else.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "kind",
@@ -14,16 +14,21 @@
14
14
  ],
15
15
  "properties": {
16
16
  "kind": { "type": "string", "const": "story-deliver-terminal" },
17
- "storyId": { "type": "integer", "minimum": 1 },
17
+ "storyId": {
18
+ "type": ["integer", "null"],
19
+ "minimum": 1,
20
+ "description": "The Story this envelope reports on. null ONLY for status escalated, where the run refused before authoring a receipt Story — the allOf below pins that correspondence in both directions, so an escalated envelope cannot name a Story it did not create and a landed one cannot omit the Story it landed."
21
+ },
18
22
  "status": {
19
23
  "type": "string",
20
- "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.",
21
- "enum": ["landed", "pending", "blocked", "failed"]
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.",
25
+ "enum": ["landed", "pending", "blocked", "failed", "escalated"]
22
26
  },
23
27
  "phase": {
24
28
  "type": "string",
25
- "description": "The pipeline phase the run ended in. Mirrors the close pipeline's phase names so a terminal envelope is attributable to one step.",
29
+ "description": "The pipeline phase the run ended in. Mirrors the close pipeline's phase names so a terminal envelope is attributable to one step. suitability-gate precedes them all — it is the /deliver-light gate, the only phase that runs before a Story exists.",
26
30
  "enum": [
31
+ "suitability-gate",
27
32
  "init",
28
33
  "wrong-tree-guard",
29
34
  "close-validation",
@@ -130,9 +135,35 @@
130
135
  },
131
136
  "additionalProperties": false
132
137
  },
138
+ "escalation": {
139
+ "type": ["object", "null"],
140
+ "description": "Present iff status === \"escalated\" (Story #4746). The suitability gate's decision was already correct before this block existed — what was missing was an outcome a session could not walk past. reasons carries the gate's own words; created records, per artifact, that the run started nothing.",
141
+ "required": ["reasons", "created"],
142
+ "properties": {
143
+ "reasons": {
144
+ "type": "array",
145
+ "minItems": 1,
146
+ "items": { "type": "string", "minLength": 1 },
147
+ "description": "The gate's reasons verbatim — the same strings the ask-operator path prints, so attended and unattended over-scope explain themselves identically."
148
+ },
149
+ "created": {
150
+ "type": "object",
151
+ "description": "Per-artifact proof that an escalated run left nothing half-started for a later run to trip over. Deliberately three const-false booleans rather than one aggregate flag or a bare omission: an omitted field reads as \"not checked\", and an aggregate is exactly the shape that let the post-land tail once report an outcome it never verified. Every value is pinned false by the schema, so an escalated envelope claiming it authored a receipt Story, cut a branch, or materialized a worktree cannot be built at all.",
152
+ "required": ["receiptStory", "storyBranch", "worktree"],
153
+ "properties": {
154
+ "receiptStory": { "const": false },
155
+ "storyBranch": { "const": false },
156
+ "worktree": { "const": false }
157
+ },
158
+ "additionalProperties": false
159
+ }
160
+ },
161
+ "additionalProperties": false
162
+ },
133
163
  "nextCommand": {
134
164
  "type": ["string", "null"],
135
- "description": "The single command that advances this Story 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."
165
+ "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.",
166
+ "minLength": 1
136
167
  },
137
168
  "elapsedSeconds": { "type": "number", "minimum": 0 },
138
169
  "waitBudget": {
@@ -148,5 +179,28 @@
148
179
  },
149
180
  "timestamp": { "type": "string", "format": "date-time" }
150
181
  },
182
+ "allOf": [
183
+ {
184
+ "description": "escalated is the pre-Story terminal, and the correspondence is pinned in BOTH directions. An escalated envelope MUST carry the escalation block, MUST have a null storyId (it created none), and MUST name the next command that owns the work — so a run cannot report escalation while pointing at a Story it started. Every other status MUST carry an integer storyId and MUST NOT carry an escalation block, so the new status cannot leak into the close path.",
185
+ "if": {
186
+ "properties": { "status": { "const": "escalated" } },
187
+ "required": ["status"]
188
+ },
189
+ "then": {
190
+ "required": ["escalation"],
191
+ "properties": {
192
+ "storyId": { "type": "null" },
193
+ "escalation": { "type": "object" },
194
+ "nextCommand": { "type": "string", "minLength": 1 }
195
+ }
196
+ },
197
+ "else": {
198
+ "properties": {
199
+ "storyId": { "type": "integer", "minimum": 1 },
200
+ "escalation": { "type": "null" }
201
+ }
202
+ }
203
+ }
204
+ ],
151
205
  "additionalProperties": false
152
206
  }
@@ -302,4 +302,21 @@ export async function main(argv = process.argv.slice(2)) {
302
302
  return envelope;
303
303
  }
304
304
 
305
- runAsCli(import.meta.url, main, { source: 'acceptance-eval' });
305
+ runAsCli(import.meta.url, main, {
306
+ source: 'acceptance-eval',
307
+ usage: {
308
+ invocation:
309
+ 'node .agents/scripts/acceptance-eval.js --story <id> --verdict <path> [--no-signal]',
310
+ summary:
311
+ "Score an authored acceptance verdict against the Story's acceptance[] criteria and emit the bounded loop's proceed / redraft / block decision.",
312
+ flags: [
313
+ ['--story <id>', 'GitHub issue number of the Story (required).'],
314
+ ['--verdict <path>', 'Path to the authored verdict JSON (required).'],
315
+ [
316
+ '--no-signal',
317
+ "Skip appending the per-criterion signal to the Story's signals ledger.",
318
+ ],
319
+ ],
320
+ notes: ['Exit codes:\n 0 proceed or redraft\n 1 block'],
321
+ },
322
+ });
@@ -526,4 +526,25 @@ export {
526
526
  verifyApiAccess,
527
527
  };
528
528
 
529
- runAsCli(import.meta.url, main, { source: 'Bootstrap' });
529
+ runAsCli(import.meta.url, main, {
530
+ source: 'Bootstrap',
531
+ usage: {
532
+ invocation:
533
+ 'node .agents/scripts/agents-bootstrap-github.js [--assume-yes|--assume-no] [--approve-github-admin] [--with-project-board] [--reap-conflicting-workflows]',
534
+ summary:
535
+ 'Bootstrap the GitHub side of a consumer repo: label taxonomy, issue forms, workflows, and the optional project board.',
536
+ flags: [
537
+ ['--assume-yes', 'Answer yes to every prompt (non-interactive run).'],
538
+ ['--assume-no', 'Answer no to every prompt (probe only).'],
539
+ [
540
+ '--approve-github-admin',
541
+ 'Consent to admin-scoped GitHub mutations (implied by --assume-yes).',
542
+ ],
543
+ ['--with-project-board', 'Also provision the GitHub Project board.'],
544
+ [
545
+ '--reap-conflicting-workflows',
546
+ 'Remove pre-existing workflow files that clash with the framework set.',
547
+ ],
548
+ ],
549
+ },
550
+ });
@@ -76,4 +76,10 @@ async function main() {
76
76
  runAsCli(import.meta.url, main, {
77
77
  source: 'apply-quality-bootstrap',
78
78
  propagateExitCode: true,
79
+ usage: {
80
+ invocation: 'node .agents/scripts/apply-quality-bootstrap.js',
81
+ summary:
82
+ 'Install the quality-gate surface into the consumer repo (guardrails helper, pre-commit line, npm scripts, config defaults) and migrate the baselines layout. Idempotent; prints { quality, baselines } JSON to stdout.',
83
+ flags: [],
84
+ },
79
85
  });
@@ -272,4 +272,18 @@ async function main() {
272
272
  }
273
273
  }
274
274
 
275
- runAsCli(import.meta.url, main, { source: 'audit-labels-bootstrap' });
275
+ runAsCli(import.meta.url, main, {
276
+ source: 'audit-labels-bootstrap',
277
+ usage: {
278
+ invocation:
279
+ 'node .agents/scripts/audit-labels-bootstrap.js [--owner <owner>] [--repo <repo>] [--force] [--dry-run]',
280
+ summary:
281
+ 'Create the audit-finding label taxonomy in the target repository. Idempotent.',
282
+ flags: [
283
+ ['--owner <owner>', 'Repository owner (default: github.owner).'],
284
+ ['--repo <repo>', 'Repository name (default: github.repo).'],
285
+ ['--force', 'Update colour/description of labels that already exist.'],
286
+ ['--dry-run', 'Report what would be created; mutate nothing.'],
287
+ ],
288
+ },
289
+ });
@@ -626,4 +626,29 @@ async function main() {
626
626
  );
627
627
  }
628
628
 
629
- runAsCli(import.meta.url, main, { source: 'audit-to-stories' });
629
+ runAsCli(import.meta.url, main, {
630
+ source: 'audit-to-stories',
631
+ usage: {
632
+ invocation:
633
+ 'node .agents/scripts/audit-to-stories.js (--scan | --auto | --emit-plan-seed | --emit-stories) [options]',
634
+ summary:
635
+ 'Turn audit-lens findings under temp/audits/ into a dedup-checked plan seed or standalone Stories.',
636
+ flags: [
637
+ ['--scan', 'Print the grouped, deduplicated plan as JSON.'],
638
+ ['--auto', 'Run the full scan → file pipeline and print the summary.'],
639
+ ['--emit-plan-seed', 'Emit a /plan --seed-file document.'],
640
+ ['--emit-stories', 'Emit the Story drafts as JSON.'],
641
+ ['--glob <pattern>', 'Override the audit-results glob.'],
642
+ ['--severity <level>', 'Lowest severity to include (high|medium|low).'],
643
+ ['--ledger <path>', 'Path to the dedup ledger.'],
644
+ [
645
+ '--plan <path>',
646
+ 'Read a previously emitted plan instead of re-scanning.',
647
+ ],
648
+ ['--out <path>', 'Write output to a file instead of stdout.'],
649
+ ['--no-provider', 'Skip live GitHub dedup lookups (offline).'],
650
+ ['--json', 'Force JSON output.'],
651
+ ['--dry-run', 'Report what would be filed; create nothing.'],
652
+ ],
653
+ },
654
+ });
@@ -212,4 +212,7 @@ async function main() {
212
212
  }
213
213
  }
214
214
 
215
- runAsCli(import.meta.url, main, { source: 'boot-sweep' });
215
+ runAsCli(import.meta.url, main, {
216
+ source: 'boot-sweep',
217
+ usage: HELP,
218
+ });
@@ -1467,4 +1467,5 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
1467
1467
  runAsCli(import.meta.url, main, {
1468
1468
  source: 'Bootstrap',
1469
1469
  propagateExitCode: true,
1470
+ usage: HELP,
1470
1471
  });
@@ -381,4 +381,24 @@ runAsCli(import.meta.url, main, {
381
381
  source: 'arch-cycles',
382
382
  propagateExitCode: true,
383
383
  errorPrefix: '[arch-cycles] ❌ Fatal error',
384
+ usage: {
385
+ invocation:
386
+ 'node .agents/scripts/check-arch-cycles.js [--baseline <path>] [--root <dir>] [--json]',
387
+ summary:
388
+ 'Ratchet on module-dependency cycles: compare the live import graph against the recorded baseline and fail on any newly added cycle.',
389
+ flags: [
390
+ [
391
+ '--baseline <path>',
392
+ 'Baseline file (default: baselines/arch-cycles.json).',
393
+ ],
394
+ [
395
+ '--root <dir>',
396
+ 'Scan a single root instead of the distributed surface.',
397
+ ],
398
+ ['--json', 'Emit the comparison envelope as JSON.'],
399
+ ],
400
+ notes: [
401
+ 'Exit codes:\n 0 clean, or removals only\n 1 a new cycle was detected',
402
+ ],
403
+ },
384
404
  });
@@ -43,7 +43,10 @@ import {
43
43
  assertFloorAxesExist,
44
44
  compareToFloor,
45
45
  } from './lib/orchestration/check-baselines/phases/floors.js';
46
- import { parseArgs } from './lib/orchestration/check-baselines/phases/parse-args.js';
46
+ import {
47
+ HELP_TEXT,
48
+ parseArgs,
49
+ } from './lib/orchestration/check-baselines/phases/parse-args.js';
47
50
  import {
48
51
  runCheckBaselines,
49
52
  selectEnabledGates,
@@ -78,4 +81,7 @@ async function main() {
78
81
  process.exit(result.exitCode);
79
82
  }
80
83
 
81
- runAsCli(import.meta.url, main, { source: 'check-baselines' });
84
+ runAsCli(import.meta.url, main, {
85
+ source: 'check-baselines',
86
+ usage: HELP_TEXT,
87
+ });
@@ -11,6 +11,13 @@
11
11
  * - `alwaysLoaded` — the `CLAUDE.md` `@`-import closure re-paid on every
12
12
  * session and every subagent spawn (instructions.md § 4).
13
13
  * - `mandatoryRead` — the resolved `project.docsContextFiles` set.
14
+ * - `workflow` — the workflow **mandatory closure** (Story #4752): every
15
+ * `.agents/workflows/**` entry point plus the transitive
16
+ * closure of its `mandatoryReads:` frontmatter edges. The
17
+ * companion **reachable** closure (per entry point) is
18
+ * recorded under the top-level `workflowClosure` key as a
19
+ * drift signal and never gates — growth there is a
20
+ * reading-cost signal, not a contract violation.
14
21
  *
15
22
  * It additionally enforces a **per-file** ceiling on the role-scoped agent-boot
16
23
  * tier (`.agents/agents/*.md`, #4478): no single boot context may exceed
@@ -52,12 +59,12 @@ import { resolveConfig } from './lib/config-resolver.js';
52
59
  import { resolveDocTiers, tierTotalBytes } from './lib/doc-tiers.js';
53
60
 
54
61
  /**
55
- * The tiers this ratchet gates (in report order). `digestVisible` and
56
- * `onDemand` are resolved by the tier map for the lens, but the byte budget
57
- * intentionally gates only the two tiers the Epic AC names.
58
- * @type {Array<'alwaysLoaded' | 'mandatoryRead'>}
62
+ * The tiers this ratchet gates (in report order). `digestVisible`, `onDemand`
63
+ * and `workflowOnDemand` are resolved by the tier map for the lens, but the
64
+ * byte budget intentionally gates only the tiers a session is *forced* to read.
65
+ * @type {Array<'alwaysLoaded' | 'mandatoryRead' | 'workflow'>}
59
66
  */
60
- export const GATED_TIERS = ['alwaysLoaded', 'mandatoryRead'];
67
+ export const GATED_TIERS = ['alwaysLoaded', 'mandatoryRead', 'workflow'];
61
68
 
62
69
  /**
63
70
  * Default tolerance (bytes) seeded into a fresh baseline by `--update` when the
@@ -171,6 +178,12 @@ export function buildBaseline(tierMap, toleranceBytes) {
171
178
  ceilingBytes: AGENT_BOOT_CEILING_BYTES,
172
179
  files: agentBootFiles,
173
180
  },
181
+ // Recorded, never gated (#4752): the total reachable closure per workflow
182
+ // entry point. It is a drift signal — the gate is `tiers.workflow`.
183
+ workflowClosure: {
184
+ reachableTotalBytes: tierMap.workflowClosure?.reachableTotalBytes ?? 0,
185
+ entryPoints: tierMap.workflowClosure?.entryPoints ?? [],
186
+ },
174
187
  };
175
188
  }
176
189
 
@@ -249,6 +262,25 @@ export function renderDiff(diff) {
249
262
  return lines.join('\n');
250
263
  }
251
264
 
265
+ /**
266
+ * Render the workflow **reachable** closure line — recorded, never gated
267
+ * (Story #4752). Returns `''` when there is no workflow tier to report, so the
268
+ * caller can stay a one-liner.
269
+ *
270
+ * @param {{ workflowClosure?: { reachableTotalBytes?: number, entryPoints?: unknown[] } }} tierMap
271
+ * @param {{ workflowClosure?: { reachableTotalBytes?: number } } | null} [baseline]
272
+ * @returns {string}
273
+ */
274
+ export function renderReachable(tierMap, baseline) {
275
+ const closure = tierMap?.workflowClosure;
276
+ const current = closure?.reachableTotalBytes ?? 0;
277
+ if (current <= 0) return '';
278
+ const recorded = baseline?.workflowClosure?.reachableTotalBytes;
279
+ const against = Number.isFinite(recorded) ? ` (recorded ${recorded})` : '';
280
+ const entries = closure.entryPoints?.length ?? 0;
281
+ return ` workflow reachable closure: ${current} bytes across ${entries} entry points${against} — drift signal, never gated`;
282
+ }
283
+
252
284
  /**
253
285
  * Top-level CLI entry. Exported so tests can drive the full pipeline against a
254
286
  * tmpdir fixture with an injected config and sinks.
@@ -338,12 +370,15 @@ export async function runCli({
338
370
  skipped: diff.skipped,
339
371
  agentBootCeilingBytes: ceiling,
340
372
  agentBootOverflow: bootOverflow,
373
+ workflowReachableBytes: tierMap.workflowClosure?.reachableTotalBytes ?? 0,
341
374
  exitCode,
342
375
  };
343
376
  stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
344
377
  } else {
345
378
  stdout.write(`\n--- context-budget preview ---\n`);
346
379
  stdout.write(`${renderDiff(diff)}\n`);
380
+ const reachable = renderReachable(tierMap, baseline);
381
+ if (reachable) stdout.write(`${reachable}\n`);
347
382
  for (const o of bootOverflow) {
348
383
  stdout.write(
349
384
  `+ agentBoot: ${o.path} is ${o.bytes} bytes, over the ${o.ceiling}-byte per-agent ceiling\n`,
@@ -247,4 +247,25 @@ runAsCli(import.meta.url, main, {
247
247
  source: 'dead-exports',
248
248
  propagateExitCode: true,
249
249
  errorPrefix: '[dead-exports] ❌ Fatal error',
250
+ usage: {
251
+ invocation:
252
+ 'node .agents/scripts/check-dead-exports.js [--production] [--baseline <path>] [--knip-output <path>] [--json]',
253
+ summary:
254
+ 'Ratchet on unused exports reported by knip: fail when an export is added above the recorded baseline.',
255
+ flags: [
256
+ [
257
+ '--production',
258
+ 'Score the production-only surface (separate baseline).',
259
+ ],
260
+ ['--baseline <path>', 'Baseline file (default: mode-specific).'],
261
+ [
262
+ '--knip-output <path>',
263
+ 'Read a saved knip JSON envelope instead of running knip.',
264
+ ],
265
+ ['--json', 'Emit the comparison envelope as JSON.'],
266
+ ],
267
+ notes: [
268
+ 'Exit codes:\n 0 clean, or removals only\n 1 newly added unused exports',
269
+ ],
270
+ },
250
271
  });
@@ -423,4 +423,15 @@ async function main() {
423
423
  process.exit(1);
424
424
  }
425
425
 
426
- runAsCli(import.meta.url, main, { source: 'check-doc-links' });
426
+ runAsCli(import.meta.url, main, {
427
+ source: 'check-doc-links',
428
+ usage: {
429
+ invocation: 'node .agents/scripts/check-doc-links.js',
430
+ summary:
431
+ 'Validate every relative Markdown link and /slash-command token across docs/ and .agents/, and reject mentions of retired commands.',
432
+ flags: [],
433
+ notes: [
434
+ 'Exit codes:\n 0 every link and command token resolves\n 1 at least one violation (file:line on stderr)',
435
+ ],
436
+ },
437
+ });
@@ -399,4 +399,13 @@ async function main() {
399
399
  await runAsCli(import.meta.url, main, {
400
400
  source: 'check-lifecycle-doc-drift',
401
401
  propagateExitCode: true,
402
+ usage: {
403
+ invocation: 'node .agents/scripts/check-lifecycle-doc-drift.js',
404
+ summary:
405
+ "Fail when the listener-model table in docs/LIFECYCLE.md drifts from the lifecycle listeners' declared subscriptions.",
406
+ flags: [],
407
+ notes: [
408
+ 'Exit codes:\n 0 doc and code agree\n 1 drift in either direction',
409
+ ],
410
+ },
402
411
  });