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
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* have been split into phase modules under
|
|
10
10
|
* `lib/orchestration/epic-plan-decompose/phases/`:
|
|
11
11
|
*
|
|
12
|
-
* 1. planning-artifacts — `
|
|
13
|
-
* `resolveConflictPolicy`.
|
|
12
|
+
* 1. planning-artifacts — `resolveConflictPolicy`.
|
|
14
13
|
* 2. dag — `resolveDependencies`,
|
|
15
14
|
* `orderTicketsForCreation`.
|
|
16
15
|
* 3. context — `buildDecomposerSystemPrompt`,
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
* 6. cli — argument parsing + `main()` pipeline.
|
|
24
23
|
*
|
|
25
24
|
* Modes:
|
|
26
|
-
* --emit-context Prints the decomposer authoring context (
|
|
25
|
+
* --emit-context Prints the decomposer authoring context (Epic body,
|
|
27
26
|
* Tech Spec body, risk heuristics, system prompt,
|
|
28
27
|
* ticket cap) as JSON. The authoring middle is the
|
|
29
28
|
* `epic-plan-decompose-author` Skill
|
|
@@ -60,7 +59,6 @@ import {
|
|
|
60
59
|
resolveDependencies,
|
|
61
60
|
} from './lib/orchestration/epic-plan-decompose/phases/dag.js';
|
|
62
61
|
import { runDecomposePhase } from './lib/orchestration/epic-plan-decompose/phases/persist.js';
|
|
63
|
-
import { ensurePlanningArtifacts } from './lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js';
|
|
64
62
|
|
|
65
63
|
// Named exports preserved for the existing test surface. The pre-refactor
|
|
66
64
|
// module published these and the consumers (`tests/ticket-decomposer.test.js`,
|
|
@@ -70,7 +68,6 @@ import { ensurePlanningArtifacts } from './lib/orchestration/epic-plan-decompose
|
|
|
70
68
|
export {
|
|
71
69
|
buildDecomposerSystemPrompt,
|
|
72
70
|
buildDecompositionContext,
|
|
73
|
-
ensurePlanningArtifacts,
|
|
74
71
|
orderTicketsForCreation,
|
|
75
72
|
resolveDependencies,
|
|
76
73
|
runDecomposePhase,
|
|
@@ -10,18 +10,20 @@
|
|
|
10
10
|
* JSON. The authoring middle is the
|
|
11
11
|
* `epic-plan-spec-author` Skill (see
|
|
12
12
|
* `.agents/skills/core/epic-plan-spec-author/SKILL.md`),
|
|
13
|
-
* which consumes this envelope and writes the
|
|
14
|
-
*
|
|
13
|
+
* which consumes this envelope and writes the Tech Spec
|
|
14
|
+
* markdown file.
|
|
15
15
|
*
|
|
16
|
-
* 2. (default) Given author-provided
|
|
16
|
+
* 2. (default) Given author-provided Tech Spec and risk-verdict
|
|
17
17
|
* files, validates the risk verdict against
|
|
18
18
|
* `risk-verdict.schema.json`, derives the planningRisk
|
|
19
|
-
* envelope,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* envelope, folds the authored content into managed
|
|
20
|
+
* sections of the Epic body (Story #4324 — no separate
|
|
21
|
+
* context tickets), records the verdict as a
|
|
22
|
+
* `risk-verdict` structured comment, flips the Epic to
|
|
23
|
+
* `agent::review-spec`, and upserts the
|
|
22
24
|
* `epic-plan-state` structured comment.
|
|
23
25
|
*
|
|
24
|
-
* --force regenerates existing
|
|
26
|
+
* --force regenerates the existing Tech Spec.
|
|
25
27
|
* --steal forcibly transfers a foreign Epic-lease claim (the plan-lease guard
|
|
26
28
|
* fails closed, so any foreign assignee blocks the run unless stolen).
|
|
27
29
|
*
|
|
@@ -70,7 +72,6 @@ import {
|
|
|
70
72
|
} from './lib/orchestration/epic-plan-spec/phases/plan-epic.js';
|
|
71
73
|
import {
|
|
72
74
|
ACCEPTANCE_SPEC_SYSTEM_PROMPT,
|
|
73
|
-
PRD_SYSTEM_PROMPT,
|
|
74
75
|
TECH_SPEC_SYSTEM_PROMPT,
|
|
75
76
|
} from './lib/orchestration/epic-plan-spec/phases/prompts.js';
|
|
76
77
|
import {
|
|
@@ -89,7 +90,6 @@ export {
|
|
|
89
90
|
buildAuthoringContext,
|
|
90
91
|
drainPendingCleanupAtBoot,
|
|
91
92
|
loadRiskVerdict,
|
|
92
|
-
PRD_SYSTEM_PROMPT,
|
|
93
93
|
planEpic,
|
|
94
94
|
resolveAcceptancePersistence,
|
|
95
95
|
resolveMemoryDir,
|
|
@@ -158,9 +158,9 @@ async function main() {
|
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
if (!values
|
|
161
|
+
if (!values['tech-spec'] || !values['risk-verdict']) {
|
|
162
162
|
throw new Error(
|
|
163
|
-
'Missing --
|
|
163
|
+
'Missing --tech-spec and/or --risk-verdict file paths. (Use --emit-context first to gather authoring context; the epic-plan-spec-author Skill writes all artifacts including risk-verdict.json.)',
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -168,20 +168,17 @@ async function main() {
|
|
|
168
168
|
// GitHub mutation: a malformed verdict fails closed here (Epic #3865).
|
|
169
169
|
const riskVerdict = loadRiskVerdict(values['risk-verdict']);
|
|
170
170
|
|
|
171
|
-
const readPromises = [
|
|
172
|
-
|
|
173
|
-
readFile(values
|
|
174
|
-
];
|
|
175
|
-
if (values['acceptance-spec']) {
|
|
176
|
-
readPromises.push(readFile(values['acceptance-spec'], 'utf8'));
|
|
171
|
+
const readPromises = [readFile(values['tech-spec'], 'utf8')];
|
|
172
|
+
if (values['acceptance-table']) {
|
|
173
|
+
readPromises.push(readFile(values['acceptance-table'], 'utf8'));
|
|
177
174
|
}
|
|
178
|
-
const [
|
|
175
|
+
const [techSpecContent, acceptanceSpecContent = null] =
|
|
179
176
|
await Promise.all(readPromises);
|
|
180
177
|
|
|
181
178
|
const result = await runSpecPhase(
|
|
182
179
|
epicId,
|
|
183
180
|
provider,
|
|
184
|
-
{
|
|
181
|
+
{ techSpecContent, acceptanceSpecContent },
|
|
185
182
|
settings,
|
|
186
183
|
{
|
|
187
184
|
force: values.force,
|
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The two gates catch different problems and are intentionally distinct:
|
|
12
12
|
* - The wave gate misses descendants that exist on GitHub but were never
|
|
13
|
-
* in the manifest —
|
|
14
|
-
*
|
|
13
|
+
* in the manifest — mid-sprint additions, recuts that bypassed the
|
|
14
|
+
* dispatcher, or legacy `context::*` artifacts on historical Epics.
|
|
15
15
|
* - The hierarchy gate misses parked follow-ons that live as separate
|
|
16
16
|
* top-level Stories outside the Epic's sub-issue graph.
|
|
17
17
|
*
|
|
18
18
|
* Per ticket type the rule is:
|
|
19
19
|
* - Stories — must be closed.
|
|
20
|
-
* - Auxiliary (context
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - Auxiliary (legacy `context::*` artifacts) — ignored. Story #4324
|
|
21
|
+
* folded planning content into the Epic body; historical Epics keep
|
|
22
|
+
* their old context tickets, which are reference-only here.
|
|
23
23
|
*
|
|
24
24
|
* **2-tier hierarchy (Story #4041).** Mandrel ships only Epic / Story
|
|
25
25
|
* tickets. `getSubTickets(<storyId>)` returns `[]`; the walk
|
|
@@ -39,7 +39,7 @@ import { parseArgs } from 'node:util';
|
|
|
39
39
|
import { runAsCli } from './lib/cli-utils.js';
|
|
40
40
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
41
41
|
import { Logger } from './lib/Logger.js';
|
|
42
|
-
import {
|
|
42
|
+
import { TYPE_LABELS } from './lib/label-constants.js';
|
|
43
43
|
import { createProvider } from './lib/provider-factory.js';
|
|
44
44
|
import { concurrentMap } from './lib/util/concurrent-map.js';
|
|
45
45
|
|
|
@@ -52,10 +52,10 @@ const SUB_TICKET_FETCH_CONCURRENCY = 4;
|
|
|
52
52
|
function classify(ticket) {
|
|
53
53
|
const labels = ticket.labels ?? [];
|
|
54
54
|
if (labels.includes(TYPE_LABELS.STORY)) return 'story';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
) {
|
|
55
|
+
// Legacy planning artifacts (pre-#4324 `context::*` tickets on
|
|
56
|
+
// historical Epics) are ignored — they are reference artifacts, not
|
|
57
|
+
// deliverables. New Epics carry planning content on the body itself.
|
|
58
|
+
if (labels.some((l) => typeof l === 'string' && l.startsWith('context::'))) {
|
|
59
59
|
return 'auxiliary';
|
|
60
60
|
}
|
|
61
61
|
return 'other';
|
|
@@ -165,7 +165,7 @@ export async function runHierarchyGate({ epicId, injectedProvider } = {}) {
|
|
|
165
165
|
|
|
166
166
|
const auxNote =
|
|
167
167
|
auxiliaryDeferred > 0
|
|
168
|
-
? ` (${auxiliaryDeferred} auxiliary ticket${auxiliaryDeferred === 1 ? '' : 's'}
|
|
168
|
+
? ` (${auxiliaryDeferred} legacy auxiliary ticket${auxiliaryDeferred === 1 ? '' : 's'} ignored)`
|
|
169
169
|
: '';
|
|
170
170
|
Logger.info(
|
|
171
171
|
`[hierarchy-gate] ✅ All ${descendants.length - auxiliaryDeferred} planned descendant(s) under Epic #${epicId} are closed${auxNote}.`,
|
|
@@ -33,15 +33,16 @@ export class ITicketingProvider {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Fetch the Epic issue with body
|
|
36
|
+
* Fetch the Epic issue with its body — the single planning document
|
|
37
|
+
* (ideation sections plus the folded Tech Spec / Acceptance Table
|
|
38
|
+
* managed sections, Story #4324).
|
|
37
39
|
*
|
|
38
40
|
* @param {number} epicId - GitHub Issue number of the Epic.
|
|
39
41
|
* @returns {Promise<{
|
|
40
42
|
* id: number,
|
|
41
43
|
* title: string,
|
|
42
44
|
* body: string,
|
|
43
|
-
* labels: string[]
|
|
44
|
-
* linkedIssues: { prd: number|null, techSpec: number|null }
|
|
45
|
+
* labels: string[]
|
|
45
46
|
* }>}
|
|
46
47
|
*/
|
|
47
48
|
async getEpic(_epicId) {
|
|
@@ -82,6 +82,41 @@ export function resolveCrapEnvOverrides(crapConfig, env) {
|
|
|
82
82
|
return { newMethodCeiling, tolerance, refreshTag, overrides };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Pure helper: resolve the one-shot bundle-size refresh/acknowledge flag
|
|
87
|
+
* (Story #151). Unlike `coverage` / `crap` / `maintainability`, the
|
|
88
|
+
* bundle-size gate has no scorer of its own — the measured sizes come from
|
|
89
|
+
* a build step the operator already runs, not a source-tree rescan — so
|
|
90
|
+
* there is no `refreshBaseline({ kind: 'bundle-size', ... })` path to
|
|
91
|
+
* regenerate a "corrected" baseline. Instead, `BUNDLE_SIZE_REFRESH=1`
|
|
92
|
+
* (mirroring `CRAP_TOLERANCE`'s env-override precedent) tells
|
|
93
|
+
* `check-baselines --gate bundle-size` to treat this run's head
|
|
94
|
+
* measurements as the newly acknowledged baseline: head-vs-base
|
|
95
|
+
* regressions are demoted to `unchanged` for this invocation only. Floors
|
|
96
|
+
* still apply — an acknowledged PR can still fail on an absolute budget
|
|
97
|
+
* breach, only the ratchet-vs-`origin/main` comparison is suspended.
|
|
98
|
+
*
|
|
99
|
+
* The flag is **not persisted** anywhere (no config write, no committed
|
|
100
|
+
* tag): the very next `check-baselines` invocation without the env var —
|
|
101
|
+
* i.e. the next PR — reverts to full strict enforcement automatically, so
|
|
102
|
+
* there is no lingering loosened tolerance to remember to reset (AC-3).
|
|
103
|
+
*
|
|
104
|
+
* Accepted truthy values: `1`, `true` (case-insensitive). Anything else
|
|
105
|
+
* (including unset/empty) resolves to `acknowledged: false`.
|
|
106
|
+
*
|
|
107
|
+
* @param {NodeJS.ProcessEnv} env
|
|
108
|
+
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveBundleSizeEnvOverrides(env) {
|
|
111
|
+
const raw = env?.BUNDLE_SIZE_REFRESH;
|
|
112
|
+
const acknowledged =
|
|
113
|
+
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
114
|
+
const overrides = acknowledged
|
|
115
|
+
? [`acknowledged=true (BUNDLE_SIZE_REFRESH=${raw})`]
|
|
116
|
+
: [];
|
|
117
|
+
return { acknowledged, overrides };
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
/**
|
|
86
121
|
* Pure helper: resolve the effective MI tolerance by layering precedence:
|
|
87
122
|
* 1. `CRAP_TOLERANCE` env-var (CI override — the baseline-refresh-
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Task #2103; workspace-aware extension from Story #2956).
|
|
4
4
|
*
|
|
5
5
|
* Used by `epic-plan-spec.js#buildAuthoringContext` to decide whether the
|
|
6
|
-
* acceptance-
|
|
6
|
+
* acceptance-table section should plan **features-first** Story ordering (a real
|
|
7
7
|
* pending-tag is available, so the features-first Story can ship `.feature`
|
|
8
8
|
* files marked `@pending` / `@skip` ahead of the implementation Stories) or
|
|
9
9
|
* fall back to **dependencies-first** ordering (no pending tag → cannot
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Story #2637 (sibling to #2634 codebase-snapshot, #2635 spec-freshness,
|
|
5
5
|
* #2636 file-assumption gate). The Acceptance Engineer step of
|
|
6
|
-
* `epic-plan-spec-author` currently writes ACs from
|
|
6
|
+
* `epic-plan-spec-author` currently writes ACs from Epic/Tech Spec narrative
|
|
7
7
|
* alone — it never inspects the consumer project's existing `.feature`
|
|
8
8
|
* files. Planned ACs frequently duplicate scenarios that already exist or
|
|
9
9
|
* re-specify behaviour the codebase already proves; the duplication is
|
|
@@ -60,8 +60,6 @@ export function parseSprintArgs(args = process.argv) {
|
|
|
60
60
|
'no-full-scope-crap': { type: 'boolean', default: false },
|
|
61
61
|
executor: { type: 'string' },
|
|
62
62
|
cwd: { type: 'string' },
|
|
63
|
-
prd: { type: 'string' },
|
|
64
|
-
'tech-spec': { type: 'string' },
|
|
65
63
|
'recut-of': { type: 'string' },
|
|
66
64
|
resume: { type: 'boolean', default: false },
|
|
67
65
|
restart: { type: 'boolean', default: false },
|
|
@@ -88,10 +86,8 @@ export function parseSprintArgs(args = process.argv) {
|
|
|
88
86
|
process.env.AGENT_WORKTREE_ROOT ||
|
|
89
87
|
null,
|
|
90
88
|
recutOf: parseTicketId(values['recut-of']),
|
|
91
|
-
// Story #4253: pre-resolved Epic
|
|
89
|
+
// Story #4253: pre-resolved Epic linkage threaded by the /deliver
|
|
92
90
|
// fan-out so `story-init.js` can skip the per-Story `getEpic` round-trip.
|
|
93
|
-
prdId: parseTicketId(values.prd),
|
|
94
|
-
techSpecId: parseTicketId(values['tech-spec']),
|
|
95
91
|
resume: values.resume ?? false,
|
|
96
92
|
restart: values.restart ?? false,
|
|
97
93
|
noEvidence: values['no-evidence'] ?? false,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* codebase-snapshot.js — Bounded structural view of the consumer repo.
|
|
3
3
|
*
|
|
4
4
|
* Story #2634 (sibling to #2635 spec-freshness). `/plan` Phase 7
|
|
5
|
-
* authors
|
|
5
|
+
* authors the Tech Spec from documentation alone — `architecture.md`,
|
|
6
6
|
* `data-dictionary.md`, `decisions.md`, `patterns.md`. When those docs
|
|
7
7
|
* drift from the real source tree, the Architect persona cites modules
|
|
8
8
|
* and paths that no longer exist, and the mismatch only surfaces at
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Layout:
|
|
12
12
|
* temp/epic-<eid>/
|
|
13
|
-
* ├─ prd.md
|
|
14
13
|
* ├─ techspec.md
|
|
15
14
|
* ├─ manifest.md (dispatch manifest)
|
|
16
15
|
* ├─ retro.md (mirror of GitHub retro at Epic close)
|
|
@@ -228,7 +227,7 @@ export function epicTempDir(eid, config) {
|
|
|
228
227
|
* location instead of being dropped on the floor.
|
|
229
228
|
*
|
|
230
229
|
* Story #2940 introduced the intermediate `stories/` segment so that
|
|
231
|
-
* per-Epic top-level artifacts (`
|
|
230
|
+
* per-Epic top-level artifacts (`techspec.md`, `manifest.md`,
|
|
232
231
|
* `retro.md`, `lifecycle.ndjson`, `baselines/`, `checkpoints/`) are
|
|
233
232
|
* visually and structurally separated from the per-Story siblings.
|
|
234
233
|
*
|
|
@@ -291,8 +290,6 @@ export function storyArtifactPath(eid, sid, name, config) {
|
|
|
291
290
|
|
|
292
291
|
// --- Canonical Epic-level filenames (Tech Spec #1032 §tempRoot) ---
|
|
293
292
|
|
|
294
|
-
export const epicPrdPath = (eid, config) =>
|
|
295
|
-
epicArtifactPath(eid, 'prd.md', config);
|
|
296
293
|
export const epicTechSpecPath = (eid, config) =>
|
|
297
294
|
epicArtifactPath(eid, 'techspec.md', config);
|
|
298
295
|
export const epicManifestPath = (eid, config) =>
|
|
@@ -263,6 +263,11 @@ const TASK_SIZING_SCHEMA = {
|
|
|
263
263
|
hardFiles: { type: 'integer', minimum: 1 },
|
|
264
264
|
maxAcceptance: { type: 'integer', minimum: 1 },
|
|
265
265
|
softAcceptanceCount: { type: 'integer', minimum: 1 },
|
|
266
|
+
// Under-size (merge-candidate) thresholds (Story #4312). A Story whose
|
|
267
|
+
// footprint is at or below BOTH ceilings and that carries a `depends_on`
|
|
268
|
+
// edge to a sibling trips the advisory `merge-candidate` soft finding.
|
|
269
|
+
mergeCandidateMaxFiles: { type: 'integer', minimum: 1 },
|
|
270
|
+
mergeCandidateMaxAcceptance: { type: 'integer', minimum: 1 },
|
|
266
271
|
},
|
|
267
272
|
additionalProperties: false,
|
|
268
273
|
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epic-body-sections.js — marker-delimited managed sections of the Epic body.
|
|
3
|
+
*
|
|
4
|
+
* Story #4324 retired the `context::tech-spec` / `context::acceptance-spec`
|
|
5
|
+
* ticket classes: the Epic body is now the single planning document. The
|
|
6
|
+
* Tech Spec (opening with `## Delivery Slicing`, per #4316) and the
|
|
7
|
+
* Acceptance Spec's AC-ID table (`## Acceptance Table`, Outcomes keyed to
|
|
8
|
+
* Epic AC bullets per #4315) land as **managed sections** of the Epic body,
|
|
9
|
+
* delimited by invisible HTML comment markers so each writer can update
|
|
10
|
+
* *only its own region* without rewriting the rest of the body.
|
|
11
|
+
*
|
|
12
|
+
* Section-scoped writes are the load-bearing contract (extending the
|
|
13
|
+
* single-writer discipline #4303 established for the body trailer):
|
|
14
|
+
*
|
|
15
|
+
* - The Phase 7 spec persist path (`epic-plan-spec/phases/plan-epic.js`)
|
|
16
|
+
* upserts the `techSpec` and `acceptanceTable` regions.
|
|
17
|
+
* - The close-time acceptance reconciler
|
|
18
|
+
* (`acceptance-spec-reconciler.js`) reads and rewrites the
|
|
19
|
+
* `acceptanceTable` region only (verification dispositions).
|
|
20
|
+
*
|
|
21
|
+
* Everything outside a managed region is byte-preserved by every helper in
|
|
22
|
+
* this module. Markers are chosen so GitHub renders nothing for them; the
|
|
23
|
+
* human-visible headings (`## Delivery Slicing`, `## Acceptance Table`)
|
|
24
|
+
* live INSIDE the regions as ordinary content.
|
|
25
|
+
*
|
|
26
|
+
* Pure ESM, no I/O.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Managed-region descriptors. `start`/`end` are the literal marker lines;
|
|
31
|
+
* `label` is the human name used in log lines and error messages.
|
|
32
|
+
*
|
|
33
|
+
* @type {Readonly<Record<'techSpec'|'acceptanceTable', { start: string, end: string, label: string }>>}
|
|
34
|
+
*/
|
|
35
|
+
export const EPIC_BODY_SECTIONS = Object.freeze({
|
|
36
|
+
techSpec: Object.freeze({
|
|
37
|
+
start: '<!-- mandrel:tech-spec:start -->',
|
|
38
|
+
end: '<!-- mandrel:tech-spec:end -->',
|
|
39
|
+
label: 'Tech Spec',
|
|
40
|
+
}),
|
|
41
|
+
acceptanceTable: Object.freeze({
|
|
42
|
+
start: '<!-- mandrel:acceptance-table:start -->',
|
|
43
|
+
end: '<!-- mandrel:acceptance-table:end -->',
|
|
44
|
+
label: 'Acceptance Table',
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Canonical heading the acceptance-table region opens with. Distinct from
|
|
50
|
+
* the Epic's ideation `## Acceptance Criteria` bullets (which remain the
|
|
51
|
+
* SSOT for *what* the table verifies — the table anchors to those bullets).
|
|
52
|
+
*/
|
|
53
|
+
export const ACCEPTANCE_TABLE_HEADING = '## Acceptance Table';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Regex matching the Tech Spec's required opening heading (same variants
|
|
57
|
+
* `spec-section-validator.js` accepts).
|
|
58
|
+
*/
|
|
59
|
+
const DELIVERY_SLICING_RE = /^##\s+(?:Delivery\s+)?Slicing\s*$/im;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
63
|
+
* @returns {{ start: string, end: string, label: string }}
|
|
64
|
+
*/
|
|
65
|
+
function descriptor(kind) {
|
|
66
|
+
const d = EPIC_BODY_SECTIONS[kind];
|
|
67
|
+
if (!d) {
|
|
68
|
+
throw new TypeError(
|
|
69
|
+
`epic-body-sections: unknown section kind "${kind}" (expected ${Object.keys(EPIC_BODY_SECTIONS).join(' | ')})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Locate a managed region. Returns `null` when either marker is absent or
|
|
77
|
+
* the end marker precedes the start marker (malformed body — treated as
|
|
78
|
+
* absent so a writer re-appends a well-formed region rather than
|
|
79
|
+
* corrupting the body further).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} body
|
|
82
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
83
|
+
* @returns {{ startIdx: number, contentStart: number, contentEnd: number, endIdx: number }|null}
|
|
84
|
+
*/
|
|
85
|
+
function locate(body, kind) {
|
|
86
|
+
const { start, end } = descriptor(kind);
|
|
87
|
+
if (typeof body !== 'string' || body.length === 0) return null;
|
|
88
|
+
const startIdx = body.indexOf(start);
|
|
89
|
+
if (startIdx === -1) return null;
|
|
90
|
+
const contentStart = startIdx + start.length;
|
|
91
|
+
const endIdx = body.indexOf(end, contentStart);
|
|
92
|
+
if (endIdx === -1) return null;
|
|
93
|
+
return { startIdx, contentStart, contentEnd: endIdx, endIdx };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* True when the body carries a well-formed managed region of `kind`.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} body
|
|
100
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function hasEpicSection(body, kind) {
|
|
104
|
+
return locate(body, kind) !== null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract a managed region's content (between the markers, trimmed).
|
|
109
|
+
* Returns `null` when the region is absent.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} body
|
|
112
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
113
|
+
* @returns {string|null}
|
|
114
|
+
*/
|
|
115
|
+
export function extractEpicSection(body, kind) {
|
|
116
|
+
const loc = locate(body, kind);
|
|
117
|
+
if (!loc) return null;
|
|
118
|
+
return body.slice(loc.contentStart, loc.contentEnd).trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Insert or replace a managed region, preserving every byte outside it.
|
|
123
|
+
*
|
|
124
|
+
* When the region exists, only the content between the markers is
|
|
125
|
+
* replaced. When absent, the region is appended at the end of the body
|
|
126
|
+
* (trailing whitespace normalised to a single blank-line separator). The
|
|
127
|
+
* `techSpec` region is appended before an existing `acceptanceTable`
|
|
128
|
+
* region so the rendered document keeps its canonical order
|
|
129
|
+
* (ideation sections → Tech Spec → Acceptance Table).
|
|
130
|
+
*
|
|
131
|
+
* @param {string} body
|
|
132
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
133
|
+
* @param {string} content Section content (headings included).
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
export function upsertEpicSection(body, kind, content) {
|
|
137
|
+
const { start, end } = descriptor(kind);
|
|
138
|
+
const safeBody = typeof body === 'string' ? body : '';
|
|
139
|
+
const trimmedContent = typeof content === 'string' ? content.trim() : '';
|
|
140
|
+
const region = `${start}\n\n${trimmedContent}\n\n${end}`;
|
|
141
|
+
|
|
142
|
+
const loc = locate(safeBody, kind);
|
|
143
|
+
if (loc) {
|
|
144
|
+
return (
|
|
145
|
+
safeBody.slice(0, loc.startIdx) +
|
|
146
|
+
region +
|
|
147
|
+
safeBody.slice(loc.endIdx + end.length)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Keep canonical order when appending: the Tech Spec region goes before
|
|
152
|
+
// an already-present acceptance-table region.
|
|
153
|
+
if (kind === 'techSpec') {
|
|
154
|
+
const acceptanceLoc = locate(safeBody, 'acceptanceTable');
|
|
155
|
+
if (acceptanceLoc) {
|
|
156
|
+
const head = safeBody
|
|
157
|
+
.slice(0, acceptanceLoc.startIdx)
|
|
158
|
+
.replace(/\s+$/, '');
|
|
159
|
+
const tail = safeBody.slice(acceptanceLoc.startIdx);
|
|
160
|
+
return `${head}\n\n${region}\n\n${tail}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const trimmedBody = safeBody.replace(/\s+$/, '');
|
|
165
|
+
return trimmedBody.length > 0
|
|
166
|
+
? `${trimmedBody}\n\n${region}\n`
|
|
167
|
+
: `${region}\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove a managed region (markers and content). Byte-preserving outside
|
|
172
|
+
* the region; collapses the surrounding blank lines the writer added.
|
|
173
|
+
* No-op when the region is absent.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} body
|
|
176
|
+
* @param {'techSpec'|'acceptanceTable'} kind
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export function stripEpicSection(body, kind) {
|
|
180
|
+
const { end } = descriptor(kind);
|
|
181
|
+
const loc = locate(body, kind);
|
|
182
|
+
if (!loc) return typeof body === 'string' ? body : '';
|
|
183
|
+
const before = body.slice(0, loc.startIdx).replace(/\n+$/, '\n');
|
|
184
|
+
const after = body.slice(loc.endIdx + end.length).replace(/^\n+/, '\n');
|
|
185
|
+
return (before + after).replace(/\n{3,}/g, '\n\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Re-plan / decompose detection: true when the Epic body carries folded
|
|
190
|
+
* Tech Spec content — the managed region, or (defence in depth for a
|
|
191
|
+
* hand-authored body) a bare `## Delivery Slicing` heading.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} body
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
export function hasTechSpecContent(body) {
|
|
197
|
+
if (hasEpicSection(body, 'techSpec')) return true;
|
|
198
|
+
return typeof body === 'string' && DELIVERY_SLICING_RE.test(body);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Strip the retired machine-managed `## Planning Artifacts` checklist from
|
|
203
|
+
* an Epic body (the section that linked the now-retired context tickets).
|
|
204
|
+
* The slice ends at the next `## ` heading, a managed-region marker, or
|
|
205
|
+
* EOF. Historical bodies without the section pass through untouched.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} body
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function stripPlanningArtifactsSection(body) {
|
|
211
|
+
if (typeof body !== 'string' || body.length === 0) return '';
|
|
212
|
+
const headingMatch = body.match(/^##\s+Planning Artifacts[^\n]*$/m);
|
|
213
|
+
if (!headingMatch || typeof headingMatch.index !== 'number') return body;
|
|
214
|
+
const start = headingMatch.index;
|
|
215
|
+
const afterHeading = start + headingMatch[0].length;
|
|
216
|
+
const rest = body.slice(afterHeading);
|
|
217
|
+
const boundary = rest.search(/^(?:##\s|<!-- mandrel:)/m);
|
|
218
|
+
const end = boundary === -1 ? body.length : afterHeading + boundary;
|
|
219
|
+
const before = body.slice(0, start).replace(/\n+$/, '\n');
|
|
220
|
+
const after = body.slice(end);
|
|
221
|
+
return (before + after).replace(/\n{3,}/g, '\n\n').replace(/^\n+/, '');
|
|
222
|
+
}
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
* `## Work Breakdown`
|
|
29
29
|
* - `## Acceptance Criteria`, `## Acceptance`, `## AC`
|
|
30
30
|
*
|
|
31
|
+
* Story #4324 folded the Tech Spec and Acceptance Spec into the Epic body
|
|
32
|
+
* as managed sections (`## Delivery Slicing`-led spec sections and the
|
|
33
|
+
* `## Acceptance Table`). The gate *recognises* those planning sections —
|
|
34
|
+
* reporting their presence under `planningSections[]` so a post-fold Epic
|
|
35
|
+
* body scores exactly as its ideation content deserves — but they never
|
|
36
|
+
* count toward (or against) the five-ideation-section verdict: Phase 6
|
|
37
|
+
* runs before Phase 7 authors them, and a re-planned Epic that already
|
|
38
|
+
* carries them must not be penalised or auto-passed by their presence.
|
|
39
|
+
*
|
|
31
40
|
* Pure ESM, no I/O.
|
|
32
41
|
*/
|
|
33
42
|
|
|
@@ -54,6 +63,26 @@ export const SECTION_NAMES = Object.freeze([
|
|
|
54
63
|
'acceptanceCriteria',
|
|
55
64
|
]);
|
|
56
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Post-fold planning sections (Story #4324) — recognised and reported,
|
|
68
|
+
* never scored. `deliverySlicing` accepts the same heading variants as
|
|
69
|
+
* `spec-section-validator.js`.
|
|
70
|
+
*/
|
|
71
|
+
const PLANNING_SECTION_RE = {
|
|
72
|
+
deliverySlicing: /^##\s+(?:Delivery\s+)?Slicing\s*$/im,
|
|
73
|
+
acceptanceTable: /^##\s+Acceptance\s+Table\s*$/im,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Names of the recognised (unscored) planning sections, in document order.
|
|
78
|
+
* Module-private: consumers read the reported `planningSections[]` rows on
|
|
79
|
+
* the scoreEpicBody result rather than importing the name list.
|
|
80
|
+
*/
|
|
81
|
+
const PLANNING_SECTION_NAMES = Object.freeze([
|
|
82
|
+
'deliverySlicing',
|
|
83
|
+
'acceptanceTable',
|
|
84
|
+
]);
|
|
85
|
+
|
|
57
86
|
const CLEAR_THRESHOLD = 4;
|
|
58
87
|
const PLACEHOLDER_PATTERN = /^_\(not\s+specified\)_$/i;
|
|
59
88
|
|
|
@@ -80,6 +109,7 @@ function classify(content) {
|
|
|
80
109
|
* verdict: 'clear' | 'needs-refinement',
|
|
81
110
|
* sections: Array<{ name: string, status: 'present' | 'placeholder' | 'missing' }>,
|
|
82
111
|
* missingOrPlaceholder: string[],
|
|
112
|
+
* planningSections: Array<{ name: string, status: 'present' | 'missing' }>,
|
|
83
113
|
* }}
|
|
84
114
|
*/
|
|
85
115
|
const REQUIRED_SECTION = 'acceptanceCriteria';
|
|
@@ -138,5 +168,12 @@ export function scoreEpicBody({ body } = {}) {
|
|
|
138
168
|
? 'clear'
|
|
139
169
|
: 'needs-refinement';
|
|
140
170
|
|
|
141
|
-
|
|
171
|
+
// Informational only: presence of the post-fold planning sections
|
|
172
|
+
// (Story #4324). Never feeds the verdict.
|
|
173
|
+
const planningSections = PLANNING_SECTION_NAMES.map((name) => ({
|
|
174
|
+
name,
|
|
175
|
+
status: PLANNING_SECTION_RE[name].test(source) ? 'present' : 'missing',
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
return { verdict, sections, missingOrPlaceholder, planningSections };
|
|
142
179
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* using the canonical template at `.agents/templates/epic-from-idea.md`.
|
|
6
6
|
* Phase 4: open the GitHub Issue via an injected provider with the
|
|
7
7
|
* `type::epic` label only — no `state::draft` (the Epic carries only
|
|
8
|
-
* `type::epic` until
|
|
8
|
+
* `type::epic` until spec authoring writes `agent::review-spec`).
|
|
9
9
|
*
|
|
10
10
|
* The template is parsed from a string the caller has already loaded
|
|
11
11
|
* (typically via `fs.readFile`). The renderer is pure — no I/O — and
|
|
@@ -30,13 +30,23 @@ const SECTION_RE = {
|
|
|
30
30
|
/^##\s+(?:Non[\s-]?Goals|Out\s+of\s+Scope|Not\s+Doing(?:\s+\(and\s+Why\))?)\s*$/im,
|
|
31
31
|
scope:
|
|
32
32
|
/^##\s+(?:MVP\s+|Proposed\s+)?Scope(?:\s+\([^)]+\))?\s*$|^##\s+Work\s+Breakdown\s*$/im,
|
|
33
|
+
// Story #4314 — the PRD's one novel section (User Stories) now folds into
|
|
34
|
+
// the Epic body as a canonical section.
|
|
35
|
+
userStories: /^##\s+(?:User\s+Stories|Stories|Personas?\s+&\s+Stories)\s*$/im,
|
|
33
36
|
acceptanceCriteria: /^##\s+(?:Acceptance(?:\s+Criteria)?|AC)\s*$/im,
|
|
34
37
|
};
|
|
35
38
|
|
|
36
|
-
const ORDER = [
|
|
39
|
+
const ORDER = [
|
|
40
|
+
'context',
|
|
41
|
+
'goal',
|
|
42
|
+
'nonGoals',
|
|
43
|
+
'scope',
|
|
44
|
+
'userStories',
|
|
45
|
+
'acceptanceCriteria',
|
|
46
|
+
];
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
|
-
* Extract the
|
|
49
|
+
* Extract the six canonical sections from an idea-refinement one-pager.
|
|
40
50
|
*
|
|
41
51
|
* @param {string} onePager - Markdown produced by Phase 3 of the
|
|
42
52
|
* `idea-refinement` skill.
|
|
@@ -46,6 +56,7 @@ const ORDER = ['context', 'goal', 'nonGoals', 'scope', 'acceptanceCriteria'];
|
|
|
46
56
|
* goal: string,
|
|
47
57
|
* nonGoals: string,
|
|
48
58
|
* scope: string,
|
|
59
|
+
* userStories: string,
|
|
49
60
|
* acceptanceCriteria: string,
|
|
50
61
|
* }}
|
|
51
62
|
*/
|
|
@@ -72,6 +83,7 @@ export function parseOnePager(onePager) {
|
|
|
72
83
|
goal: '',
|
|
73
84
|
nonGoals: '',
|
|
74
85
|
scope: '',
|
|
86
|
+
userStories: '',
|
|
75
87
|
acceptanceCriteria: '',
|
|
76
88
|
};
|
|
77
89
|
|