mandrel 1.81.0 → 1.83.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 +46 -5
- package/.agents/docs/SDLC.md +97 -82
- package/.agents/docs/agentrc-reference.json +10 -2
- package/.agents/docs/configuration.md +4 -1
- package/.agents/docs/execution-reference.md +52 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +85 -45
- 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 +41 -3
- package/.agents/schemas/qa-ledger.schema.json +2 -2
- package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
- package/.agents/scripts/epic-deliver-prepare.js +40 -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/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/explain.js +4 -1
- package/.agents/scripts/lib/config/temp-paths.js +1 -4
- package/.agents/scripts/lib/config-settings-schema.js +30 -1
- package/.agents/scripts/lib/epic-body-sections.js +310 -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/findings/promote-finding.js +3 -3
- package/.agents/scripts/lib/findings/severity.js +5 -6
- 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 +65 -2
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +105 -9
- package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
- package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
- 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/story-close/baseline-attribution/phases/refresh-commit.js +15 -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/console-allowlist.js +5 -4
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
- 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 +43 -22
- package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +27 -40
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +15 -13
- package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +143 -151
- package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
- 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 +8 -8
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
- 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-reference.md +514 -0
- package/.agents/workflows/helpers/deliver-epic.md +173 -490
- package/.agents/workflows/helpers/epic-audit.md +11 -8
- package/.agents/workflows/helpers/epic-deliver-story.md +45 -27
- 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-reference.md +136 -0
- package/.agents/workflows/helpers/plan-epic.md +141 -256
- package/.agents/workflows/helpers/plan-story.md +31 -61
- package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
- package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
- package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
- package/.agents/workflows/helpers/single-story-deliver.md +129 -393
- 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/.agents/workflows/qa-explore.md +63 -32
- package/.agents/workflows/qa-run.md +293 -130
- package/docs/CHANGELOG.md +35 -0
- package/package.json +1 -1
- package/.agents/schemas/qa-finding.schema.json +0 -133
- 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
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* acceptance-spec-reconciler.js — Story #2106 / Task #2113 (Epic #2001).
|
|
4
4
|
*
|
|
5
|
-
* Diffs the AC IDs declared in
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Diffs the AC IDs declared in the Epic body's `## Acceptance Table`
|
|
6
|
+
* managed section (Story #4324 retired the `context::acceptance-spec`
|
|
7
|
+
* ticket class — the table now lives on the Epic body itself) against the
|
|
8
|
+
* **per-Epic-namespaced** `@epic-<id>-ac-*` / `@pending` tags emitted by
|
|
9
|
+
* scenarios under `tests/features/**`. The namespace is load-bearing
|
|
10
|
+
* (Story #3362): `tests/features` is a single global tree shared by every
|
|
11
|
+
* Epic, so a bare `@ac-N` tag authored under an unrelated Epic's scenarios
|
|
12
|
+
* must not count as coverage for this Epic. Surfaces three categories:
|
|
12
13
|
*
|
|
13
14
|
* - `satisfied[]` — AC IDs covered by at least one non-pending scenario.
|
|
14
15
|
* - `pending[]` — AC IDs covered only by scenarios tagged `@pending`.
|
|
15
16
|
* - `missing[]` — AC IDs declared in the spec with no matching scenario.
|
|
16
17
|
*
|
|
17
18
|
* Used by `epic-deliver-finalize.js` (Task #2111) as a close-time gate: a
|
|
18
|
-
* non-OK result aborts finalize before
|
|
19
|
-
*
|
|
19
|
+
* non-OK result aborts finalize before the PR opens, so the Epic stays
|
|
20
|
+
* blocked until the AC coverage gap is fixed.
|
|
20
21
|
*
|
|
21
22
|
* Per `.agents/rules/orchestration-error-handling.md`, this module
|
|
22
23
|
* **throws `Error`** for unrecoverable conditions (rather than calling
|
|
@@ -27,16 +28,25 @@
|
|
|
27
28
|
* Usage:
|
|
28
29
|
* node .agents/scripts/acceptance-spec-reconciler.js --epic <epicId>
|
|
29
30
|
*
|
|
31
|
+
* When invoked with `writeDispositions: true` (the close-time lifecycle
|
|
32
|
+
* listener path), the reconciler records the verification outcome of each
|
|
33
|
+
* AC row — `satisfied` / `pending` / `missing` — into the Disposition
|
|
34
|
+
* column of the `## Acceptance Table` section. The write is
|
|
35
|
+
* **section-scoped**: only the managed acceptance-table region of the Epic
|
|
36
|
+
* body is rewritten; everything outside it is byte-preserved (Story #4324
|
|
37
|
+
* guardrail, extending the single-writer discipline of #4303).
|
|
38
|
+
*
|
|
30
39
|
* Stdout: a single JSON envelope:
|
|
31
40
|
* {
|
|
32
41
|
* "epicId": <number>,
|
|
33
|
-
* "acceptanceSpecId": <number|null>,
|
|
34
42
|
* "ok": <boolean>,
|
|
35
|
-
* "
|
|
43
|
+
* "status": "ok"|"waived"|"empty-spec"|"gap",
|
|
44
|
+
* "acIds": ["AC-1", "AC-2", ...],
|
|
36
45
|
* "satisfied": ["AC-1", ...],
|
|
37
46
|
* "pending": ["AC-2", ...],
|
|
38
47
|
* "missing": ["AC-3", ...],
|
|
39
|
-
* "featureFilesScanned": <number
|
|
48
|
+
* "featureFilesScanned": <number>,
|
|
49
|
+
* "dispositionsUpdated": <boolean>
|
|
40
50
|
* }
|
|
41
51
|
*/
|
|
42
52
|
|
|
@@ -47,34 +57,40 @@ import { parseArgs } from 'node:util';
|
|
|
47
57
|
import { PENDING_TAGS } from './lib/bdd-runner-detect.js';
|
|
48
58
|
import { runAsCli } from './lib/cli-utils.js';
|
|
49
59
|
import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
|
|
50
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
extractEpicSection,
|
|
62
|
+
upsertEpicSection,
|
|
63
|
+
} from './lib/epic-body-sections.js';
|
|
51
64
|
import { Logger } from './lib/Logger.js';
|
|
52
65
|
import { ACCEPTANCE_NA } from './lib/label-constants.js';
|
|
53
66
|
import { createProvider } from './lib/provider-factory.js';
|
|
54
67
|
|
|
55
68
|
const HELP = `Usage: node .agents/scripts/acceptance-spec-reconciler.js --epic <epicId>
|
|
56
69
|
|
|
57
|
-
Diffs the AC IDs in the Epic's
|
|
58
|
-
@ac-*/@pending tags in tests/features/**. Emits a JSON envelope on
|
|
59
|
-
Throws (exit 1) when missing or pending ACs are detected, or when
|
|
60
|
-
has no
|
|
61
|
-
absent.
|
|
70
|
+
Diffs the AC IDs in the Epic body's ## Acceptance Table section against the
|
|
71
|
+
@epic-<id>-ac-*/@pending tags in tests/features/**. Emits a JSON envelope on
|
|
72
|
+
stdout. Throws (exit 1) when missing or pending ACs are detected, or when
|
|
73
|
+
the Epic body has no acceptance-table section and the acceptance::n-a
|
|
74
|
+
waiver label is absent.
|
|
62
75
|
|
|
63
76
|
Options:
|
|
64
77
|
--epic <id> Epic ticket id (required)
|
|
65
78
|
--features-dir <path> Override features directory (default: tests/features)
|
|
66
79
|
--skip-when-waived Exit 0 with status='waived' when acceptance::n-a is
|
|
67
|
-
set instead of throwing on the missing
|
|
80
|
+
set instead of throwing on the missing section.
|
|
81
|
+
--write-dispositions Record each AC's verification outcome into the
|
|
82
|
+
Disposition column of the ## Acceptance Table
|
|
83
|
+
section (section-scoped write).
|
|
68
84
|
-h, --help Show this message and exit.
|
|
69
85
|
`;
|
|
70
86
|
|
|
71
87
|
/**
|
|
72
|
-
* Pure: parse stable AC IDs (AC-<n>) out of an acceptance-
|
|
73
|
-
* authoring style is "Acceptance
|
|
88
|
+
* Pure: parse stable AC IDs (AC-<n>) out of an acceptance-table section.
|
|
89
|
+
* AC authoring style is "Acceptance Table — Markdown table whose first
|
|
74
90
|
* column is the AC ID" — see ACCEPTANCE_SPEC_SYSTEM_PROMPT in
|
|
75
|
-
* epic-plan-spec.js. We scan the entire
|
|
76
|
-
* because operators are free to format the
|
|
77
|
-
* the canonical table.
|
|
91
|
+
* epic-plan-spec.js. We scan the entire section with a permissive regex
|
|
92
|
+
* because operators are free to format the content however they wish
|
|
93
|
+
* around the canonical table.
|
|
78
94
|
*
|
|
79
95
|
* Returns IDs **in document order**, deduplicated, normalised to
|
|
80
96
|
* upper-case (`AC-7`, not `ac-7`).
|
|
@@ -282,15 +298,10 @@ export function classifyCoverage({ acIds, tagSets, epicId = null }) {
|
|
|
282
298
|
* Pure: render the operator-visible blocker message for a non-OK
|
|
283
299
|
* reconciliation result. Exported so finalize can surface the same text.
|
|
284
300
|
*/
|
|
285
|
-
export function renderBlockerMessage({
|
|
286
|
-
epicId,
|
|
287
|
-
acceptanceSpecId,
|
|
288
|
-
missing,
|
|
289
|
-
pending,
|
|
290
|
-
}) {
|
|
301
|
+
export function renderBlockerMessage({ epicId, missing, pending }) {
|
|
291
302
|
const lines = [
|
|
292
303
|
`[acceptance-spec-reconciler] Epic #${epicId} cannot finalize:`,
|
|
293
|
-
`
|
|
304
|
+
`the Epic body's ## Acceptance Table section has uncovered AC IDs.`,
|
|
294
305
|
];
|
|
295
306
|
if (missing.length > 0) {
|
|
296
307
|
lines.push(
|
|
@@ -306,25 +317,69 @@ export function renderBlockerMessage({
|
|
|
306
317
|
return lines.join('\n');
|
|
307
318
|
}
|
|
308
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Pure: rewrite the Disposition column of the acceptance-table section so
|
|
322
|
+
* each AC row records its close-time verification outcome. Only table rows
|
|
323
|
+
* whose first data cell is an `AC-<n>` id are touched; header/divider rows,
|
|
324
|
+
* prose, and rows for unclassified ACs pass through verbatim.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} sectionContent The `## Acceptance Table` section content.
|
|
327
|
+
* @param {{ satisfied: string[], pending: string[], missing: string[] }} classification
|
|
328
|
+
* @returns {string}
|
|
329
|
+
*/
|
|
330
|
+
export function renderDispositions(sectionContent, classification) {
|
|
331
|
+
const outcomeById = new Map();
|
|
332
|
+
for (const id of classification.satisfied ?? []) {
|
|
333
|
+
outcomeById.set(id.toUpperCase(), 'satisfied');
|
|
334
|
+
}
|
|
335
|
+
for (const id of classification.pending ?? []) {
|
|
336
|
+
outcomeById.set(id.toUpperCase(), 'pending');
|
|
337
|
+
}
|
|
338
|
+
for (const id of classification.missing ?? []) {
|
|
339
|
+
outcomeById.set(id.toUpperCase(), 'missing');
|
|
340
|
+
}
|
|
341
|
+
const lines = String(sectionContent ?? '').split('\n');
|
|
342
|
+
const out = lines.map((line) => {
|
|
343
|
+
const trimmed = line.trim();
|
|
344
|
+
if (!trimmed.startsWith('|')) return line;
|
|
345
|
+
const cells = trimmed.split('|');
|
|
346
|
+
// `| a | b |` splits into ['', ' a ', ' b ', ''] — data cells are 1..-2.
|
|
347
|
+
if (cells.length < 4) return line;
|
|
348
|
+
const idMatch = cells[1].trim().match(/^AC-(\d+)$/i);
|
|
349
|
+
if (!idMatch) return line;
|
|
350
|
+
const outcome = outcomeById.get(`AC-${idMatch[1]}`.toUpperCase());
|
|
351
|
+
if (!outcome) return line;
|
|
352
|
+
cells[cells.length - 2] = ` ${outcome} `;
|
|
353
|
+
return cells.join('|');
|
|
354
|
+
});
|
|
355
|
+
return out.join('\n');
|
|
356
|
+
}
|
|
357
|
+
|
|
309
358
|
/**
|
|
310
359
|
* End-to-end reconcile. DI-friendly for tests.
|
|
311
360
|
*
|
|
312
361
|
* Behaviour:
|
|
313
362
|
* - If the Epic carries the `acceptance::n-a` waiver label, returns
|
|
314
363
|
* `{ ok: true, status: 'waived', ... }` without scanning features.
|
|
315
|
-
* - If
|
|
316
|
-
* a clear `Error` — this should never
|
|
317
|
-
*
|
|
318
|
-
* but we defend against direct CLI invocation.
|
|
319
|
-
* - If the
|
|
364
|
+
* - If the Epic body has no `## Acceptance Table` managed section and
|
|
365
|
+
* the waiver is absent, **throws** a clear `Error` — this should never
|
|
366
|
+
* happen in practice because `/deliver` runs after `runSnapshotPhase`'s
|
|
367
|
+
* start gate, but we defend against direct CLI invocation.
|
|
368
|
+
* - If the acceptance-table section declares zero AC IDs, returns
|
|
320
369
|
* `{ ok: true, status: 'empty-spec', ... }`.
|
|
321
370
|
* - Otherwise classifies coverage and returns `{ ok, status, ... }`.
|
|
371
|
+
* With `writeDispositions: true`, the classification is also recorded
|
|
372
|
+
* into the Disposition column of the acceptance-table section — a
|
|
373
|
+
* section-scoped write that preserves every byte outside the managed
|
|
374
|
+
* region (best-effort: a write failure downgrades to a warning and
|
|
375
|
+
* never changes the verdict).
|
|
322
376
|
*
|
|
323
377
|
* @param {{
|
|
324
378
|
* epicId: number,
|
|
325
379
|
* cwd?: string,
|
|
326
380
|
* featuresDir?: string,
|
|
327
381
|
* skipWhenWaived?: boolean,
|
|
382
|
+
* writeDispositions?: boolean,
|
|
328
383
|
* injectedProvider?: object,
|
|
329
384
|
* injectedConfig?: object,
|
|
330
385
|
* loggerImpl?: { info?: Function, warn?: Function, error?: Function },
|
|
@@ -333,7 +388,6 @@ export function renderBlockerMessage({
|
|
|
333
388
|
* }} args
|
|
334
389
|
* @returns {Promise<{
|
|
335
390
|
* epicId: number,
|
|
336
|
-
* acceptanceSpecId: number|null,
|
|
337
391
|
* status: 'ok'|'waived'|'empty-spec'|'gap',
|
|
338
392
|
* ok: boolean,
|
|
339
393
|
* acIds: string[],
|
|
@@ -341,6 +395,7 @@ export function renderBlockerMessage({
|
|
|
341
395
|
* pending: string[],
|
|
342
396
|
* missing: string[],
|
|
343
397
|
* featureFilesScanned: number,
|
|
398
|
+
* dispositionsUpdated: boolean,
|
|
344
399
|
* }>}
|
|
345
400
|
*/
|
|
346
401
|
export async function reconcileAcceptanceSpec({
|
|
@@ -348,6 +403,7 @@ export async function reconcileAcceptanceSpec({
|
|
|
348
403
|
cwd,
|
|
349
404
|
featuresDir,
|
|
350
405
|
skipWhenWaived = false,
|
|
406
|
+
writeDispositions = false,
|
|
351
407
|
injectedProvider,
|
|
352
408
|
injectedConfig,
|
|
353
409
|
loggerImpl,
|
|
@@ -367,9 +423,9 @@ export async function reconcileAcceptanceSpec({
|
|
|
367
423
|
? path.resolve(repoCwd, featuresDir)
|
|
368
424
|
: path.resolve(repoCwd, 'tests', 'features');
|
|
369
425
|
|
|
370
|
-
// 1. Load the Epic
|
|
371
|
-
//
|
|
372
|
-
//
|
|
426
|
+
// 1. Load the Epic. `getEpic` is preferred; fall back to `getTicket`
|
|
427
|
+
// for providers that don't expose the Epic-shaped reader (test
|
|
428
|
+
// doubles, primarily). Only `body` and `labels` are consumed.
|
|
373
429
|
let epic;
|
|
374
430
|
if (typeof provider.getEpic === 'function') {
|
|
375
431
|
epic = await provider.getEpic(epicId);
|
|
@@ -387,7 +443,6 @@ export async function reconcileAcceptanceSpec({
|
|
|
387
443
|
);
|
|
388
444
|
return {
|
|
389
445
|
epicId,
|
|
390
|
-
acceptanceSpecId: null,
|
|
391
446
|
status: 'waived',
|
|
392
447
|
ok: true,
|
|
393
448
|
acIds: [],
|
|
@@ -395,20 +450,22 @@ export async function reconcileAcceptanceSpec({
|
|
|
395
450
|
pending: [],
|
|
396
451
|
missing: [],
|
|
397
452
|
featureFilesScanned: 0,
|
|
453
|
+
dispositionsUpdated: false,
|
|
398
454
|
};
|
|
399
455
|
}
|
|
400
456
|
|
|
401
|
-
const
|
|
402
|
-
|
|
457
|
+
const acceptanceSection = extractEpicSection(
|
|
458
|
+
epic.body ?? '',
|
|
459
|
+
'acceptanceTable',
|
|
460
|
+
);
|
|
403
461
|
|
|
404
|
-
if (
|
|
462
|
+
if (acceptanceSection === null) {
|
|
405
463
|
if (skipWhenWaived) {
|
|
406
464
|
logger.info?.(
|
|
407
|
-
`[acceptance-spec-reconciler] Epic #${epicId} has no
|
|
465
|
+
`[acceptance-spec-reconciler] Epic #${epicId} body has no ## Acceptance Table section; --skip-when-waived set, returning status='waived'.`,
|
|
408
466
|
);
|
|
409
467
|
return {
|
|
410
468
|
epicId,
|
|
411
|
-
acceptanceSpecId: null,
|
|
412
469
|
status: 'waived',
|
|
413
470
|
ok: true,
|
|
414
471
|
acIds: [],
|
|
@@ -416,22 +473,16 @@ export async function reconcileAcceptanceSpec({
|
|
|
416
473
|
pending: [],
|
|
417
474
|
missing: [],
|
|
418
475
|
featureFilesScanned: 0,
|
|
476
|
+
dispositionsUpdated: false,
|
|
419
477
|
};
|
|
420
478
|
}
|
|
421
479
|
// Defence in depth — the start gate would normally catch this.
|
|
422
480
|
throw new Error(
|
|
423
|
-
`[acceptance-spec-reconciler] Epic #${epicId} has no
|
|
481
|
+
`[acceptance-spec-reconciler] Epic #${epicId} body has no ## Acceptance Table section and no acceptance::n-a waiver label. Re-run /plan Phase 7 or apply the waiver.`,
|
|
424
482
|
);
|
|
425
483
|
}
|
|
426
484
|
|
|
427
|
-
const
|
|
428
|
-
if (!spec) {
|
|
429
|
-
throw new Error(
|
|
430
|
-
`[acceptance-spec-reconciler] Linked acceptance-spec #${acceptanceSpecId} not found.`,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const acIds = parseAcIds(spec.body ?? '');
|
|
485
|
+
const acIds = parseAcIds(acceptanceSection);
|
|
435
486
|
|
|
436
487
|
// 2. Scan feature files.
|
|
437
488
|
const featureFiles = listFeatureFiles(dir);
|
|
@@ -453,11 +504,10 @@ export async function reconcileAcceptanceSpec({
|
|
|
453
504
|
|
|
454
505
|
if (acIds.length === 0) {
|
|
455
506
|
logger.warn?.(
|
|
456
|
-
`[acceptance-spec-reconciler]
|
|
507
|
+
`[acceptance-spec-reconciler] Epic #${epicId} acceptance-table section declares zero AC IDs — treating as empty spec.`,
|
|
457
508
|
);
|
|
458
509
|
return {
|
|
459
510
|
epicId,
|
|
460
|
-
acceptanceSpecId,
|
|
461
511
|
status: 'empty-spec',
|
|
462
512
|
ok: true,
|
|
463
513
|
acIds: [],
|
|
@@ -465,6 +515,7 @@ export async function reconcileAcceptanceSpec({
|
|
|
465
515
|
pending: [],
|
|
466
516
|
missing: [],
|
|
467
517
|
featureFilesScanned: featureFiles.length,
|
|
518
|
+
dispositionsUpdated: false,
|
|
468
519
|
};
|
|
469
520
|
}
|
|
470
521
|
|
|
@@ -475,9 +526,39 @@ export async function reconcileAcceptanceSpec({
|
|
|
475
526
|
});
|
|
476
527
|
const ok = missing.length === 0 && pending.length === 0;
|
|
477
528
|
|
|
529
|
+
// 3. Optional close-time disposition write-back. Section-scoped: the
|
|
530
|
+
// upsert replaces only the managed acceptance-table region; every
|
|
531
|
+
// byte outside it is preserved. Best-effort — a failed write is a
|
|
532
|
+
// warning, never a verdict change.
|
|
533
|
+
let dispositionsUpdated = false;
|
|
534
|
+
if (writeDispositions && typeof provider.updateTicket === 'function') {
|
|
535
|
+
try {
|
|
536
|
+
const rewrittenSection = renderDispositions(acceptanceSection, {
|
|
537
|
+
satisfied,
|
|
538
|
+
pending,
|
|
539
|
+
missing,
|
|
540
|
+
});
|
|
541
|
+
if (rewrittenSection !== acceptanceSection) {
|
|
542
|
+
const newBody = upsertEpicSection(
|
|
543
|
+
epic.body ?? '',
|
|
544
|
+
'acceptanceTable',
|
|
545
|
+
rewrittenSection,
|
|
546
|
+
);
|
|
547
|
+
await provider.updateTicket(epicId, { body: newBody });
|
|
548
|
+
dispositionsUpdated = true;
|
|
549
|
+
logger.info?.(
|
|
550
|
+
`[acceptance-spec-reconciler] Recorded verification dispositions for ${acIds.length} AC row(s) in Epic #${epicId}'s ## Acceptance Table section.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
} catch (err) {
|
|
554
|
+
logger.warn?.(
|
|
555
|
+
`[acceptance-spec-reconciler] disposition write-back failed (verdict unaffected): ${err?.message ?? err}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
478
560
|
return {
|
|
479
561
|
epicId,
|
|
480
|
-
acceptanceSpecId,
|
|
481
562
|
status: ok ? 'ok' : 'gap',
|
|
482
563
|
ok,
|
|
483
564
|
acIds,
|
|
@@ -485,6 +566,7 @@ export async function reconcileAcceptanceSpec({
|
|
|
485
566
|
pending,
|
|
486
567
|
missing,
|
|
487
568
|
featureFilesScanned: featureFiles.length,
|
|
569
|
+
dispositionsUpdated,
|
|
488
570
|
};
|
|
489
571
|
}
|
|
490
572
|
|
|
@@ -510,6 +592,7 @@ export function classifyReconcilerInvocation(values) {
|
|
|
510
592
|
epicId,
|
|
511
593
|
featuresDir: values['features-dir'] ?? null,
|
|
512
594
|
skipWhenWaived: values['skip-when-waived'] === true,
|
|
595
|
+
writeDispositions: values['write-dispositions'] === true,
|
|
513
596
|
};
|
|
514
597
|
}
|
|
515
598
|
|
|
@@ -519,6 +602,7 @@ async function main() {
|
|
|
519
602
|
epic: { type: 'string' },
|
|
520
603
|
'features-dir': { type: 'string' },
|
|
521
604
|
'skip-when-waived': { type: 'boolean' },
|
|
605
|
+
'write-dispositions': { type: 'boolean' },
|
|
522
606
|
help: { type: 'boolean', short: 'h' },
|
|
523
607
|
},
|
|
524
608
|
strict: false,
|
|
@@ -536,6 +620,7 @@ async function main() {
|
|
|
536
620
|
epicId: intent.epicId,
|
|
537
621
|
featuresDir: intent.featuresDir ?? undefined,
|
|
538
622
|
skipWhenWaived: intent.skipWhenWaived,
|
|
623
|
+
writeDispositions: intent.writeDispositions,
|
|
539
624
|
});
|
|
540
625
|
// Always emit the structured envelope to stdout, even on non-OK, so a
|
|
541
626
|
// caller capturing stdout can read the diff payload before reacting to
|
|
@@ -545,7 +630,6 @@ async function main() {
|
|
|
545
630
|
throw new Error(
|
|
546
631
|
renderBlockerMessage({
|
|
547
632
|
epicId: result.epicId,
|
|
548
|
-
acceptanceSpecId: result.acceptanceSpecId,
|
|
549
633
|
missing: result.missing,
|
|
550
634
|
pending: result.pending,
|
|
551
635
|
}),
|
|
@@ -32,14 +32,16 @@
|
|
|
32
32
|
* node .agents/scripts/epic-deliver-prepare.js --epic <epicId>
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
35
37
|
import { parseArgs } from 'node:util';
|
|
36
38
|
|
|
37
39
|
import { runAsCli } from './lib/cli-utils.js';
|
|
38
|
-
import { getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
40
|
+
import { getPaths, getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
39
41
|
import { currentBranch as gitCurrentBranch } from './lib/git-branch-lifecycle.js';
|
|
40
42
|
import { getEpicBranch, gitSpawn } from './lib/git-utils.js';
|
|
41
|
-
import { parseLinkedIssues } from './lib/issue-link-parser.js';
|
|
42
43
|
import { Logger } from './lib/Logger.js';
|
|
44
|
+
import { buildDocsDigest } from './lib/orchestration/docs-digest.js';
|
|
43
45
|
import {
|
|
44
46
|
resolveOperator,
|
|
45
47
|
runPrepareGuards,
|
|
@@ -144,8 +146,6 @@ function resolveGitUserEmail(cwd) {
|
|
|
144
146
|
* storyCount: number,
|
|
145
147
|
* concurrencyCap: number,
|
|
146
148
|
* stories: Array<{ storyId: number, title: string, worktree?: string }>,
|
|
147
|
-
* prdId: number|null,
|
|
148
|
-
* techSpecId: number|null,
|
|
149
149
|
* checkpointInitializedAt: string,
|
|
150
150
|
* }>}
|
|
151
151
|
*/
|
|
@@ -289,6 +289,40 @@ function evaluatePrepareConcurrencyGate({
|
|
|
289
289
|
return gate;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Build the per-Epic docs digest and write it to
|
|
294
|
+
* `<tempRoot>/epic-<id>/docs-digest.md`, returning its repo-relative path.
|
|
295
|
+
* Story #4338 — the parent threads this path into every child prompt so
|
|
296
|
+
* delivery sub-agents read one compact outline instead of re-ingesting the
|
|
297
|
+
* full `project.docsContextFiles` set per Story.
|
|
298
|
+
*
|
|
299
|
+
* Keyed off the **un-defaulted** config (`config.raw`): when the operator has
|
|
300
|
+
* not configured `project.docsContextFiles`, this returns `null` (no file
|
|
301
|
+
* written) rather than digesting the resolver's built-in default set — the
|
|
302
|
+
* digest is an opt-in surface for projects that curate their docs context.
|
|
303
|
+
*
|
|
304
|
+
* @param {{ epicId: number, cwd?: string, config: object }} args
|
|
305
|
+
* @returns {Promise<string|null>} repo-relative digest path, or null when
|
|
306
|
+
* `project.docsContextFiles` is empty/unset (or every file is missing).
|
|
307
|
+
*/
|
|
308
|
+
async function writeDocsDigest({ epicId, cwd, config }) {
|
|
309
|
+
const rawFiles = config?.raw?.project?.docsContextFiles;
|
|
310
|
+
const docsContextFiles = Array.isArray(rawFiles) ? rawFiles : [];
|
|
311
|
+
if (docsContextFiles.length === 0) return null;
|
|
312
|
+
|
|
313
|
+
const paths = getPaths(config);
|
|
314
|
+
const root = path.resolve(cwd ?? process.cwd());
|
|
315
|
+
const docsRoot = path.resolve(root, paths.docsRoot);
|
|
316
|
+
const digest = await buildDocsDigest({ docsContextFiles, docsRoot });
|
|
317
|
+
if (digest == null) return null;
|
|
318
|
+
|
|
319
|
+
const relPath = path.join(paths.tempRoot, `epic-${epicId}`, 'docs-digest.md');
|
|
320
|
+
const absPath = path.resolve(root, relPath);
|
|
321
|
+
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
|
|
322
|
+
await fs.promises.writeFile(absPath, digest, 'utf-8');
|
|
323
|
+
return relPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
export async function runEpicDeliverPrepare({
|
|
293
327
|
epicId,
|
|
294
328
|
cwd,
|
|
@@ -377,45 +411,20 @@ export async function runEpicDeliverPrepare({
|
|
|
377
411
|
});
|
|
378
412
|
}
|
|
379
413
|
|
|
380
|
-
|
|
381
|
-
// surface them in the prepare envelope. The /deliver fan-out threads these
|
|
382
|
-
// into each per-Story `story-init.js --prd/--tech-spec`, collapsing the
|
|
383
|
-
// N per-Story `getEpic` round-trips to this single parent-side resolution.
|
|
384
|
-
// The Epic snapshot is already in hand (`state.epic`), so this adds no
|
|
385
|
-
// extra fetch; the body-parse fallback mirrors hierarchy-tracer's source.
|
|
386
|
-
const { prdId, techSpecId } = resolveEpicLinkages(state.epic);
|
|
414
|
+
const docsDigestPath = await writeDocsDigest({ epicId, cwd, config });
|
|
387
415
|
|
|
388
416
|
return {
|
|
389
417
|
epicId,
|
|
390
418
|
storyCount: openStories.length,
|
|
391
419
|
concurrencyCap,
|
|
392
420
|
stories,
|
|
393
|
-
prdId,
|
|
394
|
-
techSpecId,
|
|
395
421
|
checkpointInitializedAt:
|
|
396
422
|
checkpointState.startedAt ??
|
|
397
423
|
checkpointState.lastUpdatedAt ??
|
|
398
424
|
new Date().toISOString(),
|
|
399
425
|
concurrencyHazardsBypassed: gate.bypassed,
|
|
400
426
|
preflightCache: cacheStatus,
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Resolve an Epic's linked PRD / Tech-Spec issue ids from the snapshot ticket.
|
|
406
|
-
* Prefers the provider-supplied `linkedIssues` map and falls back to parsing
|
|
407
|
-
* the Epic body's `## Planning Artifacts` section — the same two sources
|
|
408
|
-
* `hierarchy-tracer.js` reads — so the threaded ids match what an unthreaded
|
|
409
|
-
* `story-init.js` run would have resolved itself. Story #4253.
|
|
410
|
-
*
|
|
411
|
-
* @param {{ linkedIssues?: { prd?: number|null, techSpec?: number|null }|null, body?: string }|null|undefined} epic
|
|
412
|
-
* @returns {{ prdId: number|null, techSpecId: number|null }}
|
|
413
|
-
*/
|
|
414
|
-
function resolveEpicLinkages(epic) {
|
|
415
|
-
const linked = epic?.linkedIssues ?? parseLinkedIssues(epic?.body ?? '');
|
|
416
|
-
return {
|
|
417
|
-
prdId: linked?.prd ?? null,
|
|
418
|
-
techSpecId: linked?.techSpec ?? null,
|
|
427
|
+
docsDigestPath,
|
|
419
428
|
};
|
|
420
429
|
}
|
|
421
430
|
|
|
@@ -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,
|