mandrel 1.80.0 → 1.82.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 (101) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +93 -80
  3. package/.agents/docs/configuration.md +11 -1
  4. package/.agents/docs/quality-gates.md +61 -0
  5. package/.agents/docs/workflows.md +1 -1
  6. package/.agents/instructions.md +9 -7
  7. package/.agents/personas/architect.md +8 -5
  8. package/.agents/personas/engineer-mobile.md +3 -2
  9. package/.agents/personas/engineer-web.md +3 -2
  10. package/.agents/personas/engineer.md +6 -5
  11. package/.agents/personas/product.md +19 -13
  12. package/.agents/personas/project-manager.md +9 -8
  13. package/.agents/personas/qa-engineer.md +10 -6
  14. package/.agents/personas/refactorer.md +3 -2
  15. package/.agents/personas/technical-writer.md +2 -1
  16. package/.agents/personas/ux-designer.md +2 -2
  17. package/.agents/schemas/agentrc.schema.json +10 -0
  18. package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
  19. package/.agents/scripts/epic-deliver-prepare.js +0 -31
  20. package/.agents/scripts/epic-plan-decompose.js +2 -5
  21. package/.agents/scripts/epic-plan-spec.js +16 -19
  22. package/.agents/scripts/hierarchy-gate.js +11 -11
  23. package/.agents/scripts/lib/ITicketingProvider.js +4 -3
  24. package/.agents/scripts/lib/baselines/env-overrides.js +35 -0
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
  27. package/.agents/scripts/lib/cli-args.js +1 -5
  28. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  29. package/.agents/scripts/lib/config/temp-paths.js +1 -4
  30. package/.agents/scripts/lib/config-settings-schema.js +5 -0
  31. package/.agents/scripts/lib/epic-body-sections.js +222 -0
  32. package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
  33. package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
  34. package/.agents/scripts/lib/label-constants.js +7 -17
  35. package/.agents/scripts/lib/label-taxonomy.js +4 -21
  36. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +47 -1
  37. package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
  38. package/.agents/scripts/lib/orchestration/check-baselines/phases/pipeline.js +1 -1
  39. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +2 -1
  40. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +19 -8
  41. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
  42. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
  43. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
  44. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
  45. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
  46. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
  47. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
  48. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
  49. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
  50. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
  51. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
  52. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
  53. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
  54. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
  55. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
  56. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
  57. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  58. package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
  59. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  60. package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
  61. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  62. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
  63. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
  64. package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
  65. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
  66. package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
  67. package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
  68. package/.agents/scripts/lifecycle-emit.js +1 -1
  69. package/.agents/scripts/lint-label-vocabulary.js +2 -3
  70. package/.agents/scripts/providers/github/mappers.js +0 -3
  71. package/.agents/scripts/providers/github/tickets.js +7 -18
  72. package/.agents/scripts/single-story-init.js +0 -1
  73. package/.agents/scripts/story-init.js +1 -29
  74. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +37 -18
  75. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -18
  76. package/.agents/skills/core/epic-plan-premortem/SKILL.md +7 -6
  77. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +161 -109
  78. package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
  79. package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
  80. package/.agents/skills/core/scope-triage/SKILL.md +2 -1
  81. package/.agents/skills/skills.index.json +6 -6
  82. package/.agents/templates/epic-from-idea.md +4 -0
  83. package/.agents/workflows/audit-to-stories.md +2 -2
  84. package/.agents/workflows/helpers/code-review.md +11 -9
  85. package/.agents/workflows/helpers/deliver-epic.md +32 -44
  86. package/.agents/workflows/helpers/epic-audit.md +11 -8
  87. package/.agents/workflows/helpers/epic-deliver-story.md +10 -16
  88. package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
  89. package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
  90. package/.agents/workflows/helpers/parallel-tooling.md +2 -1
  91. package/.agents/workflows/helpers/plan-epic.md +114 -99
  92. package/.agents/workflows/helpers/single-story-deliver.md +1 -1
  93. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
  94. package/.agents/workflows/plan.md +8 -8
  95. package/.agents/workflows/qa-assist.md +2 -1
  96. package/docs/CHANGELOG.md +28 -0
  97. package/package.json +1 -1
  98. package/.agents/scripts/lib/issue-link-parser.js +0 -74
  99. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
  100. package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
  101. package/.agents/scripts/lib/story-init/hierarchy-tracer.js +0 -57
@@ -1,25 +1,39 @@
1
1
  /**
2
- * phases/plan-epic.js — PRD / Tech Spec / Acceptance Spec persistence phase.
2
+ * phases/plan-epic.js — Tech Spec / Acceptance Table persistence phase.
3
3
  *
4
- * Heals any prior planning artifacts (PRD / Tech Spec issues, "Planning
5
- * Artifacts" body section, lifecycle labels) before writing the new issues.
6
- * Idempotent against partial state: when the Epic already has a PRD but no
7
- * Tech Spec, the existing PRD is reused. Pass `force: true` to re-plan: the
8
- * canonical context tickets (PRD / Tech Spec / Acceptance Spec) are
9
- * **overwritten in place** same issue numbers, refreshed bodies, kept open,
10
- * with a one-line regeneration audit comment on each. Only redundant
11
- * duplicate artifacts are closed; Feature/Story child tickets retain
12
- * close-and-recreate behaviour (handled by the decomposer, not here).
4
+ * Story #4324: the `context::tech-spec` / `context::acceptance-spec` ticket
5
+ * classes are retired. The Epic body is the single planning document: this
6
+ * phase upserts the authored Tech Spec (opening with `## Delivery Slicing`)
7
+ * and the Acceptance Spec's AC-ID table (`## Acceptance Table`) as
8
+ * marker-delimited managed sections of the Epic body see
9
+ * `lib/epic-body-sections.js`. Each write is **section-scoped**: only the
10
+ * managed region is replaced; the rest of the body is byte-preserved.
11
+ *
12
+ * Idempotent against partial state: when the Epic body already carries all
13
+ * requested sections, the phase short-circuits with `already-planned`.
14
+ * Pass `force: true` to re-plan: the managed sections are overwritten in
15
+ * place and a one-line regeneration audit comment lands on the Epic.
16
+ *
17
+ * The retired machine-managed `## Planning Artifacts` checklist (which
18
+ * linked the old context tickets) is stripped on every persist so a
19
+ * re-planned historical Epic stops advertising stale ticket links; the
20
+ * legacy tickets themselves are ignored (forward-only cutover — no
21
+ * backfill, no fetch).
13
22
  */
14
23
 
24
+ import {
25
+ hasEpicSection,
26
+ stripEpicSection,
27
+ stripPlanningArtifactsSection,
28
+ upsertEpicSection,
29
+ } from '../../../epic-body-sections.js';
15
30
  import { Logger } from '../../../Logger.js';
16
31
  import { ACCEPTANCE_NA } from '../../../label-constants.js';
17
- import { PlanningStateManager } from '../../planning-state-manager.js';
18
32
 
19
33
  /**
20
- * Resolve whether Phase 7 should persist an acceptance-spec ticket or apply
21
- * the `acceptance::n-a` waiver, from the planningRisk envelope derived off
22
- * the planner-authored risk verdict (`deriveRiskEnvelope`, Epic #3865).
34
+ * Resolve whether Phase 7 should persist an acceptance-table section or
35
+ * apply the `acceptance::n-a` waiver, from the planningRisk envelope derived
36
+ * off the planner-authored risk verdict (`deriveRiskEnvelope`, Epic #3865).
23
37
  *
24
38
  * @param {import('../../planning-risk.js').PlanningRiskEnvelope|null} planningRisk
25
39
  * Derived envelope; `null` (direct invocations without a verdict) never
@@ -50,52 +64,10 @@ export function resolveAcceptancePersistence(
50
64
  };
51
65
  }
52
66
 
53
- /**
54
- * Overwrite an existing context ticket (PRD / Tech Spec / Acceptance Spec)
55
- * in place: push the freshly-authored body and refresh the title prefix so a
56
- * clarity-gate Epic rename does not strand a stale spec title, then post a
57
- * single one-line regeneration audit comment so the preserved discussion
58
- * history stays self-explanatory.
59
- *
60
- * The ticket keeps its issue number, its sub-issue link to the Epic, and all
61
- * pre-existing comments — only the body, title, and a new audit comment are
62
- * added.
63
- *
64
- * @param {import('../../../ITicketingProvider.js').ITicketingProvider} provider
65
- * @param {number} ticketId Existing context-ticket issue number.
66
- * @param {{ title: string, body: string, artifact: string }} fields
67
- * `title` is the refreshed `[PRD] <epic title>`-style prefix; `body` is the
68
- * regenerated content; `artifact` is the human label used in the audit
69
- * comment (`PRD`, `Tech Spec`, `Acceptance Spec`).
70
- * @returns {Promise<void>}
71
- */
72
- export async function overwriteContextTicket(
73
- provider,
74
- ticketId,
75
- { title, body, artifact },
76
- ) {
77
- await provider.updateTicket(ticketId, { title, body });
78
- try {
79
- await provider.postComment(ticketId, {
80
- type: 'notification',
81
- body: `♻️ **Regeneration Audit**: This ${artifact} body was regenerated in place by a \`/plan --force\` re-plan. The issue number and prior discussion history are preserved.`,
82
- });
83
- } catch (_err) {
84
- // Audit comment is best-effort — never fail the overwrite on a comment
85
- // post error.
86
- }
87
- }
88
-
89
67
  export function validatePlanEpicInputs({
90
- prdContent,
91
68
  techSpecContent,
92
69
  acceptanceSpecContent,
93
70
  }) {
94
- if (typeof prdContent !== 'string' || prdContent.trim() === '') {
95
- throw new Error(
96
- '[Epic Planner] prdContent is required and must be non-empty.',
97
- );
98
- }
99
71
  if (typeof techSpecContent !== 'string' || techSpecContent.trim() === '') {
100
72
  throw new Error(
101
73
  '[Epic Planner] techSpecContent is required and must be non-empty.',
@@ -112,184 +84,46 @@ export function validatePlanEpicInputs({
112
84
  }
113
85
  }
114
86
 
115
- export function getExistingArtifactIds(epic) {
87
+ /**
88
+ * Snapshot which managed planning sections the Epic body already carries.
89
+ *
90
+ * @param {{ body?: string }} epic
91
+ * @returns {{ techSpec: boolean, acceptanceTable: boolean }}
92
+ */
93
+ export function getExistingSections(epic) {
94
+ const body = epic?.body ?? '';
116
95
  return {
117
- prd: epic.linkedIssues?.prd ?? null,
118
- techSpec: epic.linkedIssues?.techSpec ?? null,
119
- acceptanceSpec: epic.linkedIssues?.acceptanceSpec ?? null,
96
+ techSpec: hasEpicSection(body, 'techSpec'),
97
+ acceptanceTable: hasEpicSection(body, 'acceptanceTable'),
120
98
  };
121
99
  }
122
100
 
123
- export function hasAllRequestedArtifacts({ existing, wantsAcceptanceSpec }) {
101
+ export function hasAllRequestedSections({ existing, wantsAcceptanceSpec }) {
124
102
  return Boolean(
125
- existing.prd &&
126
- existing.techSpec &&
127
- (wantsAcceptanceSpec ? existing.acceptanceSpec : true),
128
- );
129
- }
130
-
131
- async function persistPrd({
132
- provider,
133
- epicId,
134
- epic,
135
- prdContent,
136
- existingId,
137
- force,
138
- }) {
139
- if (existingId) {
140
- if (force) {
141
- Logger.info(
142
- `[Epic Planner] --force: Overwriting PRD #${existingId} in place.`,
143
- );
144
- await overwriteContextTicket(provider, existingId, {
145
- title: `[PRD] ${epic.title}`,
146
- body: prdContent,
147
- artifact: 'PRD',
148
- });
149
- } else {
150
- Logger.info(
151
- `[Epic Planner] Reusing existing PRD #${existingId}. Skipping PRD creation.`,
152
- );
153
- }
154
- return existingId;
155
- }
156
-
157
- Logger.info(`[Epic Planner] Creating PRD issue for "${epic.title}"...`);
158
- const prdTicket = await provider.createTicket(epicId, {
159
- title: `[PRD] ${epic.title}`,
160
- body: prdContent,
161
- labels: ['context::prd'],
162
- dependencies: [],
163
- });
164
- Logger.info(
165
- `[Epic Planner] Created PRD Issue #${prdTicket.id} (${prdTicket.url})`,
166
- );
167
- return prdTicket.id;
168
- }
169
-
170
- async function persistTechSpec({
171
- provider,
172
- epicId,
173
- epic,
174
- techSpecContent,
175
- prdId,
176
- existingId,
177
- force,
178
- }) {
179
- if (existingId) {
180
- if (force) {
181
- Logger.info(
182
- `[Epic Planner] --force: Overwriting Tech Spec #${existingId} in place.`,
183
- );
184
- await overwriteContextTicket(provider, existingId, {
185
- title: `[Tech Spec] ${epic.title}`,
186
- body: techSpecContent,
187
- artifact: 'Tech Spec',
188
- });
189
- } else {
190
- Logger.info(
191
- `[Epic Planner] Reusing existing Tech Spec #${existingId}. Skipping Tech Spec creation.`,
192
- );
193
- }
194
- return existingId;
195
- }
196
-
197
- Logger.info(
198
- `[Epic Planner] Creating Tech Spec issue linking to PRD #${prdId}...`,
199
- );
200
- const techSpecTicket = await provider.createTicket(epicId, {
201
- title: `[Tech Spec] ${epic.title}`,
202
- body: techSpecContent,
203
- labels: ['context::tech-spec'],
204
- dependencies: [prdId],
205
- });
206
- Logger.info(
207
- `[Epic Planner] Created Tech Spec Issue #${techSpecTicket.id} (${techSpecTicket.url})`,
103
+ existing.techSpec &&
104
+ (wantsAcceptanceSpec ? existing.acceptanceTable : true),
208
105
  );
209
- return techSpecTicket.id;
210
- }
211
-
212
- async function persistAcceptanceSpec({
213
- provider,
214
- epicId,
215
- epic,
216
- acceptanceSpecContent,
217
- techSpecId,
218
- existingId,
219
- force,
220
- }) {
221
- if (existingId) {
222
- if (force) {
223
- Logger.info(
224
- `[Epic Planner] --force: Overwriting Acceptance Spec #${existingId} in place.`,
225
- );
226
- await overwriteContextTicket(provider, existingId, {
227
- title: `[Acceptance Spec] ${epic.title}`,
228
- body: acceptanceSpecContent,
229
- artifact: 'Acceptance Spec',
230
- });
231
- } else {
232
- Logger.info(
233
- `[Epic Planner] Reusing existing Acceptance Spec #${existingId}. Skipping Acceptance Spec creation.`,
234
- );
235
- }
236
- return existingId;
237
- }
238
-
239
- Logger.info(
240
- `[Epic Planner] Creating Acceptance Spec issue linking to Tech Spec #${techSpecId}...`,
241
- );
242
- const acceptanceTicket = await provider.createTicket(epicId, {
243
- title: `[Acceptance Spec] ${epic.title}`,
244
- body: acceptanceSpecContent,
245
- labels: ['context::acceptance-spec'],
246
- dependencies: [techSpecId],
247
- });
248
- Logger.info(
249
- `[Epic Planner] Created Acceptance Spec Issue #${acceptanceTicket.id} (${acceptanceTicket.url})`,
250
- );
251
- return acceptanceTicket.id;
252
- }
253
-
254
- async function closeWaivedAcceptanceSpec({
255
- provider,
256
- epicId,
257
- epic,
258
- existingAcceptanceSpecId,
259
- }) {
260
- Logger.info(
261
- `[Epic Planner] Acceptance disposition now waived — closing existing Acceptance Spec #${existingAcceptanceSpecId}.`,
262
- );
263
- try {
264
- await provider.updateTicket(existingAcceptanceSpecId, {
265
- state: 'closed',
266
- state_reason: 'not_planned',
267
- });
268
- } catch (err) {
269
- if (!err.message.includes('404') && !err.message.includes('410')) {
270
- throw err;
271
- }
272
- }
273
- try {
274
- await provider.removeSubIssue(epicId, existingAcceptanceSpecId);
275
- } catch (_err) {
276
- // Already detached or unsupported — safe to ignore.
277
- }
278
- if (epic.linkedIssues) epic.linkedIssues.acceptanceSpec = null;
279
106
  }
280
107
 
281
108
  /**
282
- * Persist the host-authored PRD and Tech Spec under the Epic.
109
+ * Persist the host-authored Tech Spec (and optional Acceptance Table) as
110
+ * managed sections of the Epic body.
111
+ *
112
+ * @returns {Promise<{
113
+ * persisted: boolean,
114
+ * reason: 'already-planned'|'persisted'|'force-replan',
115
+ * techSpecPersisted: boolean,
116
+ * acceptanceTable: 'persisted'|'waived'|'none',
117
+ * }>}
283
118
  */
284
119
  export async function planEpic(
285
120
  epicId,
286
121
  provider,
287
- { prdContent, techSpecContent, acceptanceSpecContent = null },
122
+ { techSpecContent, acceptanceSpecContent = null },
288
123
  _settings = {},
289
124
  { force = false, planningRisk = null } = {},
290
125
  ) {
291
126
  validatePlanEpicInputs({
292
- prdContent,
293
127
  techSpecContent,
294
128
  acceptanceSpecContent,
295
129
  });
@@ -301,111 +135,59 @@ export async function planEpic(
301
135
  throw new Error(`Epic #${epicId} not found.`);
302
136
  }
303
137
 
304
- const stateManager = new PlanningStateManager(provider);
305
- await stateManager.healAndCleanupArtifacts(epic, force);
306
-
307
138
  const { wantsAcceptanceSpec, applyAcceptanceWaiver } =
308
139
  resolveAcceptancePersistence(planningRisk, acceptanceSpecContent);
309
140
 
310
141
  Logger.info(
311
142
  `[Epic Planner] Acceptance disposition: ${planningRisk?.acceptanceDisposition ?? 'unspecified'}` +
312
143
  (applyAcceptanceWaiver
313
- ? ` — applying ${ACCEPTANCE_NA} waiver (no acceptance-spec ticket).`
144
+ ? ` — applying ${ACCEPTANCE_NA} waiver (no acceptance-table section).`
314
145
  : wantsAcceptanceSpec
315
- ? ' — persisting context::acceptance-spec.'
146
+ ? ' — persisting the ## Acceptance Table section.'
316
147
  : ' — no acceptance-spec content supplied.'),
317
148
  );
318
149
 
319
- const existing = getExistingArtifactIds(epic);
320
- if (!force && hasAllRequestedArtifacts({ existing, wantsAcceptanceSpec })) {
150
+ const existing = getExistingSections(epic);
151
+ if (!force && hasAllRequestedSections({ existing, wantsAcceptanceSpec })) {
321
152
  Logger.warn(
322
- `[Epic Planner] Epic #${epicId} already has all requested planning artifacts. Aborting to prevent duplicates. Use --force to re-plan.`,
153
+ `[Epic Planner] Epic #${epicId} body already carries all requested planning sections. Aborting to prevent an unintended overwrite. Use --force to re-plan.`,
323
154
  );
324
155
  return {
325
156
  persisted: false,
326
157
  reason: 'already-planned',
327
- prdId: existing.prd,
328
- techSpecId: existing.techSpec,
329
- acceptanceSpecId: existing.acceptanceSpec,
158
+ techSpecPersisted: existing.techSpec,
159
+ acceptanceTable: existing.acceptanceTable ? 'persisted' : 'none',
330
160
  };
331
161
  }
332
- // Under --force we now OVERWRITE the canonical context tickets in place
333
- // (same issue numbers, refreshed bodies) rather than closing + recreating
334
- // them. `healAndCleanupArtifacts(epic, force=true)` preserved the canonical
335
- // IDs on `epic.linkedIssues`, so reuse them in both the force and non-force
336
- // paths. The difference: under force we push the freshly-authored body via
337
- // `provider.updateTicket`, whereas the non-force partial-state reuse keeps
338
- // the existing body untouched.
339
- const prdId = await persistPrd({
340
- provider,
341
- epicId,
342
- epic,
343
- prdContent,
344
- existingId: existing.prd,
345
- force,
346
- });
347
- const techSpecId = await persistTechSpec({
348
- provider,
349
- epicId,
350
- epic,
351
- techSpecContent,
352
- prdId,
353
- existingId: existing.techSpec,
354
- force,
355
- });
356
162
 
357
- let acceptanceSpecId = null;
163
+ // Section-scoped writes: each upsert replaces only its own managed
164
+ // region; the ideation sections and any operator-authored prose are
165
+ // byte-preserved. The retired `## Planning Artifacts` checklist is
166
+ // stripped so re-planned historical Epics stop advertising the old
167
+ // context-ticket links (the legacy tickets themselves are ignored).
168
+ let newBody = stripPlanningArtifactsSection(epic.body ?? '');
169
+ newBody = upsertEpicSection(newBody, 'techSpec', techSpecContent);
170
+
171
+ let acceptanceTable = 'none';
358
172
  if (wantsAcceptanceSpec) {
359
- acceptanceSpecId = await persistAcceptanceSpec({
360
- provider,
361
- epicId,
362
- epic,
173
+ newBody = upsertEpicSection(
174
+ newBody,
175
+ 'acceptanceTable',
363
176
  acceptanceSpecContent,
364
- techSpecId,
365
- existingId: existing.acceptanceSpec,
366
- force,
367
- });
368
- } else if (applyAcceptanceWaiver && existing.acceptanceSpec) {
369
- // Acceptance-spec transition: was present, now waived (acceptance::n-a).
370
- // This is a genuine close — there is no longer an acceptance spec to
371
- // overwrite — and the stale ticket must be detached so the Epic body's
372
- // Planning Artifacts section stops referencing it.
373
- await closeWaivedAcceptanceSpec({
374
- provider,
375
- epicId,
376
- epic,
377
- existingAcceptanceSpecId: existing.acceptanceSpec,
378
- });
379
- }
380
-
381
- Logger.info(
382
- `[Epic Planner] Updating Epic #${epicId} with linked documents...`,
383
- );
384
-
385
- // Format exactly so the issue-link-parser regexes still catch each line.
386
- // The parser is the source of truth for which prefixes are accepted; we
387
- // emit the canonical "PRD: #N" / "Tech Spec: #N" / "Acceptance Spec: #N"
388
- // shape so the epic-deliver finalize/cascade-close call shape and the
389
- // Phase 2 decomposer-context picker both see the third link.
390
- const artifactLines = [
391
- `- [ ] PRD: #${prdId}`,
392
- `- [ ] Tech Spec: #${techSpecId}`,
393
- ];
394
- if (acceptanceSpecId !== null) {
395
- artifactLines.push(`- [ ] Acceptance Spec: #${acceptanceSpecId}`);
177
+ );
178
+ acceptanceTable = 'persisted';
179
+ } else if (applyAcceptanceWaiver) {
180
+ // Acceptance transition: was present, now waived (acceptance::n-a).
181
+ // Strip the stale table so the close-time reconciler cannot read a
182
+ // section the planner no longer stands behind.
183
+ if (existing.acceptanceTable) {
184
+ Logger.info(
185
+ `[Epic Planner] Acceptance disposition now waived — removing the stale ## Acceptance Table section from Epic #${epicId}.`,
186
+ );
187
+ newBody = stripEpicSection(newBody, 'acceptanceTable');
188
+ }
189
+ acceptanceTable = 'waived';
396
190
  }
397
- // Idempotent append (Story #4019): strip any pre-existing
398
- // `## Planning Artifacts` section before re-appending. The `--force`
399
- // path already stripped it in `healAndCleanupArtifacts`, but the
400
- // partial-recovery rerun (e.g. PRD present, Tech Spec missing) reaches
401
- // here with a body that may still carry a stale section — without the
402
- // strip, every rerun stacked a duplicate section onto the Epic body.
403
- const appendBody = `\n\n## Planning Artifacts\n${artifactLines.join('\n')}\n`;
404
- const strippedBody = epic.body.replace(
405
- /\n*## Planning Artifacts[\s\S]*$/,
406
- '',
407
- );
408
- const newBody = strippedBody + appendBody;
409
191
 
410
192
  /** @type {{ add?: string[], remove?: string[] }} */
411
193
  const labelMutations = {};
@@ -418,6 +200,9 @@ export async function planEpic(
418
200
  labelMutations.remove = [ACCEPTANCE_NA];
419
201
  }
420
202
 
203
+ Logger.info(
204
+ `[Epic Planner] Updating Epic #${epicId} body with the planning sections...`,
205
+ );
421
206
  await provider.updateTicket(epicId, {
422
207
  body: newBody,
423
208
  ...(labelMutations.add || labelMutations.remove
@@ -425,14 +210,27 @@ export async function planEpic(
425
210
  : {}),
426
211
  });
427
212
 
213
+ if (force) {
214
+ // Preserve the regeneration audit trail the retired per-ticket
215
+ // overwrite used to leave. Best-effort — never fail the persist on a
216
+ // comment post error.
217
+ try {
218
+ await provider.postComment(epicId, {
219
+ type: 'notification',
220
+ body: '♻️ **Regeneration Audit**: the Tech Spec / Acceptance Table sections of this Epic body were regenerated in place by a `/plan --force` re-plan. Content outside the managed sections was preserved.',
221
+ });
222
+ } catch (_err) {
223
+ // Swallow: audit comment is advisory.
224
+ }
225
+ }
226
+
428
227
  Logger.info(`[Epic Planner] Epic #${epicId} updated successfully.`);
429
228
  Logger.info(`[Epic Planner] Planning pipeline complete!`);
430
229
 
431
230
  return {
432
231
  persisted: true,
433
232
  reason: force ? 'force-replan' : 'persisted',
434
- prdId,
435
- techSpecId,
436
- acceptanceSpecId,
233
+ techSpecPersisted: true,
234
+ acceptanceTable,
437
235
  };
438
236
  }
@@ -1,55 +1,58 @@
1
1
  /**
2
- * phases/prompts.js — Canonical PRD / Tech Spec / Acceptance Spec system
3
- * prompts for the spec phase of `/plan`.
2
+ * phases/prompts.js — Canonical Tech Spec / Acceptance Spec system prompts for
3
+ * the spec phase of `/plan`.
4
4
  *
5
5
  * These ride along on the `--emit-context` envelope as a backstop. The
6
6
  * `epic-plan-spec-author` Skill
7
7
  * (`.agents/skills/core/epic-plan-spec-author/SKILL.md`) embeds the
8
8
  * authoritative copies of these strings — keep the two surfaces in sync when
9
9
  * either is edited.
10
+ *
11
+ * Story #4314: the PRD artifact class is retired. The Epic body (which now
12
+ * carries its `## User Stories` section inline) is the sole authoring input;
13
+ * both prompts consume the Epic body directly rather than a paraphrased PRD.
14
+ *
15
+ * Story #4324: the Tech Spec and Acceptance Spec are no longer separate
16
+ * `context::*` tickets — the authored content lands as managed sections of
17
+ * the same Epic body (`## Delivery Slicing`-led Tech Spec sections, and the
18
+ * `## Acceptance Table` AC-ID table). Content semantics are unchanged; only
19
+ * WHERE the output lives moved.
10
20
  */
11
21
 
12
- export const PRD_SYSTEM_PROMPT = `You are an expert Technical Product Manager.
13
- Your job is to convert a high-level Epic description into a structured Product Requirements Document (PRD).
14
-
15
- The PRD should outline:
16
- 1. Context & Goals
17
- 2. User Stories
18
- 3. Acceptance Criteria
19
- 4. Out of Scope
20
-
21
- CRITICAL REQUIREMENTS:
22
- - Respond ONLY with valid Markdown.
23
- - Do not use top-level <h1> (# ) tags. Start with ## Overview.
24
- - Format requirements clearly with bullet points and bold text where appropriate.`;
25
-
26
22
  export const TECH_SPEC_SYSTEM_PROMPT = `You are an expert Engineering Architect.
27
- Your job is to convert a PRD into a Technical Specification for implementation.
23
+ Your job is to convert an Epic into a Technical Specification for implementation.
28
24
 
29
25
  The Tech Spec should outline:
30
- 1. Architecture & Design
31
- 2. Data Models (if any)
32
- 3. API Changes (if any)
33
- 4. Core Components
34
- 5. Security & Privacy Considerations
26
+ 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.
27
+ 2. Architecture & Design
28
+ 3. Data Models (if any)
29
+ 4. API Changes (if any)
30
+ 5. Core Components
31
+ 6. Security & Privacy Considerations
35
32
 
36
33
  CRITICAL REQUIREMENTS:
37
34
  - Respond ONLY with valid Markdown.
38
- - Do not use top-level <h1> (# ) tags. Start with ## Technical Overview.
39
- - Format architectural decisions clearly with bullet points.`;
35
+ - 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.
36
+ - 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.
37
+ - Format architectural decisions clearly with bullet points.
38
+ - 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.`;
40
39
 
41
40
  export const ACCEPTANCE_SPEC_SYSTEM_PROMPT = `You are an expert Acceptance Engineer.
42
- Your job is to convert a PRD and a Tech Spec into a structured Acceptance Specification that drives features-first BDD authoring.
41
+ Your job is to convert an Epic and a Tech Spec into a structured Acceptance Specification that drives features-first BDD authoring.
43
42
 
44
43
  The Acceptance Spec should outline:
45
- 1. Acceptance Criteria — one row per user-visible outcome, expressed as a Markdown table with columns: AC ID | Outcome | Feature File | Scenario | Disposition
44
+ 1. Acceptance Table — one row per user-visible outcome, expressed as a Markdown table with columns: AC ID | Outcome | Feature File | Scenario | Disposition
46
45
  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
47
46
  3. Disposition — tag each row with one of: new | updated | unchanged
48
47
 
48
+ 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.
49
+
49
50
  CRITICAL REQUIREMENTS:
50
51
  - Respond ONLY with valid Markdown.
51
- - Do not use top-level <h1> (# ) tags. Start with ## Acceptance Criteria.
52
+ - 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.
52
53
  - 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.
53
- - Every AC row MUST carry a Disposition value from the enum: new | updated | unchanged.
54
- - Each Outcome MUST be a single user-visible behaviour no DB assertions, no HTTP status codes, no internal implementation details.
54
+ - 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.)
55
+ - 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.
56
+ - 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.
57
+ - 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.
55
58
  - Cite proposed feature file paths under tests/features/** so Phase 8 can scaffold matching scenarios.`;