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
|
@@ -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
|
|
107
|
-
* intentionally have no acceptance-
|
|
108
|
-
* `context
|
|
109
|
-
*
|
|
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
|
|
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 —
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
89
|
+
async function fetchPlanningEpic(provider, epicId) {
|
|
87
90
|
const epic = await provider.getEpic(epicId);
|
|
88
|
-
if (!epic
|
|
91
|
+
if (!epic || !hasTechSpecContent(epic.body ?? '')) {
|
|
89
92
|
throw new Error(
|
|
90
|
-
`[Decomposer] Epic #${epicId}
|
|
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
|
-
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
|
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 [
|
|
137
|
+
const [epicItem] = budgeted.items;
|
|
140
138
|
return {
|
|
141
139
|
epic: { id: epic.id, title: epic.title },
|
|
142
|
-
|
|
143
|
-
|
|
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`;
|
|
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.
|
|
44
|
+
if (!hasTechSpecContent(epic.body ?? '')) {
|
|
47
45
|
throw new Error(
|
|
48
|
-
`[epic-plan-decompose] Epic #${epicId}
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4
|
-
* cross-Story conflict-policy resolver.
|
|
3
|
+
* (Story #2466). Owns the cross-Story conflict-policy resolver.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
* cross-run mutual exclusion and the child-duplication refusal
|
|
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
|
-
//
|
|
345
|
-
// `context::*` label
|
|
346
|
-
//
|
|
347
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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] | --
|
|
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
|
|