mandrel 2.14.0 → 2.16.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 (75) hide show
  1. package/.agents/audit-checklists/navigability.md +1 -1
  2. package/.agents/docs/workflows.md +4 -4
  3. package/.agents/scripts/acceptance-eval.js +18 -1
  4. package/.agents/scripts/agents-bootstrap-github.js +22 -1
  5. package/.agents/scripts/apply-quality-bootstrap.js +6 -0
  6. package/.agents/scripts/audit-labels-bootstrap.js +15 -1
  7. package/.agents/scripts/audit-to-stories.js +26 -1
  8. package/.agents/scripts/boot-sweep.js +4 -1
  9. package/.agents/scripts/bootstrap.js +1 -0
  10. package/.agents/scripts/check-arch-cycles.js +20 -0
  11. package/.agents/scripts/check-baselines.js +8 -2
  12. package/.agents/scripts/check-context-budget.js +40 -5
  13. package/.agents/scripts/check-dead-exports.js +21 -0
  14. package/.agents/scripts/check-doc-links.js +12 -1
  15. package/.agents/scripts/check-lifecycle-doc-drift.js +9 -0
  16. package/.agents/scripts/check-workflow-citations.js +332 -0
  17. package/.agents/scripts/deliver-light.js +32 -3
  18. package/.agents/scripts/deliver-recover.js +4 -1
  19. package/.agents/scripts/diagnose-friction.js +17 -1
  20. package/.agents/scripts/diagnose.js +20 -14
  21. package/.agents/scripts/drain-pending-cleanup.js +20 -1
  22. package/.agents/scripts/evidence-gate.js +20 -1
  23. package/.agents/scripts/generate-config-docs.js +14 -1
  24. package/.agents/scripts/generate-lifecycle-docs.js +14 -1
  25. package/.agents/scripts/generate-workflows-doc.js +14 -1
  26. package/.agents/scripts/git-cleanup.js +32 -1
  27. package/.agents/scripts/lib/cli-usage.js +174 -0
  28. package/.agents/scripts/lib/cli-utils.js +12 -0
  29. package/.agents/scripts/lib/close-validation/process.js +61 -15
  30. package/.agents/scripts/lib/doc-tiers.js +53 -10
  31. package/.agents/scripts/lib/orchestration/complexity-gate.js +307 -89
  32. package/.agents/scripts/lib/orchestration/light-suitability.js +31 -9
  33. package/.agents/scripts/lib/orchestration/plan-context.js +205 -20
  34. package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +87 -13
  35. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +24 -15
  36. package/.agents/scripts/lib/workflow-closure.js +431 -0
  37. package/.agents/scripts/mandrel-update-preflight.js +9 -0
  38. package/.agents/scripts/nav-registry-diff.js +13 -0
  39. package/.agents/scripts/plan-context.js +21 -2
  40. package/.agents/scripts/plan-critics.js +10 -0
  41. package/.agents/scripts/plan-persist.js +33 -1
  42. package/.agents/scripts/plan-run-epilogue.js +12 -1
  43. package/.agents/scripts/quality-preview.js +17 -1
  44. package/.agents/scripts/resolve-doc-tiers.js +13 -0
  45. package/.agents/scripts/resolve-stories.js +1 -0
  46. package/.agents/scripts/resync-status-column.js +4 -1
  47. package/.agents/scripts/signals-view.js +11 -0
  48. package/.agents/scripts/single-story-close.js +24 -0
  49. package/.agents/scripts/single-story-confirm-merge.js +16 -0
  50. package/.agents/scripts/single-story-init.js +21 -1
  51. package/.agents/scripts/stories-wave-tick.js +1 -0
  52. package/.agents/scripts/sync-agentrc.js +16 -4
  53. package/.agents/scripts/update-ticket-state.js +23 -2
  54. package/.agents/workflows/audit-navigability.md +2 -2
  55. package/.agents/workflows/audit-to-stories.md +1 -1
  56. package/.agents/workflows/deliver.md +80 -81
  57. package/.agents/workflows/git-cleanup.md +9 -14
  58. package/.agents/workflows/helpers/acceptance-self-eval.md +14 -13
  59. package/.agents/workflows/helpers/audit-lens-core.md +2 -2
  60. package/.agents/workflows/helpers/code-review.md +11 -11
  61. package/.agents/workflows/helpers/deliver-digest.md +7 -8
  62. package/.agents/workflows/helpers/deliver-light.md +222 -0
  63. package/.agents/workflows/helpers/deliver-reference.md +46 -14
  64. package/.agents/workflows/helpers/deliver-story-reference.md +55 -63
  65. package/.agents/workflows/helpers/deliver-story.md +22 -22
  66. package/.agents/workflows/helpers/mandrel-sync-config.md +2 -2
  67. package/.agents/workflows/helpers/parallel-tooling.md +1 -2
  68. package/.agents/workflows/helpers/plan-reference.md +96 -17
  69. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -4
  70. package/.agents/workflows/mandrel-update.md +6 -6
  71. package/.agents/workflows/plan.md +90 -85
  72. package/.agents/workflows/prototype.md +104 -0
  73. package/docs/CHANGELOG.md +32 -0
  74. package/package.json +2 -1
  75. package/.agents/workflows/deliver-light.md +0 -148
@@ -0,0 +1,332 @@
1
+ /**
2
+ * CLI: ratchet-down gate on provenance citations in workflow prose.
3
+ *
4
+ * Workflow documents ride resident in an executing agent's context. A
5
+ * `(Story #1234)` aside costs the same tokens as instruction and teaches
6
+ * the model Mandrel's own history instead of the task in front of it. The
7
+ * citations that survive are the ones a reader must follow to act; the
8
+ * rest belong in the commit trail and `docs/decisions.md`.
9
+ *
10
+ * Nothing stops the tax re-accumulating one well-meaning aside at a time,
11
+ * so this gate holds the line: count every issue-shaped reference across
12
+ * `.agents/workflows/**\/*.md` and fail when the total rises above the
13
+ * committed baseline at `baselines/workflow-citations.json`.
14
+ *
15
+ * The counted token is the bare `#NNNN` form rather than the
16
+ * `(Story|Epic|issue|refs) #NNNN` phrasing, because the same citation is
17
+ * written both ways — `(Story #4593)` and a bare `(#4593)` — and a gate
18
+ * that only saw the prefixed form would wave the other one through.
19
+ *
20
+ * Ratchet semantics mirror `check-arch-cycles.js`:
21
+ * - Total above the baseline → exit 1, with the files that grew.
22
+ * - Total below the baseline → printed as shrinkage, exit 0. Refresh the
23
+ * baseline with `--update` to lock the reduction in.
24
+ * - Equal → exit 0.
25
+ *
26
+ * Flags:
27
+ * --baseline <path> override the baseline path (default
28
+ * `baselines/workflow-citations.json`, resolved from cwd)
29
+ * --root <path> scan a different workflow root (default
30
+ * `.agents/workflows`)
31
+ * --update rewrite the baseline from the live count
32
+ * --json write the structured envelope to stdout
33
+ */
34
+
35
+ import fs from 'node:fs';
36
+ import path from 'node:path';
37
+ import process from 'node:process';
38
+ import { runAsCli } from './lib/cli-utils.js';
39
+
40
+ /** Default workflow root, relative to the repository root. */
41
+ const DEFAULT_ROOT = path.join('.agents', 'workflows');
42
+
43
+ /** Default baseline path, relative to the repository root. */
44
+ const DEFAULT_BASELINE = path.join('baselines', 'workflow-citations.json');
45
+
46
+ /**
47
+ * Issue-shaped reference: a `#` followed by 3–5 digits. Narrow enough to
48
+ * skip markdown headings and anchor links, wide enough to catch both the
49
+ * `(Story #1234)` and bare `(#1234)` spellings of one citation.
50
+ */
51
+ const CITATION_RE = /#\d{3,5}\b/g;
52
+
53
+ /**
54
+ * Parse argv. Exported so unit tests can pin the parser.
55
+ *
56
+ * @param {string[]} argv
57
+ * @returns {{ baselinePath: string | null, rootPath: string | null, update: boolean, json: boolean }}
58
+ */
59
+ export function parseArgv(argv = []) {
60
+ const out = {
61
+ baselinePath: null,
62
+ rootPath: null,
63
+ update: false,
64
+ json: false,
65
+ };
66
+ for (let i = 0; i < argv.length; i += 1) {
67
+ const flag = argv[i];
68
+ const next = argv[i + 1];
69
+ const takesValue = next && !next.startsWith('--');
70
+ if (flag === '--baseline' && takesValue) {
71
+ out.baselinePath = next;
72
+ i += 1;
73
+ } else if (flag === '--root' && takesValue) {
74
+ out.rootPath = next;
75
+ i += 1;
76
+ } else if (flag === '--update') {
77
+ out.update = true;
78
+ } else if (flag === '--json') {
79
+ out.json = true;
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Recursively collect `.md` files under `rootDir`. Returns absolute paths,
87
+ * sorted for determinism.
88
+ *
89
+ * @param {string} rootDir
90
+ * @returns {string[]}
91
+ */
92
+ export function collectMarkdownFiles(rootDir) {
93
+ const out = [];
94
+ const walk = (dir) => {
95
+ let entries;
96
+ try {
97
+ entries = fs.readdirSync(dir, { withFileTypes: true });
98
+ } catch {
99
+ return;
100
+ }
101
+ for (const entry of entries) {
102
+ const full = path.join(dir, entry.name);
103
+ if (entry.isDirectory()) walk(full);
104
+ else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
105
+ }
106
+ };
107
+ walk(rootDir);
108
+ return out.sort();
109
+ }
110
+
111
+ /**
112
+ * Pure helper: count citations in source text.
113
+ *
114
+ * @param {string} source
115
+ * @returns {number}
116
+ */
117
+ export function countCitations(source) {
118
+ return (String(source ?? '').match(CITATION_RE) ?? []).length;
119
+ }
120
+
121
+ /**
122
+ * Count citations across a file set, relativizing paths against `cwd` so
123
+ * the report serializes identically on every platform. Files with zero
124
+ * citations are omitted — the baseline records where the tax lives.
125
+ *
126
+ * @param {string[]} files absolute paths
127
+ * @param {string} cwd
128
+ * @param {{ readFile?: (p: string) => string }} [opts]
129
+ * @returns {{ total: number, files: Array<{ path: string, count: number }> }}
130
+ */
131
+ export function tallyCitations(files, cwd, { readFile } = {}) {
132
+ const read = readFile ?? ((p) => fs.readFileSync(p, 'utf-8'));
133
+ const rows = [];
134
+ let total = 0;
135
+ for (const file of files) {
136
+ let count;
137
+ try {
138
+ count = countCitations(read(file));
139
+ } catch {
140
+ continue;
141
+ }
142
+ if (count === 0) continue;
143
+ total += count;
144
+ rows.push({
145
+ path: path.relative(cwd, file).split(path.sep).join('/'),
146
+ count,
147
+ });
148
+ }
149
+ return { total, files: rows.sort((a, b) => a.path.localeCompare(b.path)) };
150
+ }
151
+
152
+ /**
153
+ * Pure helper: read the baseline envelope. Returns `null` when the file is
154
+ * missing or unparseable — the baseline is not distributed to consumers, so
155
+ * its absence degrades to "no ceiling" rather than a false failure.
156
+ *
157
+ * @param {string} baselinePath
158
+ * @returns {{ total?: number, files?: Array<{ path: string, count: number }> } | null}
159
+ */
160
+ export function loadBaseline(baselinePath) {
161
+ try {
162
+ const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
163
+ return parsed && typeof parsed === 'object' ? parsed : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Pure helper: diff the live tally against the baseline. `grew` names the
171
+ * files whose per-file count rose, so a failure points at the edit rather
172
+ * than only at the total.
173
+ *
174
+ * @param {{ total?: number, files?: Array<{ path: string, count: number }> } | null} baseline
175
+ * @param {{ total: number, files: Array<{ path: string, count: number }> }} tally
176
+ * @returns {{ baselineTotal: number | null, delta: number | null, grew: Array<{ path: string, from: number, to: number }> }}
177
+ */
178
+ export function diffTally(baseline, tally) {
179
+ const baselineTotal =
180
+ typeof baseline?.total === 'number' ? baseline.total : null;
181
+ const prior = new Map(
182
+ (baseline?.files ?? []).map((row) => [row.path, row.count]),
183
+ );
184
+ const grew = tally.files
185
+ .map((row) => ({
186
+ path: row.path,
187
+ from: prior.get(row.path) ?? 0,
188
+ to: row.count,
189
+ }))
190
+ .filter((row) => row.to > row.from);
191
+ return {
192
+ baselineTotal,
193
+ delta: baselineTotal === null ? null : tally.total - baselineTotal,
194
+ grew,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Pure helper: render the human-readable diff, mirroring the `+` / `-`
200
+ * vocabulary of the sibling ratchets.
201
+ *
202
+ * @param {{ baselineTotal: number | null, delta: number | null, grew: Array<{ path: string, from: number, to: number }> }} diff
203
+ * @param {{ total: number }} tally
204
+ * @returns {string}
205
+ */
206
+ export function renderDiff(diff, tally) {
207
+ const lines = [];
208
+ for (const row of diff.grew) {
209
+ lines.push(`+ ${row.path}: ${row.from} -> ${row.to}`);
210
+ }
211
+ if (diff.delta !== null && diff.delta < 0) {
212
+ lines.push(
213
+ `[workflow-citations] ⚠ ${-diff.delta} citation(s) below baseline — refresh with --update to lock the reduction in`,
214
+ );
215
+ }
216
+ const tag = diff.delta !== null && diff.delta > 0 ? '(gate fail)' : '(ok)';
217
+ lines.push(
218
+ `[workflow-citations] total=${tally.total} baseline=${diff.baselineTotal ?? 'none'} ${tag}`,
219
+ );
220
+ return lines.join('\n');
221
+ }
222
+
223
+ /**
224
+ * Top-level CLI entry. Exported so tests can drive the pipeline against a
225
+ * tmpdir fixture corpus.
226
+ *
227
+ * @param {{
228
+ * argv?: string[],
229
+ * cwd?: string,
230
+ * stdout?: { write: (s: string) => void },
231
+ * stderr?: { write: (s: string) => void },
232
+ * }} [opts]
233
+ * @returns {Promise<number>} 0 = at or below baseline; 1 = regression
234
+ */
235
+ export async function runCli({
236
+ argv = process.argv.slice(2),
237
+ cwd = process.cwd(),
238
+ stdout = process.stdout,
239
+ stderr = process.stderr,
240
+ } = {}) {
241
+ const { baselinePath, rootPath, update, json } = parseArgv(argv);
242
+ const root = path.resolve(cwd, rootPath ?? DEFAULT_ROOT);
243
+ const resolvedBaselinePath = path.resolve(
244
+ cwd,
245
+ baselinePath ?? DEFAULT_BASELINE,
246
+ );
247
+ if (!fs.existsSync(root)) {
248
+ throw new Error(`[workflow-citations] workflow root not found: ${root}`);
249
+ }
250
+
251
+ const tally = tallyCitations(collectMarkdownFiles(root), cwd);
252
+
253
+ if (update) {
254
+ const envelope = {
255
+ $schema: 'https://mandrel.dev/baselines/workflow-citations.schema.json',
256
+ generatedAt: new Date().toISOString(),
257
+ total: tally.total,
258
+ files: tally.files,
259
+ };
260
+ fs.mkdirSync(path.dirname(resolvedBaselinePath), { recursive: true });
261
+ fs.writeFileSync(
262
+ resolvedBaselinePath,
263
+ `${JSON.stringify(envelope, null, 2)}\n`,
264
+ );
265
+ stdout.write(
266
+ `[workflow-citations] baseline written: ${resolvedBaselinePath} (total=${tally.total})\n`,
267
+ );
268
+ return 0;
269
+ }
270
+
271
+ const baseline = loadBaseline(resolvedBaselinePath);
272
+ const diff = diffTally(baseline, tally);
273
+ const exitCode = diff.delta !== null && diff.delta > 0 ? 1 : 0;
274
+
275
+ if (json) {
276
+ stdout.write(
277
+ `${JSON.stringify(
278
+ {
279
+ kind: 'workflow-citations-report',
280
+ root,
281
+ baselinePath: resolvedBaselinePath,
282
+ total: tally.total,
283
+ baselineTotal: diff.baselineTotal,
284
+ delta: diff.delta,
285
+ files: tally.files,
286
+ grew: diff.grew,
287
+ exitCode,
288
+ },
289
+ null,
290
+ 2,
291
+ )}\n`,
292
+ );
293
+ return exitCode;
294
+ }
295
+
296
+ if (!baseline) {
297
+ stderr.write(
298
+ `[workflow-citations] ⚠ baseline not found at ${resolvedBaselinePath} — no ceiling enforced\n`,
299
+ );
300
+ }
301
+ stdout.write(`\n--- workflow-citations preview ---\n`);
302
+ stdout.write(`${renderDiff(diff, tally)}\n`);
303
+ return exitCode;
304
+ }
305
+
306
+ async function main() {
307
+ return runCli();
308
+ }
309
+
310
+ runAsCli(import.meta.url, main, {
311
+ source: 'workflow-citations',
312
+ propagateExitCode: true,
313
+ errorPrefix: '[workflow-citations] ❌ Fatal error',
314
+ usage: {
315
+ invocation:
316
+ 'node .agents/scripts/check-workflow-citations.js [--baseline <path>] [--root <dir>] [--update] [--json]',
317
+ summary:
318
+ 'Ratchet on provenance citations in workflow prose: count issue-shaped references across .agents/workflows/** and fail when the total rises above the recorded baseline.',
319
+ flags: [
320
+ [
321
+ '--baseline <path>',
322
+ 'Baseline file (default: baselines/workflow-citations.json).',
323
+ ],
324
+ ['--root <dir>', 'Workflow root to scan (default: .agents/workflows).'],
325
+ ['--update', 'Rewrite the baseline from the live count.'],
326
+ ['--json', 'Emit the comparison envelope as JSON.'],
327
+ ],
328
+ notes: [
329
+ 'Exit codes:\n 0 at or below the baseline\n 1 the citation total regressed above the baseline',
330
+ ],
331
+ },
332
+ });
@@ -79,18 +79,29 @@ import { createProvider } from './lib/provider-factory.js';
79
79
  const HELP = `\
80
80
  Usage:
81
81
  deliver-light.js --prompt <text> [--creates csv] [--refactors csv]
82
- [--acceptance n] [--route lite|full] [--reason <text>]
82
+ [--acceptance n] [--kinds csv] [--magnitude m]
83
+ [--uncertainty u] [--route lite|full] [--reason <text>]
83
84
  [--amends '#id'] [--yes]
84
85
  deliver-light.js --backstop --story <id>
85
86
 
86
87
  The thin /deliver-light entry point: suitability gate → inline receipt Story →
87
88
  the same single-story-init.js / single-story-close.js engine /deliver uses.
88
89
 
90
+ The gate judges EFFORT and RISK, not artifact counts: N instances of one
91
+ mechanical edit is one kind at N sites. It rejects only clearly-epic work; the
92
+ --backstop pass enforces size against the actual diff.
93
+
89
94
  Gate options:
90
95
  --prompt <text> Operator prompt describing the change. Required for the gate.
91
96
  --creates <csv> Predicted NEW file paths (comma-separated).
92
97
  --refactors <csv> Predicted edited/existing file paths (comma-separated).
93
- --acceptance <n> Predicted acceptance-criteria count (default 1).
98
+ --acceptance <n> Predicted acceptance-criteria count (default 1). Not capped.
99
+ --kinds <csv> Distinct KINDS of change (default: one per assumption, so
100
+ N same-shaped edits count once).
101
+ --magnitude <m> Coarse effort bucket: trivial | moderate | substantial
102
+ (default moderate; substantial routes to /plan).
103
+ --uncertainty <u> determined (the request fixes the shape) | needs-design
104
+ (default determined; needs-design routes to /plan).
94
105
  --route <r> Ledgered model verdict route: lite | full.
95
106
  --reason <text> Recorded reason for a lite verdict (required for lite).
96
107
  --amends <#id> Mark this as an amendment of an existing issue.
@@ -166,17 +177,25 @@ export function synthesizeAcceptance(count) {
166
177
  * creates?: string[],
167
178
  * refactors?: string[],
168
179
  * acceptance?: number,
180
+ * kinds?: string[],
181
+ * magnitude?: string,
182
+ * uncertainty?: string,
169
183
  * route?: string,
170
184
  * reason?: string,
171
185
  * yes?: boolean,
172
186
  * injectedRules?: object,
173
- * }} args
187
+ * }} args `kinds` / `magnitude` / `uncertainty` are the declared effort-and-risk
188
+ * axes the gate judges (Story #4764); omitting them declares no signal, not a
189
+ * small one — an unrecognized bucket fails closed.
174
190
  * @returns {{ action: string, suitability: object, outcome: object }}
175
191
  */
176
192
  export function runLightGate({
177
193
  creates = [],
178
194
  refactors = [],
179
195
  acceptance,
196
+ kinds,
197
+ magnitude,
198
+ uncertainty,
180
199
  route,
181
200
  reason,
182
201
  yes = false,
@@ -186,6 +205,9 @@ export function runLightGate({
186
205
  const suitability = deriveLightSuitability({
187
206
  predictedChanges,
188
207
  predictedAcceptance: synthesizeAcceptance(acceptance),
208
+ predictedKinds: kinds,
209
+ predictedMagnitude: magnitude,
210
+ predictedUncertainty: uncertainty,
189
211
  verdict: { route, reason },
190
212
  injectedRules,
191
213
  });
@@ -355,6 +377,9 @@ export async function runGateMode(values, deps = {}) {
355
377
  acceptance: values.acceptance
356
378
  ? Number.parseInt(String(values.acceptance), 10)
357
379
  : 1,
380
+ kinds: parseCsvPaths(values.kinds),
381
+ magnitude: values.magnitude,
382
+ uncertainty: values.uncertainty,
358
383
  route: values.route,
359
384
  reason: values.reason,
360
385
  yes: values.yes === true,
@@ -417,6 +442,9 @@ async function main() {
417
442
  creates: { type: 'string' },
418
443
  refactors: { type: 'string' },
419
444
  acceptance: { type: 'string' },
445
+ kinds: { type: 'string' },
446
+ magnitude: { type: 'string' },
447
+ uncertainty: { type: 'string' },
420
448
  route: { type: 'string' },
421
449
  reason: { type: 'string' },
422
450
  amends: { type: 'string' },
@@ -443,4 +471,5 @@ async function main() {
443
471
  runAsCli(import.meta.url, main, {
444
472
  source: 'deliver-light',
445
473
  propagateExitCode: true,
474
+ usage: HELP,
446
475
  });
@@ -136,4 +136,7 @@ export async function runDeliverRecover({
136
136
  return { success: true, result: recovery };
137
137
  }
138
138
 
139
- runAsCli(import.meta.url, runDeliverRecover, { source: 'deliver-recover' });
139
+ runAsCli(import.meta.url, runDeliverRecover, {
140
+ source: 'deliver-recover',
141
+ usage: HELP,
142
+ });
@@ -248,4 +248,20 @@ export async function main(args = process.argv.slice(2)) {
248
248
 
249
249
  import { runAsCli } from './lib/cli-utils.js';
250
250
 
251
- runAsCli(import.meta.url, main, { source: 'DiagnoseFriction' });
251
+ runAsCli(import.meta.url, main, {
252
+ source: 'DiagnoseFriction',
253
+ usage: {
254
+ invocation:
255
+ 'node .agents/scripts/diagnose-friction.js [--story <id>] [--epic <id>] --cmd <command with args...>',
256
+ summary:
257
+ 'Run a command through the diagnostic interceptor: stream its output, then append a local friction signal describing the failure. Never posts to the ticket.',
258
+ flags: [
259
+ ['--story <id>', 'Story the friction belongs to.'],
260
+ ['--epic <id>', 'Epic the friction belongs to.'],
261
+ [
262
+ '--cmd <command...>',
263
+ 'The command to execute; everything after it is the argv (required).',
264
+ ],
265
+ ],
266
+ },
267
+ });
@@ -47,6 +47,21 @@ import { runAsCli } from './lib/cli-utils.js';
47
47
 
48
48
  const DEFAULT_SCOPE = 'diagnose';
49
49
 
50
+ /**
51
+ * The `--help` block. Single-homed here so the CLI gate in `runAsCli` and the
52
+ * `validateDiagnoseArgs` HELP-sentinel branch print the same bytes.
53
+ */
54
+ const HELP = [
55
+ 'Usage: diagnose [--scope <scope>] [--fail-on-blocker] [--json]',
56
+ '',
57
+ 'Options:',
58
+ ' --scope <s> Filter checks by scope (default: diagnose).',
59
+ ' Use `all` to run every registered check.',
60
+ ' --fail-on-blocker Exit 2 when at least one finding is a blocker.',
61
+ ' --json Emit findings as a single line of JSON.',
62
+ '',
63
+ ].join('\n');
64
+
50
65
  /**
51
66
  * Parse the CLI argv slice (i.e. argv without the leading node/script
52
67
  * elements). Exported for direct unit testing — keeps the parse pure so
@@ -203,19 +218,7 @@ export async function runDiagnose({
203
218
  */
204
219
  export function validateDiagnoseArgs(err) {
205
220
  if (err && err.message === 'HELP') {
206
- return {
207
- kind: 'help',
208
- text: [
209
- 'Usage: diagnose [--scope <scope>] [--fail-on-blocker] [--json]',
210
- '',
211
- 'Options:',
212
- ' --scope <s> Filter checks by scope (default: diagnose).',
213
- ' Use `all` to run every registered check.',
214
- ' --fail-on-blocker Exit 2 when at least one finding is a blocker.',
215
- ' --json Emit findings as a single line of JSON.',
216
- '',
217
- ].join('\n'),
218
- };
221
+ return { kind: 'help', text: HELP };
219
222
  }
220
223
  const message = err?.message ? err.message : String(err);
221
224
  return { kind: 'error', text: `[diagnose] ${message}\n`, exitCode: 1 };
@@ -237,4 +240,7 @@ async function main() {
237
240
  }
238
241
  }
239
242
 
240
- runAsCli(import.meta.url, main, { source: 'diagnose' });
243
+ runAsCli(import.meta.url, main, {
244
+ source: 'diagnose',
245
+ usage: HELP,
246
+ });
@@ -144,4 +144,23 @@ async function main() {
144
144
  );
145
145
  }
146
146
 
147
- runAsCli(import.meta.url, main, { source: 'drain-pending-cleanup' });
147
+ runAsCli(import.meta.url, main, {
148
+ source: 'drain-pending-cleanup',
149
+ usage: {
150
+ invocation:
151
+ 'node .agents/scripts/drain-pending-cleanup.js [--dry-run] [--no-escalate] [--worktree-root <path>]',
152
+ summary:
153
+ 'Drain the pending-worktree-cleanup manifest, removing trees whose holders have exited.',
154
+ flags: [
155
+ ['--dry-run', 'Report each entry and its holders; remove nothing.'],
156
+ [
157
+ '--no-escalate',
158
+ 'Do not escalate to a forced removal for stuck entries.',
159
+ ],
160
+ [
161
+ '--worktree-root <path>',
162
+ 'Worktree root (default: delivery.worktreeIsolation.root).',
163
+ ],
164
+ ],
165
+ },
166
+ });
@@ -238,4 +238,23 @@ async function main() {
238
238
  await runEvidenceGate({ ...args, runnerArgs });
239
239
  }
240
240
 
241
- runAsCli(import.meta.url, main, { source: 'evidence-gate' });
241
+ runAsCli(import.meta.url, main, {
242
+ source: 'evidence-gate',
243
+ usage: {
244
+ invocation:
245
+ 'node .agents/scripts/evidence-gate.js --scope-id <id> --gate <name> [--standalone] [--no-evidence] [--cwd <path>] [--worktree <path>]',
246
+ summary:
247
+ 'Run one named gate, reusing a prior evidence stamp for the same HEAD instead of re-running it.',
248
+ flags: [
249
+ ['--scope-id <id>', 'Story id the evidence is scoped to (required).'],
250
+ ['--gate <name>', 'Gate to run (e.g. lint, typecheck) (required).'],
251
+ ['--standalone', 'Run outside a close pipeline and stamp the evidence.'],
252
+ [
253
+ '--no-evidence',
254
+ 'Ignore and do not write evidence — always run the gate.',
255
+ ],
256
+ ['--cwd <path>', 'Repository root (default: project root).'],
257
+ ['--worktree <path>', 'Worktree the gate runs in.'],
258
+ ],
259
+ },
260
+ });
@@ -608,4 +608,17 @@ export {
608
608
  spliceRegion,
609
609
  };
610
610
 
611
- runAsCli(import.meta.url, main, { source: 'generate-config-docs' });
611
+ runAsCli(import.meta.url, main, {
612
+ source: 'generate-config-docs',
613
+ usage: {
614
+ invocation: 'node .agents/scripts/generate-config-docs.js [--check]',
615
+ summary:
616
+ 'Regenerate the configuration reference from the .agentrc schema. Writes only when the generated content differs.',
617
+ flags: [
618
+ [
619
+ '--check',
620
+ 'Verify the doc is current and fail if stale; write nothing.',
621
+ ],
622
+ ],
623
+ },
624
+ });
@@ -221,4 +221,17 @@ export {
221
221
  spliceRegion,
222
222
  };
223
223
 
224
- runAsCli(import.meta.url, main, { source: 'generate-lifecycle-docs' });
224
+ runAsCli(import.meta.url, main, {
225
+ source: 'generate-lifecycle-docs',
226
+ usage: {
227
+ invocation: 'node .agents/scripts/generate-lifecycle-docs.js [--check]',
228
+ summary:
229
+ 'Regenerate the lifecycle-event table in docs/LIFECYCLE.md from the event schemas. Writes only when the generated content differs.',
230
+ flags: [
231
+ [
232
+ '--check',
233
+ 'Verify the doc is current and fail if stale; write nothing.',
234
+ ],
235
+ ],
236
+ },
237
+ });
@@ -198,4 +198,17 @@ async function main(argv = process.argv.slice(2)) {
198
198
 
199
199
  export { DOC_PATH, WORKFLOWS_DIR };
200
200
 
201
- runAsCli(import.meta.url, main, { source: 'generate-workflows-doc' });
201
+ runAsCli(import.meta.url, main, {
202
+ source: 'generate-workflows-doc',
203
+ usage: {
204
+ invocation: 'node .agents/scripts/generate-workflows-doc.js [--check]',
205
+ summary:
206
+ 'Regenerate the workflow catalog from .agents/workflows/. Writes only when the generated content differs.',
207
+ flags: [
208
+ [
209
+ '--check',
210
+ 'Verify the doc is current and fail if stale; write nothing.',
211
+ ],
212
+ ],
213
+ },
214
+ });
@@ -129,4 +129,35 @@ async function main() {
129
129
  process.exit(exitCode);
130
130
  }
131
131
 
132
- runAsCli(import.meta.url, main, { source: 'git-cleanup' });
132
+ runAsCli(import.meta.url, main, {
133
+ source: 'git-cleanup',
134
+ usage: {
135
+ invocation:
136
+ 'node .agents/scripts/git-cleanup.js [--execute] [--yes] [--json] [phase flags] [filters]',
137
+ summary:
138
+ 'Tidy the local checkout in four phases — fast-forward the base branch, prune stale remote refs, reap merged branches, triage stashes. Dry-run unless --execute.',
139
+ flags: [
140
+ ['--execute', 'Perform the mutations (default is a dry run).'],
141
+ ['--dry-run', 'Force a dry run even alongside --execute.'],
142
+ ['--yes', 'Skip the interactive confirmation prompts.'],
143
+ ['--json', 'Emit the plan/result envelope as JSON.'],
144
+ ['--remote', 'Also delete the matching remote branches.'],
145
+ ['--fast-forward-main', 'Run only the fast-forward-base phase.'],
146
+ ['--prune-remotes', 'Run only the prune-remotes phase.'],
147
+ ['--branches', 'Run only the merged-branch reap phase.'],
148
+ ['--stashes', 'Run only the stash-triage phase.'],
149
+ [
150
+ '--include <glob>',
151
+ 'Only consider branches matching the glob (repeatable).',
152
+ ],
153
+ [
154
+ '--exclude <glob>',
155
+ 'Never consider branches matching the glob (repeatable).',
156
+ ],
157
+ ['--drop-stashes <ref>', 'Stash ref approved for dropping (repeatable).'],
158
+ ['--base <branch>', 'Base branch (default: project.baseBranch).'],
159
+ ['--cwd <path>', 'Repository root (default: process cwd).'],
160
+ ],
161
+ notes: ['With no phase flag, every phase runs in order.'],
162
+ },
163
+ });