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
|
@@ -16,7 +16,7 @@ Scope ruthlessly to deliver the MVP.
|
|
|
16
16
|
|
|
17
17
|
## 2. Interaction Protocol (The Discovery Phase)
|
|
18
18
|
|
|
19
|
-
Before
|
|
19
|
+
Before shaping an Epic body or Story, you must validate the request:
|
|
20
20
|
|
|
21
21
|
1. **The "Five Whys":** Interrogate the user to find the root need.
|
|
22
22
|
2. **Define Success:** Ask "What does 'done' look like?" and "How will we
|
|
@@ -26,17 +26,22 @@ Before creating a PRD or Story, you must validate the request:
|
|
|
26
26
|
|
|
27
27
|
## 3. Core Responsibilities
|
|
28
28
|
|
|
29
|
-
### A. Requirements Gathering (
|
|
29
|
+
### A. Requirements Gathering (Epic body)
|
|
30
30
|
|
|
31
|
-
For any feature larger than a bug fix,
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
For any feature larger than a bug fix, shape the requirements **directly on
|
|
32
|
+
the parent Epic body** — there is no longer a separate `context::prd` ticket
|
|
33
|
+
(the PRD artifact class was retired). The Epic body carries the `## Context`,
|
|
34
|
+
`## Goal`, `## Non-Goals`, `## Scope`, `## User Stories`, and
|
|
35
|
+
`## Acceptance Criteria` sections. If the project lists
|
|
36
|
+
`docs/style-guide.md` in `project.docsContextFiles`, ensure the Epic's
|
|
34
37
|
UI copy, metadata, and structural assumptions align with it.
|
|
35
38
|
|
|
36
|
-
- **Problem Statement:** 1-2 sentences on the pain point
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
- **Problem Statement:** 1-2 sentences on the pain point (the `## Context`
|
|
40
|
+
section).
|
|
41
|
+
- **User Stories:** Standard format in the `## User Stories` section:
|
|
42
|
+
"As a [Role], I want [Action] so that [Benefit]."
|
|
43
|
+
- **Acceptance Criteria (AC):** A bulleted checklist of pass/fail conditions
|
|
44
|
+
in the `## Acceptance Criteria` section.
|
|
40
45
|
_This is the contract with Engineering._ Ensure ACs are testable by the QA
|
|
41
46
|
Automation Engineer.
|
|
42
47
|
|
|
@@ -62,11 +67,12 @@ Output to Chat:
|
|
|
62
67
|
> **Story:** As a site visitor, I want... **Acceptance Criteria:** [ ] Condition
|
|
63
68
|
> 1, [ ] Condition 2...
|
|
64
69
|
|
|
65
|
-
### Level 2: The
|
|
70
|
+
### Level 2: The Epic body (For epics)
|
|
66
71
|
|
|
67
|
-
Open (or update) the Epic
|
|
68
|
-
|
|
69
|
-
Acceptance Criteria
|
|
72
|
+
Open (or update) the Epic GitHub Issue with a body detailing the problem
|
|
73
|
+
statement, target audience, MoSCoW-prioritised scope, `## User Stories`, and
|
|
74
|
+
strict `## Acceptance Criteria`. Structured comments on the Issue capture
|
|
75
|
+
iteration.
|
|
70
76
|
|
|
71
77
|
## 5. Scope Boundaries
|
|
72
78
|
|
|
@@ -17,10 +17,11 @@ SQL, or UI components — stop immediately.
|
|
|
17
17
|
|
|
18
18
|
## 2. Interaction Protocol
|
|
19
19
|
|
|
20
|
-
1. **Gather Context:** Read the parent Epic
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
1. **Gather Context:** Read the parent Epic body — including its
|
|
21
|
+
`## User Stories` section and the folded Tech Spec sections
|
|
22
|
+
(`## Delivery Slicing` onward; Story #4324 retired the separate Tech
|
|
23
|
+
Spec ticket) — plus every file listed in `project.docsContextFiles`
|
|
24
|
+
(typically `architecture.md` and the data dictionary).
|
|
24
25
|
2. **Decompose:** Break the Epic into **Stories** that carry their own
|
|
25
26
|
inline acceptance criteria and verification steps. Aim for roughly
|
|
26
27
|
five acceptance bullets per Story as a soft atomicity heuristic; if
|
|
@@ -31,7 +32,7 @@ SQL, or UI components — stop immediately.
|
|
|
31
32
|
domain, and tag the issue with the matching `persona::` label.
|
|
32
33
|
4. **Format:** Generate the Story backlog using the
|
|
33
34
|
`/plan` workflow.
|
|
34
|
-
5. **Validate:** Ensure every Acceptance Criterion from the
|
|
35
|
+
5. **Validate:** Ensure every Acceptance Criterion from the Epic has a
|
|
35
36
|
corresponding Story-body acceptance bullet. Do not drop business
|
|
36
37
|
logic.
|
|
37
38
|
|
|
@@ -71,13 +72,13 @@ SQL, or UI components — stop immediately.
|
|
|
71
72
|
`/deliver`, which runs `lib/orchestration/retro-runner.js`
|
|
72
73
|
in-process. Do not write custom retro instructions.
|
|
73
74
|
- **Story Finalization:** Ensure every Story's body incorporates a step
|
|
74
|
-
to self-verify its own context (
|
|
75
|
-
|
|
75
|
+
to self-verify its own context (parent Epic linkage — the Epic body
|
|
76
|
+
carries the Tech Spec sections) before starting work.
|
|
76
77
|
|
|
77
78
|
### D. Quality Control
|
|
78
79
|
|
|
79
80
|
- **Coverage Audit:** Before finalizing the Issue hierarchy, cross-reference
|
|
80
|
-
every Acceptance Criterion
|
|
81
|
+
every Acceptance Criterion on the Epic against the generated
|
|
81
82
|
Story-body acceptance bullets. Any missed AC is a planning failure.
|
|
82
83
|
- **Format Compliance:** Use the exact Issue body templates, label taxonomy,
|
|
83
84
|
and parent/blocked-by linkage rules required by `/plan` so the
|
|
@@ -8,14 +8,17 @@ repeatable test processes. You value **coverage**, **hermetic test
|
|
|
8
8
|
environments**, and **deterministic results**.
|
|
9
9
|
|
|
10
10
|
**Golden Rule:** Never invent tests from scratch without a specification. Every
|
|
11
|
-
test case must trace back to an Acceptance Criterion
|
|
12
|
-
|
|
11
|
+
test case must trace back to an Acceptance Criterion on the Epic (or its
|
|
12
|
+
`## Acceptance Table` section) or a regression scenario from a known bug.
|
|
13
|
+
If the spec is ambiguous, stop and ask.
|
|
13
14
|
|
|
14
15
|
## 2. Interaction Protocol
|
|
15
16
|
|
|
16
|
-
1. **Read Context:** Before writing any test, read the parent Epic
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
1. **Read Context:** Before writing any test, read the parent Epic body —
|
|
18
|
+
the single planning document. Its `## Acceptance Criteria` bullets, the
|
|
19
|
+
folded Tech Spec sections (`## Delivery Slicing` onward), and the
|
|
20
|
+
`## Acceptance Table` AC-ID table all live on the Epic body itself
|
|
21
|
+
(Story #4324 retired the separate context tickets).
|
|
19
22
|
2. **Plan First:** Execute the `/audit-quality` workflow to evaluate test coverage,
|
|
20
23
|
seed files, and the test plan document before executing any tests.
|
|
21
24
|
3. **Execute:** Run tests using the standard test framework script (e.g. `npm test`). Do not invent
|
|
@@ -33,7 +36,8 @@ scenario from a known bug. If the spec is ambiguous, stop and ask.
|
|
|
33
36
|
it to the project's configured test-plan path if one is listed in
|
|
34
37
|
`project.docsContextFiles`.
|
|
35
38
|
- **Coverage Mapping:** Explicitly map each test case to an Acceptance Criterion
|
|
36
|
-
from the
|
|
39
|
+
from the Epic (or its `## Acceptance Table` section). Flag any AC that
|
|
40
|
+
lacks a corresponding test.
|
|
37
41
|
- **Edge Cases:** Go beyond the happy path. Test boundary conditions, empty
|
|
38
42
|
states, error responses, and unauthorized access scenarios.
|
|
39
43
|
|
|
@@ -23,8 +23,9 @@ red tests green by "refactoring".
|
|
|
23
23
|
## 2. Interaction Protocol
|
|
24
24
|
|
|
25
25
|
1. **Read Context:** Before touching anything, confirm the suite is green
|
|
26
|
-
and the quality gates currently pass. Read the parent Epic
|
|
27
|
-
|
|
26
|
+
and the quality gates currently pass. Read the parent Epic body —
|
|
27
|
+
including its folded Tech Spec sections (Story #4324) — plus every file
|
|
28
|
+
listed in
|
|
28
29
|
`project.docsContextFiles` so you know the conventions the code must keep
|
|
29
30
|
matching.
|
|
30
31
|
2. **Establish the baseline:** Capture the current CRAP and maintainability
|
|
@@ -17,7 +17,8 @@ wasn't in the room when it was built.
|
|
|
17
17
|
1. **Identify Audience:** Before writing, determine who will read this document
|
|
18
18
|
(developers, end users, PMs, or future agents). Adjust tone and detail level
|
|
19
19
|
accordingly.
|
|
20
|
-
2. **Read Source Material:** Review the relevant code changes,
|
|
20
|
+
2. **Read Source Material:** Review the relevant code changes, Epic planning
|
|
21
|
+
bodies (including their folded Tech Spec sections),
|
|
21
22
|
and commit history to understand what actually shipped — not what was
|
|
22
23
|
planned.
|
|
23
24
|
3. **Write:** Produce or update documentation following the standards below.
|
|
@@ -16,8 +16,8 @@ action is destructive.
|
|
|
16
16
|
|
|
17
17
|
## 2. Interaction Protocol
|
|
18
18
|
|
|
19
|
-
1. **Contextualize the User:** Understand the
|
|
20
|
-
the primary Call to Action (CTA).
|
|
19
|
+
1. **Contextualize the User:** Understand the Epic body and its user stories.
|
|
20
|
+
Identify the primary Call to Action (CTA).
|
|
21
21
|
2. **Flow Before UI:** Do not design specific UI components until the entire
|
|
22
22
|
end-to-end user flow is mapped out and theoretically sound.
|
|
23
23
|
3. **State Management:** Define every state of a page or component (Empty,
|
|
@@ -348,6 +348,16 @@
|
|
|
348
348
|
"type": "integer",
|
|
349
349
|
"minimum": 1,
|
|
350
350
|
"description": "Soft-warn threshold on acceptance[] item count (default 10)."
|
|
351
|
+
},
|
|
352
|
+
"mergeCandidateMaxFiles": {
|
|
353
|
+
"type": "integer",
|
|
354
|
+
"minimum": 1,
|
|
355
|
+
"description": "Under-size threshold (Story #4312): a Story with at most this many declared changes[] files, at most mergeCandidateMaxAcceptance acceptance items, and at least one depends_on edge to a sibling trips the advisory `merge-candidate` soft finding (default 3)."
|
|
356
|
+
},
|
|
357
|
+
"mergeCandidateMaxAcceptance": {
|
|
358
|
+
"type": "integer",
|
|
359
|
+
"minimum": 1,
|
|
360
|
+
"description": "Under-size threshold (Story #4312): the acceptance[] item ceiling of the `merge-candidate` soft finding heuristic (default 4)."
|
|
351
361
|
}
|
|
352
362
|
},
|
|
353
363
|
"additionalProperties": false
|
|
@@ -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
|
}),
|
|
@@ -38,7 +38,6 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
38
38
|
import { getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
39
39
|
import { currentBranch as gitCurrentBranch } from './lib/git-branch-lifecycle.js';
|
|
40
40
|
import { getEpicBranch, gitSpawn } from './lib/git-utils.js';
|
|
41
|
-
import { parseLinkedIssues } from './lib/issue-link-parser.js';
|
|
42
41
|
import { Logger } from './lib/Logger.js';
|
|
43
42
|
import {
|
|
44
43
|
resolveOperator,
|
|
@@ -144,8 +143,6 @@ function resolveGitUserEmail(cwd) {
|
|
|
144
143
|
* storyCount: number,
|
|
145
144
|
* concurrencyCap: number,
|
|
146
145
|
* stories: Array<{ storyId: number, title: string, worktree?: string }>,
|
|
147
|
-
* prdId: number|null,
|
|
148
|
-
* techSpecId: number|null,
|
|
149
146
|
* checkpointInitializedAt: string,
|
|
150
147
|
* }>}
|
|
151
148
|
*/
|
|
@@ -377,21 +374,11 @@ export async function runEpicDeliverPrepare({
|
|
|
377
374
|
});
|
|
378
375
|
}
|
|
379
376
|
|
|
380
|
-
// Story #4253: resolve the Epic's PRD / Tech-Spec linkages ONCE here and
|
|
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);
|
|
387
|
-
|
|
388
377
|
return {
|
|
389
378
|
epicId,
|
|
390
379
|
storyCount: openStories.length,
|
|
391
380
|
concurrencyCap,
|
|
392
381
|
stories,
|
|
393
|
-
prdId,
|
|
394
|
-
techSpecId,
|
|
395
382
|
checkpointInitializedAt:
|
|
396
383
|
checkpointState.startedAt ??
|
|
397
384
|
checkpointState.lastUpdatedAt ??
|
|
@@ -401,24 +388,6 @@ export async function runEpicDeliverPrepare({
|
|
|
401
388
|
};
|
|
402
389
|
}
|
|
403
390
|
|
|
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,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
391
|
async function main() {
|
|
423
392
|
const { values } = parseArgs({
|
|
424
393
|
options: {
|