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,191 @@
1
+ /**
2
+ * summary.js — plan-persist terminal summary (Epic #4474, PR3).
3
+ *
4
+ * Owns the single `plan-summary` structured comment the collapsed persist
5
+ * surface upserts at terminal success, closing with the dry-run wave table.
6
+ * This replaces two retired round-trips of the 12-phase pipeline:
7
+ *
8
+ * - the Phase 9 plan-time `dispatch-manifest` comment, whose claimed
9
+ * consumer ("Wave Completeness Gate, /deliver Step 0.5") does not exist
10
+ * — the live manifest is written at deliver time by `wave-record-io.js`
11
+ * (#4474 design §3: DROP, keep the wave table as summary text only);
12
+ * - the Phase 12 notify round-trip ("informational — no webhook for
13
+ * planning").
14
+ *
15
+ * The wave table is computed from the validated ticket set's `depends_on`
16
+ * slug edges via the same `computeStoryWaves` layering the deliver-time
17
+ * dispatch pipeline uses, so the preview matches what `/deliver` will
18
+ * actually fan out (barring later manual ticket edits).
19
+ *
20
+ * @module lib/orchestration/plan-persist/summary
21
+ */
22
+
23
+ import { computeStoryWaves } from '../dependency-analyzer.js';
24
+
25
+ /**
26
+ * Structured-comment type for the persist summary. Registered in
27
+ * `ticketing/reads.js` `STRUCTURED_COMMENT_TYPES`; upsert-idempotent so a
28
+ * `--force`/`--resume` re-persist replaces the prior summary in place.
29
+ */
30
+ export const PLAN_SUMMARY_COMMENT_TYPE = 'plan-summary';
31
+
32
+ /**
33
+ * Compute the dry-run wave assignment for a validated ticket set.
34
+ *
35
+ * Adapts the slug-keyed ticket shape onto `computeStoryWaves`' storyGroups
36
+ * contract (slug → { storyId, tasks: [] }; explicit deps from `depends_on`).
37
+ * Under the 2-tier hierarchy Stories carry no tasks, so all edges are
38
+ * explicit.
39
+ *
40
+ * @param {Array<{ slug: string, title?: string, depends_on?: string[] }>} tickets
41
+ * @returns {Array<{ wave: number, stories: Array<{ slug: string, title: string }> }>}
42
+ * Waves in execution order.
43
+ */
44
+ export function buildWaveTable(tickets) {
45
+ const list = Array.isArray(tickets) ? tickets : [];
46
+ if (list.length === 0) return [];
47
+ const storyGroups = new Map();
48
+ const explicitDeps = new Map();
49
+ for (const t of list) {
50
+ storyGroups.set(t.slug, { storyId: t.slug, tasks: [] });
51
+ explicitDeps.set(
52
+ t.slug,
53
+ (t.depends_on ?? []).filter((dep) => typeof dep === 'string'),
54
+ );
55
+ }
56
+ const assignment = computeStoryWaves(storyGroups, explicitDeps);
57
+ const byWave = new Map();
58
+ for (const t of list) {
59
+ const wave = assignment.get(t.slug) ?? 0;
60
+ if (!byWave.has(wave)) byWave.set(wave, []);
61
+ byWave.get(wave).push({ slug: t.slug, title: t.title ?? t.slug });
62
+ }
63
+ return [...byWave.keys()]
64
+ .sort((a, b) => a - b)
65
+ .map((wave) => ({ wave, stories: byWave.get(wave) }));
66
+ }
67
+
68
+ /**
69
+ * Render the dry-run wave table as GitHub-flavoured markdown.
70
+ *
71
+ * @param {ReturnType<typeof buildWaveTable>} waveTable
72
+ * @returns {string[]} markdown lines
73
+ */
74
+ function renderWaveTableLines(waveTable) {
75
+ if (!Array.isArray(waveTable) || waveTable.length === 0) {
76
+ return ['_No stories to wave (empty plan)._'];
77
+ }
78
+ const rows = waveTable.map(
79
+ ({ wave, stories }) =>
80
+ `| ${wave + 1} | ${stories.map((s) => `\`${s.slug}\``).join(', ')} |`,
81
+ );
82
+ return ['| Wave | Stories |', '| --- | --- |', ...rows];
83
+ }
84
+
85
+ /**
86
+ * Build the `plan-summary` structured-comment body: risk + routing +
87
+ * freshness + healthcheck receipts, closing with the mode-specific tail —
88
+ * the dry-run wave table for fan-out/amend, or the single-delivery routing
89
+ * record `{ deliveryShape, sliceCount, routingReasons }` (Epic #4474 PR4)
90
+ * for the spec-only mode.
91
+ *
92
+ * @param {{
93
+ * epicId: number,
94
+ * ticketCount: number,
95
+ * planningRisk: { overallLevel?: string, gateDecision?: string },
96
+ * reviewRouting: { decision?: string },
97
+ * freshness?: { stale?: number, ambiguous?: number },
98
+ * healthcheck?: { ok?: boolean, waived?: boolean, skipped?: boolean },
99
+ * waveTable: ReturnType<typeof buildWaveTable>,
100
+ * mode?: 'fan-out'|'single'|'amend',
101
+ * planMetricsLine?: string|null,
102
+ * single?: { deliveryShape: 'single', sliceCount: number|null, routingReasons: string[] }|null,
103
+ * amend?: {
104
+ * closed: Array<{ slug: string, issueNumber: number }>,
105
+ * recreated: Array<{ slug: string, oldIssueNumber: number, issueNumber: number }>,
106
+ * created: Array<{ slug: string, issueNumber: number }>,
107
+ * keptCount: number,
108
+ * }|null,
109
+ * }} input
110
+ * @returns {string}
111
+ */
112
+ export function buildPlanSummaryCommentBody({
113
+ epicId,
114
+ ticketCount,
115
+ planningRisk,
116
+ reviewRouting,
117
+ freshness,
118
+ healthcheck,
119
+ waveTable,
120
+ mode = 'fan-out',
121
+ planMetricsLine = null,
122
+ single = null,
123
+ amend = null,
124
+ }) {
125
+ const freshnessLine =
126
+ (freshness?.stale ?? 0) > 0 || (freshness?.ambiguous ?? 0) > 0
127
+ ? `- ⚠️ Spec freshness: ${freshness.stale} stale / ${freshness.ambiguous} ambiguous reference(s) — see the spec-freshness comment.`
128
+ : '- Spec freshness: clean.';
129
+ const healthcheckLine = healthcheck?.skipped
130
+ ? '- Healthcheck: skipped (test seam).'
131
+ : healthcheck?.ok
132
+ ? '- Healthcheck: passed.'
133
+ : `- Healthcheck: failed, waived by operator label.`;
134
+
135
+ const headLine =
136
+ mode === 'single'
137
+ ? `- Single-delivery plan (\`delivery::single\`): no Story tree — the Delivery Slicing table is the audit trail.`
138
+ : `- ${ticketCount} Story ticket(s) persisted across ${waveTable.length} wave(s).`;
139
+
140
+ const amendLines = amend
141
+ ? [
142
+ `- Amend delta: ${amend.created.length} added, ${amend.recreated.length} modified (closed + recreated), ${amend.closed.length} closed, ${amend.keptCount} kept untouched.`,
143
+ ]
144
+ : [];
145
+
146
+ const tail =
147
+ mode === 'single'
148
+ ? [
149
+ '#### Delivery routing record',
150
+ '',
151
+ '```json',
152
+ JSON.stringify(
153
+ {
154
+ deliveryShape: 'single',
155
+ sliceCount: single?.sliceCount ?? null,
156
+ routingReasons: single?.routingReasons ?? [],
157
+ },
158
+ null,
159
+ 2,
160
+ ),
161
+ '```',
162
+ '',
163
+ '_Marker is inert until #4475 lands the deliver-side reader — `/deliver` still treats this Epic as fan-out until then._',
164
+ ]
165
+ : [
166
+ '#### Dry-run wave table',
167
+ '',
168
+ ...renderWaveTableLines(waveTable),
169
+ '',
170
+ '_Preview only — the authoritative dispatch manifest is written at deliver time (`wave-record-io.js`)._',
171
+ ];
172
+
173
+ return [
174
+ `### 📋 Plan Summary — Epic #${epicId} is \`agent::ready\``,
175
+ '',
176
+ headLine,
177
+ ...amendLines,
178
+ `- Risk: ${planningRisk?.overallLevel ?? 'unknown'} · ${planningRisk?.gateDecision ?? 'unknown'} (review routing: ${reviewRouting?.decision ?? 'unknown'}).`,
179
+ freshnessLine,
180
+ healthcheckLine,
181
+ // G2 measurement receipt (Epic #4474 PR1/PR7): the plan-CLI invocation
182
+ // ledger roll-up (turns-per-plan proxy, per-mode counts, critic skips)
183
+ // rides the summary comment so the cohort reader never has to pull the
184
+ // temp ledger off the runner's disk. Omitted when the ledger is empty.
185
+ ...(typeof planMetricsLine === 'string' && planMetricsLine.length > 0
186
+ ? [`- ${planMetricsLine}`]
187
+ : []),
188
+ '',
189
+ ...tail,
190
+ ].join('\n');
191
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * plan-reachability.js — deterministic draft-ticket reachability check for
3
+ * the persist surface (Epic #4474 PR6, design §4: the 8.4 reachability
4
+ * critic demoted from a fresh-context sub-agent to a persist-side scan).
5
+ *
6
+ * Mirrors the mechanics of the existing `--paranoid` F7 healthcheck
7
+ * (`epic-plan-healthcheck.js#checkReachability`) — route-glob scan of the
8
+ * paths a Story declares vs the `planning.navigation.navRegistry` token
9
+ * list — but runs it over the **draft** ticket set inside `plan-persist.js`
10
+ * step 4.5, before any provider call, so an orphaned surface is caught
11
+ * while a one-line targeted amend is still free (nothing has been written
12
+ * to GitHub yet).
13
+ *
14
+ * Plan-level coverage semantics (the convergence contract): a route-adding
15
+ * story that never references the nav registry produces orphan surfaces —
16
+ * UNLESS every one of its route paths is also mentioned by some story in
17
+ * the plan that DOES reference the registry (the "navigation owner"). That
18
+ * is exactly what the documented recovery produces: the author appends the
19
+ * single reachability Story (which cites the orphaned routes and the nav
20
+ * registry) in one targeted amend, and the re-run persist passes.
21
+ *
22
+ * Silent no-op when `planning.navigation` is unconfigured (`routeGlobs`
23
+ * empty) — same opt-in contract as F7 / AC-13 — reported as
24
+ * `status: 'skipped'` so the caller can append the audit record to the
25
+ * plan-metrics ledger.
26
+ *
27
+ * Pure over its inputs (tickets + resolved config), no I/O.
28
+ */
29
+
30
+ import {
31
+ extractStoryPaths,
32
+ globToRegExp,
33
+ resolveNavConfig,
34
+ } from '../../epic-plan-healthcheck.js';
35
+
36
+ /**
37
+ * Fallback tokens when `navRegistry` is unconfigured but `routeGlobs` is —
38
+ * identical to the F7 healthcheck's fallback, so the two checks agree on
39
+ * what counts as a registry reference.
40
+ */
41
+ const FALLBACK_REGISTRY_TOKENS = ['nav registry', 'navigation'];
42
+
43
+ /**
44
+ * @typedef {Object} ReachabilityOrphan
45
+ * @property {string} story The offending draft story's slug (or title).
46
+ * @property {string[]} paths The route-matching paths with no navigation
47
+ * owner anywhere in the plan.
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} DraftReachabilityResult
52
+ * @property {'skipped'|'ok'|'orphans'} status
53
+ * @property {string[]} reasons
54
+ * @property {ReachabilityOrphan[]} orphans Empty unless `status` is
55
+ * `'orphans'`.
56
+ * @property {number} scanned Draft stories scanned (0 when skipped).
57
+ */
58
+
59
+ /**
60
+ * Evaluate draft-ticket reachability against the configured navigation
61
+ * surface.
62
+ *
63
+ * @param {object} input
64
+ * @param {Array<{ slug?: string, title?: string, body?: string }>} input.tickets
65
+ * The draft ticket set the persist is about to create (fan-out: the
66
+ * authored `tickets.json`; amend: the merged set).
67
+ * @param {object} [input.config] Resolved `.agentrc.json` (threads
68
+ * `planning.navigation`).
69
+ * @returns {DraftReachabilityResult}
70
+ */
71
+ export function evaluateDraftReachability({ tickets, config }) {
72
+ const { routeGlobs, navRegistry } = resolveNavConfig(config);
73
+
74
+ if (routeGlobs.length === 0) {
75
+ return {
76
+ status: 'skipped',
77
+ reasons: ['No planning.navigation.routeGlobs configured — skipped.'],
78
+ orphans: [],
79
+ scanned: 0,
80
+ };
81
+ }
82
+
83
+ const stories = Array.isArray(tickets) ? tickets : [];
84
+ const matchers = routeGlobs.map(globToRegExp);
85
+ const registryTokens =
86
+ navRegistry.length > 0
87
+ ? navRegistry.map((t) => t.toLowerCase())
88
+ : FALLBACK_REGISTRY_TOKENS;
89
+
90
+ // Pass 1: per-story scan — declared paths, route matches, registry refs.
91
+ const scannedStories = stories.map((story) => {
92
+ const body = typeof story?.body === 'string' ? story.body : '';
93
+ const paths = extractStoryPaths(body);
94
+ const routePaths = paths.filter((p) => matchers.some((rx) => rx.test(p)));
95
+ const text = [body, story?.title ?? ''].join('\n').toLowerCase();
96
+ const referencesRegistry = registryTokens.some((tok) => text.includes(tok));
97
+ return { story, paths, routePaths, referencesRegistry };
98
+ });
99
+
100
+ // Pass 2: navigation owners — every path mentioned by a
101
+ // registry-referencing story is covered plan-wide.
102
+ const coveredPaths = new Set();
103
+ for (const s of scannedStories) {
104
+ if (!s.referencesRegistry) continue;
105
+ for (const p of s.paths) coveredPaths.add(p);
106
+ }
107
+
108
+ const orphans = [];
109
+ for (const s of scannedStories) {
110
+ if (s.referencesRegistry || s.routePaths.length === 0) continue;
111
+ const uncovered = s.routePaths.filter((p) => !coveredPaths.has(p));
112
+ if (uncovered.length === 0) continue;
113
+ orphans.push({
114
+ story: s.story?.slug ?? s.story?.title ?? '<unnamed story>',
115
+ paths: uncovered,
116
+ });
117
+ }
118
+
119
+ if (orphans.length > 0) {
120
+ const registryHint =
121
+ navRegistry.length > 0 ? navRegistry.join(', ') : 'the nav registry';
122
+ return {
123
+ status: 'orphans',
124
+ reasons: [
125
+ `${orphans.length} route-adding draft story(ies) leave orphan surfaces with no navigation owner (registry: ${registryHint}).`,
126
+ ],
127
+ orphans,
128
+ scanned: stories.length,
129
+ };
130
+ }
131
+
132
+ return {
133
+ status: 'ok',
134
+ reasons: [
135
+ `${stories.length} draft story(ies) scanned — every route-adding story has a navigation owner.`,
136
+ ],
137
+ orphans: [],
138
+ scanned: stories.length,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Render the named soft-failure message the persist CLI prints — the
144
+ * orphan-surface list plus the one-targeted-amend recovery contract.
145
+ *
146
+ * @param {DraftReachabilityResult} result A `status: 'orphans'` result.
147
+ * @returns {string}
148
+ */
149
+ export function renderReachabilityOrphans(result) {
150
+ const lines = [
151
+ '[plan-persist] SOFT FAILURE — reachability orphans (route-glob vs navRegistry):',
152
+ ...result.orphans.map((o) => ` - ${o.story}: ${o.paths.join(', ')}`),
153
+ '',
154
+ 'Nothing was written to GitHub. Apply ONE targeted amend to tickets.json',
155
+ 'adding a navigation owner (at most one reachability Story per plan) that',
156
+ 'cites the orphaned routes and the nav registry, then re-run the persist',
157
+ 'once.',
158
+ ];
159
+ return lines.join('\n');
160
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * plan-runner/worktree-sweep.js
3
3
  *
4
- * Reap-sweep used at the start of `/epic-plan-spec` and
5
- * `/epic-plan-decompose` (via `drainPendingCleanupAtBoot` in
6
- * `epic-plan-spec.js`, which see). Iterates the `.worktrees/story-<id>/`
4
+ * Reap-sweep run at plan boot (via `drainPendingCleanupAtBoot` in
5
+ * `lib/orchestration/epic-plan-spec/phases/drain.js`, wired into
6
+ * `plan-persist.js`). Iterates the `.worktrees/story-<id>/`
7
7
  * entries registered with git, looks up each parent Story, and force-removes
8
8
  * any whose Story is already closed or labeled `agent::done`.
9
9
  *
@@ -138,7 +138,7 @@ function resolveRequiresReview(overallLevel, axes) {
138
138
  /**
139
139
  * Derive the stable planningRisk envelope from a schema-validated planner
140
140
  * verdict. Pure derivation — schema validation happens at the read boundary
141
- * (`epic-plan-spec.js`), never here, so a malformed verdict fails closed
141
+ * (`plan-persist.js` via `loadRiskVerdict`), never here, so a malformed verdict fails closed
142
142
  * before this function runs.
143
143
  *
144
144
  * **No-BDD-runner waiver (Story #4145).** The acceptance disposition the risk
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * `validateSpecFreshness` scans a Tech Spec body for path-shaped references,
12
12
  * probes each against `baseBranchRef` (via `git cat-file -e`), and returns a
13
- * `{ stale, fresh, ambiguous }` envelope. The caller (epic-plan-spec.js)
13
+ * `{ stale, fresh, ambiguous }` envelope. The caller (plan-persist.js)
14
14
  * uses the result to write a JSON report and post an advisory structured
15
15
  * comment on the Epic. The check is intentionally non-blocking —
16
16
  * planning continues even when stale references are present, because the
@@ -33,7 +33,7 @@ import {
33
33
  * 5xx, transport timeouts) inside the cascade transition. Three attempts with
34
34
  * exponential backoff (250ms / 500ms / 1000ms) mirrors the budget used by
35
35
  * `gitFetchWithRetry` (see `lib/git-utils.js`) and the HTTP-client retry path
36
- * referenced by `epic-plan-decompose.js`. Backoff is overridable via
36
+ * referenced by the plan persist surface. Backoff is overridable via
37
37
  * {@link __setCascadeRetryDelays} so tests don't pay real wall-clock time.
38
38
  */
39
39
  const CASCADE_RETRY_BACKOFF_MS = [250, 500, 1000];
@@ -102,7 +102,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
102
102
  // a `clarity-gate-update` comment on the Epic when the operator approves
103
103
  // a sharpened body rewrite, recording the persistence event for audit.
104
104
  'clarity-gate-update',
105
- // Story #2635 — Phase 7 Tech Spec freshness check. `epic-plan-spec.js`
105
+ // Story #2635 — Tech Spec freshness check. `plan-persist.js`
106
106
  // upserts a `spec-freshness` comment on the Epic listing any
107
107
  // path-shaped references that don't exist at the base branch, so the
108
108
  // operator can correct drift before Phase 8 decomposes from a stale
@@ -147,7 +147,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
147
147
  // the documented remediation actually executable
148
148
  // (assertValidStructuredCommentType would otherwise throw).
149
149
  'wave-stall',
150
- // Story #3873 (Epic #3865) — `epic-plan-spec.js` upserts a `risk-verdict`
150
+ // Story #3873 (Epic #3865) — `plan-persist.js` upserts a `risk-verdict`
151
151
  // comment on the Epic at persist time, recording the planner-authored,
152
152
  // schema-validated risk verdict and the planningRisk envelope derived
153
153
  // from it (`deriveRiskEnvelope`). One entry per Epic; re-plans upsert in
@@ -171,6 +171,15 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
171
171
  // `graduator="audit-results|code-review"` attr so the two graduators
172
172
  // upsert independent comments; re-runs upsert in place.
173
173
  'cross-repo-deferred',
174
+ // Epic #4474 (PR3) — `plan-persist.js` upserts a single `plan-summary`
175
+ // comment on the Epic at terminal persist success, carrying the risk /
176
+ // routing / freshness / healthcheck receipts and the dry-run wave table
177
+ // as closing text. Replaces the retired plan-time `dispatch-manifest`
178
+ // comment (whose claimed consumer did not exist — the live manifest is
179
+ // written at deliver time by `wave-record-io.js`) and the Phase 12
180
+ // notify round-trip. One entry per Epic; a --force/--resume re-persist
181
+ // upserts in place.
182
+ 'plan-summary',
174
183
  ]);
175
184
 
176
185
  export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
@@ -41,6 +41,22 @@ export const PHASE_TEMP_BASENAMES = Object.freeze({
41
41
  'acceptance-spec.md',
42
42
  ]),
43
43
  decompose: Object.freeze(['decomposer-context.json', 'tickets.json']),
44
+ // Epic #4474 (PR3) — the collapsed `plan-persist.js` surface owns every
45
+ // plan-phase artifact and deletes them ONLY at terminal success (after
46
+ // the `agent::ready` flip), fixing the mid-pipeline deletion defect where
47
+ // per-phase cleanup removed artifacts a `--force`/`--resume` re-persist
48
+ // was still entitled to reuse. `risk-verdict.json` joins the set here:
49
+ // the split-phase cleanup never owned it, which orphaned it in temp/.
50
+ // `plan-metrics.json` stays deliberately excluded (PR1) — the ledger
51
+ // must survive cleanup so the whole plan run is visible in one stream.
52
+ persist: Object.freeze([
53
+ 'planner-context.json',
54
+ 'techspec.md',
55
+ 'acceptance-spec.md',
56
+ 'risk-verdict.json',
57
+ 'decomposer-context.json',
58
+ 'tickets.json',
59
+ ]),
44
60
  });
45
61
 
46
62
  /**
@@ -120,10 +120,9 @@ export function deleteLegacyFlatManifest(epicId, opts = {}) {
120
120
  * Atomic write-then-rename. On any failure, best-effort remove the `.tmp`
121
121
  * file and rethrow so the caller can surface a structured result.
122
122
  *
123
- * Exported so other writers (e.g. `render-manifest.js`) can route their
124
- * `.md` / `.json` artefact writes through the same crash-safe primitive.
123
+ * Internal crash-safe write primitive for the manifest artefact pair.
125
124
  */
126
- export function atomicWrite(finalPath, content) {
125
+ function atomicWrite(finalPath, content) {
127
126
  const tmpPath = `${finalPath}.tmp`;
128
127
  try {
129
128
  fs.writeFileSync(tmpPath, content, 'utf8');
@@ -0,0 +1,74 @@
1
+ /**
2
+ * spec-author-prompts.js — single source of the Tech Spec and Acceptance
3
+ * Spec authoring system prompts (Epic #4474, M3 PR2).
4
+ *
5
+ * **Single source of the prompt bodies.** These two prompts were lifted
6
+ * VERBATIM from the "(authoritative)" fenced blocks in
7
+ * `.agents/skills/core/epic-plan-spec-author/SKILL.md` (§ "Tech Spec system
8
+ * prompt" and § "Acceptance Spec system prompt") as the M3 side of the
9
+ * M3/M8 boundary handshake: the `plan-context.js` envelope renders them
10
+ * into `systemPrompts.spec` / `systemPrompts.acceptance` so the envelope
11
+ * is authoritative from day one, and M8 (#4479) then deletes the
12
+ * skill-side copies. This mirrors the Story #4162 pattern established by
13
+ * `decomposer-prompts.js` for the decompose prompt: one rendered carrier,
14
+ * no second verbatim copy to drift.
15
+ */
16
+
17
+ /**
18
+ * The Tech Spec authoring system prompt (verbatim from the
19
+ * `epic-plan-spec-author` SKILL's authoritative block).
20
+ *
21
+ * @returns {string}
22
+ */
23
+ export function renderTechSpecSystemPrompt() {
24
+ return TECH_SPEC_SYSTEM_PROMPT;
25
+ }
26
+
27
+ /**
28
+ * The Acceptance Spec authoring system prompt (verbatim from the
29
+ * `epic-plan-spec-author` SKILL's authoritative block).
30
+ *
31
+ * @returns {string}
32
+ */
33
+ export function renderAcceptanceSpecSystemPrompt() {
34
+ return ACCEPTANCE_SPEC_SYSTEM_PROMPT;
35
+ }
36
+
37
+ const TECH_SPEC_SYSTEM_PROMPT = `You are an expert Engineering Architect.
38
+ Your job is to convert an Epic into a Technical Specification for implementation.
39
+
40
+ The Tech Spec should outline:
41
+ 1. Delivery Slicing — propose how the Epic's enumerated capabilities cluster into shippable Stories. This count is a CEILING, not a target: the Phase 8 consolidation pass may merge below your proposed count when slices form dependent single-consumer chains, but never splits above it. Do NOT coarsen the Epic enumeration to produce this; the grouping recommendation is the granularity lever.
42
+ 2. Architecture & Design
43
+ 3. Data Models (if any)
44
+ 4. API Changes (if any)
45
+ 5. Core Components
46
+ 6. Security & Privacy Considerations
47
+
48
+ CRITICAL REQUIREMENTS:
49
+ - Respond ONLY with valid Markdown.
50
+ - Do not use top-level <h1> (# ) tags. Open the document with the \`## Delivery Slicing\` section — it is the primary input to Phase 8 consolidation, so author it first and hang the rest of the spec off it.
51
+ - Do NOT restate the Epic's Context, Goal, or Scope — your output lands as sections of the same Epic body, which travels into every downstream story agent's prompt, so any restatement is pure duplication and a drift risk. If a brief technical orientation is genuinely useful, add an optional \`## Technical Overview\` of no more than 2–3 sentences that names the *technical approach* only (which subsystems are touched and reused); never re-narrate the problem statement, goals, or scope.
52
+ - Format architectural decisions clearly with bullet points.
53
+ - Author the \`## Delivery Slicing\` section as a markdown table with columns \`Slice | What ships | Independent?\`, using noun-phrase slice names (e.g. "Foundation", "Transport seam", "Send helper") that map onto Feature titles. "Independent?" answers: can this slice ship to production and provide value without the next slice landing? A slice you mark "Independent? No" MUST carry a one-line justification (parallelism, risk isolation, or delivery-envelope pressure); an unjustified dependent single-consumer slice folds into its consumer by default rather than shipping as its own Story.`;
54
+
55
+ const ACCEPTANCE_SPEC_SYSTEM_PROMPT = `You are an expert Acceptance Engineer.
56
+ Your job is to convert an Epic and a Tech Spec into a structured Acceptance Specification that drives features-first BDD authoring.
57
+
58
+ The Acceptance Spec should outline:
59
+ 1. Acceptance Table — one row per user-visible outcome, expressed as a Markdown table with columns: AC ID | Outcome | Feature File | Scenario | Disposition
60
+ 2. Stable AC IDs — assign AC-1, AC-2, ... in document order; reuse the same ID across re-plans when an Outcome is materially unchanged so scenario tags (@ac-N) stay aligned
61
+ 3. Disposition — tag each row with one of: new | updated | unchanged
62
+
63
+ The Epic body's \`## Acceptance Criteria\` bullets are the single source of truth for what the spec verifies. Your table does not re-invent criteria — it anchors each one to a specific Epic AC bullet.
64
+
65
+ CRITICAL REQUIREMENTS:
66
+ - Respond ONLY with valid Markdown.
67
+ - Do not use top-level <h1> (# ) tags. Start with ## Acceptance Table — the table lands as a section of the Epic body, so it must NOT reuse the Epic's own ## Acceptance Criteria heading.
68
+ - Every AC row MUST have a stable AC ID of the form AC-<n> (AC-1, AC-2, ...) — do not reorder IDs across re-plans; new ACs get fresh sequential IDs.
69
+ - Every AC row MUST carry a Disposition value from the enum: new | updated | unchanged. (At Epic close, the acceptance reconciler overwrites Disposition with the verification outcome — satisfied | pending | missing — inside this section only; on re-plan, reset each row to the authoring enum.)
70
+ - Each Outcome MUST be a **terse restatement keyed to a specific Epic \`## Acceptance Criteria\` bullet** — lead the Outcome with the bullet's anchor (its quoted lead phrase or an explicit "Epic AC N" index) and keep the rest to a single user-visible behaviour. Do NOT re-elaborate the Epic bullet in independent words: a free-standing Outcome that paraphrases the criterion without naming the bullet it verifies is forbidden, because it drifts from the Epic silently. No DB assertions, no HTTP status codes, no internal implementation details.
71
+ - Where one Epic AC bullet genuinely expands into several user-visible outcomes, emit one row per outcome and declare the split on each — e.g. lead with "splits Epic AC 3" — so the fan-out is explicit rather than hidden.
72
+ - Anchor coverage MUST be complete and auditable: every Epic AC bullet MUST be covered by at least one row, and every row MUST anchor to an Epic AC bullet. Flag divergence in the authored spec instead of dropping it — if an Epic AC bullet has no corresponding row, or a row has no Epic anchor, call it out explicitly (a note beneath the table) rather than silently omitting the bullet or emitting an unanchored row.
73
+ - Cite proposed feature file paths under tests/features/** so Phase 8 can scaffold matching scenarios.
74
+ - Acceptance Outcomes MUST NOT prescribe a commit subject that begins with a non-Conventional-Commits prefix (allowed leading types: feat|fix|chore|refactor|perf|docs|style|test|build|ci|revert). The legacy \`baseline-refresh\` token used as a leading subject prescription is forbidden — commitlint will reject it at commit time, and the decompose-time validator (\`ticket-validator.js\` → \`validateAcceptanceSubjectPrefix\`) will reject the decompose with \`code: 'forbidden-subject-prefix'\`. Use a Conventional-Commits subject (e.g. \`chore(baselines): refresh ...\`) and a body trailer (e.g. \`baseline-refresh: true\` — trailer with a value, not a subject prefix) when a machine-readable marker is needed. See Epic #2501 for rationale.`;