mandrel 1.81.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 (95) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +93 -80
  3. package/.agents/docs/configuration.md +4 -1
  4. package/.agents/docs/workflows.md +1 -1
  5. package/.agents/instructions.md +9 -7
  6. package/.agents/personas/architect.md +8 -5
  7. package/.agents/personas/engineer-mobile.md +3 -2
  8. package/.agents/personas/engineer-web.md +3 -2
  9. package/.agents/personas/engineer.md +6 -5
  10. package/.agents/personas/product.md +19 -13
  11. package/.agents/personas/project-manager.md +9 -8
  12. package/.agents/personas/qa-engineer.md +10 -6
  13. package/.agents/personas/refactorer.md +3 -2
  14. package/.agents/personas/technical-writer.md +2 -1
  15. package/.agents/personas/ux-designer.md +2 -2
  16. package/.agents/schemas/agentrc.schema.json +10 -0
  17. package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
  18. package/.agents/scripts/epic-deliver-prepare.js +0 -31
  19. package/.agents/scripts/epic-plan-decompose.js +2 -5
  20. package/.agents/scripts/epic-plan-spec.js +16 -19
  21. package/.agents/scripts/hierarchy-gate.js +11 -11
  22. package/.agents/scripts/lib/ITicketingProvider.js +4 -3
  23. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  24. package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
  25. package/.agents/scripts/lib/cli-args.js +1 -5
  26. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  27. package/.agents/scripts/lib/config/temp-paths.js +1 -4
  28. package/.agents/scripts/lib/config-settings-schema.js +5 -0
  29. package/.agents/scripts/lib/epic-body-sections.js +222 -0
  30. package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
  31. package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
  32. package/.agents/scripts/lib/label-constants.js +7 -17
  33. package/.agents/scripts/lib/label-taxonomy.js +4 -21
  34. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +19 -8
  35. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
  36. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
  38. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
  39. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
  40. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
  41. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
  42. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
  43. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
  44. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
  45. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
  46. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
  47. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
  48. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
  49. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
  50. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
  51. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  52. package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
  53. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  54. package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
  55. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  56. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
  57. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
  58. package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
  59. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
  60. package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
  61. package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
  62. package/.agents/scripts/lifecycle-emit.js +1 -1
  63. package/.agents/scripts/lint-label-vocabulary.js +2 -3
  64. package/.agents/scripts/providers/github/mappers.js +0 -3
  65. package/.agents/scripts/providers/github/tickets.js +7 -18
  66. package/.agents/scripts/single-story-init.js +0 -1
  67. package/.agents/scripts/story-init.js +1 -29
  68. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +37 -18
  69. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -18
  70. package/.agents/skills/core/epic-plan-premortem/SKILL.md +7 -6
  71. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +161 -109
  72. package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
  73. package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
  74. package/.agents/skills/core/scope-triage/SKILL.md +2 -1
  75. package/.agents/skills/skills.index.json +6 -6
  76. package/.agents/templates/epic-from-idea.md +4 -0
  77. package/.agents/workflows/audit-to-stories.md +2 -2
  78. package/.agents/workflows/helpers/code-review.md +11 -9
  79. package/.agents/workflows/helpers/deliver-epic.md +32 -44
  80. package/.agents/workflows/helpers/epic-audit.md +11 -8
  81. package/.agents/workflows/helpers/epic-deliver-story.md +10 -16
  82. package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
  83. package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
  84. package/.agents/workflows/helpers/parallel-tooling.md +2 -1
  85. package/.agents/workflows/helpers/plan-epic.md +114 -99
  86. package/.agents/workflows/helpers/single-story-deliver.md +1 -1
  87. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
  88. package/.agents/workflows/plan.md +8 -8
  89. package/.agents/workflows/qa-assist.md +2 -1
  90. package/docs/CHANGELOG.md +21 -0
  91. package/package.json +1 -1
  92. package/.agents/scripts/lib/issue-link-parser.js +0 -74
  93. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
  94. package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
  95. package/.agents/scripts/lib/story-init/hierarchy-tracer.js +0 -57
@@ -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,
@@ -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