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
@@ -91,22 +91,13 @@ export const STATUS_LABELS = {
91
91
  */
92
92
  export const PERSONA_LABEL_PREFIX = 'persona::';
93
93
 
94
- export const CONTEXT_LABELS = {
95
- PRD: 'context::prd',
96
- TECH_SPEC: 'context::tech-spec',
97
- ACCEPTANCE_SPEC: 'context::acceptance-spec',
98
- };
99
-
100
- /** Convenience aliases so callers can reach the new constants by name without
101
- * indexing into CONTEXT_LABELS. Mirrors the export ergonomics used by other
102
- * consumers that import named constants (e.g. PERSONA_LABEL_PREFIX). */
103
- export const CONTEXT_ACCEPTANCE_SPEC = CONTEXT_LABELS.ACCEPTANCE_SPEC;
104
-
105
94
  /**
106
- * Acceptance-axis labels for opt-out signalling on Stories that
107
- * intentionally have no acceptance-spec coverage. Separate namespace from
108
- * `context::` because it expresses absence rather than a linked context
109
- * ticket.
95
+ * Acceptance-axis labels for opt-out signalling on Epics that
96
+ * intentionally have no acceptance-table coverage. (Story #4324 retired
97
+ * the `context::tech-spec` / `context::acceptance-spec` label classes
98
+ * planning content now lives as managed sections of the Epic body. This
99
+ * waiver survives with unchanged meaning: it waives the Epic body's
100
+ * `## Acceptance Table` section instead of a ticket.)
110
101
  */
111
102
  export const ACCEPTANCE_LABELS = {
112
103
  N_A: 'acceptance::n-a',
@@ -149,7 +140,7 @@ export const PLANNING_LABELS = {
149
140
  /**
150
141
  * Convenience named export so callers can reach the constant without
151
142
  * indexing into PLANNING_LABELS — mirrors the ergonomics used by
152
- * ACCEPTANCE_NA / CONTEXT_ACCEPTANCE_SPEC. The literal value is
143
+ * ACCEPTANCE_NA. The literal value is
153
144
  * duplicated here (rather than aliased through PLANNING_LABELS) so a
154
145
  * grep for `PLANNING_HEALTHCHECK_WAIVED.*planning::healthcheck-waived`
155
146
  * matches a single line — see Story #2921 Task #2933 AC #1.
@@ -162,7 +153,6 @@ export const LABEL_COLORS = {
162
153
  AGENT: '#0E8A16',
163
154
  STATUS_BLOCKED: '#D93F0B',
164
155
  PERSONA: '#C5DEF5',
165
- CONTEXT: '#D4C5F9',
166
156
  ACCEPTANCE: '#FBCA04',
167
157
  PLANNING: '#FEF2C0',
168
158
  };
@@ -12,7 +12,6 @@ import { fileURLToPath } from 'node:url';
12
12
  import {
13
13
  ACCEPTANCE_LABELS,
14
14
  AGENT_LABELS,
15
- CONTEXT_LABELS,
16
15
  LABEL_COLORS,
17
16
  PERSONA_LABEL_PREFIX,
18
17
  PLANNING_LABELS,
@@ -64,7 +63,7 @@ export const LABEL_TAXONOMY = [
64
63
  name: AGENT_LABELS.REVIEW_SPEC,
65
64
  color: LABEL_COLORS.AGENT,
66
65
  description:
67
- 'Parking state — PRD + Tech Spec exist; awaiting human review before decomposition',
66
+ 'Parking state — Tech Spec exists; awaiting human review before decomposition',
68
67
  },
69
68
  {
70
69
  name: AGENT_LABELS.READY,
@@ -98,25 +97,9 @@ export const LABEL_TAXONOMY = [
98
97
  // Persona — dynamically derived from .agents/personas/*.md
99
98
  ...buildPersonaLabels(),
100
99
 
101
- // Context
102
- {
103
- name: CONTEXT_LABELS.PRD,
104
- color: LABEL_COLORS.CONTEXT,
105
- description: 'Product Requirements Document',
106
- },
107
- {
108
- name: CONTEXT_LABELS.TECH_SPEC,
109
- color: LABEL_COLORS.CONTEXT,
110
- description: 'Technical Specification',
111
- },
112
- {
113
- name: CONTEXT_LABELS.ACCEPTANCE_SPEC,
114
- color: LABEL_COLORS.CONTEXT,
115
- description: 'Acceptance Specification (Gherkin scenarios)',
116
- },
117
-
118
- // Acceptance axis — explicit opt-out signal for Stories that
119
- // intentionally have no acceptance-spec coverage.
100
+ // Acceptance axis — explicit opt-out signal for Epics that
101
+ // intentionally have no acceptance-table coverage (waives the Epic
102
+ // body's ## Acceptance Table section — Story #4324).
120
103
  {
121
104
  name: ACCEPTANCE_LABELS.N_A,
122
105
  color: LABEL_COLORS.ACCEPTANCE,
@@ -7,8 +7,10 @@
7
7
  * @module lib/orchestration/check-baselines/phases/evaluate
8
8
  */
9
9
 
10
+ import { resolveBundleSizeEnvOverrides } from '../../../baselines/env-overrides.js';
10
11
  import { checkKernelVersion } from '../../../baselines/kernel.js';
11
12
  import * as reader from '../../../baselines/reader.js';
13
+ import { Logger } from '../../../Logger.js';
12
14
  import { applyTolerance, evaluateCompare, runCompareStage } from './compare.js';
13
15
  import { applyFloors, flattenBreaches } from './floors.js';
14
16
 
@@ -22,6 +24,38 @@ function loadHeadBaseline(kind, cwd, configPath) {
22
24
  }
23
25
  }
24
26
 
27
+ /**
28
+ * One-shot bundle-size refresh/acknowledge (Story #151). When
29
+ * `BUNDLE_SIZE_REFRESH=1` is set, demote every `bundle-size` regression to
30
+ * `unchanged` for this run only — floors still apply, so a genuine budget
31
+ * breach is still caught. The flag is read fresh on every invocation and
32
+ * never persisted, so the ratchet returns to full strength automatically on
33
+ * the very next run (no lingering loosened tolerance to remember to reset).
34
+ *
35
+ * No-op for every other kind.
36
+ */
37
+ function applyBundleSizeAcknowledgment(kind, compareOutput, env) {
38
+ if (kind !== 'bundle-size') return { compareOutput, acknowledged: false };
39
+ const { acknowledged, overrides } = resolveBundleSizeEnvOverrides(env);
40
+ if (!acknowledged || compareOutput.regressions.length === 0) {
41
+ return { compareOutput, acknowledged: false };
42
+ }
43
+ Logger.warn(
44
+ `[bundle-size] ⚠ ${overrides.join(', ')} — ` +
45
+ `${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
46
+ 'floors still enforced. This does not persist: the next run without ' +
47
+ 'BUNDLE_SIZE_REFRESH re-enforces the ratchet at full strength.',
48
+ );
49
+ return {
50
+ acknowledged: true,
51
+ compareOutput: {
52
+ ...compareOutput,
53
+ regressions: [],
54
+ unchanged: [...compareOutput.unchanged, ...compareOutput.regressions],
55
+ },
56
+ };
57
+ }
58
+
25
59
  function buildGateReport({
26
60
  kind,
27
61
  gateBlock,
@@ -30,6 +64,7 @@ function buildGateReport({
30
64
  breaches,
31
65
  compareOutput,
32
66
  cmp,
67
+ acknowledged,
33
68
  }) {
34
69
  const kernel = checkKernelVersion(kind, baseline.kernelVersion);
35
70
  return {
@@ -50,6 +85,7 @@ function buildGateReport({
50
85
  regressionCount: compareOutput.regressions.length,
51
86
  baseRef: cmp.baseRef ?? null,
52
87
  generatedAt: baseline.generatedAt,
88
+ acknowledged,
53
89
  };
54
90
  }
55
91
 
@@ -59,6 +95,7 @@ export async function evaluateKind({
59
95
  scope,
60
96
  cwd,
61
97
  configPath,
98
+ env = process.env,
62
99
  }) {
63
100
  const headLoad = loadHeadBaseline(kind, cwd, configPath);
64
101
  if (headLoad.schemaError) return { kind, schemaError: headLoad.schemaError };
@@ -67,7 +104,15 @@ export async function evaluateKind({
67
104
  const breaches = flattenBreaches(findings);
68
105
  const cmp = await evaluateCompare({ kind, gateBlock, scope, cwd });
69
106
  const rawCompare = runCompareStage(baseline, cmp);
70
- const compareOutput = applyTolerance(rawCompare, gateBlock.tolerance ?? null);
107
+ const toleratedCompare = applyTolerance(
108
+ rawCompare,
109
+ gateBlock.tolerance ?? null,
110
+ );
111
+ const { compareOutput, acknowledged } = applyBundleSizeAcknowledgment(
112
+ kind,
113
+ toleratedCompare,
114
+ env,
115
+ );
71
116
  return buildGateReport({
72
117
  kind,
73
118
  gateBlock,
@@ -76,5 +121,6 @@ export async function evaluateKind({
76
121
  breaches,
77
122
  compareOutput,
78
123
  cmp,
124
+ acknowledged,
79
125
  });
80
126
  }
@@ -43,6 +43,13 @@ Unified baseline dispatcher. Per-kind pipeline (schema → floor → tolerance
43
43
  compare) over every configured gate, with centralised friction emission and
44
44
  aggregated exit codes.
45
45
 
46
+ Env vars:
47
+ BUNDLE_SIZE_REFRESH=1 One-shot acknowledge for an intentional bundle-size
48
+ growth: demotes bundle-size regressions to
49
+ "unchanged" for this run only (floors still
50
+ enforced). Never persisted — the next run without
51
+ this flag re-enforces the ratchet.
52
+
46
53
  Exit codes:
47
54
  0 every enabled gate passes
48
55
  1 any floor breach
@@ -59,7 +59,7 @@ function dispatchPerKind({ wanted, quality, env, cwd, configPath }) {
59
59
  wanted.map((kind) => {
60
60
  const gateBlock = quality.gates[kind];
61
61
  const scope = resolveDispatchScope({ kind, quality, env });
62
- return evaluateKind({ kind, gateBlock, scope, cwd, configPath });
62
+ return evaluateKind({ kind, gateBlock, scope, cwd, configPath, env });
63
63
  }),
64
64
  );
65
65
  }
@@ -27,7 +27,8 @@ function formatGateLine(g) {
27
27
  ? ''
28
28
  : ` [kernel drift ${g.kernelBaseline} → ${g.kernelCurrent}]`;
29
29
  const baseRef = g.baseRef ? ` [baseRef=${g.baseRef}]` : '';
30
- return ` - ${g.kind}: ${status}${drift}${baseRef}`;
30
+ const ack = g.acknowledged ? ' [ACKNOWLEDGED — this run only]' : '';
31
+ return ` - ${g.kind}: ${status}${drift}${baseRef}${ack}`;
31
32
  }
32
33
 
33
34
  function formatViolationLine(component, v) {
@@ -27,7 +27,7 @@ import {
27
27
  PROJECT_ROOT,
28
28
  resolveConfig,
29
29
  } from '../config-resolver.js';
30
-
30
+ import { stripEpicSection } from '../epic-body-sections.js';
31
31
  import { Logger } from '../Logger.js';
32
32
  import {
33
33
  buildEnvelope,
@@ -163,8 +163,7 @@ function getVersion() {
163
163
  /**
164
164
  * Parse the work-breakdown hierarchy from a Task ticket body.
165
165
  *
166
- * Looks for patterns like: `Epic: #1`, `Story: #3`,
167
- * `PRD: #4`, `Tech Spec: #5`.
166
+ * Looks for patterns like: `Epic: #1`, `Story: #3`.
168
167
  *
169
168
  * @param {string} body
170
169
  * @returns {Record<string, number>}
@@ -177,7 +176,7 @@ export function parseHierarchy(body) {
177
176
  for (const match of matches) {
178
177
  const key = match[1].trim().toLowerCase().replace(/\s+/g, '');
179
178
  const val = Number.parseInt(match[2], 10);
180
- result[key] = val; // e.g. { epic: 1, story: 3, prd: 4, techspec: 5 }
179
+ result[key] = val; // e.g. { epic: 1, story: 3 }
181
180
  }
182
181
  return result;
183
182
  }
@@ -351,14 +350,16 @@ async function buildHierarchySections(task, provider, epicId, agentSettings) {
351
350
  const depth = agentSettings?.contextDepth ?? 'standard';
352
351
  const idsToFetch = [];
353
352
 
353
+ // Story #4324 — the context-ticket classes are retired: the Epic body IS
354
+ // the planning document (ideation sections + folded Tech Spec sections).
355
+ // `full` and `standard` fetch the identical Epic / Story pair; both
356
+ // branches are kept so the `contextDepth` setting stays a stable API
357
+ // surface.
354
358
  if (depth === 'full') {
355
359
  idsToFetch.push({ key: 'Epic', id: epicId || hierarchyKeys.epic });
356
- idsToFetch.push({ key: 'PRD', id: hierarchyKeys.prd });
357
- idsToFetch.push({ key: 'Tech Spec', id: hierarchyKeys.techspec });
358
360
  idsToFetch.push({ key: 'Story', id: hierarchyKeys.story });
359
361
  } else if (depth === 'standard') {
360
362
  idsToFetch.push({ key: 'Epic', id: epicId || hierarchyKeys.epic });
361
- idsToFetch.push({ key: 'Tech Spec', id: hierarchyKeys.techspec });
362
363
  idsToFetch.push({ key: 'Story', id: hierarchyKeys.story });
363
364
  } else if (depth === 'minimal') {
364
365
  idsToFetch.push({ key: 'Story', id: hierarchyKeys.story });
@@ -370,7 +371,17 @@ async function buildHierarchySections(task, provider, epicId, agentSettings) {
370
371
  try {
371
372
  const t = await provider.getTicket(item.id);
372
373
  provenance.push(ticketSnapshot(t, retrievedAt));
373
- return `### ${item.key}: ${t.title} (#${t.id})\n\n${t.body}\n`;
374
+ // Hydration section-elision guardrail (Story #4324): the Epic
375
+ // body's ## Acceptance Table managed section is authoring/close
376
+ // machinery — never delivery context. Strip it before it reaches
377
+ // a story agent's prompt so per-story prompt size stays flat
378
+ // versus the pre-fold baseline (the old acceptance-spec ticket
379
+ // was never hydrated either).
380
+ const body =
381
+ item.key === 'Epic'
382
+ ? stripEpicSection(t.body ?? '', 'acceptanceTable')
383
+ : t.body;
384
+ return `### ${item.key}: ${t.title} (#${t.id})\n\n${body}\n`;
374
385
  } catch (err) {
375
386
  const detail = err?.message ? `: ${err.message}` : '';
376
387
  Logger.warn(
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * context.js — Phase 3 of the epic-plan-decompose pipeline (Story #2466).
3
3
  *
4
- * Builds the authoring context (PRD + Tech Spec bodies, heuristics, system
5
- * prompt, ticket cap) the host LLM / `epic-plan-decompose-author` Skill
6
- * consumes when producing the ticket JSON array.
4
+ * Builds the authoring context (the Epic body which carries the folded
5
+ * Tech Spec sections and Acceptance Table per Story #4324 — plus
6
+ * heuristics, system prompt, ticket cap) the host LLM /
7
+ * `epic-plan-decompose-author` Skill consumes when producing the ticket
8
+ * JSON array.
7
9
  *
8
10
  * Extracted verbatim from `epic-plan-decompose.js`; both
9
11
  * `buildDecomposerSystemPrompt` and `buildDecompositionContext` retain
@@ -16,6 +18,7 @@ import {
16
18
  getLimits,
17
19
  resolvePreflightCeilings,
18
20
  } from '../../../config-resolver.js';
21
+ import { hasTechSpecContent } from '../../../epic-body-sections.js';
19
22
  import { renderDecomposerSystemPrompt } from '../../../templates/decomposer-prompts.js';
20
23
  import { read as readPlanState } from '../../epic-plan-state-store.js';
21
24
  import { applyBudget } from '../../planning-context-budget.js';
@@ -83,27 +86,25 @@ async function readPlanningDecision(provider, epicId) {
83
86
  };
84
87
  }
85
88
 
86
- async function fetchPlanningTickets(provider, epicId) {
89
+ async function fetchPlanningEpic(provider, epicId) {
87
90
  const epic = await provider.getEpic(epicId);
88
- if (!epic?.linkedIssues?.prd || !epic.linkedIssues.techSpec) {
91
+ if (!epic || !hasTechSpecContent(epic.body ?? '')) {
89
92
  throw new Error(
90
- `[Decomposer] Epic #${epicId} is missing linked PRD or Tech Spec. Run the Epic Planner first.`,
93
+ `[Decomposer] Epic #${epicId} body carries no Tech Spec sections (no ## Delivery Slicing). Run the Epic Planner (Phase 7) first.`,
91
94
  );
92
95
  }
93
- const [prd, techSpec] = await Promise.all([
94
- provider.getTicket(epic.linkedIssues.prd),
95
- provider.getTicket(epic.linkedIssues.techSpec),
96
- ]);
97
- return { epic, prd, techSpec };
96
+ return { epic };
98
97
  }
99
98
 
100
99
  /**
101
100
  * Build the authoring context the host LLM (or the
102
101
  * `epic-plan-decompose-author` Skill) needs to produce the ticket JSON.
103
102
  *
104
- * PRD and Tech Spec bodies are bounded by the planning-context budget
105
- * (Epic #817 Story 9). Pass `{ fullContext: true }` (CLI: `--full-context`)
106
- * to restore the unbounded full bodies.
103
+ * The Epic body (ideation sections + folded Tech Spec sections +
104
+ * Acceptance Table the AC-ID source for wave-0 BDD scaffold tags) is
105
+ * bounded by the planning-context budget (Epic #817 Story 9). Pass
106
+ * `{ fullContext: true }` (CLI: `--full-context`) to restore the
107
+ * unbounded full body.
107
108
  */
108
109
  export async function buildDecompositionContext(
109
110
  epicId,
@@ -111,7 +112,7 @@ export async function buildDecompositionContext(
111
112
  config = {},
112
113
  opts = {},
113
114
  ) {
114
- const { epic, prd, techSpec } = await fetchPlanningTickets(provider, epicId);
115
+ const { epic } = await fetchPlanningEpic(provider, epicId);
115
116
  const { planningRisk, reviewRouting } = await readPlanningDecision(
116
117
  provider,
117
118
  epicId,
@@ -129,18 +130,18 @@ export async function buildDecompositionContext(
129
130
  });
130
131
 
131
132
  const budgeted = applyBudget(
132
- [
133
- { path: `prd-${prd.id}.md`, content: prd.body ?? '' },
134
- { path: `tech-spec-${techSpec.id}.md`, content: techSpec.body ?? '' },
135
- ],
133
+ [{ path: `epic-${epic.id}.md`, content: epic.body ?? '' }],
136
134
  planningLimits,
137
135
  { fullContext },
138
136
  );
139
- const [prdItem, techSpecItem] = budgeted.items;
137
+ const [epicItem] = budgeted.items;
140
138
  return {
141
139
  epic: { id: epic.id, title: epic.title },
142
- prd: projectBudgetedEntry(prdItem, prd, budgeted.mode),
143
- techSpec: projectBudgetedEntry(techSpecItem, techSpec, budgeted.mode),
140
+ // Story #4324 — the Epic body is the single planning document: it
141
+ // carries the ideation sections, the folded Tech Spec sections
142
+ // (## Delivery Slicing first), and the ## Acceptance Table the wave-0
143
+ // BDD scaffold reads its AC IDs from.
144
+ epicBody: projectBudgetedEntry(epicItem, epic, budgeted.mode),
144
145
  heuristics,
145
146
  systemPrompt,
146
147
  maxTickets,
@@ -5,7 +5,7 @@
5
5
  * Exports:
6
6
  * - `assertDecomposeInputs(epic, epicId, tickets)` — entry guards.
7
7
  * - `buildEpicSpecInput(epic, epicId)` — projection used by
8
- * `renderSpec`; runs `ensurePlanningArtifacts` defensively so the
8
+ * `renderSpec`; the Epic body already carries the folded planning
9
9
  * spec body carries the `## Planning Artifacts` section the
10
10
  * cascade-close path depends on.
11
11
  * - `validateTickets(tickets, config)` — runs the cross-link / freshness
@@ -19,6 +19,7 @@
19
19
  * @module lib/orchestration/epic-plan-decompose/phases/persist-helpers
20
20
  */
21
21
 
22
+ import { hasTechSpecContent } from '../../../epic-body-sections.js';
22
23
  import { gitSpawn } from '../../../git-utils.js';
23
24
  import { Logger } from '../../../Logger.js';
24
25
  import { TYPE_LABELS } from '../../../label-constants.js';
@@ -29,10 +30,7 @@ import {
29
30
  } from '../../epic-plan-state-store.js';
30
31
  import { validateTaskBodies } from '../../task-body-validator.js';
31
32
  import { validateAndNormalizeTickets } from '../../ticket-validator.js';
32
- import {
33
- ensurePlanningArtifacts,
34
- resolveConflictPolicy,
35
- } from './planning-artifacts.js';
33
+ import { resolveConflictPolicy } from './planning-artifacts.js';
36
34
 
37
35
  export function assertDecomposeInputs(epic, epicId, tickets) {
38
36
  if (!epic) {
@@ -43,9 +41,9 @@ export function assertDecomposeInputs(epic, epicId, tickets) {
43
41
  `[epic-plan-decompose] Ticket #${epicId} is not a ${TYPE_LABELS.EPIC}.`,
44
42
  );
45
43
  }
46
- if (!epic.linkedIssues?.prd || !epic.linkedIssues?.techSpec) {
44
+ if (!hasTechSpecContent(epic.body ?? '')) {
47
45
  throw new Error(
48
- `[epic-plan-decompose] Epic #${epicId} is missing a linked PRD or Tech Spec. Run /epic-plan-spec first.`,
46
+ `[epic-plan-decompose] Epic #${epicId} body carries no Tech Spec sections (no ## Delivery Slicing). Run /plan Phase 7 first.`,
49
47
  );
50
48
  }
51
49
  if (!Array.isArray(tickets)) {
@@ -56,7 +54,7 @@ export function assertDecomposeInputs(epic, epicId, tickets) {
56
54
  }
57
55
 
58
56
  export function buildEpicSpecInput(epic, epicId) {
59
- const epicBody = ensurePlanningArtifacts(epic.body ?? '', epic.linkedIssues);
57
+ const epicBody = epic.body ?? '';
60
58
  const epicSpecInput = { id: epicId, title: epic.title };
61
59
  if (epicBody.length > 0) epicSpecInput.body = epicBody;
62
60
  return epicSpecInput;
@@ -117,8 +115,7 @@ export async function seedPlanState(provider, epicId, epic) {
117
115
  epicId,
118
116
  seed: {
119
117
  spec: {
120
- prdId: epic.linkedIssues.prd,
121
- techSpecId: epic.linkedIssues.techSpec,
118
+ techSpecPersisted: hasTechSpecContent(epic.body ?? ''),
122
119
  completedAt: null,
123
120
  },
124
121
  },
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * planning-artifacts.js — Phase 1 of the epic-plan-decompose pipeline
3
- * (Story #2466). Owns the `## Planning Artifacts` body shim and the
4
- * cross-Story conflict-policy resolver.
3
+ * (Story #2466). Owns the cross-Story conflict-policy resolver.
5
4
  *
6
- * Extracted verbatim from `epic-plan-decompose.js` so the named exports
7
- * the existing unit tests import (`ensurePlanningArtifacts`) keep their
8
- * public surface byte-identical.
5
+ * Story #4324 retired the `## Planning Artifacts` body shim
6
+ * (`ensurePlanningArtifacts`) with the context-ticket classes — the Epic
7
+ * body now carries the planning content itself as managed sections.
9
8
  *
10
9
  * @module lib/orchestration/epic-plan-decompose/phases/planning-artifacts
11
10
  */
@@ -13,39 +12,6 @@
13
12
  import { resolveListValue } from '../../../config/shared.js';
14
13
  import { DEFAULT_REGISTRY_PATTERNS } from '../../ticket-validator-conflicts.js';
15
14
 
16
- /**
17
- * Ensure the supplied Epic body carries a `## Planning Artifacts` section.
18
- * Idempotent — when the section already exists the body is returned
19
- * verbatim; when it's missing and `linkedIssues` carries resolved ids
20
- * the section is appended exactly once using the canonical
21
- * `- [ ] PRD: #N` / `Tech Spec: #N` / `Acceptance Spec: #N` lines that
22
- * `issue-link-parser.js` recognises (so cascade-close still resolves the
23
- * linked tickets).
24
- *
25
- * Story #2283.
26
- *
27
- * @param {string} body
28
- * @param {{ prd: number|null, techSpec: number|null, acceptanceSpec: number|null } | undefined | null} linkedIssues
29
- * @returns {string}
30
- */
31
- export function ensurePlanningArtifacts(body, linkedIssues) {
32
- const safeBody = typeof body === 'string' ? body : '';
33
- if (safeBody.includes('## Planning Artifacts')) return safeBody;
34
- if (!linkedIssues) return safeBody;
35
- const lines = [];
36
- if (Number.isInteger(linkedIssues.prd)) {
37
- lines.push(`- [ ] PRD: #${linkedIssues.prd}`);
38
- }
39
- if (Number.isInteger(linkedIssues.techSpec)) {
40
- lines.push(`- [ ] Tech Spec: #${linkedIssues.techSpec}`);
41
- }
42
- if (Number.isInteger(linkedIssues.acceptanceSpec)) {
43
- lines.push(`- [ ] Acceptance Spec: #${linkedIssues.acceptanceSpec}`);
44
- }
45
- if (lines.length === 0) return safeBody;
46
- return `${safeBody}\n\n## Planning Artifacts\n${lines.join('\n')}\n`;
47
- }
48
-
49
15
  /**
50
16
  * Resolve the cross-Story conflict-finding policy from `_config.planning`.
51
17
  * Both flags default to `false` so existing repos keep the advisory-only
@@ -28,9 +28,10 @@
28
28
  * operator passed `--force` (a deliberate
29
29
  * re-decompose that closes the old tree).
30
30
  *
31
- * The PRD / Tech Spec find-or-create idempotency already lives in
32
- * `phases/plan-epic.js` (keyed on `epic.linkedIssues`); these guards add the
33
- * cross-run mutual exclusion and the child-duplication refusal around it.
31
+ * The spec-persist idempotency already lives in `phases/plan-epic.js`
32
+ * (keyed on the Epic body's managed planning sections); these guards add
33
+ * the cross-run mutual exclusion and the child-duplication refusal
34
+ * around it.
34
35
  */
35
36
 
36
37
  import { getGitHub } from '../config/github.js';
@@ -341,12 +342,10 @@ export async function assertNoOpenPlanChildren({
341
342
  const openChildren = (children ?? []).filter((t) => {
342
343
  const labels = Array.isArray(t.labels) ? t.labels : [];
343
344
  const isOpen = t.state === undefined || t.state === 'open';
344
- // Context spec tickets (PRD / Tech Spec / Acceptance Spec) carry a
345
- // `context::*` label and since the `createTicket` factory injects
346
- // `type::story` by default also `type::story`. They are reference
347
- // artifacts that stay open across delivery, NOT plan children, so they
348
- // MUST be excluded here: counting them would make every first decompose
349
- // (where the three context tickets are the only open children) refuse.
345
+ // Legacy context spec tickets (the pre-#4324 Tech Spec / Acceptance
346
+ // Spec artifacts) carry a `context::*` label. Historical Epics keep
347
+ // them (forward-only cutover, no backfill), so they are still
348
+ // excluded here: they are reference artifacts, not plan children.
350
349
  const isContext = labels.some(
351
350
  (l) => typeof l === 'string' && l.startsWith('context::'),
352
351
  );
@@ -2,7 +2,7 @@
2
2
  * phases/authoring-context.js — emit-context phase.
3
3
  *
4
4
  * Builds the authoring context the host LLM (or the
5
- * `epic-plan-spec-author` Skill) needs to write the PRD and Tech Spec.
5
+ * `epic-plan-spec-author` Skill) needs to write the Tech Spec.
6
6
  * Returns a plain JSON-serialisable object; never hits the network beyond
7
7
  * the provider call needed to load the Epic.
8
8
  */
@@ -16,6 +16,7 @@ import {
16
16
  import { scanBddScenarios } from '../../../bdd-scenario-scanner.js';
17
17
  import { buildCodebaseSnapshot } from '../../../codebase-snapshot.js';
18
18
  import { getLimits, PROJECT_ROOT } from '../../../config-resolver.js';
19
+ import { hasEpicSection } from '../../../epic-body-sections.js';
19
20
  import { scanMemoryFreshness } from '../../../feedback-loop/memory-freshness.js';
20
21
  import { fetchPriorFeedback } from '../../../feedback-loop/prior-feedback-fetcher.js';
21
22
  import { Logger } from '../../../Logger.js';
@@ -24,7 +25,6 @@ import { applyBudget } from '../../planning-context-budget.js';
24
25
  import { collectReferences, hasNewFileCue } from '../../spec-freshness.js';
25
26
  import {
26
27
  ACCEPTANCE_SPEC_SYSTEM_PROMPT,
27
- PRD_SYSTEM_PROMPT,
28
28
  TECH_SPEC_SYSTEM_PROMPT,
29
29
  } from './prompts.js';
30
30
  import { buildAuthoringGrounding } from './spec-authoring-grounding.js';
@@ -59,7 +59,7 @@ export function resolveMemoryDir({ github } = {}) {
59
59
 
60
60
  /**
61
61
  * Build the authoring context the host LLM (or the
62
- * `epic-plan-spec-author` Skill) needs to write the PRD and Tech Spec.
62
+ * `epic-plan-spec-author` Skill) needs to write the Tech Spec.
63
63
  *
64
64
  * `docsContext` is bounded by the planning-context budget (Epic #817 Story 9):
65
65
  * over-budget payloads downgrade to a summary representation with headings +
@@ -205,12 +205,18 @@ export async function buildAuthoringContext(
205
205
  title: epic.title,
206
206
  body: epicBody.mode === 'full' ? epic.body : null,
207
207
  bodySummary: epicBody.mode === 'summary' ? epicBody.items[0] : null,
208
- linkedIssues: epic.linkedIssues ?? { prd: null, techSpec: null },
208
+ // Story #4324: the context-ticket classes are retired. A re-planned
209
+ // Epic's previous Tech Spec / Acceptance Table content rides along
210
+ // inside `body` (managed sections), which is how the author keeps
211
+ // AC IDs stable across re-plans.
212
+ planningSections: {
213
+ techSpec: hasEpicSection(epic.body ?? '', 'techSpec'),
214
+ acceptanceTable: hasEpicSection(epic.body ?? '', 'acceptanceTable'),
215
+ },
209
216
  },
210
217
  docsContext,
211
218
  codebaseSnapshot,
212
219
  systemPrompts: {
213
- prd: PRD_SYSTEM_PROMPT,
214
220
  techSpec: TECH_SPEC_SYSTEM_PROMPT,
215
221
  acceptanceSpec: ACCEPTANCE_SPEC_SYSTEM_PROMPT,
216
222
  },
@@ -4,25 +4,46 @@
4
4
  * Extracted from `epic-plan-spec.js` (refs #3685) so the top-level entry
5
5
  * stays a thin wire-up that scores at or above the maintainability floor;
6
6
  * the option schema and the epic-id guards live here.
7
+ *
8
+ * Story #4324: the separate-ticket persist flags `--techspec` /
9
+ * `--acceptance-spec` are retired with the context-ticket classes. The
10
+ * authored content now lands as sections of the Epic body; pass it via
11
+ * `--tech-spec <file>` and `--acceptance-table <file>`. The old flags fail
12
+ * with a usage error naming the removal.
7
13
  */
8
14
 
9
15
  import { parseArgs } from 'node:util';
10
16
 
17
+ /** Retired flags → the replacement guidance surfaced in the usage error. */
18
+ const RETIRED_FLAGS = Object.freeze({
19
+ '--techspec':
20
+ '--techspec was retired by the context-ticket fold (Story #4324): the Tech Spec is no longer a separate context::tech-spec ticket. Pass --tech-spec <file> — the content lands as a managed section of the Epic body.',
21
+ '--acceptance-spec':
22
+ '--acceptance-spec was retired by the context-ticket fold (Story #4324): the Acceptance Spec is no longer a separate context::acceptance-spec ticket. Pass --acceptance-table <file> — the AC-ID table lands as the ## Acceptance Table section of the Epic body.',
23
+ });
24
+
11
25
  /**
12
26
  * Parse and validate the `epic-plan-spec` CLI arguments.
13
27
  *
14
28
  * @param {string[]} [argv] Defaults to `process.argv.slice(2)`.
15
29
  * @returns {{ values: Record<string, unknown>, epicId: number }}
16
- * @throws {Error} when `--epic` is missing or not a number.
30
+ * @throws {Error} when `--epic` is missing or not a number, or when a
31
+ * retired separate-ticket persist flag is supplied.
17
32
  */
18
33
  export function parseEpicPlanSpecArgs(argv = process.argv.slice(2)) {
34
+ for (const arg of argv) {
35
+ const flag = arg.split('=')[0];
36
+ if (RETIRED_FLAGS[flag]) {
37
+ throw new Error(`[epic-plan-spec] ${RETIRED_FLAGS[flag]}`);
38
+ }
39
+ }
40
+
19
41
  const { values } = parseArgs({
20
42
  args: argv,
21
43
  options: {
22
44
  epic: { type: 'string' },
23
- prd: { type: 'string' },
24
- techspec: { type: 'string' },
25
- 'acceptance-spec': { type: 'string' },
45
+ 'tech-spec': { type: 'string' },
46
+ 'acceptance-table': { type: 'string' },
26
47
  'risk-verdict': { type: 'string' },
27
48
  force: { type: 'boolean', default: false },
28
49
  'force-review': { type: 'boolean', default: false },
@@ -35,7 +56,7 @@ export function parseEpicPlanSpecArgs(argv = process.argv.slice(2)) {
35
56
 
36
57
  if (!values.epic) {
37
58
  throw new Error(
38
- 'Usage: epic-plan-spec.js --epic <EpicId> (--emit-context [--pretty] [--full-context] | --prd <file> --techspec <file> --risk-verdict <file> [--acceptance-spec <file>]) [--force]',
59
+ 'Usage: epic-plan-spec.js --epic <EpicId> (--emit-context [--pretty] [--full-context] | --tech-spec <file> --risk-verdict <file> [--acceptance-table <file>]) [--force]',
39
60
  );
40
61
  }
41
62