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
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * plan-persist.js — single GitHub-write surface for the collapsed /plan
5
+ * flow (Epic #4474, PR3 — design §1 Step 3).
6
+ *
7
+ * Supersedes the retired 12-phase pipeline's separate persist halves
8
+ * (Epic #4474 PR7 — the delegate CLIs are import shims now, deleted next
9
+ * release). Given the author-written planning artifacts
10
+ * (Tech Spec, optional Acceptance Table, risk verdict, tickets JSON), this
11
+ * CLI performs every GitHub mutation of the plan flow in one ordered,
12
+ * fail-closed pass:
13
+ *
14
+ * section gate → risk-verdict + mode-coherence → ticket validator /
15
+ * file-assumption gate / DAG / budget → (ideation: open the Epic) →
16
+ * Epic lease → managed sections + risk comment + freshness advisory →
17
+ * story creation (structural reconciler) → inline healthcheck →
18
+ * single terminal `agent::ready` flip (no intermediate
19
+ * `agent::review-spec`) → checkpoint v2 + `plan-summary` comment with
20
+ * the dry-run wave table → temp cleanup at terminal success only.
21
+ *
22
+ * Modes:
23
+ * --epic <id> Persist against an existing Epic. Artifact paths
24
+ * default to the per-Epic temp tree
25
+ * (`temp/epic-<id>/techspec.md`, `risk-verdict.json`,
26
+ * `tickets.json`, and `acceptance-spec.md` when
27
+ * present).
28
+ * --one-pager <path> Ideation mode: render + open the Epic from a
29
+ * sharpened one-pager first (folds the former
30
+ * Phase 3/4 steps in). Artifact paths must be
31
+ * explicit (there is no Epic id to derive them from).
32
+ *
33
+ * Modes (Epic #4474 PR4, design §2 mode matrix): the risk verdict's
34
+ * optional `deliveryShape` field selects between the full fan-out persist
35
+ * (default; requires tickets) and the spec-only single-delivery variant
36
+ * (`deliveryShape: "single"`; NO tickets — the ticket validator + DAG are
37
+ * skipped, fenced by construction, and the `delivery::single` routing
38
+ * marker is applied instead of a Story tree; inert until #4475 lands the
39
+ * deliver-side reader). `--amend` selects the change-request delta path:
40
+ * tickets carry `op: add|modify|keep|close` and the persist maps the ops
41
+ * onto the existing tree.
42
+ *
43
+ * Flags:
44
+ * --force Deliberate re-persist: overwrite managed sections,
45
+ * close + recreate the story tree (reconciler
46
+ * --explicit-delete). Reuses on-disk artifacts —
47
+ * cleanup is deferred to terminal success, so a
48
+ * failed run leaves them in place.
49
+ * --resume Continue a partial persist after a crash
50
+ * (rate-limit, network): sections short-circuit
51
+ * idempotently, the reconciler creates only the
52
+ * missing slugs from its per-slug state ledger.
53
+ * --amend Change-request delta persist: every ticket carries
54
+ * `op: add|modify|keep|close`; close-and-recreate is
55
+ * scoped to modify/close slugs only, keeps are
56
+ * untouched, and the DAG is validated over the
57
+ * merged set.
58
+ * --explicit-delete Confirm the close ops of an --amend plan (mirrors
59
+ * epic-reconcile.js). Without it, an amend carrying
60
+ * close ops prints the dry-run diff and exits 2.
61
+ * --steal Force-transfer a live foreign Epic-lease claim.
62
+ * --force-review Operator-forced review routing (recorded in the
63
+ * checkpoint's reviewRouting envelope).
64
+ * --allow-over-budget / --allow-large-fan-out
65
+ * Same overrides as the retired split persist.
66
+ *
67
+ * Exit codes: 0 — persist complete, Epic is `agent::ready`; 1 — fatal
68
+ * error (see stderr); 2 — amend close ops require --explicit-delete (the
69
+ * dry-run diff is printed; nothing was mutated); 3 — reachability orphans
70
+ * (Epic #4474 PR6 named soft failure: the deterministic route-glob vs
71
+ * `planning.navigation.navRegistry` scan found route-adding draft stories
72
+ * with no navigation owner — the orphan-surface list is printed, nothing
73
+ * was mutated; apply one targeted amend and re-run the persist once). The
74
+ * Epic lease is released on every exit path.
75
+ */
76
+
77
+ // Fail-fast if the framework's runtime deps are not installed — must be the
78
+ // first import so the check runs before any third-party-importing sibling
79
+ // module is evaluated (Story #3432).
80
+ import './lib/runtime-deps/ensure-installed.js';
81
+ import { readFile } from 'node:fs/promises';
82
+ import path from 'node:path';
83
+ import { parseArgs } from 'node:util';
84
+
85
+ import { runAsCli } from './lib/cli-utils.js';
86
+ import { epicArtifactPath } from './lib/config/temp-paths.js';
87
+ import {
88
+ PROJECT_ROOT,
89
+ resolveConfig,
90
+ validateOrchestrationConfig,
91
+ } from './lib/config-resolver.js';
92
+ import { Logger } from './lib/Logger.js';
93
+ import { drainPendingCleanupAtBoot } from './lib/orchestration/epic-plan-spec/phases/drain.js';
94
+ import { loadRiskVerdict } from './lib/orchestration/epic-plan-spec/phases/risk-verdict.js';
95
+ import {
96
+ readPlanMetrics,
97
+ recordPlanInvocation,
98
+ renderPlanMetricsSummaryLine,
99
+ summarizePlanMetrics,
100
+ } from './lib/orchestration/plan-metrics.js';
101
+ import {
102
+ resolveDeliveryMode,
103
+ runPlanPersist,
104
+ writeCheckpointV2,
105
+ } from './lib/orchestration/plan-persist/run-plan-persist.js';
106
+ import {
107
+ buildPlanSummaryCommentBody,
108
+ buildWaveTable,
109
+ PLAN_SUMMARY_COMMENT_TYPE,
110
+ } from './lib/orchestration/plan-persist/summary.js';
111
+ import { createProvider } from './lib/provider-factory.js';
112
+
113
+ // Re-exports for the stable public API (tests import through the CLI
114
+ // module, mirroring the plan-context.js CLI-shell shape).
115
+ export {
116
+ buildPlanSummaryCommentBody,
117
+ buildWaveTable,
118
+ PLAN_SUMMARY_COMMENT_TYPE,
119
+ resolveDeliveryMode,
120
+ runPlanPersist,
121
+ writeCheckpointV2,
122
+ };
123
+
124
+ export const EPIC_FROM_IDEA_TEMPLATE_PATH = path.resolve(
125
+ PROJECT_ROOT,
126
+ '.agents',
127
+ 'templates',
128
+ 'epic-from-idea.md',
129
+ );
130
+
131
+ const CLI_OPTIONS = {
132
+ epic: { type: 'string' },
133
+ 'one-pager': { type: 'string' },
134
+ 'tech-spec': { type: 'string' },
135
+ 'acceptance-table': { type: 'string' },
136
+ 'risk-verdict': { type: 'string' },
137
+ tickets: { type: 'string' },
138
+ force: { type: 'boolean', default: false },
139
+ resume: { type: 'boolean', default: false },
140
+ steal: { type: 'boolean', default: false },
141
+ 'force-review': { type: 'boolean', default: false },
142
+ 'allow-over-budget': { type: 'boolean', default: false },
143
+ 'allow-large-fan-out': { type: 'boolean', default: false },
144
+ amend: { type: 'boolean', default: false },
145
+ 'explicit-delete': { type: 'boolean', default: false },
146
+ };
147
+
148
+ const USAGE =
149
+ 'Usage: plan-persist.js (--epic <EpicId> | --one-pager <file>) ' +
150
+ '[--tech-spec <file>] [--acceptance-table <file>] [--risk-verdict <file>] ' +
151
+ '[--tickets <file>] [--force | --resume | --amend [--explicit-delete]] ' +
152
+ '[--steal] [--force-review] [--allow-over-budget] [--allow-large-fan-out]';
153
+
154
+ /**
155
+ * Parse `--epic`; returns null when absent (ideation mode).
156
+ */
157
+ function parseEpicId(rawEpic) {
158
+ if (rawEpic === undefined) return null;
159
+ const epicId = Number.parseInt(rawEpic, 10);
160
+ if (Number.isNaN(epicId)) {
161
+ throw new Error(
162
+ `Invalid epic ID: "${rawEpic}" — must be a number.\n${USAGE}`,
163
+ );
164
+ }
165
+ return epicId;
166
+ }
167
+
168
+ /**
169
+ * Resolve the artifact file paths. Existing-Epic mode defaults each path to
170
+ * the per-Epic temp tree — the same locations the authoring skill writes to
171
+ * — so a `--force`/`--resume` re-persist reuses the on-disk artifacts with
172
+ * no re-typing (they survive until terminal success now that cleanup is
173
+ * deferred). Ideation mode has no Epic id to derive from, so the paths must
174
+ * be explicit.
175
+ */
176
+ function resolveArtifactPaths({ epicId, values, config }) {
177
+ const fallback = (basename) =>
178
+ epicId === null ? undefined : epicArtifactPath(epicId, basename, config);
179
+ const techSpecPath = values['tech-spec'] ?? fallback('techspec.md');
180
+ const riskVerdictPath =
181
+ values['risk-verdict'] ?? fallback('risk-verdict.json');
182
+ const ticketsPath = values.tickets ?? fallback('tickets.json');
183
+ const acceptancePath =
184
+ values['acceptance-table'] ?? fallback('acceptance-spec.md');
185
+ if (!techSpecPath || !riskVerdictPath) {
186
+ throw new Error(
187
+ `Missing artifact path(s): ideation mode requires explicit --tech-spec and --risk-verdict.\n${USAGE}`,
188
+ );
189
+ }
190
+ return {
191
+ techSpecPath,
192
+ riskVerdictPath,
193
+ ticketsPath,
194
+ acceptancePath,
195
+ acceptanceExplicit: values['acceptance-table'] !== undefined,
196
+ ticketsExplicit: values.tickets !== undefined,
197
+ };
198
+ }
199
+
200
+ async function readOptional(filePath, { required }) {
201
+ try {
202
+ return await readFile(filePath, 'utf8');
203
+ } catch (err) {
204
+ if (!required && err?.code === 'ENOENT') return null;
205
+ throw new Error(`Cannot read ${filePath}: ${err.message}`);
206
+ }
207
+ }
208
+
209
+ async function main() {
210
+ const { values } = parseArgs({ options: CLI_OPTIONS });
211
+
212
+ if (values.force && values.resume) {
213
+ throw new Error('--force and --resume are mutually exclusive.');
214
+ }
215
+ if (values.amend && (values.force || values.resume)) {
216
+ throw new Error(
217
+ '--amend is mutually exclusive with --force/--resume — the amend ' +
218
+ 'delta is already an incremental re-persist.',
219
+ );
220
+ }
221
+
222
+ const epicId = parseEpicId(values.epic);
223
+ const onePagerPath = values['one-pager'];
224
+ if (epicId === null && !onePagerPath) {
225
+ throw new Error(USAGE);
226
+ }
227
+ if (epicId !== null && onePagerPath) {
228
+ throw new Error(
229
+ '--epic and --one-pager are mutually exclusive (ideation mode opens ' +
230
+ `the Epic itself).\n${USAGE}`,
231
+ );
232
+ }
233
+
234
+ let config;
235
+ try {
236
+ config = resolveConfig();
237
+ validateOrchestrationConfig(config);
238
+ } catch (err) {
239
+ throw new Error(`Config schema validation failed:\n${err.message}`);
240
+ }
241
+ const settings = {
242
+ baseBranch: config.project?.baseBranch,
243
+ paths: config.project?.paths,
244
+ planning: config.planning,
245
+ docsContextFiles: config.project?.docsContextFiles,
246
+ };
247
+ const provider = createProvider(config);
248
+
249
+ try {
250
+ await drainPendingCleanupAtBoot({
251
+ repoRoot: PROJECT_ROOT,
252
+ config,
253
+ provider,
254
+ });
255
+ } catch (err) {
256
+ Logger.warn(`[plan-persist] pending-cleanup drain skipped: ${err.message}`);
257
+ }
258
+
259
+ const {
260
+ techSpecPath,
261
+ riskVerdictPath,
262
+ ticketsPath,
263
+ acceptancePath,
264
+ acceptanceExplicit,
265
+ ticketsExplicit,
266
+ } = resolveArtifactPaths({ epicId, values, config });
267
+
268
+ // Deterministic local reads + validation before any GitHub call. A
269
+ // malformed risk verdict fails closed here (Epic #3865); the section
270
+ // gate itself runs first inside runPlanPersist.
271
+ const techSpecContent = await readOptional(techSpecPath, { required: true });
272
+ const riskVerdict = loadRiskVerdict(riskVerdictPath);
273
+ // Tickets are mode-dependent (#4474 PR4): required when explicitly
274
+ // passed or in --amend mode; otherwise best-effort — a single-delivery
275
+ // plan authors no tickets file at all, and the mode-coherence gate
276
+ // (`resolveDeliveryMode`) hard-errors on every contradictory combination
277
+ // (including a stale tickets.json next to a `deliveryShape: "single"`
278
+ // verdict).
279
+ const ticketsRequired = ticketsExplicit || values.amend;
280
+ const ticketsRaw = ticketsPath
281
+ ? await readOptional(ticketsPath, { required: ticketsRequired })
282
+ : null;
283
+ let tickets = null;
284
+ if (ticketsRaw !== null) {
285
+ try {
286
+ tickets = JSON.parse(ticketsRaw);
287
+ } catch (err) {
288
+ throw new Error(
289
+ `Failed to parse tickets file "${ticketsPath}" as JSON: ${err.message}`,
290
+ );
291
+ }
292
+ }
293
+ // Acceptance table: explicit path is required to exist; the per-Epic
294
+ // default is best-effort (absent file → no acceptance section, matching
295
+ // the waived/none dispositions).
296
+ const acceptanceSpecContent = acceptancePath
297
+ ? await readOptional(acceptancePath, { required: acceptanceExplicit })
298
+ : null;
299
+ const onePagerContent = onePagerPath
300
+ ? await readOptional(onePagerPath, { required: true })
301
+ : null;
302
+ const templateContent = onePagerContent
303
+ ? await readOptional(EPIC_FROM_IDEA_TEMPLATE_PATH, { required: true })
304
+ : null;
305
+
306
+ // Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode. Ideation runs
307
+ // have no Epic id at entry, so they stamp on the standalone stream.
308
+ const mode = values.amend
309
+ ? 'amend'
310
+ : values.resume
311
+ ? 'resume'
312
+ : values.force
313
+ ? 'force'
314
+ : 'persist';
315
+ let result;
316
+ try {
317
+ result = await recordPlanInvocation(
318
+ { cli: 'plan-persist', mode, epicId, config },
319
+ () =>
320
+ runPlanPersist({
321
+ epicId,
322
+ provider,
323
+ artifacts: {
324
+ techSpecContent,
325
+ acceptanceSpecContent,
326
+ riskVerdict,
327
+ tickets,
328
+ onePagerContent,
329
+ templateContent,
330
+ },
331
+ config,
332
+ settings,
333
+ opts: {
334
+ force: values.force,
335
+ resume: values.resume,
336
+ amend: values.amend,
337
+ explicitDelete: values['explicit-delete'],
338
+ steal: values.steal,
339
+ forceReview: values['force-review'],
340
+ allowOverBudget: values['allow-over-budget'],
341
+ allowLargeFanOut: values['allow-large-fan-out'],
342
+ },
343
+ }),
344
+ );
345
+ } catch (err) {
346
+ // Amend close-op confirmation gate (exit 2, mirroring the
347
+ // epic-reconcile.js contract): print the dry-run diff so the operator
348
+ // reviews exactly what would close, mutate nothing, and exit 2 so
349
+ // non-interactive callers can branch on the code.
350
+ if (err?.code === 'PLAN_AMEND_EXPLICIT_DELETE_REQUIRED') {
351
+ process.stdout.write(`${err.diff}\n\n${err.message}\n`);
352
+ process.exitCode = 2;
353
+ return;
354
+ }
355
+ // Reachability soft failure (#4474 PR6): the orphan-surface list is the
356
+ // message; nothing was mutated (the check runs before any provider
357
+ // call). Exit 3 so non-interactive callers can branch on the code.
358
+ if (err?.code === 'PLAN_REACHABILITY_ORPHANS') {
359
+ process.stdout.write(`${err.message}\n`);
360
+ process.exitCode = 3;
361
+ return;
362
+ }
363
+ throw err;
364
+ }
365
+
366
+ // Surface the whole plan run's invocation ledger in the persist summary
367
+ // (#4474 PR1). Additive and best-effort.
368
+ try {
369
+ const summary = summarizePlanMetrics(
370
+ await readPlanMetrics(result.epicId, config),
371
+ );
372
+ if (summary) {
373
+ result.planMetrics = summary;
374
+ Logger.info(`[plan-persist] ${renderPlanMetricsSummaryLine(summary)}`);
375
+ }
376
+ } catch (err) {
377
+ Logger.warn(`[plan-persist] plan-metrics summary skipped: ${err.message}`);
378
+ }
379
+
380
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
381
+ }
382
+
383
+ runAsCli(import.meta.url, main, { source: 'plan-persist' });
@@ -19,13 +19,13 @@
19
19
  * IMMEDIATELY, consuming no resume budget. On red the CLI
20
20
  * writes `temp/epic-<id>-ci-digest.{json,md}` (failing check
21
21
  * name, run id, a `gh run view --log-failed` tail, and a
22
- * coarse classification) and prints the `/loop
23
- * /loops:fix-failing-tests` handoff.
22
+ * coarse classification) and prints the red-green
23
+ * remediation handoff.
24
24
  * - STILL-RUNNING — the poll cap fired with checks still pending and
25
25
  * none failed; the watcher re-armed up to
26
26
  * `delivery.ci.watch.maxResumes` times, then returned a
27
27
  * `still-running` verdict → exit 2 (NEVER 1, NEVER
28
- * `timed_out`). The CLI prints the `/loop 5m /loops:watch-ci`
28
+ * `timed_out`). The CLI prints the `gh pr checks --watch`
29
29
  * handoff so the host can keep polling on its own cadence.
30
30
  *
31
31
  * Config (Story #4356 namespace, read via `getCiDelivery`):
@@ -350,9 +350,9 @@ export async function runPrWatch({
350
350
  .map(([k]) => k)
351
351
  .join(', ');
352
352
  logger.warn?.(
353
- `[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}. Hand off to the interval watch loop:`,
353
+ `[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}. Keep polling natively:`,
354
354
  );
355
- logger.warn?.('[pr-watch] /loop 5m /loops:watch-ci');
355
+ logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
356
356
  return STILL_RUNNING_EXIT_CODE;
357
357
  }
358
358
 
@@ -392,8 +392,9 @@ export async function runPrWatch({
392
392
  if (digestPaths) {
393
393
  logger.error?.(`[pr-watch] CI failure digest → ${digestPaths.jsonPath}`);
394
394
  }
395
- logger.error?.('[pr-watch] a required check failed. Drive it to green with:');
396
- logger.error?.('[pr-watch] /loop /loops:fix-failing-tests');
395
+ logger.error?.(
396
+ '[pr-watch] a required check failed. Read the digest, apply the smallest fix, and re-run the suite until green.',
397
+ );
397
398
  return 1;
398
399
  }
399
400
 
@@ -56,6 +56,16 @@ const tasks = [
56
56
  cmd: 'node',
57
57
  args: ['.agents/scripts/check-lifecycle-lint.js'],
58
58
  },
59
+ {
60
+ // Custom Node-based lint for the workflow prose surface (Epic #4474
61
+ // PR5). Enforces: no workflow may instruct calling an exported
62
+ // library function that has no CLI entrypoint — the measured
63
+ // shim-writing failure mode the /plan collapse killed. See
64
+ // check-workflow-cli-lint.js for the paragraph-level heuristic.
65
+ name: 'workflow-cli-lint',
66
+ cmd: 'node',
67
+ args: ['.agents/scripts/check-workflow-cli-lint.js'],
68
+ },
59
69
  {
60
70
  // Custom Node-based lint for label-vocabulary citations in
61
71
  // `.agents/docs/SDLC.md` and `.agents/workflows/**/*.md` (Story #2892,
@@ -78,17 +88,6 @@ const tasks = [
78
88
  cmd: 'node',
79
89
  args: ['.agents/scripts/check-arch-cycles.js'],
80
90
  },
81
- {
82
- // Loop-unit frontmatter gate (Story #4288, Epic #4284). Validates
83
- // every `.agents/workflows/loops/*.md` loop unit against
84
- // `.agents/schemas/loop-unit.schema.json`. An absent/empty loops
85
- // directory is a clean pass; a malformed unit (e.g. a self-paced
86
- // cadence missing its required `verify`) fails the lint gate with a
87
- // message naming the offending file + field.
88
- name: 'loop-units',
89
- cmd: 'node',
90
- args: ['.agents/scripts/check-loop-units.js'],
91
- },
92
91
  ];
93
92
 
94
93
  function runTask({ name, cmd, args }) {
@@ -39,6 +39,7 @@ import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
39
39
  import { exec as ghExec } from './lib/gh-exec.js';
40
40
  import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
41
41
  import { TYPE_LABELS } from './lib/label-constants.js';
42
+ import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
42
43
  import { buildCorpusContext } from './lib/planning-corpus.js';
43
44
  import { createProvider } from './lib/provider-factory.js';
44
45
  import {
@@ -298,16 +299,27 @@ async function main() {
298
299
  if (values['emit-context']) {
299
300
  // Reserve stdout for the JSON envelope so a captured file is
300
301
  // unconditionally parseable by `JSON.parse`. Mirrors the contract
301
- // `epic-plan-spec.js` enforces for its own --emit-context mode.
302
+ // `plan-context.js` enforces for its emit mode.
302
303
  routeAllOutputToStderr();
303
- return runEmitContext({ values, provider, projectRoot, config });
304
+ // Plan-metrics ledger (#4474 PR1): standalone plans have no Epic, so
305
+ // `epicId: null` routes the stamp to the standalone stream
306
+ // (`temp/standalone/plan-metrics.json`) — same pattern as friction.
307
+ return recordPlanInvocation(
308
+ { cli: 'story-plan', mode: 'emit-context', epicId: null, config },
309
+ () => runEmitContext({ values, provider, projectRoot, config }),
310
+ );
304
311
  }
305
312
 
306
- return runPersist({
307
- values,
308
- provider,
309
- dryRun: values['dry-run'],
310
- });
313
+ // Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
314
+ return recordPlanInvocation(
315
+ { cli: 'story-plan', mode: 'persist', epicId: null, config },
316
+ () =>
317
+ runPersist({
318
+ values,
319
+ provider,
320
+ dryRun: values['dry-run'],
321
+ }),
322
+ );
311
323
  }
312
324
 
313
325
  runAsCli(import.meta.url, main, { source: 'story-plan' });
@@ -47,7 +47,7 @@
47
47
  import fs from 'node:fs';
48
48
  import path from 'node:path';
49
49
 
50
- import { applyHeader } from './lib/command-header.js';
50
+ import { applyHeader, isCommandExcluded } from './lib/command-header.js';
51
51
  import { Logger } from './lib/Logger.js';
52
52
 
53
53
  // Resolve the project root from the invocation cwd — the consumer project where
@@ -154,9 +154,7 @@ const isTopLevelWorkflow = (entry) =>
154
154
  /**
155
155
  * `README.md` (any case) under `loops/` is namespace documentation, not a
156
156
  * loop unit — it carries no `loop:` frontmatter and must not project as a
157
- * `/loops:README` command. Exclude it from the loop-unit enumeration (this
158
- * mirrors `check-loop-units.js#isLoopUnitFile`, which excludes it from the
159
- * lint gate).
157
+ * `/loops:README` command. Exclude it from the loop-unit enumeration.
160
158
  *
161
159
  * @param {import('node:fs').Dirent} entry
162
160
  * @returns {boolean}
@@ -204,13 +202,20 @@ const entries = SRC_DIRS.flatMap((dir) => [
204
202
 
205
203
  // Collision policy: payload wins, warn on a shadowed local file. Keyed by the
206
204
  // destination-relative path so a flat `foo.md` and a `loops/foo.md` are
207
- // distinct entries.
205
+ // distinct entries. A workflow whose frontmatter carries `command: false`
206
+ // (#4482 — dual-use audit lens files with a host-native standalone
207
+ // equivalent) is excluded from projection entirely: it never enters
208
+ // `sourceSet`, so any previously projected copy is reaped below.
208
209
  const byRel = new Map();
209
210
  for (const e of entries) {
210
211
  if (byRel.has(e.rel)) {
211
212
  Logger.warn(` shadowed ${e.rel} (local copy ignored; payload wins)`);
212
213
  continue;
213
214
  }
215
+ if (isCommandExcluded(fs.readFileSync(path.join(e.dir, e.rel), 'utf8'))) {
216
+ Logger.info(` excluded ${e.rel} (frontmatter command: false)`);
217
+ continue;
218
+ }
214
219
  byRel.set(e.rel, e);
215
220
  }
216
221
 
@@ -151,13 +151,17 @@ host's equivalent structured-choice surface.
151
151
 
152
152
  This skill is the engine behind two operator-facing entry points:
153
153
 
154
- - [`/explain`](../../../workflows/explain.md) runs the loop over a realized
155
- change (a PR, branch, or diff).
156
- - [`/plan`](../../../workflows/helpers/plan-epic.md) **Phase 11 Plan
157
- Comprehension Gate** runs the loop over a freshly planned backlog before
158
- the operator hands off to `/deliver`. That phase decides *whether* to
159
- run via an LM-judgment predicate; this skill owns *how* it runs once
160
- invoked.
154
+ - **An ad-hoc conversational walkthrough** of a realized change (a PR,
155
+ branch, or diff) — the host's native mode. Read this skill and run the
156
+ loop directly when the operator asks to be walked through a change.
157
+ (The former `/explain` workflow wrapper was retired in #4482.)
158
+ - **A post-plan backlog walkthrough** run the loop over a freshly planned
159
+ backlog (the Epic body's Tech Spec sections, the Stories, the wave table)
160
+ before the operator hands off to `/deliver`. Offer it by LM judgment on
161
+ non-trivial plans; it is no longer a wired phase of
162
+ [`/plan`](../../../workflows/helpers/plan-epic.md) (the comprehension gate
163
+ was unwired from the pipeline in Epic #4474), always advisory, and never
164
+ blocks the hand-off.
161
165
 
162
166
  It is discovered through the standard skill-activation contract in
163
167
  [`.agents/instructions.md` § 1.B](../../../instructions.md) — no separate
@@ -283,14 +283,12 @@ bloated tail over many Epics, and no single change-set-scoped `/deliver`
283
283
  Phase 4 run sees the whole picture. Run this lens **full-scope** on a
284
284
  recurring cadence so the drift is caught before it compounds:
285
285
 
286
- - **Cron loop** — [`loops:nightly-audit`](loops/nightly-audit.md)
287
- runs the audit sweep unattended each night; the host (`/schedule` or a
288
- cron-driven `/loop`) owns the cadence. Point it at this lens full-scope
289
- (no `--paths`, no change-set filter — the `{{changedFiles}}` block renders
290
- the literal token, so the whole target-set union is audited).
291
286
  - **Scheduled invocation** — `/schedule` running `/audit-documentation`
292
- full-scope on a daily/weekly cron is the equivalent one-shot form when a
293
- standing loop is not wired up.
287
+ full-scope on a daily/weekly cron. Point it at this lens full-scope
288
+ (no `--paths`, no change-set filter — the `{{changedFiles}}` block renders
289
+ the literal token, so the whole target-set union is audited). The nightly
290
+ sweep-then-route recipe lives in
291
+ [`audit-to-stories.md` § Scheduling a nightly sweep](audit-to-stories.md#scheduling-a-nightly-sweep).
294
292
 
295
293
  Route the resulting `audit-documentation-results.md` through
296
294
  [`/audit-to-stories`](audit-to-stories.md), which groups the findings,
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Run a Lighthouse audit (Performance / Accessibility / Best Practices / SEO) and produce a structured findings report
3
+ command: false
3
4
  ---
4
5
 
5
6
  # Lighthouse Audit & Analysis
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Audit dependency CVEs, input-validation gaps, secrets handling, and auth boundaries; emit a structured High/Medium/Low findings report.
3
+ command: false
3
4
  ---
4
5
 
5
6
  # Security & Vulnerability Audit
@@ -120,7 +120,7 @@ Ask:
120
120
  > - **Single Epic via `/plan`** **[Recommended]** — opens one Epic,
121
121
  > then chains into `/plan --idea` so the standard spec-and-WBS
122
122
  > authoring handles decomposition. Grouped Stories become the
123
- > seed for Phase 7 decomposition.
123
+ > seed for the authoring step's decomposition.
124
124
  > - **Individual standalone Stories** — opens one GitHub Issue per
125
125
  > group directly, no Epic wrapper.
126
126
 
@@ -138,8 +138,8 @@ node .agents/scripts/audit-to-stories.js --emit-epic-seed \
138
138
 
139
139
  The seed renders the canonical one-pager sections — Problem Statement,
140
140
  Recommended Direction, Key Assumptions (with links to every source
141
- report), MVP Scope (the M proposed Stories), Key Files (so `/plan`
142
- Phase 7 decompose has concrete anchors), Not Doing.
141
+ report), MVP Scope (the M proposed Stories), Key Files (so `/plan`'s
142
+ authoring step has concrete anchors), Not Doing.
143
143
 
144
144
  Chain into the existing planning entrypoint:
145
145
 
@@ -147,8 +147,8 @@ Chain into the existing planning entrypoint:
147
147
  /plan --idea "<path-to-seed>"
148
148
  ```
149
149
 
150
- `/plan` then runs ideation → duplicate-searchrender Epic body
151
- → open Epic → Phase 7 / 8 decompose, as documented in its workflow.
150
+ `/plan` then runs its 3-step Epic path (interrogate author
151
+ persist), as documented in its workflow.
152
152
  Each Story it spawns from the seed carries `context::audit:
153
153
  <reportLink>` and `audit-fingerprint: <sha>` in its body so future
154
154
  Phase 6 idempotency works on the next run.
@@ -248,6 +248,17 @@ opened. When the Standalone-Stories path ran, list every Issue URL.
248
248
  creation; fall back to `gh issue create` when the MCP tool is
249
249
  unavailable.
250
250
 
251
+ ## Scheduling a nightly sweep
252
+
253
+ To run an unattended maintenance sweep, `/schedule` a nightly (or weekly)
254
+ job that (1) runs the relevant `audit-*` lens workflows full-scope — no
255
+ `--paths`, no change-set filter, so the whole target-set union is audited —
256
+ writing their `temp/audits/audit-*-results.md` reports, then (2) invokes
257
+ `/audit-to-stories` over those results to dedupe and route the findings.
258
+ The host scheduler owns the cadence; this workflow owns the routing. (This
259
+ paragraph folds in the `loops/nightly-audit.md` starter unit retired in
260
+ issue 4482.)
261
+
251
262
  ## See also
252
263
 
253
264
  - [`/plan`](helpers/plan-epic.md) — the planning pipeline `/audit-to-stories`