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.
- package/.agents/README.md +5 -5
- package/.agents/docs/SDLC.md +93 -80
- package/.agents/docs/configuration.md +11 -1
- package/.agents/docs/quality-gates.md +61 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +9 -7
- package/.agents/personas/architect.md +8 -5
- package/.agents/personas/engineer-mobile.md +3 -2
- package/.agents/personas/engineer-web.md +3 -2
- package/.agents/personas/engineer.md +6 -5
- package/.agents/personas/product.md +19 -13
- package/.agents/personas/project-manager.md +9 -8
- package/.agents/personas/qa-engineer.md +10 -6
- package/.agents/personas/refactorer.md +3 -2
- package/.agents/personas/technical-writer.md +2 -1
- package/.agents/personas/ux-designer.md +2 -2
- package/.agents/schemas/agentrc.schema.json +10 -0
- package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
- package/.agents/scripts/epic-deliver-prepare.js +0 -31
- package/.agents/scripts/epic-plan-decompose.js +2 -5
- package/.agents/scripts/epic-plan-spec.js +16 -19
- package/.agents/scripts/hierarchy-gate.js +11 -11
- package/.agents/scripts/lib/ITicketingProvider.js +4 -3
- package/.agents/scripts/lib/baselines/env-overrides.js +35 -0
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
- package/.agents/scripts/lib/cli-args.js +1 -5
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +1 -4
- package/.agents/scripts/lib/config-settings-schema.js +5 -0
- package/.agents/scripts/lib/epic-body-sections.js +222 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
- package/.agents/scripts/lib/label-constants.js +7 -17
- package/.agents/scripts/lib/label-taxonomy.js +4 -21
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +47 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/pipeline.js +1 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +2 -1
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +19 -8
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
- package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
- package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/lint-label-vocabulary.js +2 -3
- package/.agents/scripts/providers/github/mappers.js +0 -3
- package/.agents/scripts/providers/github/tickets.js +7 -18
- package/.agents/scripts/single-story-init.js +0 -1
- package/.agents/scripts/story-init.js +1 -29
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +37 -18
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -18
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +7 -6
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +161 -109
- package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
- package/.agents/skills/core/scope-triage/SKILL.md +2 -1
- package/.agents/skills/skills.index.json +6 -6
- package/.agents/templates/epic-from-idea.md +4 -0
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/code-review.md +11 -9
- package/.agents/workflows/helpers/deliver-epic.md +32 -44
- package/.agents/workflows/helpers/epic-audit.md +11 -8
- package/.agents/workflows/helpers/epic-deliver-story.md +10 -16
- package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
- package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
- package/.agents/workflows/helpers/parallel-tooling.md +2 -1
- package/.agents/workflows/helpers/plan-epic.md +114 -99
- package/.agents/workflows/helpers/single-story-deliver.md +1 -1
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
- package/.agents/workflows/plan.md +8 -8
- package/.agents/workflows/qa-assist.md +2 -1
- package/docs/CHANGELOG.md +28 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/issue-link-parser.js +0 -74
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
- package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
- package/.agents/scripts/lib/story-init/hierarchy-tracer.js +0 -57
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* phases/plan-epic.js —
|
|
2
|
+
* phases/plan-epic.js — Tech Spec / Acceptance Table persistence phase.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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-
|
|
21
|
-
* the `acceptance::n-a` waiver, from the planningRisk envelope derived
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
101
|
+
export function hasAllRequestedSections({ existing, wantsAcceptanceSpec }) {
|
|
124
102
|
return Boolean(
|
|
125
|
-
existing.
|
|
126
|
-
existing.
|
|
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
|
|
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
|
-
{
|
|
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-
|
|
144
|
+
? ` — applying ${ACCEPTANCE_NA} waiver (no acceptance-table section).`
|
|
314
145
|
: wantsAcceptanceSpec
|
|
315
|
-
? ' — persisting
|
|
146
|
+
? ' — persisting the ## Acceptance Table section.'
|
|
316
147
|
: ' — no acceptance-spec content supplied.'),
|
|
317
148
|
);
|
|
318
149
|
|
|
319
|
-
const existing =
|
|
320
|
-
if (!force &&
|
|
150
|
+
const existing = getExistingSections(epic);
|
|
151
|
+
if (!force && hasAllRequestedSections({ existing, wantsAcceptanceSpec })) {
|
|
321
152
|
Logger.warn(
|
|
322
|
-
`[Epic Planner] Epic #${epicId} already
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
epic,
|
|
173
|
+
newBody = upsertEpicSection(
|
|
174
|
+
newBody,
|
|
175
|
+
'acceptanceTable',
|
|
363
176
|
acceptanceSpecContent,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
acceptanceSpecId,
|
|
233
|
+
techSpecPersisted: true,
|
|
234
|
+
acceptanceTable,
|
|
437
235
|
};
|
|
438
236
|
}
|
|
@@ -1,55 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* phases/prompts.js — Canonical
|
|
3
|
-
*
|
|
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
|
|
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.
|
|
31
|
-
2.
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
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.
|
|
39
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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.`;
|