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
@@ -1,116 +0,0 @@
1
- // .agents/scripts/lib/orchestration/finalize/close-planning-tickets.js
2
- /**
3
- * close-planning-tickets.js — finalize helper that closes the three
4
- * planning context tickets (PRD / Tech Spec / Acceptance Spec) linked
5
- * from the Epic body's `## Planning Artifacts` section.
6
- *
7
- * Extracted from `/deliver` Phase 7.1 prose (the `gh issue close`
8
- * sequence) so the lifecycle Finalizer listener has a single async
9
- * helper to call. Reuses `parseLinkedIssues` so the three planning ids
10
- * are read from the same canonical body shape that `epic-plan` writes
11
- * via `ensurePlanningArtifacts` and that `closePlanningArtifacts` (the
12
- * existing post-merge close-tail helper) consumes.
13
- *
14
- * Story #2894 / Task #2904 (Epic #2880).
15
- *
16
- * Contract:
17
- * - Input: { epicId, provider, transitionFn?, logger? }
18
- * - Output: { closed, alreadyClosed, failed, details[] }
19
- *
20
- * Idempotency: a planning ticket that is already closed (provider
21
- * `state === 'closed'`) is counted under `alreadyClosed` and not
22
- * re-transitioned. A failed close on one ticket records a `failed`
23
- * entry and the helper continues with the remaining tickets — finalize
24
- * surfaces the count and the listener decides whether to escalate via
25
- * `agent::blocked`.
26
- */
27
-
28
- import { parseLinkedIssues } from '../../issue-link-parser.js';
29
- import { Logger } from '../../Logger.js';
30
- import { STATE_LABELS, transitionTicketState } from '../ticketing.js';
31
-
32
- /**
33
- * @param {object} args
34
- * @param {number} args.epicId — numeric Epic ticket id.
35
- * @param {object} args.provider — ITicketingProvider. Must implement
36
- * `getTicket(id)` returning at least `{ body, state, linkedIssues? }`.
37
- * @param {Function} [args.transitionFn] — override of
38
- * `transitionTicketState` for tests.
39
- * @param {object} [args.logger] — { info, warn, debug } surface.
40
- * @returns {Promise<{
41
- * closed: number,
42
- * alreadyClosed: number,
43
- * failed: number,
44
- * details: Array<{ kind: 'prd'|'techSpec'|'acceptanceSpec', id: number|null, status: 'closed'|'already-closed'|'failed'|'skipped', detail?: string }>,
45
- * }>}
46
- */
47
- export async function closePlanningTickets({
48
- epicId,
49
- provider,
50
- transitionFn = transitionTicketState,
51
- logger = Logger,
52
- } = {}) {
53
- if (!Number.isInteger(epicId) || epicId < 1) {
54
- throw new TypeError(
55
- 'closePlanningTickets: epicId must be a positive integer',
56
- );
57
- }
58
- if (!provider || typeof provider.getTicket !== 'function') {
59
- throw new TypeError(
60
- 'closePlanningTickets: provider must implement getTicket()',
61
- );
62
- }
63
-
64
- const epic = await provider.getTicket(epicId);
65
- const linked = epic?.linkedIssues ?? parseLinkedIssues(epic?.body ?? '');
66
-
67
- const kinds = /** @type {const} */ ([
68
- ['prd', linked?.prd ?? null],
69
- ['techSpec', linked?.techSpec ?? null],
70
- ['acceptanceSpec', linked?.acceptanceSpec ?? null],
71
- ]);
72
-
73
- const settled = await Promise.all(
74
- kinds.map(async ([kind, id]) => {
75
- if (!Number.isInteger(id) || id <= 0) {
76
- return { kind, id: null, status: 'skipped', detail: 'no-link' };
77
- }
78
- let snapshot;
79
- try {
80
- snapshot = await provider.getTicket(id);
81
- } catch (err) {
82
- const detail = err?.message ?? String(err);
83
- logger.warn?.(
84
- `[finalize/close-planning-tickets] read of ${kind} #${id} failed: ${detail}`,
85
- );
86
- return { kind, id, status: 'failed', detail };
87
- }
88
- if (snapshot?.state === 'closed') {
89
- return { kind, id, status: 'already-closed' };
90
- }
91
- try {
92
- await transitionFn(provider, id, STATE_LABELS.DONE, { cascade: false });
93
- logger.info?.(
94
- `[finalize/close-planning-tickets] closed ${kind} #${id} for Epic #${epicId}`,
95
- );
96
- return { kind, id, status: 'closed' };
97
- } catch (err) {
98
- const detail = err?.message ?? String(err);
99
- logger.warn?.(
100
- `[finalize/close-planning-tickets] close of ${kind} #${id} failed: ${detail}`,
101
- );
102
- return { kind, id, status: 'failed', detail };
103
- }
104
- }),
105
- );
106
-
107
- let closed = 0;
108
- let alreadyClosed = 0;
109
- let failed = 0;
110
- for (const row of settled) {
111
- if (row.status === 'closed') closed += 1;
112
- else if (row.status === 'already-closed') alreadyClosed += 1;
113
- else if (row.status === 'failed') failed += 1;
114
- }
115
- return { closed, alreadyClosed, failed, details: settled };
116
- }
@@ -1,318 +0,0 @@
1
- /**
2
- * @file planning-state-manager.js
3
- * Extracted state-healing and artifact idempotency logic for epic planning.
4
- *
5
- * Invariant: After planning completes, exactly ONE open PRD and ONE open
6
- * Tech Spec must exist as sub-issues of the Epic. All others are closed
7
- * (state_reason: 'not_planned') and detached.
8
- */
9
-
10
- import { Logger } from '../Logger.js';
11
- import {
12
- ACCEPTANCE_NA,
13
- AGENT_LABELS,
14
- CONTEXT_LABELS,
15
- } from '../label-constants.js';
16
- import { concurrentMap } from '../util/concurrent-map.js';
17
-
18
- /**
19
- * Snapshot of the Epic's planning-artifact state as seen / mutated by
20
- * {@link PlanningStateManager}. Mirrors the `epic-plan-state` structured
21
- * comment schema owned by `epic-plan-state-store.js`, narrowed to the fields
22
- * this manager reads and rewrites.
23
- *
24
- * @typedef {object} PlanCheckpointState
25
- * @property {number} epicId Epic ticket id.
26
- * @property {{ prd: (number | null), techSpec: (number | null), acceptanceSpec: (number | null) }} linkedIssues Canonical planning-artifact references persisted on the Epic.
27
- * @property {string} body Current Epic body (may include a `## Planning Artifacts` section).
28
- */
29
-
30
- /**
31
- * Heals and de-duplicates the Epic's PRD / Tech Spec planning artifacts so
32
- * the post-state invariant holds: exactly ONE open PRD and ONE open Tech
33
- * Spec, both linked from the Epic body. All redundant artifacts are closed
34
- * (`state_reason: 'not_planned'`) and detached.
35
- */
36
- export class PlanningStateManager {
37
- /**
38
- * @param {import('../ITicketingProvider.js').ITicketingProvider} provider Ticketing provider used for ticket + sub-issue mutations.
39
- */
40
- constructor(provider) {
41
- this.provider = provider;
42
- }
43
-
44
- /**
45
- * Resolve existing planning artifacts and heal / clean up the graph.
46
- *
47
- * With `force=false` (normal run):
48
- * - Pick the canonical PRD / Tech Spec (first open one, else first overall).
49
- * - Heal dangling `epic.linkedIssues` references.
50
- * - Close + detach any redundant artifacts (posting an audit-trace
51
- * notification first).
52
- * - Persist the healed references back to the Epic body if they were not
53
- * already written.
54
- *
55
- * With `force=true` (re-plan requested): **overwrite the canonical
56
- * context tickets in place.** Keep the canonical PRD / Tech Spec /
57
- * Acceptance Spec **open** and keep `epic.linkedIssues` pointing at them
58
- * (so the caller can refresh their bodies via `provider.updateTicket`),
59
- * close + detach only the redundant duplicate artifacts (exactly as the
60
- * non-force path does), and strip the `## Planning Artifacts` section
61
- * from the Epic body so the caller re-appends it pointing at the same
62
- * preserved IDs. Context tickets are no longer closed-and-recreated on a
63
- * forced re-plan; only Feature/Story child tickets keep that behaviour
64
- * (handled outside this manager by `forceCloseExistingChildren`).
65
- *
66
- * Mutates `epic.linkedIssues` and `epic.body` in place.
67
- *
68
- * @param {PlanCheckpointState & { linkedIssues: object, body: string, id: number }} epic Epic ticket with mutable planning state.
69
- * @param {boolean} [force=false] When true, preserve canonical context tickets (overwrite-in-place) and strip the Planning Artifacts body section for a forced re-plan.
70
- * @returns {Promise<void>}
71
- * @throws {Error} Propagates non-404/410 errors from `provider.updateTicket`. All other provider errors are intentionally swallowed.
72
- */
73
- async healAndCleanupArtifacts(epic, force = false) {
74
- const epicId = epic.id;
75
- const relatedTickets = await this.provider.getTickets(epicId);
76
- this.provider.primeTicketCache(relatedTickets);
77
-
78
- // One descriptor per planning-artifact type. Each entry parameterizes
79
- // the label filter, the canonical reference key on `epic.linkedIssues`,
80
- // and a human-readable name for the heal log line, so the per-type
81
- // filter / canonical / heal / successor logic runs from a single loop
82
- // instead of three inlined copies.
83
- const ARTIFACT_TYPES = [
84
- { label: CONTEXT_LABELS.PRD, key: 'prd', name: 'PRD' },
85
- { label: CONTEXT_LABELS.TECH_SPEC, key: 'techSpec', name: 'Tech Spec' },
86
- {
87
- label: CONTEXT_LABELS.ACCEPTANCE_SPEC,
88
- key: 'acceptanceSpec',
89
- name: 'Acceptance Spec',
90
- },
91
- ];
92
-
93
- // Resolve each artifact type: collect ALL matching tickets (open AND
94
- // closed) so stale sub-issue links get cleaned up regardless of state;
95
- // pick the canonical one (first open, else first overall); heal a
96
- // dangling `epic.linkedIssues` reference; and record the resolved
97
- // canonical id for successor resolution below.
98
- const resolved = ARTIFACT_TYPES.map((descriptor) => {
99
- const all = relatedTickets.filter((t) =>
100
- t.labels.includes(descriptor.label),
101
- );
102
- const canonical = all.find((t) => t.state === 'open') ?? all[0] ?? null;
103
-
104
- if (!epic.linkedIssues[descriptor.key] && canonical?.state === 'open') {
105
- epic.linkedIssues[descriptor.key] = canonical.id;
106
- Logger.info(
107
- `[Epic Planner] Healed dangling ${descriptor.name} reference: #${epic.linkedIssues[descriptor.key]}`,
108
- );
109
- }
110
-
111
- const canonicalId = epic.linkedIssues[descriptor.key] ?? canonical?.id;
112
- return { ...descriptor, all, canonicalId };
113
- });
114
-
115
- // Identify redundant artifacts: everything that is NOT the canonical one.
116
- const redundant = resolved.flatMap((r) =>
117
- r.all.filter((t) => t.id !== r.canonicalId),
118
- );
119
-
120
- // Map artifact label → resolved canonical id, for successor resolution.
121
- const successorByLabel = new Map(
122
- resolved.map((r) => [r.label, r.canonicalId]),
123
- );
124
-
125
- // Bound the close+detach mutation burst at 3 so wide redundancy
126
- // cleanup does not race the GitHub secondary rate limit.
127
- await concurrentMap(
128
- redundant,
129
- async (t) => {
130
- const descriptor = ARTIFACT_TYPES.find((d) =>
131
- t.labels.includes(d.label),
132
- );
133
- const successorId = descriptor
134
- ? successorByLabel.get(descriptor.label)
135
- : undefined;
136
- Logger.info(
137
- `[Epic Planner] Cleaning up redundant artifact #${t.id} (superseded by #${successorId})...`,
138
- );
139
-
140
- // Close the issue if it's still open
141
- if (t.state === 'open') {
142
- try {
143
- await this.provider.postComment(t.id, {
144
- type: 'notification',
145
- body: `⚠️ **Audit Trace**: This planning artifact was created during an interrupted or failed orchestration run and is now **superseded by #${successorId}**. \n\nClosing this issue to maintain a single source of truth for Epic #${epicId}.`,
146
- });
147
- } catch (err) {
148
- Logger.warn(
149
- `[Epic Planner] Could not post audit-trace comment on #${t.id}: ${err.message}`,
150
- );
151
- }
152
- await this.provider.updateTicket(t.id, {
153
- state: 'closed',
154
- state_reason: 'not_planned',
155
- });
156
- }
157
-
158
- // Detach the sub-issue from the Epic to prevent orphaned links
159
- try {
160
- await this.provider.removeSubIssue(epicId, t.id);
161
- Logger.info(
162
- `[Epic Planner] Detached #${t.id} from Epic #${epicId}.`,
163
- );
164
- } catch (_err) {
165
- // Already detached or API doesn't support — safe to ignore
166
- Logger.info(
167
- `[Epic Planner] Could not detach #${t.id} (may already be detached).`,
168
- );
169
- }
170
- },
171
- { concurrency: 3 },
172
- );
173
-
174
- // Persist healed references to the body if needed.
175
- if (
176
- !force &&
177
- epic.linkedIssues.prd &&
178
- epic.linkedIssues.techSpec &&
179
- !epic.body.includes('## Planning Artifacts')
180
- ) {
181
- Logger.info(
182
- `[Epic Planner] Persisting healed references to Epic body...`,
183
- );
184
- const appendBody = `\n\n## Planning Artifacts\n- [ ] PRD: #${epic.linkedIssues.prd}\n- [ ] Tech Spec: #${epic.linkedIssues.techSpec}\n`;
185
- await this.provider.updateTicket(epicId, {
186
- body: epic.body + appendBody,
187
- });
188
- epic.body += appendBody;
189
- }
190
-
191
- // Force re-plan: overwrite the canonical context tickets in place.
192
- // The redundant duplicates have already been closed + detached by the
193
- // cleanup pass above. Here we only strip the `## Planning Artifacts`
194
- // body section so `planEpic` re-appends it pointing at the same
195
- // preserved canonical IDs. We deliberately DO NOT close the canonical
196
- // PRD / Tech Spec / Acceptance Spec, and we keep `epic.linkedIssues`
197
- // pointing at them so the caller can refresh their bodies.
198
- if (force) {
199
- const stripped = epic.body.replace(
200
- /\n*## Planning Artifacts[\s\S]*$/,
201
- '',
202
- );
203
- if (stripped !== epic.body) {
204
- await this.provider.updateTicket(epicId, { body: stripped });
205
- epic.body = stripped;
206
- Logger.info(
207
- '[Epic Planner] Stripped old Planning Artifacts section from Epic body (canonical context tickets preserved for in-place overwrite).',
208
- );
209
- }
210
- }
211
- }
212
-
213
- /**
214
- * Compute whether an Epic is ready to transition from `agent::review-spec`
215
- * to `agent::ready` by inspecting the state of its context tickets.
216
- *
217
- * An Epic is ready when **all three** context tickets — PRD, Tech Spec, and
218
- * Acceptance Spec — exist and are closed. The acceptance-spec requirement
219
- * can be waived by attaching the `acceptance::n-a` label to the Epic, in
220
- * which case acceptance-spec presence and state are ignored. Missing PRD
221
- * or Tech Spec is never waivable through this method.
222
- *
223
- * This predicate is **pure** with respect to the world: it reads tickets
224
- * via the provider and computes a verdict. Callers (e.g. the planning
225
- * runner) are responsible for actually flipping the label when
226
- * `ready === true`.
227
- *
228
- * @param {number} epicId Epic ticket id.
229
- * @returns {Promise<{ ready: boolean, reason: string, contexts: { prd: ('open' | 'closed' | 'missing'), techSpec: ('open' | 'closed' | 'missing'), acceptanceSpec: ('open' | 'closed' | 'missing' | 'waived') } }>}
230
- * `ready` is `true` when the Epic satisfies the readiness gate.
231
- * `reason` is a machine-readable code suitable for logging / metrics
232
- * (`all-context-closed`, `acceptance-waived`, `prd-missing`,
233
- * `prd-open`, `tech-spec-missing`, `tech-spec-open`,
234
- * `acceptance-spec-missing`, `acceptance-spec-open`).
235
- * `contexts` is per-axis status for callers that want to render the
236
- * verdict alongside ticket links.
237
- */
238
- async computeReviewReadiness(epicId) {
239
- const epic = await this.provider.getTicket(epicId);
240
- const relatedTickets = await this.provider.getTickets(epicId);
241
- this.provider.primeTicketCache(relatedTickets);
242
-
243
- const epicLabels = epic?.labels ?? [];
244
- const acceptanceWaived = epicLabels.includes(ACCEPTANCE_NA);
245
-
246
- const findByLabel = (label) =>
247
- relatedTickets.find((t) => (t.labels ?? []).includes(label)) ?? null;
248
-
249
- const prd = findByLabel(CONTEXT_LABELS.PRD);
250
- const techSpec = findByLabel(CONTEXT_LABELS.TECH_SPEC);
251
- const acceptanceSpec = findByLabel(CONTEXT_LABELS.ACCEPTANCE_SPEC);
252
-
253
- const axisStatus = (ticket) => {
254
- if (!ticket) return 'missing';
255
- return ticket.state === 'closed' ? 'closed' : 'open';
256
- };
257
-
258
- const contexts = {
259
- prd: axisStatus(prd),
260
- techSpec: axisStatus(techSpec),
261
- acceptanceSpec: acceptanceWaived ? 'waived' : axisStatus(acceptanceSpec),
262
- };
263
-
264
- if (contexts.prd === 'missing') {
265
- return { ready: false, reason: 'prd-missing', contexts };
266
- }
267
- if (contexts.prd === 'open') {
268
- return { ready: false, reason: 'prd-open', contexts };
269
- }
270
- if (contexts.techSpec === 'missing') {
271
- return { ready: false, reason: 'tech-spec-missing', contexts };
272
- }
273
- if (contexts.techSpec === 'open') {
274
- return { ready: false, reason: 'tech-spec-open', contexts };
275
- }
276
- if (!acceptanceWaived) {
277
- if (contexts.acceptanceSpec === 'missing') {
278
- return { ready: false, reason: 'acceptance-spec-missing', contexts };
279
- }
280
- if (contexts.acceptanceSpec === 'open') {
281
- return { ready: false, reason: 'acceptance-spec-open', contexts };
282
- }
283
- }
284
-
285
- return {
286
- ready: true,
287
- reason: acceptanceWaived ? 'acceptance-waived' : 'all-context-closed',
288
- contexts,
289
- };
290
- }
291
-
292
- /**
293
- * If the Epic satisfies {@link computeReviewReadiness}, flip it from
294
- * `agent::review-spec` to `agent::ready`. Returns the verdict plus the
295
- * label transition that was applied (if any). No-ops when readiness is
296
- * not yet satisfied — callers are expected to retry on the next planning
297
- * tick rather than block.
298
- *
299
- * @param {number} epicId
300
- * @returns {Promise<{ ready: boolean, reason: string, contexts: object, transitioned: boolean }>}
301
- */
302
- async flipEpicToReadyIfContextClosed(epicId) {
303
- const verdict = await this.computeReviewReadiness(epicId);
304
- if (!verdict.ready) {
305
- return { ...verdict, transitioned: false };
306
- }
307
- await this.provider.updateTicket(epicId, {
308
- labels: {
309
- add: [AGENT_LABELS.READY],
310
- remove: [AGENT_LABELS.REVIEW_SPEC],
311
- },
312
- });
313
- Logger.info(
314
- `[Epic Planner] Epic #${epicId} → ${AGENT_LABELS.READY} (${verdict.reason}).`,
315
- );
316
- return { ...verdict, transitioned: true };
317
- }
318
- }
@@ -1,57 +0,0 @@
1
- import { Logger } from '../Logger.js';
2
- /**
3
- * hierarchy-tracer.js — Stage 2 of the story-init pipeline.
4
- *
5
- * Resolves the linked PRD and Tech Spec issue IDs for a Story's parent Epic.
6
- *
7
- * Story #4253: when both `prdId` and `techSpecId` are supplied as input
8
- * (the `/deliver` fan-out resolves the immutable Epic once at the top of the
9
- * run and threads the two ids down via `story-init.js --prd/--tech-spec`),
10
- * this stage short-circuits and does NOT call `provider.getEpic`. The Epic
11
- * issue is invariant for the lifetime of a delivery run, so the N per-Story
12
- * `getEpic` round-trips collapse to one parent-side resolution.
13
- *
14
- * When the flags are absent (interactive / single-story use), the legacy
15
- * `getEpic` resolution runs unchanged. Fetch failures are logged but
16
- * non-fatal — the result simply reports `null` for whichever linkage could
17
- * not be resolved, preserving the graceful degradation on a missing Epic.
18
- */
19
-
20
- /**
21
- * @param {object} deps
22
- * @param {object} deps.provider
23
- * @param {object} [deps.logger]
24
- * @param {object} deps.input
25
- * @param {number} deps.input.epicId
26
- * @param {number|null} [deps.input.prdId] Pre-resolved PRD id (from --prd).
27
- * @param {number|null} [deps.input.techSpecId] Pre-resolved Tech Spec id
28
- * (from --tech-spec). When both `prdId` and `techSpecId` are supplied,
29
- * `getEpic` is skipped.
30
- * @returns {Promise<{ prdId: number|null, techSpecId: number|null }>}
31
- */
32
- export async function traceHierarchy({ provider, logger, input }) {
33
- const { epicId } = input;
34
- const warn = logger?.warn ?? ((msg) => Logger.error(msg));
35
-
36
- // Short-circuit: the parent already resolved both linkages once and threaded
37
- // them in, so there is nothing left to fetch. Skip the per-Story getEpic.
38
- const suppliedPrdId = input.prdId ?? null;
39
- const suppliedTechSpecId = input.techSpecId ?? null;
40
- if (suppliedPrdId !== null && suppliedTechSpecId !== null) {
41
- return { prdId: suppliedPrdId, techSpecId: suppliedTechSpecId };
42
- }
43
-
44
- let prdId = null;
45
- let techSpecId = null;
46
- try {
47
- const epic = await provider.getEpic(epicId);
48
- prdId = epic.linkedIssues?.prd ?? null;
49
- techSpecId = epic.linkedIssues?.techSpec ?? null;
50
- } catch (err) {
51
- warn(
52
- `[story-init] Warning: Could not fetch Epic #${epicId}: ${err.message}`,
53
- );
54
- }
55
-
56
- return { prdId, techSpecId };
57
- }