mandrel 1.90.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 (115) 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-deliver-preflight.js +37 -1
  17. package/.agents/scripts/epic-plan-clarity.js +16 -6
  18. package/.agents/scripts/epic-plan-decompose.js +37 -59
  19. package/.agents/scripts/epic-plan-healthcheck.js +39 -6
  20. package/.agents/scripts/epic-plan-spec.js +41 -168
  21. package/.agents/scripts/epic-reconcile.js +7 -1
  22. package/.agents/scripts/lib/Logger.js +4 -4
  23. package/.agents/scripts/lib/audit-suite/index.js +4 -5
  24. package/.agents/scripts/lib/audit-suite/runner.js +6 -4
  25. package/.agents/scripts/lib/audit-suite/selector.js +2 -3
  26. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  27. package/.agents/scripts/lib/command-header.js +20 -0
  28. package/.agents/scripts/lib/config/github.js +1 -3
  29. package/.agents/scripts/lib/config-settings-schema.js +4 -3
  30. package/.agents/scripts/lib/degraded-mode.js +1 -1
  31. package/.agents/scripts/lib/label-constants.js +18 -3
  32. package/.agents/scripts/lib/label-taxonomy.js +14 -3
  33. package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
  34. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
  35. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
  36. package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
  38. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
  39. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
  40. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
  41. package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
  42. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
  43. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +32 -0
  44. package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
  45. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
  46. package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
  48. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
  49. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
  51. package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
  52. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
  53. package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
  54. package/.agents/scripts/lib/orchestration/remote-verifier.js +165 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +10 -0
  56. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  57. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
  58. package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
  59. package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
  60. package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
  61. package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
  62. package/.agents/scripts/plan-context.js +186 -0
  63. package/.agents/scripts/plan-critics.js +227 -0
  64. package/.agents/scripts/plan-persist.js +383 -0
  65. package/.agents/scripts/pr-watch-with-update.js +8 -7
  66. package/.agents/scripts/run-lint.js +10 -11
  67. package/.agents/scripts/single-story-init.js +22 -0
  68. package/.agents/scripts/story-plan.js +19 -7
  69. package/.agents/scripts/sync-claude-commands.js +10 -5
  70. package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
  71. package/.agents/workflows/audit-documentation.md +5 -7
  72. package/.agents/workflows/audit-lighthouse.md +1 -0
  73. package/.agents/workflows/audit-security.md +1 -0
  74. package/.agents/workflows/audit-to-stories.md +16 -5
  75. package/.agents/workflows/deliver.md +8 -0
  76. package/.agents/workflows/git-deliver.md +76 -243
  77. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  78. package/.agents/workflows/helpers/code-review.md +7 -6
  79. package/.agents/workflows/helpers/deliver-epic.md +16 -4
  80. package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
  81. package/.agents/workflows/helpers/plan-epic.md +246 -999
  82. package/.agents/workflows/helpers/plan-story.md +14 -14
  83. package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
  84. package/.agents/workflows/helpers/single-story-deliver.md +13 -4
  85. package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
  86. package/.agents/workflows/mandrel-update.md +144 -466
  87. package/.agents/workflows/plan.md +73 -143
  88. package/docs/CHANGELOG.md +20 -0
  89. package/lib/cli/registry.js +21 -3
  90. package/package.json +1 -1
  91. package/.agents/schemas/loop-unit.schema.json +0 -70
  92. package/.agents/scripts/assert-branch.js +0 -81
  93. package/.agents/scripts/check-loop-units.js +0 -204
  94. package/.agents/scripts/detect-merges.js +0 -111
  95. package/.agents/scripts/git-pr-quality-gate.js +0 -205
  96. package/.agents/scripts/git-rebase-and-resolve.js +0 -234
  97. package/.agents/scripts/hierarchy-gate.js +0 -192
  98. package/.agents/scripts/hydrate-context.js +0 -179
  99. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
  100. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
  101. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
  102. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
  103. package/.agents/scripts/loc-delta.js +0 -205
  104. package/.agents/scripts/render-manifest.js +0 -143
  105. package/.agents/scripts/run-audit-suite.js +0 -97
  106. package/.agents/scripts/select-audits.js +0 -155
  107. package/.agents/scripts/update-mutation-baseline.js +0 -189
  108. package/.agents/workflows/explain.md +0 -118
  109. package/.agents/workflows/git-merge-pr.md +0 -377
  110. package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
  111. package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
  112. package/.agents/workflows/loops/README.md +0 -65
  113. package/.agents/workflows/loops/fix-failing-tests.md +0 -74
  114. package/.agents/workflows/loops/nightly-audit.md +0 -81
  115. 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
@@ -11,7 +11,12 @@
11
11
  * `storyCount`, `installCostSeconds`, `dependencyDepth`,
12
12
  * `githubApiRequests`, `claudeQuotaTokens`, plus `breaches`
13
13
  * (the non-empty subset of `delivery.preflight.max*` thresholds the
14
- * estimate exceeds).
14
+ * estimate exceeds), plus `remoteVerified` / `remoteProbe` — the
15
+ * issue #4483 deterministic remote evidence (`git remote get-url
16
+ * origin` + bounded `git ls-remote origin HEAD`). On
17
+ * `remoteVerified: false` the workflow MUST flip the Epic to
18
+ * `agent::blocked` quoting `remoteProbe.detail` — inline delivery to
19
+ * local `main` is never a sanctioned fallback.
15
20
  * 2. When `--post` is set (and `--dry-run` is not), an upserted
16
21
  * `delivery-preflight` structured comment on the Epic ticket so
17
22
  * reviewers reading the Epic discover the same numbers without
@@ -66,6 +71,7 @@ import {
66
71
  computeBaseSha,
67
72
  writePreflightCache,
68
73
  } from './lib/orchestration/preflight-cache.js';
74
+ import { verifyRemote } from './lib/orchestration/remote-verifier.js';
69
75
  import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
70
76
  import { createProvider } from './lib/provider-factory.js';
71
77
 
@@ -218,10 +224,23 @@ export function renderPreflightBody({
218
224
  estimate,
219
225
  breaches,
220
226
  thresholds,
227
+ remote,
221
228
  }) {
222
229
  const lines = [];
223
230
  lines.push(`### 🛫 Delivery preflight — Epic #${epicId}`);
224
231
  lines.push('');
232
+ // Issue #4483 — verified remote evidence at entry. Rendered before the
233
+ // metric table so a reviewer (and the orchestrating agent) sees the
234
+ // land-or-block fact first. Omitted when the caller has no probe result
235
+ // (legacy callers / tests that only exercise the estimate math).
236
+ if (remote) {
237
+ lines.push(
238
+ remote.remoteVerified
239
+ ? `✅ **remoteVerified: true** — ${remote.detail}`
240
+ : `⛔ **remoteVerified: false** — ${remote.detail} — \`/deliver\` MUST transition the Epic to \`agent::blocked\` quoting this probe; inline delivery to local \`main\` is forbidden.`,
241
+ );
242
+ lines.push('');
243
+ }
225
244
  lines.push('| Metric | Estimate | Threshold |');
226
245
  lines.push('| --- | ---: | ---: |');
227
246
  const rows = [
@@ -276,6 +295,7 @@ export function renderPreflightBody({
276
295
  * perStoryClaudeTokens?: number,
277
296
  * injectedProvider?: object,
278
297
  * injectedConfig?: object,
298
+ * verifyRemoteFn?: typeof verifyRemote,
279
299
  * }} args
280
300
  */
281
301
  export async function runPreflight({
@@ -288,6 +308,7 @@ export async function runPreflight({
288
308
  perStoryClaudeTokens,
289
309
  injectedProvider,
290
310
  injectedConfig,
311
+ verifyRemoteFn = verifyRemote,
291
312
  }) {
292
313
  if (!Number.isInteger(epicId) || epicId <= 0) {
293
314
  throw new TypeError('runPreflight: --epic must be a positive integer');
@@ -297,6 +318,13 @@ export async function runPreflight({
297
318
  const provider = injectedProvider ?? createProvider(config);
298
319
  const thresholds = getPreflight(config);
299
320
 
321
+ // Issue #4483 — deterministic remote evidence at entry. Probe BEFORE any
322
+ // provider work so the envelope always carries verified fact (not the
323
+ // agent's perception) about whether a live, pushable `origin` exists.
324
+ // The CLI records; the `/deliver` workflow owns the `agent::blocked`
325
+ // transition on `remoteVerified: false` (same split as breach handling).
326
+ const remote = verifyRemoteFn({ cwd });
327
+
300
328
  // Compose the same two phases /deliver Phase 1 runs so the
301
329
  // preflight numbers match the actual dispatch plan.
302
330
  const ctx = { epicId, provider };
@@ -348,6 +376,13 @@ export async function runPreflight({
348
376
  thresholds,
349
377
  baseSha,
350
378
  cacheWritten,
379
+ // Issue #4483 — verified remote evidence. `remoteVerified: false`
380
+ // REQUIRES the workflow to block explicitly (never build inline).
381
+ remoteVerified: remote.remoteVerified,
382
+ remoteProbe: {
383
+ remoteUrl: remote.remoteUrl,
384
+ detail: remote.detail,
385
+ },
351
386
  };
352
387
 
353
388
  if (post && !dryRun) {
@@ -356,6 +391,7 @@ export async function runPreflight({
356
391
  estimate,
357
392
  breaches,
358
393
  thresholds,
394
+ remote,
359
395
  });
360
396
  await upsertStructuredComment(provider, epicId, 'delivery-preflight', body);
361
397
  envelope.commentUpserted = true;
@@ -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
+ }