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.
Files changed (101) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +93 -80
  3. package/.agents/docs/configuration.md +11 -1
  4. package/.agents/docs/quality-gates.md +61 -0
  5. package/.agents/docs/workflows.md +1 -1
  6. package/.agents/instructions.md +9 -7
  7. package/.agents/personas/architect.md +8 -5
  8. package/.agents/personas/engineer-mobile.md +3 -2
  9. package/.agents/personas/engineer-web.md +3 -2
  10. package/.agents/personas/engineer.md +6 -5
  11. package/.agents/personas/product.md +19 -13
  12. package/.agents/personas/project-manager.md +9 -8
  13. package/.agents/personas/qa-engineer.md +10 -6
  14. package/.agents/personas/refactorer.md +3 -2
  15. package/.agents/personas/technical-writer.md +2 -1
  16. package/.agents/personas/ux-designer.md +2 -2
  17. package/.agents/schemas/agentrc.schema.json +10 -0
  18. package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
  19. package/.agents/scripts/epic-deliver-prepare.js +0 -31
  20. package/.agents/scripts/epic-plan-decompose.js +2 -5
  21. package/.agents/scripts/epic-plan-spec.js +16 -19
  22. package/.agents/scripts/hierarchy-gate.js +11 -11
  23. package/.agents/scripts/lib/ITicketingProvider.js +4 -3
  24. package/.agents/scripts/lib/baselines/env-overrides.js +35 -0
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
  27. package/.agents/scripts/lib/cli-args.js +1 -5
  28. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  29. package/.agents/scripts/lib/config/temp-paths.js +1 -4
  30. package/.agents/scripts/lib/config-settings-schema.js +5 -0
  31. package/.agents/scripts/lib/epic-body-sections.js +222 -0
  32. package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
  33. package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
  34. package/.agents/scripts/lib/label-constants.js +7 -17
  35. package/.agents/scripts/lib/label-taxonomy.js +4 -21
  36. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +47 -1
  37. package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
  38. package/.agents/scripts/lib/orchestration/check-baselines/phases/pipeline.js +1 -1
  39. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +2 -1
  40. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +19 -8
  41. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
  42. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
  43. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
  44. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
  45. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
  46. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
  47. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
  48. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
  49. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
  50. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
  51. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
  52. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
  53. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
  54. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
  55. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
  56. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
  57. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  58. package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
  59. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  60. package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
  61. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  62. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
  63. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
  64. package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
  65. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
  66. package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
  67. package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
  68. package/.agents/scripts/lifecycle-emit.js +1 -1
  69. package/.agents/scripts/lint-label-vocabulary.js +2 -3
  70. package/.agents/scripts/providers/github/mappers.js +0 -3
  71. package/.agents/scripts/providers/github/tickets.js +7 -18
  72. package/.agents/scripts/single-story-init.js +0 -1
  73. package/.agents/scripts/story-init.js +1 -29
  74. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +37 -18
  75. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -18
  76. package/.agents/skills/core/epic-plan-premortem/SKILL.md +7 -6
  77. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +161 -109
  78. package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
  79. package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
  80. package/.agents/skills/core/scope-triage/SKILL.md +2 -1
  81. package/.agents/skills/skills.index.json +6 -6
  82. package/.agents/templates/epic-from-idea.md +4 -0
  83. package/.agents/workflows/audit-to-stories.md +2 -2
  84. package/.agents/workflows/helpers/code-review.md +11 -9
  85. package/.agents/workflows/helpers/deliver-epic.md +32 -44
  86. package/.agents/workflows/helpers/epic-audit.md +11 -8
  87. package/.agents/workflows/helpers/epic-deliver-story.md +10 -16
  88. package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
  89. package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
  90. package/.agents/workflows/helpers/parallel-tooling.md +2 -1
  91. package/.agents/workflows/helpers/plan-epic.md +114 -99
  92. package/.agents/workflows/helpers/single-story-deliver.md +1 -1
  93. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
  94. package/.agents/workflows/plan.md +8 -8
  95. package/.agents/workflows/qa-assist.md +2 -1
  96. package/docs/CHANGELOG.md +28 -0
  97. package/package.json +1 -1
  98. package/.agents/scripts/lib/issue-link-parser.js +0 -74
  99. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
  100. package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
  101. 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 creating a PRD or Story, you must validate the request:
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 (PRDs)
29
+ ### A. Requirements Gathering (Epic body)
30
30
 
31
- For any feature larger than a bug fix, open (or update) a GitHub Issue
32
- labelled `context::prd` and linked to the parent Epic. If the project lists
33
- `docs/style-guide.md` in `project.docsContextFiles`, ensure the PRD's
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
- - **User Stories:** Standard format: "As a [Role], I want [Action] so that
38
- [Benefit]."
39
- - **Acceptance Criteria (AC):** A bulleted checklist of pass/fail conditions.
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 PRD (For epics)
70
+ ### Level 2: The Epic body (For epics)
66
71
 
67
- Open (or update) the Epic's linked `context::prd` GitHub Issue with a body
68
- detailing problem statement, target audience, MoSCoW priorities, and strict
69
- Acceptance Criteria. Structured comments on the Issue capture iteration.
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's linked PRD (`context::prd`)
21
- and Tech Spec (`context::tech-spec`) GitHub Issues, plus every file
22
- listed in `project.docsContextFiles` (typically `architecture.md`
23
- and the data dictionary).
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 PRD has a
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 (PRD/Tech Spec linkage, parent
75
- Epic) before starting work.
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 in the PRD against the generated
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 in the PRD or a regression
12
- scenario from a known bug. If the spec is ambiguous, stop and ask.
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's linked
17
- PRD (`context::prd`) and Tech Spec (`context::tech-spec`) GitHub Issues
18
- to extract all Acceptance Criteria.
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 PRD. Flag any AC that lacks a corresponding test.
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's Tech Spec
27
- (`context::tech-spec`) and PRD (`context::prd`) plus every file listed in
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, PRDs, tech specs,
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 PRD and the user story. Identify
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 an Epic's linked `context::acceptance-spec`
6
- * body against the **per-Epic-namespaced** `@epic-<id>-ac-*` / `@pending`
7
- * tags emitted by scenarios under `tests/features/**`. The namespace is
8
- * load-bearing (Story #3362): `tests/features` is a single global tree
9
- * shared by every Epic, so a bare `@ac-N` tag authored under an unrelated
10
- * Epic's scenarios must not count as coverage for this Epic. Surfaces three
11
- * categories:
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 `closePlanningArtifacts` fires, so
19
- * planning artifacts stay open until the AC coverage gap is fixed.
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
- * "ackIds": ["AC-1", "AC-2", ...],
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 { parseLinkedIssues } from './lib/issue-link-parser.js';
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 linked acceptance-spec body against the
58
- @ac-*/@pending tags in tests/features/**. Emits a JSON envelope on stdout.
59
- Throws (exit 1) when missing or pending ACs are detected, or when the Epic
60
- has no linked acceptance-spec and the acceptance::n-a waiver label is
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 spec.
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-spec body. AC
73
- * authoring style is "Acceptance Criteria — Markdown table whose first
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 body with a permissive regex
76
- * because operators are free to format the body however they wish around
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
- `linked acceptance-spec #${acceptanceSpecId ?? '(none)'} has uncovered AC IDs.`,
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 no acceptance-spec is linked and the waiver is absent, **throws**
316
- * a clear `Error` — this should never happen in practice because
317
- * `epic-deliver-finalize` runs after `runSnapshotPhase`'s start gate,
318
- * but we defend against direct CLI invocation.
319
- * - If the linked acceptance-spec body declares zero AC IDs, returns
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; prefer getEpic for the linkedIssues hydration but
371
- // fall back to getTicket + body parsing for providers that don't
372
- // expose the Epic-shaped reader (test doubles, primarily).
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 linkedIssues = epic.linkedIssues ?? parseLinkedIssues(epic.body ?? '');
402
- const acceptanceSpecId = linkedIssues?.acceptanceSpec ?? null;
457
+ const acceptanceSection = extractEpicSection(
458
+ epic.body ?? '',
459
+ 'acceptanceTable',
460
+ );
403
461
 
404
- if (!acceptanceSpecId) {
462
+ if (acceptanceSection === null) {
405
463
  if (skipWhenWaived) {
406
464
  logger.info?.(
407
- `[acceptance-spec-reconciler] Epic #${epicId} has no linked context::acceptance-spec ticket; --skip-when-waived set, returning status='waived'.`,
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 linked context::acceptance-spec ticket and no acceptance::n-a waiver label. Re-run /plan Phase 7 or apply the waiver.`,
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 spec = await provider.getTicket(acceptanceSpecId);
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] Acceptance-spec #${acceptanceSpecId} declares zero AC IDs — treating as empty spec.`,
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: {