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.
Files changed (121) hide show
  1. package/.agents/README.md +46 -5
  2. package/.agents/docs/SDLC.md +97 -82
  3. package/.agents/docs/agentrc-reference.json +10 -2
  4. package/.agents/docs/configuration.md +4 -1
  5. package/.agents/docs/execution-reference.md +52 -0
  6. package/.agents/docs/workflows.md +1 -1
  7. package/.agents/instructions.md +85 -45
  8. package/.agents/personas/architect.md +8 -5
  9. package/.agents/personas/engineer-mobile.md +3 -2
  10. package/.agents/personas/engineer-web.md +3 -2
  11. package/.agents/personas/engineer.md +6 -5
  12. package/.agents/personas/product.md +19 -13
  13. package/.agents/personas/project-manager.md +9 -8
  14. package/.agents/personas/qa-engineer.md +10 -6
  15. package/.agents/personas/refactorer.md +3 -2
  16. package/.agents/personas/technical-writer.md +2 -1
  17. package/.agents/personas/ux-designer.md +2 -2
  18. package/.agents/schemas/agentrc.schema.json +41 -3
  19. package/.agents/schemas/qa-ledger.schema.json +2 -2
  20. package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
  21. package/.agents/scripts/epic-deliver-prepare.js +40 -31
  22. package/.agents/scripts/epic-plan-decompose.js +2 -5
  23. package/.agents/scripts/epic-plan-spec.js +16 -19
  24. package/.agents/scripts/hierarchy-gate.js +11 -11
  25. package/.agents/scripts/lib/ITicketingProvider.js +4 -3
  26. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  27. package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
  28. package/.agents/scripts/lib/cli-args.js +1 -5
  29. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  30. package/.agents/scripts/lib/config/explain.js +4 -1
  31. package/.agents/scripts/lib/config/temp-paths.js +1 -4
  32. package/.agents/scripts/lib/config-settings-schema.js +30 -1
  33. package/.agents/scripts/lib/epic-body-sections.js +310 -0
  34. package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
  35. package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
  36. package/.agents/scripts/lib/findings/promote-finding.js +3 -3
  37. package/.agents/scripts/lib/findings/severity.js +5 -6
  38. package/.agents/scripts/lib/label-constants.js +7 -17
  39. package/.agents/scripts/lib/label-taxonomy.js +4 -21
  40. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +65 -2
  41. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +105 -9
  42. package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
  43. package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
  44. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
  45. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
  46. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
  47. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
  48. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
  49. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
  50. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
  51. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
  52. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
  53. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
  54. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
  55. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
  56. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
  57. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
  58. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
  59. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
  60. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  61. package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
  62. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  63. package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
  64. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  65. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +15 -1
  66. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
  67. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
  68. package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
  69. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
  70. package/.agents/scripts/lib/qa/console-allowlist.js +5 -4
  71. package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
  72. package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
  73. package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
  74. package/.agents/scripts/lifecycle-emit.js +1 -1
  75. package/.agents/scripts/lint-label-vocabulary.js +2 -3
  76. package/.agents/scripts/providers/github/mappers.js +0 -3
  77. package/.agents/scripts/providers/github/tickets.js +7 -18
  78. package/.agents/scripts/single-story-init.js +0 -1
  79. package/.agents/scripts/story-init.js +1 -29
  80. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +43 -22
  81. package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
  82. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +27 -40
  83. package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
  84. package/.agents/skills/core/epic-plan-premortem/SKILL.md +15 -13
  85. package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
  86. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +143 -151
  87. package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
  88. package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
  89. package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
  90. package/.agents/skills/core/scope-triage/SKILL.md +2 -1
  91. package/.agents/skills/skills.index.json +8 -8
  92. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
  93. package/.agents/templates/epic-from-idea.md +4 -0
  94. package/.agents/workflows/audit-to-stories.md +2 -2
  95. package/.agents/workflows/helpers/code-review.md +11 -9
  96. package/.agents/workflows/helpers/deliver-epic-reference.md +514 -0
  97. package/.agents/workflows/helpers/deliver-epic.md +173 -490
  98. package/.agents/workflows/helpers/epic-audit.md +11 -8
  99. package/.agents/workflows/helpers/epic-deliver-story.md +45 -27
  100. package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
  101. package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
  102. package/.agents/workflows/helpers/parallel-tooling.md +2 -1
  103. package/.agents/workflows/helpers/plan-epic-reference.md +136 -0
  104. package/.agents/workflows/helpers/plan-epic.md +141 -256
  105. package/.agents/workflows/helpers/plan-story.md +31 -61
  106. package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
  107. package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
  108. package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
  109. package/.agents/workflows/helpers/single-story-deliver.md +129 -393
  110. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
  111. package/.agents/workflows/plan.md +8 -8
  112. package/.agents/workflows/qa-assist.md +2 -1
  113. package/.agents/workflows/qa-explore.md +63 -32
  114. package/.agents/workflows/qa-run.md +293 -130
  115. package/docs/CHANGELOG.md +35 -0
  116. package/package.json +1 -1
  117. package/.agents/schemas/qa-finding.schema.json +0 -133
  118. package/.agents/scripts/lib/issue-link-parser.js +0 -74
  119. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
  120. package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
  121. package/.agents/scripts/lib/story-init/hierarchy-tracer.js +0 -57
@@ -82,15 +82,15 @@ async function setEpicLabel(provider, epicId, targetLabel) {
82
82
  *
83
83
  * @param {number} epicId
84
84
  * @param {import('../../../ITicketingProvider.js').ITicketingProvider} provider
85
- * @param {{ prdContent: string, techSpecContent: string, acceptanceSpecContent?: string|null }} artifacts
85
+ * @param {{ techSpecContent: string, acceptanceSpecContent?: string|null }} artifacts
86
86
  * @param {object} settings
87
87
  * @param {{ force?: boolean, forceReview?: boolean, steal?: boolean, config?: object, riskVerdict?: import('../../planning-risk.js').RiskVerdict }} [opts]
88
- * @returns {Promise<{ epicId: number, prdId: number|null, techSpecId: number|null, acceptanceSpecId: number|null, checkpoint: object, planningRisk: import('../../planning-risk.js').PlanningRiskEnvelope, reviewRouting: import('../../plan-review-routing.js').ReviewRoutingEnvelope }>}
88
+ * @returns {Promise<{ epicId: number, techSpecPersisted: boolean, acceptanceTable: 'persisted'|'waived'|'none', checkpoint: object, planningRisk: import('../../planning-risk.js').PlanningRiskEnvelope, reviewRouting: import('../../plan-review-routing.js').ReviewRoutingEnvelope }>}
89
89
  */
90
90
  export async function runSpecPhase(
91
91
  epicId,
92
92
  provider,
93
- { prdContent, techSpecContent, acceptanceSpecContent = null },
93
+ { techSpecContent, acceptanceSpecContent = null },
94
94
  settings = {},
95
95
  {
96
96
  force = false,
@@ -153,7 +153,7 @@ export async function runSpecPhase(
153
153
  const planResult = await planEpic(
154
154
  epicId,
155
155
  provider,
156
- { prdContent, techSpecContent, acceptanceSpecContent },
156
+ { techSpecContent, acceptanceSpecContent },
157
157
  settings,
158
158
  {
159
159
  force,
@@ -163,15 +163,13 @@ export async function runSpecPhase(
163
163
  const specChanged = planResult?.persisted !== false;
164
164
 
165
165
  const afterPlan = await provider.getEpic(epicId);
166
- const prdId = afterPlan.linkedIssues?.prd ?? null;
167
- const techSpecId = afterPlan.linkedIssues?.techSpec ?? null;
168
- const acceptanceSpecId = afterPlan.linkedIssues?.acceptanceSpec ?? null;
169
166
 
170
167
  // Story #2635 — cross-validate the authored Tech Spec body against the
171
- // base branch and surface any stale path-shaped references on the Tech
172
- // Spec issue. Non-blocking: a missing base ref, an unreadable temp
173
- // directory, or a provider failure downgrades to a warning so Phase 7
174
- // never fails on the advisory check.
168
+ // base branch and surface any stale path-shaped references. Story #4324:
169
+ // with the Tech Spec folded into the Epic body, the advisory comment
170
+ // lands on the Epic itself. Non-blocking: a missing base ref, an
171
+ // unreadable temp directory, or a provider failure downgrades to a
172
+ // warning so Phase 7 never fails on the advisory check.
175
173
  const baseBranchRef = settings?.baseBranch ?? 'main';
176
174
  const tempRoot = path.resolve(
177
175
  PROJECT_ROOT,
@@ -179,7 +177,6 @@ export async function runSpecPhase(
179
177
  );
180
178
  const freshness = await runSpecFreshnessCheck({
181
179
  epicId,
182
- techSpecId,
183
180
  techSpecContent,
184
181
  baseBranchRef,
185
182
  tempRoot,
@@ -220,9 +217,8 @@ export async function runSpecPhase(
220
217
  },
221
218
  spec: {
222
219
  ...currentState.spec,
223
- prdId,
224
- techSpecId,
225
- acceptanceSpecId,
220
+ techSpecPersisted: planResult?.techSpecPersisted === true,
221
+ acceptanceTable: planResult?.acceptanceTable ?? 'none',
226
222
  completedAt: new Date().toISOString(),
227
223
  },
228
224
  },
@@ -256,13 +252,17 @@ export async function runSpecPhase(
256
252
  const cleanup = await cleanupPhaseTempFiles({ phase: 'spec', epicId });
257
253
 
258
254
  const acceptanceSummary =
259
- acceptanceSpecId !== null ? `, Acceptance Spec #${acceptanceSpecId}` : '';
255
+ planResult?.acceptanceTable === 'persisted'
256
+ ? ' + ## Acceptance Table section'
257
+ : planResult?.acceptanceTable === 'waived'
258
+ ? ' (acceptance waived)'
259
+ : '';
260
260
  const freshnessSummary =
261
261
  freshness.stale > 0 || freshness.ambiguous > 0
262
262
  ? ` ⚠️ Spec freshness: ${freshness.stale} stale / ${freshness.ambiguous} ambiguous reference(s) — see ${freshness.reportPath ?? 'report'}.`
263
263
  : '';
264
264
  Logger.info(
265
- `[epic-plan-spec] ✅ Spec phase complete for Epic #${epicId}. PRD #${prdId}, Tech Spec #${techSpecId}${acceptanceSummary}.${freshnessSummary}`,
265
+ `[epic-plan-spec] ✅ Spec phase complete for Epic #${epicId}. Tech Spec sections folded into the Epic body${acceptanceSummary}.${freshnessSummary}`,
266
266
  );
267
267
  if (cleanup.deleted.length > 0) {
268
268
  Logger.info(
@@ -272,9 +272,8 @@ export async function runSpecPhase(
272
272
 
273
273
  return {
274
274
  epicId,
275
- prdId,
276
- techSpecId,
277
- acceptanceSpecId,
275
+ techSpecPersisted: planResult?.techSpecPersisted === true,
276
+ acceptanceTable: planResult?.acceptanceTable ?? 'none',
278
277
  checkpoint,
279
278
  cleanup,
280
279
  freshness,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * phases/spec-authoring-grounding.js — F10 spec-authoring code-grounding.
3
3
  *
4
- * Story #4139 (Epic #4131). `/plan` Phase 7 authors the PRD + Tech Spec
4
+ * Story #4139 (Epic #4131). `/plan` Phase 7 authors the Tech Spec
5
5
  * from the Epic body, the scraped project docs, and the
6
6
  * `codebaseSnapshot` structural view of the consumer repo. Two failure
7
7
  * modes made that grounding silently partial:
@@ -2,15 +2,16 @@
2
2
  * phases/spec-freshness.js — Tech Spec freshness check phase.
3
3
  *
4
4
  * Cross-validates the authored Tech Spec body against the base branch and
5
- * surfaces any stale path-shaped references on the Tech Spec issue. Two
6
- * side effects, both best-effort and non-blocking:
5
+ * surfaces any stale path-shaped references on the Epic (Story #4324: the
6
+ * Tech Spec is folded into the Epic body, so the Epic is the comment
7
+ * target). Two side effects, both best-effort and non-blocking:
7
8
  *
8
9
  * 1. Write the full `{ stale, fresh, ambiguous }` report to
9
10
  * `<tempRoot>/epic-<id>-spec-freshness.json` so downstream tooling
10
11
  * (the `--code-freshness` health check on the roadmap, or an
11
12
  * operator inspecting the run) can read it without re-probing git.
12
13
  * 2. When `stale.length > 0`, upsert a `spec-freshness` structured
13
- * comment on the Tech Spec issue listing each suspect citation.
14
+ * comment on the Epic listing each suspect citation.
14
15
  * The comment is upserted, so re-running spec re-renders the same
15
16
  * comment in place rather than spamming the issue.
16
17
  *
@@ -32,7 +33,6 @@ import { upsertStructuredComment } from '../../ticketing.js';
32
33
  /**
33
34
  * @param {object} opts
34
35
  * @param {number} opts.epicId
35
- * @param {number|null} opts.techSpecId
36
36
  * @param {string} opts.techSpecContent
37
37
  * @param {string} opts.baseBranchRef
38
38
  * @param {string} opts.tempRoot
@@ -44,7 +44,6 @@ import { upsertStructuredComment } from '../../ticketing.js';
44
44
  */
45
45
  export async function runSpecFreshnessCheck({
46
46
  epicId,
47
- techSpecId,
48
47
  techSpecContent,
49
48
  baseBranchRef,
50
49
  tempRoot,
@@ -64,7 +63,6 @@ export async function runSpecFreshnessCheck({
64
63
  );
65
64
  const payload = {
66
65
  epicId,
67
- techSpecId,
68
66
  baseBranchRef,
69
67
  generatedAt: new Date().toISOString(),
70
68
  summary: {
@@ -77,13 +75,12 @@ export async function runSpecFreshnessCheck({
77
75
  await fileWriter(reportPath, `${JSON.stringify(payload, null, 2)}\n`);
78
76
 
79
77
  let commentPosted = false;
80
- if (report.stale.length > 0 && Number.isFinite(techSpecId)) {
78
+ if (report.stale.length > 0 && Number.isFinite(epicId)) {
81
79
  const body = renderSpecFreshnessComment(report, {
82
80
  baseBranchRef,
83
- techSpecId,
84
81
  epicId,
85
82
  });
86
- await commentUpserter(provider, techSpecId, 'spec-freshness', body);
83
+ await commentUpserter(provider, epicId, 'spec-freshness', body);
87
84
  commentPosted = true;
88
85
  }
89
86
 
@@ -18,7 +18,7 @@
18
18
  * "epicId": 349,
19
19
  * "startedAt": "...",
20
20
  * "lastUpdatedAt": "...",
21
- * "spec": { "prdId": null, "techSpecId": null, "acceptanceSpecId": null, "completedAt": null },
21
+ * "spec": { "techSpecPersisted": false, "acceptanceTable": "none", "completedAt": null },
22
22
  * "decompose": { "ticketCount": null, "completedAt": null },
23
23
  * "planningRisk": { "overallLevel": "...", "requiresReview": true, "gateDecision": "..." },
24
24
  * "reviewRouting": { "decision": "review-required|auto-proceed|operator-override-review", "requiresStop": true, "forceReviewApplied": false },
@@ -106,9 +106,8 @@ export async function initialize({ provider, epicId, seed = {} } = {}) {
106
106
  epicId,
107
107
  startedAt: now,
108
108
  spec: {
109
- prdId: null,
110
- techSpecId: null,
111
- acceptanceSpecId: null,
109
+ techSpecPersisted: false,
110
+ acceptanceTable: 'none',
112
111
  completedAt: null,
113
112
  },
114
113
  decompose: { ticketCount: null, completedAt: null },
@@ -35,7 +35,7 @@ export async function discoverOpenStories({ epicId, provider }) {
35
35
  for (const t of descendants) {
36
36
  const labels = t.labels ?? [];
37
37
  if (!labels.includes(TYPE_LABELS.STORY)) continue;
38
- // Defense-in-depth: never enumerate a context spec ticket (PRD / Tech
38
+ // Defense-in-depth: never enumerate a context spec ticket (Tech
39
39
  // Spec / Acceptance Spec) as a deliverable Story even if it carries
40
40
  // `type::story`. The `createTicket` factory no longer stamps context
41
41
  // tickets that way, but a context ticket mislabelled out-of-band must
@@ -5,20 +5,19 @@
5
5
  * Auto-close is now the default for `/deliver` (the human PR-merge is
6
6
  * the gate); no per-Epic label snapshot is required.
7
7
  *
8
- * Acceptance-spec start gate (relaxed): an Epic may be delivered when
9
- * *either* the operator has explicitly waived the acceptance-spec
10
- * requirement via the `acceptance::n-a` label, *or* a
11
- * `context::acceptance-spec` ticket is linked to the Epic. The ticket's
12
- * GitHub state (open / closed) is **not** checked — presence is
13
- * sufficient, matching the PRD and Tech Spec contract. The reviewer's
14
- * OK during /plan Phase 7 is the approval signal, not a manual
15
- * ticket-close action. This still refuses to launch Epics that skipped
16
- * the /plan Phase 7 acceptance-spec authoring step (or didn't
17
- * waive), surfacing the gap at delivery time rather than letting Story
18
- * dispatch race ahead without a spec at all.
8
+ * Acceptance-spec start gate (Story #4324): an Epic may be delivered when
9
+ * *either* the operator has explicitly waived the acceptance requirement
10
+ * via the `acceptance::n-a` label, *or* the Epic body carries the
11
+ * `## Acceptance Table` managed section (the folded home of the retired
12
+ * `context::acceptance-spec` ticket's AC-ID table). Presence is
13
+ * sufficient the reviewer's OK during /plan Phase 7 is the approval
14
+ * signal. This still refuses to launch Epics that skipped the /plan
15
+ * Phase 7 acceptance authoring step (or didn't waive), surfacing the gap
16
+ * at delivery time rather than letting Story dispatch race ahead without
17
+ * a spec at all.
19
18
  */
20
19
 
21
- import { parseLinkedIssues } from '../../../issue-link-parser.js';
20
+ import { hasEpicSection } from '../../../epic-body-sections.js';
22
21
  import { ACCEPTANCE_NA } from '../../../label-constants.js';
23
22
  import { discoverOpenStories } from './build-wave-dag.js';
24
23
 
@@ -82,29 +81,23 @@ async function discoverStoryIds({ epicId, provider }) {
82
81
  }
83
82
 
84
83
  /**
85
- * Refuse to launch /deliver when the acceptance-spec precondition has
86
- * not been satisfied. Throws a clear `Error` (per
87
- * orchestration-error-handling rule) so the `runAsCli` boundary maps it to
88
- * `process.exit(1)` with the operator-visible message intact.
84
+ * Refuse to launch /deliver when the acceptance precondition has not been
85
+ * satisfied. Throws a clear `Error` (per orchestration-error-handling
86
+ * rule) so the `runAsCli` boundary maps it to `process.exit(1)` with the
87
+ * operator-visible message intact.
89
88
  *
90
- * The gate now only checks presence (or waiver). Ticket state is
91
- * deliberately ignored — closure is no longer required as the approval
92
- * signal.
89
+ * The gate checks section presence (or the waiver label) only.
93
90
  *
94
- * @param {{ epic: { labels?: string[], linkedIssues?: { acceptanceSpec?: number|null }|null, body?: string }, epicId: number }} args
91
+ * @param {{ epic: { labels?: string[], body?: string }, epicId: number }} args
95
92
  */
96
93
  function assertAcceptanceSpecGate({ epic, epicId }) {
97
94
  const labels = epic?.labels ?? [];
98
95
  if (labels.includes(ACCEPTANCE_NA)) return;
99
96
 
100
- const linkedIssues =
101
- epic?.linkedIssues ?? parseLinkedIssues(epic?.body ?? '');
102
- const acceptanceSpecId = linkedIssues?.acceptanceSpec ?? null;
103
-
104
- if (!acceptanceSpecId) {
97
+ if (!hasEpicSection(epic?.body ?? '', 'acceptanceTable')) {
105
98
  throw new Error(
106
- `[epic-deliver] Epic #${epicId} cannot launch: no context::acceptance-spec is linked and the acceptance::n-a waiver label is absent. ` +
107
- 'Run /plan Phase 7 to author an acceptance-spec, or apply the acceptance::n-a label to the Epic to opt out.',
99
+ `[epic-deliver] Epic #${epicId} cannot launch: the Epic body has no ## Acceptance Table section and the acceptance::n-a waiver label is absent. ` +
100
+ 'Run /plan Phase 7 to author the acceptance table, or apply the acceptance::n-a label to the Epic to opt out.',
108
101
  );
109
102
  }
110
103
  }
@@ -23,8 +23,10 @@
23
23
  * - Per-instance `Set<string>` of `${event}:${seqId}` keys. A repeat
24
24
  * `(event, seqId)` short-circuits without re-running reconciliation
25
25
  * and emits no events.
26
- * - The reconciler helper itself is read-only with respect to runtime
27
- * state, so the seqId guard is the only defence the listener needs.
26
+ * - The reconciler helper's only write is the section-scoped
27
+ * disposition record inside the Epic body's ## Acceptance Table
28
+ * region (Story #4324) — itself idempotent — so the seqId guard is
29
+ * the only defence the listener needs.
28
30
  *
29
31
  * Side-effect firewall: the listener owns the reconciliation call and
30
32
  * the four outcome emits. It does NOT mutate labels, post comments, or
@@ -233,10 +235,14 @@ export class AcceptanceReconciler {
233
235
  loggerImpl: this.logger,
234
236
  // Honour `acceptance::n-a` — the helper already returns
235
237
  // `status: 'waived'` for that label, but `skipWhenWaived: true`
236
- // additionally tolerates a missing spec link under the waiver
237
- // (we want the same forgiving stance the inline finalize call
238
- // had).
238
+ // additionally tolerates a missing acceptance-table section under
239
+ // the waiver (we want the same forgiving stance the inline
240
+ // finalize call had).
239
241
  skipWhenWaived: true,
242
+ // Close time is the one place the verification outcome is
243
+ // recorded into the Epic body's ## Acceptance Table section
244
+ // (section-scoped write, Story #4324).
245
+ writeDispositions: true,
240
246
  });
241
247
  } catch (err) {
242
248
  thrown = err;
@@ -24,9 +24,10 @@
24
24
  * 4. Otherwise, invoke `runFinalizeFn`. The production default
25
25
  * (`composeBusOwnedFinalize`) chains
26
26
  * a. `openOrLocatePr({ epicId, headBranch, baseBranch })`
27
- * b. `closePlanningTickets({ epicId, provider })`
28
- * c. `postHandoffComment({ epicId, prNumber, prUrl, provider })`
29
- * and returns `{ prNumber, prUrl, planningClose, handoff }`.
27
+ * b. `postHandoffComment({ epicId, prNumber, prUrl, provider })`
28
+ * and returns `{ prNumber, prUrl, handoff }`. (Story #4324 retired
29
+ * the `closePlanningTickets` sweep with the context-ticket classes —
30
+ * there are no planning tickets to close.)
30
31
  * 5. Emit `pr.created` then `epic.finalize.end`.
31
32
  *
32
33
  * Why the Finalizer does NOT emit `epic.merge.ready` (Story #3367):
@@ -65,31 +66,27 @@ import {
65
66
  } from '../../../feedback-loop/audit-results-graduator.js';
66
67
  import { graduateFindings as defaultGraduateFindings } from '../../../feedback-loop/code-review-graduator.js';
67
68
  import { parsePrNumberFromUrl } from '../../../github-url.js';
68
- import { closePlanningTickets as defaultClosePlanningTickets } from '../../finalize/close-planning-tickets.js';
69
69
  import { openOrLocatePr as defaultOpenOrLocatePr } from '../../finalize/open-or-locate-pr.js';
70
70
  import { postHandoffComment as defaultPostHandoffComment } from '../../finalize/post-handoff-comment.js';
71
71
 
72
72
  /**
73
- * Build the production default `runFinalizeFn` that composes the three
73
+ * Build the production default `runFinalizeFn` that composes the
74
74
  * bus-owned finalize helpers. Exported as a factory so the listener can
75
75
  * inject the provider (which the default needs but tests inject by
76
76
  * supplying their own `runFinalizeFn` outright).
77
77
  *
78
- * Returns `{ prNumber, prUrl, planningClose, handoff }` on success, or
78
+ * Returns `{ prNumber, prUrl, handoff }` on success, or
79
79
  * `{ blocker: { reason, detail } }` when a step fails with an
80
80
  * unrecoverable error that should keep the Epic at `agent::blocked`.
81
81
  *
82
82
  * @param {{
83
83
  * provider?: object|null,
84
84
  * openOrLocatePrFn?: typeof defaultOpenOrLocatePr,
85
- * closePlanningTicketsFn?: typeof defaultClosePlanningTickets,
86
85
  * postHandoffCommentFn?: typeof defaultPostHandoffComment,
87
86
  * }} deps
88
87
  */
89
88
  export function composeBusOwnedFinalize(deps = {}) {
90
89
  const openOrLocatePrFn = deps.openOrLocatePrFn ?? defaultOpenOrLocatePr;
91
- const closePlanningTicketsFn =
92
- deps.closePlanningTicketsFn ?? defaultClosePlanningTickets;
93
90
  const postHandoffCommentFn =
94
91
  deps.postHandoffCommentFn ?? defaultPostHandoffComment;
95
92
  const provider = deps.provider ?? null;
@@ -132,28 +129,10 @@ export function composeBusOwnedFinalize(deps = {}) {
132
129
  };
133
130
  }
134
131
 
135
- // Planning-ticket close + handoff comment require a provider.
136
- // The lifecycle-emit CLI may construct the Finalizer without one;
137
- // in that case both steps short-circuit with a 'skipped' marker
138
- // (the run itself still succeeds — the PR is open and the bus can
139
- // arm auto-merge).
140
- let planningClose = null;
141
- if (provider) {
142
- try {
143
- planningClose = await closePlanningTicketsFn({
144
- epicId,
145
- provider,
146
- });
147
- } catch (err) {
148
- return {
149
- blocker: {
150
- reason: 'close-planning-tickets-failed',
151
- detail: err?.message ?? String(err),
152
- },
153
- };
154
- }
155
- }
156
-
132
+ // The handoff comment requires a provider. The lifecycle-emit CLI
133
+ // may construct the Finalizer without one; in that case the step
134
+ // short-circuits (the run itself still succeeds the PR is open and
135
+ // the bus can arm auto-merge).
157
136
  let handoff = null;
158
137
  if (provider) {
159
138
  try {
@@ -180,7 +159,6 @@ export function composeBusOwnedFinalize(deps = {}) {
180
159
  prNumber: openResult.prNumber,
181
160
  prUrl: openResult.url,
182
161
  created: openResult.created,
183
- planningClose,
184
162
  handoff,
185
163
  };
186
164
  };
@@ -252,8 +230,8 @@ export class Finalizer {
252
230
  * @param {Function} [opts.ghPrListHeadFn] override of the
253
231
  * idempotency probe.
254
232
  * @param {object} [opts.provider] Ticketing provider forwarded to
255
- * the default `runFinalizeFn` (for `closePlanningTickets` /
256
- * `postHandoffComment`) and to the graduators.
233
+ * the default `runFinalizeFn` (for `postHandoffComment`) and to the
234
+ * graduators.
257
235
  * @param {object} [opts.config] Resolved agent config; forwarded to
258
236
  * the graduators.
259
237
  * @param {{owner:string,repo:string}} [opts.currentRepo]
@@ -283,11 +261,9 @@ export class Finalizer {
283
261
  composeBusOwnedFinalize({ provider: this.provider });
284
262
  this.ghPrListHeadFn = opts.ghPrListHeadFn ?? ghPrListHead;
285
263
  // ultrareview bug_007: the existing-PR short-circuit must run the
286
- // planning-ticket close and the handoff-comment upsert (both
287
- // idempotent) so crash-recovery replays don't skip them. Stored as
288
- // instance overrides so tests can swap them with no-op stubs.
289
- this.closePlanningTicketsFn =
290
- opts.closePlanningTicketsFn ?? defaultClosePlanningTickets;
264
+ // handoff-comment upsert (idempotent) so crash-recovery replays
265
+ // don't skip it. Stored as an instance override so tests can swap
266
+ // it with a no-op stub.
291
267
  this.postHandoffCommentFn =
292
268
  opts.postHandoffCommentFn ?? defaultPostHandoffComment;
293
269
  this.config = opts.config ?? null;
@@ -375,24 +351,11 @@ export class Finalizer {
375
351
  );
376
352
  const prNumber = parsePrNumberFromUrl(existingUrl);
377
353
  // ultrareview bug_007: even on the short-circuit path we MUST
378
- // run the planning-ticket close and the handoff-comment upsert.
379
- // Both helpers are idempotent closePlanningTickets counts
380
- // already-closed tickets under `alreadyClosed`, postHandoffComment
381
- // edits an existing marker comment in place. Without these calls,
382
- // a crash-recovery replay against an already-open PR leaves the
383
- // three planning context tickets open and never posts the
384
- // handoff comment, even though epic.merge.ready is still emitted.
385
- try {
386
- await this.closePlanningTicketsFn({
387
- epicId: this.epicId,
388
- provider: this.provider,
389
- cwd: this.cwd,
390
- });
391
- } catch (err) {
392
- this.logger.warn?.(
393
- `[Finalizer] closePlanningTickets on short-circuit failed (swallowed; replay will retry): ${err?.message ?? err}`,
394
- );
395
- }
354
+ // run the handoff-comment upsert. The helper is idempotent —
355
+ // postHandoffComment edits an existing marker comment in place.
356
+ // Without this call, a crash-recovery replay against an
357
+ // already-open PR never posts the handoff comment, even though
358
+ // epic.merge.ready is still emitted.
396
359
  try {
397
360
  await this.postHandoffCommentFn({
398
361
  epicId: this.epicId,
@@ -425,8 +388,8 @@ export class Finalizer {
425
388
 
426
389
  // 3. Run the composed bus-owned finalize (or the test-injected
427
390
  // `runFinalizeFn`). The default chains openOrLocatePr →
428
- // closePlanningTickets → postHandoffComment in order and
429
- // returns `{ prNumber, prUrl, planningClose, handoff }`.
391
+ // postHandoffComment in order and returns
392
+ // `{ prNumber, prUrl, handoff }`.
430
393
  let finalize;
431
394
  try {
432
395
  finalize = await this.runFinalizeFn({
@@ -183,7 +183,7 @@ export async function buildDefaultListenerChain(opts = {}) {
183
183
  // 3. Finalizer — opens the PR on acceptance.reconcile.ok or .waived
184
184
  // (Story #2893 split waiver out of .skipped) via the bus-owned
185
185
  // default (`composeBusOwnedFinalize`, Story #2894), which chains
186
- // openOrLocatePr → closePlanningTickets → postHandoffComment and
186
+ // openOrLocatePr → postHandoffComment and
187
187
  // emits `epic.merge.ready` on success. The legacy
188
188
  // `d1-default-no-op` blocker is gone; the listener constructed
189
189
  // with no `runFinalizeFn` override always runs the real flow.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Bounded planning-context budget for the `--emit-context` planning scripts
5
5
  * (Epic #817 Story 9). Provides a summary representation of large markdown
6
- * payloads (project docs, PRD body, Tech Spec body) so the host LLM is not
6
+ * payloads (project docs, Epic body, Tech Spec body) so the host LLM is not
7
7
  * handed multi-100KB envelopes when the underlying material exceeds the
8
8
  * configured `maxBytes` budget.
9
9
  *
@@ -22,7 +22,7 @@
22
22
  * call (`getTicket(epicId)`) used to walk the hierarchy. The hash
23
23
  * covers the Epic's id, body, labels (sorted), and (when present)
24
24
  * `updatedAt` — the same fields that drive Story discovery and the
25
- * acceptance-spec gate.
25
+ * acceptance start gate (the Epic body's ## Acceptance Table).
26
26
  * - Cache miss is signalled by `readCache` returning `null`. Callers
27
27
  * MUST fall back to a fresh snapshot/DAG pass.
28
28
  * - `writeCache` creates the parent directory recursively and writes
@@ -2,7 +2,7 @@
2
2
  * spec-freshness.js — Tech Spec post-author cross-validation against the
3
3
  * current codebase.
4
4
  *
5
- * `/plan` Phase 7 authors PRD + Tech Spec from documentation alone.
5
+ * `/plan` Phase 7 authors the Tech Spec from documentation alone.
6
6
  * When `project.docsContextFiles` (architecture.md, etc.) drift from
7
7
  * the real source tree, the Architect persona happily cites modules and
8
8
  * paths that no longer exist. The mismatch only surfaces at delivery time,
@@ -12,7 +12,7 @@
12
12
  * probes each against `baseBranchRef` (via `git cat-file -e`), and returns a
13
13
  * `{ stale, fresh, ambiguous }` envelope. The caller (epic-plan-spec.js)
14
14
  * uses the result to write a JSON report and post an advisory structured
15
- * comment on the Tech Spec issue. The check is intentionally non-blocking —
15
+ * comment on the Epic. The check is intentionally non-blocking —
16
16
  * planning continues even when stale references are present, because the
17
17
  * operator may legitimately be planning to create the path the Tech Spec
18
18
  * cites.
@@ -274,7 +274,7 @@ export function validateSpecFreshness(specBody, opts = {}) {
274
274
  * exact rendering contract.
275
275
  *
276
276
  * @param {{ stale: Array, ambiguous: Array, fresh: Array }} report
277
- * @param {{ baseBranchRef: string, techSpecId?: number, epicId?: number }} ctx
277
+ * @param {{ baseBranchRef: string, epicId?: number }} ctx
278
278
  * @returns {string}
279
279
  */
280
280
  export function renderSpecFreshnessComment(report, ctx) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * spec-section-validator.js — Phase 7.5 Tech Spec post-authoring section gate.
3
3
  *
4
- * `/plan` Phase 7 authors a Tech Spec from documentation and the PRD.
4
+ * `/plan` Phase 7 authors a Tech Spec from documentation and the Epic body.
5
5
  * Phase 8.3 (Holistic Consolidation) then reconciles the draft ticket array
6
6
  * against the Tech Spec's `## Delivery Slicing` section, which the
7
7
  * decompose-author skill uses as the capability-boundary anchor. When the
@@ -32,6 +32,7 @@ import { Logger as DefaultLogger } from '../../../../Logger.js';
32
32
  import {
33
33
  calculateAll as defaultCalculateAll,
34
34
  scanDirectory as defaultScanDirectory,
35
+ isIgnoredByGlobs as isIgnoredByGlobsMi,
35
36
  } from '../../../../maintainability-utils.js';
36
37
 
37
38
  /**
@@ -101,7 +102,20 @@ export function buildKindScorer({
101
102
  const underTarget = targetAbsDirs.some(
102
103
  (root) => abs === root || abs.startsWith(`${root}${path.sep}`),
103
104
  );
104
- if (underTarget) sourceList.push(abs);
105
+ // Apply `ignoreGlobs` here too — the full-scope walk drops
106
+ // ignore-matched files via `scanDirectory`, so the diff-scope path
107
+ // must do the same or an ignored-but-changed file (e.g. one matched
108
+ // by `config-settings-schema*.js`) enters `rows` and drags the
109
+ // `rollup["*"].min` below the maintainability floor. This mirrors the
110
+ // canonical `buildDefaultMaintainabilityScorer` (refresh-service.js,
111
+ // Story #4293); the story-close auto-refresh routes through this
112
+ // scorer, not that one, so the fix has to live here too.
113
+ if (
114
+ underTarget &&
115
+ !isIgnoredByGlobsMi(abs, miIgnoreGlobs, effectiveCwd)
116
+ ) {
117
+ sourceList.push(abs);
118
+ }
105
119
  }
106
120
  }
107
121
  const scores = await calculateAll(sourceList);