mandrel 1.91.0 → 1.92.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 (109) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +75 -107
  3. package/.agents/docs/configuration.md +1 -1
  4. package/.agents/docs/quality-gates.md +8 -7
  5. package/.agents/docs/workflows.md +4 -10
  6. package/.agents/instructions.md +2 -2
  7. package/.agents/schemas/agentrc.schema.json +1 -1
  8. package/.agents/schemas/lifecycle/README.md +2 -1
  9. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  10. package/.agents/schemas/risk-verdict.schema.json +14 -1
  11. package/.agents/scripts/README.md +0 -46
  12. package/.agents/scripts/analyze-execution.js +76 -1
  13. package/.agents/scripts/check-doc-links.js +3 -0
  14. package/.agents/scripts/check-workflow-cli-lint.js +208 -0
  15. package/.agents/scripts/epic-audit-recheck.js +1 -1
  16. package/.agents/scripts/epic-plan-clarity.js +16 -6
  17. package/.agents/scripts/epic-plan-decompose.js +37 -59
  18. package/.agents/scripts/epic-plan-healthcheck.js +39 -6
  19. package/.agents/scripts/epic-plan-spec.js +41 -168
  20. package/.agents/scripts/epic-reconcile.js +7 -1
  21. package/.agents/scripts/lib/Logger.js +4 -4
  22. package/.agents/scripts/lib/audit-suite/index.js +4 -5
  23. package/.agents/scripts/lib/audit-suite/runner.js +6 -4
  24. package/.agents/scripts/lib/audit-suite/selector.js +2 -3
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/command-header.js +20 -0
  27. package/.agents/scripts/lib/config/github.js +1 -3
  28. package/.agents/scripts/lib/config-settings-schema.js +4 -3
  29. package/.agents/scripts/lib/degraded-mode.js +1 -1
  30. package/.agents/scripts/lib/label-constants.js +18 -3
  31. package/.agents/scripts/lib/label-taxonomy.js +14 -3
  32. package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
  33. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
  34. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
  35. package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
  36. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
  38. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
  39. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
  40. package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
  41. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
  42. package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
  43. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
  44. package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
  45. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
  46. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
  48. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
  49. package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
  50. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
  51. package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
  52. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  53. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
  54. package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
  55. package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
  56. package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
  57. package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
  58. package/.agents/scripts/plan-context.js +186 -0
  59. package/.agents/scripts/plan-critics.js +227 -0
  60. package/.agents/scripts/plan-persist.js +383 -0
  61. package/.agents/scripts/pr-watch-with-update.js +8 -7
  62. package/.agents/scripts/run-lint.js +10 -11
  63. package/.agents/scripts/story-plan.js +19 -7
  64. package/.agents/scripts/sync-claude-commands.js +10 -5
  65. package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
  66. package/.agents/workflows/audit-documentation.md +5 -7
  67. package/.agents/workflows/audit-lighthouse.md +1 -0
  68. package/.agents/workflows/audit-security.md +1 -0
  69. package/.agents/workflows/audit-to-stories.md +16 -5
  70. package/.agents/workflows/git-deliver.md +76 -243
  71. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  72. package/.agents/workflows/helpers/code-review.md +7 -6
  73. package/.agents/workflows/helpers/deliver-epic.md +5 -4
  74. package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
  75. package/.agents/workflows/helpers/plan-epic.md +246 -999
  76. package/.agents/workflows/helpers/plan-story.md +14 -14
  77. package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
  78. package/.agents/workflows/helpers/single-story-deliver.md +5 -4
  79. package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
  80. package/.agents/workflows/mandrel-update.md +144 -466
  81. package/.agents/workflows/plan.md +73 -143
  82. package/docs/CHANGELOG.md +13 -0
  83. package/lib/cli/registry.js +21 -3
  84. package/package.json +1 -1
  85. package/.agents/schemas/loop-unit.schema.json +0 -70
  86. package/.agents/scripts/assert-branch.js +0 -81
  87. package/.agents/scripts/check-loop-units.js +0 -204
  88. package/.agents/scripts/detect-merges.js +0 -111
  89. package/.agents/scripts/git-pr-quality-gate.js +0 -205
  90. package/.agents/scripts/git-rebase-and-resolve.js +0 -234
  91. package/.agents/scripts/hierarchy-gate.js +0 -192
  92. package/.agents/scripts/hydrate-context.js +0 -179
  93. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
  94. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
  96. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
  97. package/.agents/scripts/loc-delta.js +0 -205
  98. package/.agents/scripts/render-manifest.js +0 -143
  99. package/.agents/scripts/run-audit-suite.js +0 -97
  100. package/.agents/scripts/select-audits.js +0 -155
  101. package/.agents/scripts/update-mutation-baseline.js +0 -189
  102. package/.agents/workflows/explain.md +0 -118
  103. package/.agents/workflows/git-merge-pr.md +0 -377
  104. package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
  105. package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
  106. package/.agents/workflows/loops/README.md +0 -65
  107. package/.agents/workflows/loops/fix-failing-tests.md +0 -74
  108. package/.agents/workflows/loops/nightly-audit.md +0 -81
  109. package/.agents/workflows/loops/watch-ci.md +0 -68
@@ -41,6 +41,16 @@
41
41
  * node .agents/scripts/analyze-execution.js --story <sid> --epic <eid> \
42
42
  * [--phase-timings <path>]
43
43
  * node .agents/scripts/analyze-execution.js --epic <eid>
44
+ * node .agents/scripts/analyze-execution.js --epic <eid> --plan-metrics-json
45
+ *
46
+ * `--plan-metrics-json` (Epic #4474 PR7 — the G2 extraction surface) is a
47
+ * **local read-only** mode: it reads the per-Epic plan-metrics ledger
48
+ * (`temp/epic-<eid>/plan-metrics.json`), rolls it up with
49
+ * `summarizePlanMetrics`, and prints one compact JSON envelope
50
+ * `{ epicId, planMetrics, summaryLine }` to stdout — no provider call, no
51
+ * comment write, all logs on stderr — so a bench cohort can extract
52
+ * turns-per-plan / per-mode invocation counts / critic skips without
53
+ * scraping GitHub or the human log.
44
54
  *
45
55
  * @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
46
56
  */
@@ -51,7 +61,7 @@ import { parseArgs } from 'node:util';
51
61
  import { runAsCli } from './lib/cli-utils.js';
52
62
  import { signalsFile } from './lib/config/temp-paths.js';
53
63
  import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
54
- import { Logger } from './lib/Logger.js';
64
+ import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
55
65
  import { computeBaselineRefreshRate } from './lib/observability/baseline-refresh-rate.js';
56
66
  import {
57
67
  computeEpicPerfReport,
@@ -72,6 +82,11 @@ import {
72
82
  renderStoryBody,
73
83
  STORY_PERF_TYPE,
74
84
  } from './lib/observability/perf-report-render.js';
85
+ import {
86
+ readPlanMetrics,
87
+ renderPlanMetricsSummaryLine,
88
+ summarizePlanMetrics,
89
+ } from './lib/orchestration/plan-metrics.js';
75
90
  import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
76
91
  import { createProvider } from './lib/provider-factory.js';
77
92
 
@@ -284,11 +299,34 @@ export async function runEpicMode(ctx) {
284
299
  logger.info?.(
285
300
  `[analyze-execution] epic-perf-report upserted on Epic #${epicId} (commentId=${result.commentId}, stories=${summaries.length})`,
286
301
  );
302
+
303
+ // Plan-metrics roll-up (#4474 PR1) — surface the plan-CLI invocation
304
+ // ledger (`temp/epic-<id>/plan-metrics.json`) alongside the perf report.
305
+ // Additive and non-fatal: a missing or unreadable ledger yields `null`.
306
+ let planMetrics = null;
307
+ try {
308
+ planMetrics = summarizePlanMetrics(
309
+ await readPlanMetrics(epicId, ctx.config),
310
+ );
311
+ if (planMetrics) {
312
+ logger.info?.(
313
+ `[analyze-execution] ${renderPlanMetricsSummaryLine(planMetrics)}`,
314
+ );
315
+ }
316
+ } catch (err) {
317
+ logger.warn?.(
318
+ `[analyze-execution] plan-metrics read failed (non-fatal): ${
319
+ err instanceof Error ? err.message : String(err)
320
+ }`,
321
+ );
322
+ }
323
+
287
324
  return {
288
325
  commentId: result.commentId,
289
326
  payload,
290
327
  baselineRefreshRate,
291
328
  qualityGateFriction,
329
+ planMetrics,
292
330
  };
293
331
  }
294
332
 
@@ -300,6 +338,7 @@ function parseCli(argv) {
300
338
  epic: { type: 'string' },
301
339
  'phase-timings': { type: 'string' },
302
340
  'window-days': { type: 'string' },
341
+ 'plan-metrics-json': { type: 'boolean', default: false },
303
342
  cwd: { type: 'string' },
304
343
  },
305
344
  strict: false,
@@ -316,10 +355,33 @@ function parseCli(argv) {
316
355
  phaseTimingsPath: values['phase-timings'] ?? null,
317
356
  windowDays:
318
357
  Number.isInteger(windowDays) && windowDays > 0 ? windowDays : null,
358
+ planMetricsJson: values['plan-metrics-json'] === true,
319
359
  cwd: values.cwd ?? null,
320
360
  };
321
361
  }
322
362
 
363
+ /**
364
+ * `--plan-metrics-json` — local, read-only G2 extraction surface (Epic
365
+ * #4474 PR7). Reads the per-Epic plan-metrics ledger and prints one
366
+ * compact JSON envelope to stdout. No provider, no comment write; stdout
367
+ * is reserved for the envelope (all logs on stderr). A missing ledger is
368
+ * not an error — it yields `planMetrics: null` so a cohort script can
369
+ * branch on the field instead of the exit code.
370
+ *
371
+ * @param {{ epicId: number, config: object }} input
372
+ * @returns {Promise<{ epicId: number, planMetrics: object|null, summaryLine: string }>}
373
+ */
374
+ export async function runPlanMetricsJsonMode({ epicId, config }) {
375
+ const planMetrics = summarizePlanMetrics(
376
+ await readPlanMetrics(epicId, config),
377
+ );
378
+ return {
379
+ epicId,
380
+ planMetrics,
381
+ summaryLine: renderPlanMetricsSummaryLine(planMetrics),
382
+ };
383
+ }
384
+
323
385
  async function main(argv = process.argv.slice(2)) {
324
386
  const args = parseCli(argv);
325
387
 
@@ -331,6 +393,19 @@ async function main(argv = process.argv.slice(2)) {
331
393
 
332
394
  const cwd = path.resolve(args.cwd ?? PROJECT_ROOT);
333
395
  const config = resolveConfig({ cwd });
396
+
397
+ if (args.planMetricsJson) {
398
+ // Stdout is reserved for the compact envelope — flip every log sink to
399
+ // stderr before any pipeline code runs (Story #2278 discipline).
400
+ routeAllOutputToStderr();
401
+ const result = await runPlanMetricsJsonMode({
402
+ epicId: args.epicId,
403
+ config,
404
+ });
405
+ process.stdout.write(`${JSON.stringify(result)}\n`);
406
+ return;
407
+ }
408
+
334
409
  const provider = createProvider(config);
335
410
 
336
411
  if (args.storyId) {
@@ -50,6 +50,9 @@ export const RETIRED_COMMANDS = new Set([
50
50
  'agents-bootstrap-github',
51
51
  'single-story-plan',
52
52
  'mandrel',
53
+ // #4482 — dead workflow surface retired (host-native equivalents).
54
+ 'explain',
55
+ 'git-merge-pr',
53
56
  ]);
54
57
 
55
58
  // Tokens that look like `/foo` in prose but are not slash commands. Tokens
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ /* node:coverage ignore file */
3
+
4
+ /**
5
+ * check-workflow-cli-lint.js — enforce the "no workflow instructs a
6
+ * no-CLI library call" rule (Epic #4474 PR5; sibling of
7
+ * check-lifecycle-lint.js).
8
+ *
9
+ * The measured failure mode this rule kills: workflow prose that tells the
10
+ * host LLM to "Call `someExportedFunction({...})` exported from
11
+ * `lib/whatever.js`". There is no runnable form of that instruction, so the
12
+ * model greps the framework source and writes throwaway `.mjs` shims to
13
+ * invoke the export — the mandrel-bench N=2 cohort measured ~12–15 turns of
14
+ * shim-writing per plan for exactly this pattern. Workflows must instruct
15
+ * `node .agents/scripts/<cli>.js …` commands instead.
16
+ *
17
+ * Scope: every `*.md` under `.agents/workflows/`.
18
+ *
19
+ * Heuristic (tuned to zero false positives on the surviving corpus —
20
+ * descriptive mentions like "the automatic paths call `foo()`" are prose
21
+ * about script internals, not instructions, and are NOT flagged):
22
+ *
23
+ * Rule 1 — imperative library call. A paragraph (fenced code blocks
24
+ * stripped; lines joined) matching /\b(Call|Invoke)\s+`ident\s*\(/ —
25
+ * a capitalized imperative directly instructing a function call.
26
+ *
27
+ * Rule 2 — "exported from" instruction. A paragraph containing both a
28
+ * backticked call token (`ident(`) and the phrase "exported from" —
29
+ * the canonical shape of the retired Phase 2/3/4 prose.
30
+ *
31
+ * Rule 3 — prose-level lib import. `import(` / `require(` naming a
32
+ * `scripts/lib/` path OUTSIDE a fenced code block. (A complete,
33
+ * runnable `node -e` one-liner inside a fenced block is exempt: it
34
+ * costs zero shim-writing turns because it is executable as written.)
35
+ *
36
+ * Exit codes:
37
+ * 0 — clean.
38
+ * 1 — at least one violation; offending file + paragraph line printed
39
+ * to stderr.
40
+ *
41
+ * Ships as part of `npm run lint` (run-lint.js task list), alongside the
42
+ * lifecycle lint and the label-vocabulary lint.
43
+ */
44
+
45
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
46
+ import path from 'node:path';
47
+ import { fileURLToPath } from 'node:url';
48
+
49
+ import { runAsCli } from './lib/cli-utils.js';
50
+
51
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
53
+ export const DEFAULT_WORKFLOWS_DIR = path.join(
54
+ REPO_ROOT,
55
+ '.agents',
56
+ 'workflows',
57
+ );
58
+
59
+ const IMPERATIVE_CALL_RE = /\b(?:Call|Invoke)\s+`[A-Za-z_$][\w$]*\s*\(/;
60
+ const BACKTICK_CALL_RE = /`[A-Za-z_$][\w$]*\s*\(/;
61
+ const EXPORTED_FROM_RE = /\bexported from\b/;
62
+ const LIB_IMPORT_RE = /\b(?:import|require)\(\s*['"`][^'"`]*scripts\/lib\//;
63
+
64
+ /**
65
+ * Strip fenced code blocks (``` / ~~~), replacing their lines with empty
66
+ * strings so line numbers stay stable. Complete runnable commands live in
67
+ * fences and are exempt by design (see header).
68
+ *
69
+ * @param {string} source
70
+ * @returns {string[]} lines with fenced content blanked.
71
+ */
72
+ export function stripFences(source) {
73
+ const lines = source.split('\n');
74
+ let inFence = false;
75
+ return lines.map((line) => {
76
+ const fence = /^\s*(```|~~~)/.test(line);
77
+ if (fence) {
78
+ inFence = !inFence;
79
+ return '';
80
+ }
81
+ return inFence ? '' : line;
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Split blanked lines into paragraphs — runs of consecutive non-empty
87
+ * lines — keeping the 1-based line number of each paragraph's first line.
88
+ *
89
+ * @param {string[]} lines
90
+ * @returns {Array<{ text: string, line: number }>}
91
+ */
92
+ export function toParagraphs(lines) {
93
+ const paragraphs = [];
94
+ let buf = [];
95
+ let start = 0;
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (lines[i].trim().length > 0) {
98
+ if (buf.length === 0) start = i + 1;
99
+ buf.push(lines[i]);
100
+ } else if (buf.length > 0) {
101
+ paragraphs.push({ text: buf.join(' '), line: start });
102
+ buf = [];
103
+ }
104
+ }
105
+ if (buf.length > 0) paragraphs.push({ text: buf.join(' '), line: start });
106
+ return paragraphs;
107
+ }
108
+
109
+ /**
110
+ * Lint one markdown source. Returns violations
111
+ * `{ rule, line, hint }[]` (empty when clean).
112
+ *
113
+ * @param {string} source markdown content.
114
+ * @returns {Array<{ rule: string, line: number, hint: string }>}
115
+ */
116
+ export function lintWorkflowSource(source) {
117
+ const violations = [];
118
+ const lines = stripFences(source);
119
+ for (const para of toParagraphs(lines)) {
120
+ if (IMPERATIVE_CALL_RE.test(para.text)) {
121
+ violations.push({
122
+ rule: 'no-cli-library-call',
123
+ line: para.line,
124
+ hint:
125
+ 'Workflow prose instructs calling a function directly ("Call/Invoke `fn(...)`"). ' +
126
+ 'There is no runnable form of that instruction — the model must write a throwaway shim. ' +
127
+ 'Instruct a `node .agents/scripts/<cli>.js …` command instead (add a CLI if none exists).',
128
+ });
129
+ continue;
130
+ }
131
+ if (EXPORTED_FROM_RE.test(para.text) && BACKTICK_CALL_RE.test(para.text)) {
132
+ violations.push({
133
+ rule: 'no-cli-library-call',
134
+ line: para.line,
135
+ hint:
136
+ 'Workflow prose points at an exported library function ("`fn(...)` exported from …") ' +
137
+ 'with no CLI entrypoint. Instruct a `node .agents/scripts/<cli>.js …` command instead.',
138
+ });
139
+ continue;
140
+ }
141
+ if (LIB_IMPORT_RE.test(para.text)) {
142
+ violations.push({
143
+ rule: 'no-prose-lib-import',
144
+ line: para.line,
145
+ hint:
146
+ 'Workflow prose (outside a fenced code block) instructs importing a scripts/lib module. ' +
147
+ 'Give a complete runnable command in a fenced block, or add a CLI entrypoint.',
148
+ });
149
+ }
150
+ }
151
+ return violations;
152
+ }
153
+
154
+ /**
155
+ * Recursively collect `*.md` files under a directory.
156
+ *
157
+ * @param {string} dir
158
+ * @returns {string[]} absolute paths.
159
+ */
160
+ export function collectMarkdown(dir) {
161
+ const out = [];
162
+ for (const entry of readdirSync(dir)) {
163
+ const abs = path.join(dir, entry);
164
+ const st = statSync(abs);
165
+ if (st.isDirectory()) out.push(...collectMarkdown(abs));
166
+ else if (entry.endsWith('.md')) out.push(abs);
167
+ }
168
+ return out;
169
+ }
170
+
171
+ /**
172
+ * Run the check over a workflows directory. Exported for tests (pass a
173
+ * fixture directory).
174
+ *
175
+ * @param {string} [workflowsDir]
176
+ * @returns {Array<{ file: string, rule: string, line: number, hint: string }>}
177
+ */
178
+ export function runCheck(workflowsDir = DEFAULT_WORKFLOWS_DIR) {
179
+ const violations = [];
180
+ for (const file of collectMarkdown(workflowsDir)) {
181
+ const source = readFileSync(file, 'utf8');
182
+ for (const v of lintWorkflowSource(source)) {
183
+ violations.push({ file: path.relative(REPO_ROOT, file), ...v });
184
+ }
185
+ }
186
+ return violations;
187
+ }
188
+
189
+ async function main() {
190
+ const violations = runCheck();
191
+ if (violations.length === 0) {
192
+ process.stdout.write(
193
+ '[workflow-cli-lint] clean: no workflow instructs a no-CLI library call.\n',
194
+ );
195
+ return 0;
196
+ }
197
+ for (const v of violations) {
198
+ process.stderr.write(
199
+ `[workflow-cli-lint][${v.rule}] ${v.file}:${v.line}\n ${v.hint}\n`,
200
+ );
201
+ }
202
+ return 1;
203
+ }
204
+
205
+ await runAsCli(import.meta.url, main, {
206
+ source: 'check-workflow-cli-lint',
207
+ propagateExitCode: true,
208
+ });
@@ -9,7 +9,7 @@
9
9
  * `.agents/workflows/helpers/code-review.md` Step 4.6) and emit the
10
10
  * subset of audit lenses whose `filePatterns` overlap that list.
11
11
  *
12
- * The CLI is intentionally narrower than `select-audits.js`: it does NOT
12
+ * The CLI is intentionally narrower than `lib/audit-suite/selector.js`: it does NOT
13
13
  * run a git diff and does NOT consult the Epic ticket body for keyword
14
14
  * triggers. The auto-fix tail already knows exactly which paths it touched —
15
15
  * only file-pattern overlap is relevant for deciding which lenses are stale
@@ -34,6 +34,7 @@ import {
34
34
  } from './lib/config-resolver.js';
35
35
  import { scoreEpicBody } from './lib/epic-plan-clarity.js';
36
36
  import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
37
+ import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
37
38
  import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
38
39
  import { createProvider } from './lib/provider-factory.js';
39
40
 
@@ -175,7 +176,11 @@ async function main() {
175
176
  // additions cannot silently corrupt the captured file.
176
177
  if (values['emit-context']) {
177
178
  routeAllOutputToStderr();
178
- const envelope = await buildClarityContext({ epicId, provider });
179
+ // Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
180
+ const envelope = await recordPlanInvocation(
181
+ { cli: 'epic-plan-clarity', mode: 'emit-context', epicId, config },
182
+ () => buildClarityContext({ epicId, provider }),
183
+ );
179
184
  const json = values.pretty
180
185
  ? JSON.stringify(envelope, null, 2)
181
186
  : JSON.stringify(envelope);
@@ -190,11 +195,16 @@ async function main() {
190
195
  }
191
196
 
192
197
  const updatedBody = await readFile(values['updated-body'], 'utf8');
193
- const result = await persistClarityUpdate({
194
- epicId,
195
- updatedBody,
196
- provider,
197
- });
198
+ // Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
199
+ const result = await recordPlanInvocation(
200
+ { cli: 'epic-plan-clarity', mode: 'persist', epicId, config },
201
+ () =>
202
+ persistClarityUpdate({
203
+ epicId,
204
+ updatedBody,
205
+ provider,
206
+ }),
207
+ );
198
208
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
199
209
  }
200
210
 
@@ -1,76 +1,54 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /* node:coverage ignore file */
3
4
 
4
5
  /**
5
- * epic-plan-decompose.js — Story #2466 thin CLI shell.
6
- *
7
- * Phase 8 (decompose) entry point for the split planning flow. The
8
- * deterministic decomposer engine and reconciler-based persist flow
9
- * have been split into phase modules under
10
- * `lib/orchestration/epic-plan-decompose/phases/`:
11
- *
12
- * 1. planning-artifacts — `resolveConflictPolicy`.
13
- * 2. dag — `resolveDependencies`,
14
- * `orderTicketsForCreation`.
15
- * 3. context — `buildDecomposerSystemPrompt`,
16
- * `buildDecompositionContext`.
17
- * 4. creation — sub-issue link reconciliation, Epic label
18
- * transitions, and the advisory ticket-cap
19
- * warning (`reconcileSubIssueLinks`,
20
- * `setEpicLabel`, `warnTicketCapNearLimit`).
21
- * 5. persist — `runDecomposePhase` (reconciler-based flow).
22
- * 6. cli — argument parsing + `main()` pipeline.
23
- *
24
- * Modes:
25
- * --emit-context Prints the decomposer authoring context (Epic body,
26
- * Tech Spec body, risk heuristics, system prompt,
27
- * ticket cap) as JSON. The authoring middle is the
28
- * `epic-plan-decompose-author` Skill
29
- * (`.agents/skills/core/epic-plan-decompose-author/SKILL.md`).
6
+ * epic-plan-decompose.js — RETIRED delegate CLI (Epic #4474, PR7).
30
7
  *
31
- * (default) Given an author-provided tickets JSON file,
32
- * persists the Feature/Story/Task hierarchy via the
33
- * structural reconciler, flips the Epic to
34
- * `agent::ready`, and updates the `epic-plan-state`
35
- * structured comment.
8
+ * The 12-phase plan pipeline collapsed to context → author → persist:
36
9
  *
37
- * --force re-decomposes (closes existing child Features/Stories/Tasks).
10
+ * - `--emit-context` moved to `plan-context.js` (the single authoring
11
+ * envelope carries the decomposer context — ticket schema, risk
12
+ * heuristics, ticket cap — alongside the spec half).
13
+ * - The persist half (ticket validator, file-assumption gate, DAG,
14
+ * budget, story creation, healthcheck, `agent::ready` flip) moved to
15
+ * `plan-persist.js` (single GitHub-write surface).
38
16
  *
39
- * Exit codes:
40
- * 0 phase complete, Epic is now `agent::ready`.
41
- * 1 fatal error (see stderr).
17
+ * This file is a **re-export shim only** — it carries external importers of
18
+ * the historic named-export surface one more release (#4474 design §6 PR7
19
+ * risk note) and is deleted in the next release. Internal consumers import
20
+ * the phase modules directly; do not add new imports of this file.
42
21
  *
43
- * Public CLI surface and named exports are byte-identical to the pre-
44
- * refactor implementation.
22
+ * Invoking it as a CLI is refused with a pointer to the successor CLIs.
45
23
  */
46
24
 
47
- // Fail-fast if the framework's runtime deps are not installed must be the
48
- // first import so the check runs before any third-party-importing sibling
49
- // module is evaluated (Story #3432).
50
- import './lib/runtime-deps/ensure-installed.js';
51
- import { runAsCli } from './lib/cli-utils.js';
52
- import { main } from './lib/orchestration/epic-plan-decompose/phases/cli.js';
53
- import {
54
- buildDecomposerSystemPrompt,
55
- buildDecompositionContext,
56
- } from './lib/orchestration/epic-plan-decompose/phases/context.js';
57
- import {
58
- orderTicketsForCreation,
59
- resolveDependencies,
60
- } from './lib/orchestration/epic-plan-decompose/phases/dag.js';
61
- import { runDecomposePhase } from './lib/orchestration/epic-plan-decompose/phases/persist.js';
25
+ // cli-opt-out: retired delegate shim (Epic #4474 PR7)deliberately
26
+ // refuses CLI execution with a pointer to plan-context.js/plan-persist.js
27
+ // instead of wiring runAsCli around a dead main().
28
+ import { pathToFileURL } from 'node:url';
62
29
 
63
- // Named exports preserved for the existing test surface. The pre-refactor
64
- // module published these and the consumers (`tests/ticket-decomposer.test.js`,
65
- // `tests/scripts/epic-plan-decompose.body-preservation.test.js`,
66
- // `tests/scripts/epic-plan-decompose.sub-issue-safety-net.test.js`,
67
- // `tests/scripts/epic-plan.spec-flow.test.js`) still depend on them.
68
30
  export {
69
31
  buildDecomposerSystemPrompt,
70
32
  buildDecompositionContext,
33
+ } from './lib/orchestration/epic-plan-decompose/phases/context.js';
34
+ export {
71
35
  orderTicketsForCreation,
72
36
  resolveDependencies,
73
- runDecomposePhase,
74
- };
37
+ } from './lib/orchestration/epic-plan-decompose/phases/dag.js';
38
+ export { runDecomposePhase } from './lib/orchestration/epic-plan-decompose/phases/persist.js';
75
39
 
76
- runAsCli(import.meta.url, main, { source: 'epic-plan-decompose' });
40
+ // CLI execution is retired — fail loudly with the successor surface instead
41
+ // of silently doing nothing (a stale automation script should break visibly).
42
+ if (
43
+ process.argv[1] &&
44
+ import.meta.url === pathToFileURL(process.argv[1]).href
45
+ ) {
46
+ process.stderr.write(
47
+ '[epic-plan-decompose] retired (Epic #4474): the plan pipeline is ' +
48
+ 'context → author → persist.\n' +
49
+ ' - authoring envelope: node .agents/scripts/plan-context.js --epic <id>\n' +
50
+ ' - persist (all gates): node .agents/scripts/plan-persist.js --epic <id> --tickets ...\n' +
51
+ 'This file survives one release as an import shim only.\n',
52
+ );
53
+ process.exit(1);
54
+ }
@@ -5,9 +5,12 @@
5
5
  /**
6
6
  * epic-plan-healthcheck.js — Post-Plan Readiness Check
7
7
  *
8
- * Runs at the end of /plan (Phase 10) to validate the backlog and
9
- * optionally prime the execution environment before handing off to
10
- * /epic-deliver.
8
+ * Library home of `runPlanHealthcheck`, the **inline** blocking check the
9
+ * collapsed persist surface runs as the `agent::ready` exit condition
10
+ * (`plan-persist.js` step 9 — the authoritative invocation since Epic
11
+ * #4474 retired the standalone Phase 10 workflow step). The CLI form
12
+ * remains for manual re-validation and the opt-in slow paths below; no
13
+ * workflow mandates running it standalone.
11
14
  *
12
15
  * Modes (additive — the fast checks below always run):
13
16
  * (default) — config validation + git remote check only.
@@ -30,7 +33,7 @@
30
33
  * node epic-plan-healthcheck.js --epic <EPIC_ID> \
31
34
  * [--paranoid] [--prime-install] [--dry-run]
32
35
  *
33
- * @see .agents/workflows/helpers/plan-epic.md Phase 10
36
+ * @see .agents/workflows/helpers/plan-epic.md (persist step — inline gate)
34
37
  */
35
38
 
36
39
  import { spawnSync } from 'node:child_process';
@@ -47,6 +50,7 @@ import {
47
50
  import { gitSpawn } from './lib/git-utils.js';
48
51
  import { Logger } from './lib/Logger.js';
49
52
  import { TYPE_LABELS } from './lib/label-constants.js';
53
+ import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
50
54
  import { createProvider } from './lib/provider-factory.js';
51
55
 
52
56
  const progress = Logger.createProgress('plan-healthcheck', { stderr: true });
@@ -454,7 +458,11 @@ async function timed(name, fn) {
454
458
  * checks: Array<{name: string, ok: boolean, durationMs: number, detail: string}>}>}
455
459
  */
456
460
  // exported for tests — direct-unit coverage of the reachability semantics.
457
- export { checkReachability, extractStoryPaths, globToRegExp };
461
+ // (`resolveNavConfig` / `extractStoryPaths` / `globToRegExp` are also the
462
+ // shared mechanics of the persist-side draft reachability check —
463
+ // `lib/orchestration/plan-reachability.js`, Epic #4474 PR6 — so the two
464
+ // surfaces can never drift apart.)
465
+ export { checkReachability, extractStoryPaths, globToRegExp, resolveNavConfig };
458
466
 
459
467
  // exported for tests — Story-level reuse runner reserved for future test coverage
460
468
  export async function runPlanHealthcheck(opts = {}) {
@@ -543,6 +551,31 @@ export async function runPlanHealthcheck(opts = {}) {
543
551
  // Main guard
544
552
  // ---------------------------------------------------------------------------
545
553
 
546
- runAsCli(import.meta.url, runPlanHealthcheck, {
554
+ /**
555
+ * CLI wrapper: stamp the plan-metrics ledger (#4474 PR1) around the direct
556
+ * invocation only. In-process callers (the decompose persist gate imports
557
+ * `runPlanHealthcheck` directly) are already covered by their own CLI's
558
+ * stamp — wrapping here too would double-count the inline gate.
559
+ */
560
+ async function cliMain() {
561
+ const { epicId } = parseHealthcheckArgs();
562
+ let config;
563
+ try {
564
+ config = resolveConfig();
565
+ } catch {
566
+ config = undefined;
567
+ }
568
+ return recordPlanInvocation(
569
+ {
570
+ cli: 'epic-plan-healthcheck',
571
+ mode: 'healthcheck',
572
+ epicId: epicId ?? null,
573
+ config,
574
+ },
575
+ () => runPlanHealthcheck(),
576
+ );
577
+ }
578
+
579
+ runAsCli(import.meta.url, cliMain, {
547
580
  source: 'epic-plan-healthcheck',
548
581
  });