mandrel 1.57.0 → 1.59.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 (131) hide show
  1. package/.agents/README.md +89 -87
  2. package/.agents/docs/SDLC.md +11 -7
  3. package/.agents/docs/workflows.md +2 -1
  4. package/.agents/schemas/audit-rules.json +20 -0
  5. package/.agents/scripts/acceptance-eval.js +20 -3
  6. package/.agents/scripts/assert-branch.js +1 -3
  7. package/.agents/scripts/bootstrap.js +1 -1
  8. package/.agents/scripts/check-arch-cycles.js +360 -0
  9. package/.agents/scripts/coverage-capture.js +24 -3
  10. package/.agents/scripts/epic-deliver-preflight.js +5 -3
  11. package/.agents/scripts/epic-deliver-prepare.js +12 -4
  12. package/.agents/scripts/epic-execute-record-wave.js +1 -1
  13. package/.agents/scripts/evidence-gate.js +1 -1
  14. package/.agents/scripts/git-rebase-and-resolve.js +1 -1
  15. package/.agents/scripts/hierarchy-gate.js +34 -14
  16. package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
  17. package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
  18. package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
  19. package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
  20. package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
  21. package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
  22. package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
  23. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
  24. package/.agents/scripts/lib/baselines/writer.js +1 -1
  25. package/.agents/scripts/lib/close-validation/commands.js +188 -0
  26. package/.agents/scripts/lib/close-validation/gates.js +235 -0
  27. package/.agents/scripts/lib/close-validation/process.js +101 -0
  28. package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
  29. package/.agents/scripts/lib/close-validation/runner.js +325 -0
  30. package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
  31. package/.agents/scripts/lib/config/quality.js +6 -6
  32. package/.agents/scripts/lib/config-resolver.js +2 -5
  33. package/.agents/scripts/lib/coverage-capture.js +147 -4
  34. package/.agents/scripts/lib/cpu-pool.js +14 -0
  35. package/.agents/scripts/lib/crap-utils.js +6 -11
  36. package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
  37. package/.agents/scripts/lib/git-utils.js +24 -22
  38. package/.agents/scripts/lib/maintainability-engine.js +1 -1
  39. package/.agents/scripts/lib/maintainability-utils.js +4 -187
  40. package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
  41. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
  42. package/.agents/scripts/lib/orchestration/code-review.js +90 -77
  43. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
  44. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
  45. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
  46. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
  47. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
  48. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
  49. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
  50. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
  51. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
  52. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
  53. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
  54. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
  55. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
  56. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
  57. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
  58. package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
  59. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
  60. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
  61. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
  62. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
  63. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
  64. package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
  65. package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
  66. package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
  67. package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
  68. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
  69. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
  70. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +241 -0
  71. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
  72. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +16 -3
  73. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
  74. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
  75. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
  76. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
  77. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
  78. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
  79. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
  80. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
  81. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
  82. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
  83. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
  84. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
  85. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
  86. package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
  87. package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
  88. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
  89. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
  90. package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
  91. package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
  92. package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
  93. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  94. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
  95. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
  96. package/.agents/scripts/lib/project-root.js +17 -0
  97. package/.agents/scripts/lib/story-adjacency.js +76 -0
  98. package/.agents/scripts/lib/story-lifecycle.js +1 -1
  99. package/.agents/scripts/lib/transpile.js +93 -0
  100. package/.agents/scripts/lib/wave-runner/tick.js +4 -153
  101. package/.agents/scripts/lib/workers/crap-worker.js +1 -1
  102. package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
  103. package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
  104. package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
  105. package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
  106. package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
  107. package/.agents/scripts/providers/github/tickets.js +110 -6
  108. package/.agents/scripts/run-lint.js +9 -0
  109. package/.agents/scripts/run-tests.js +24 -4
  110. package/.agents/scripts/stories-wave-tick.js +8 -5
  111. package/.agents/scripts/story-init.js +149 -10
  112. package/.agents/scripts/sync-branch-from-base.js +1 -1
  113. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
  114. package/.agents/workflows/audit-documentation.md +226 -0
  115. package/.agents/workflows/epic-deliver.md +16 -23
  116. package/.agents/workflows/epic-plan.md +1 -1
  117. package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
  118. package/.agents/workflows/helpers/single-story-deliver.md +2 -1
  119. package/.agents/workflows/onboard.md +4 -3
  120. package/.agents/workflows/story-deliver.md +1 -1
  121. package/README.md +21 -8
  122. package/lib/cli/init.js +336 -0
  123. package/package.json +2 -1
  124. package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
  125. package/.agents/scripts/lib/close-validation.js +0 -897
  126. package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
  127. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
  128. package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
  129. package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
  130. package/.agents/scripts/lib/task-utils.js +0 -26
  131. package/.agents/scripts/story-deliver-prepare.js +0 -267
@@ -1,275 +0,0 @@
1
- /**
2
- * lib/orchestration/cascade-grouping.js — Cascade dispatch helpers.
3
- *
4
- * Pure helpers used by `cascadeCompletion` (see `./ticketing.js`) to
5
- * partition a list of parent tickets into disjoint shared-ancestor groups
6
- * and to buffer per-parent log output so parallel dispatch produces a
7
- * byte-identical log stream to the serial baseline.
8
- *
9
- * Pulled out of `ticketing.js` so the cascade orchestrator stays under the
10
- * project's per-file maintainability ceiling. No state is held at module
11
- * scope — every helper takes its dependencies as arguments.
12
- */
13
-
14
- /**
15
- * Walks `parent: #N` references upward from the given ticket id until no new
16
- * ancestors are discovered. Returns the set of every ticket id reachable
17
- * along the chain, including the starting id. Cycle-safe by construction —
18
- * the visited set acts as the seen guard, so a cyclic `parent: #N` graph
19
- * terminates in finite steps without revisiting nodes.
20
- *
21
- * Pure of side effects beyond the provider reads it issues. Provider
22
- * failures on a single hop fall back to "no further ancestors discovered"
23
- * for that branch (the chain truncates rather than throwing); this matches
24
- * `cascadeCompletion`'s tolerant posture toward transient reads.
25
- *
26
- * @param {import('../ITicketingProvider.js').ITicketingProvider} provider
27
- * @param {number} startId
28
- * @param {Map<number, Set<number>>} [cache] - Optional per-call cache of
29
- * already-walked chains keyed by intermediate id. Reused across parents
30
- * in {@link groupByAncestor} to amortise repeat walks.
31
- * @returns {Promise<Set<number>>} ancestor set including `startId`.
32
- */
33
- export async function walkAncestorChain(provider, startId, cache) {
34
- // Inner DFS with memoisation. `inProgress` guards cycles so a cyclic
35
- // `parent: #N` graph terminates without recursion depth issues. Each
36
- // visited node gets its own cache entry holding the set of ids reachable
37
- // from it (inclusive), so a sibling walk that re-enters this subgraph
38
- // can splice the cached set wholesale instead of re-reading the provider.
39
- async function visit(id, inProgress) {
40
- if (cache?.has(id)) return cache.get(id);
41
- if (inProgress.has(id)) {
42
- // Cycle: return a singleton so the caller still includes `id` in its
43
- // ancestor set without recursing further through this loop. Do NOT
44
- // memoise — the partial result is incomplete for `id`'s true chain.
45
- return new Set([id]);
46
- }
47
- inProgress.add(id);
48
-
49
- const set = new Set([id]);
50
- let ticket = null;
51
- try {
52
- ticket = await provider.getTicket(id);
53
- } catch {
54
- // Provider read failure: truncate the chain branch. Memoise as the
55
- // singleton so subsequent walks don't retry an already-failed read.
56
- inProgress.delete(id);
57
- cache?.set(id, set);
58
- return set;
59
- }
60
-
61
- if (ticket?.body) {
62
- const matches = [...ticket.body.matchAll(/parent:\s*#(\d+)/gi)];
63
- for (const m of matches) {
64
- const next = Number.parseInt(m[1], 10);
65
- if (!Number.isFinite(next)) continue;
66
- const subset = await visit(next, inProgress);
67
- for (const v of subset) set.add(v);
68
- }
69
- }
70
-
71
- inProgress.delete(id);
72
- cache?.set(id, set);
73
- return set;
74
- }
75
-
76
- return visit(startId, new Set());
77
- }
78
-
79
- /**
80
- * Partitions a list of parent ids into disjoint groups whose members share
81
- * at least one ancestor (transitively, via `parent: #N` references walked
82
- * to fixpoint).
83
- *
84
- * Two parents end up in the same group if and only if their ancestor sets
85
- * overlap on at least one ticket id. Parents with no shared ancestors end
86
- * up in singleton groups. The union of the returned groups equals the
87
- * input set; the order of `parents[]` is preserved within each group, and
88
- * groups are returned in the order their first member appears in the
89
- * input.
90
- *
91
- * Pure of side effects beyond the provider reads needed to walk chains.
92
- * Walked ancestor sets are cached per call so a parent that contributes
93
- * to multiple groups is not re-walked. Cycle-safe — see
94
- * {@link walkAncestorChain}.
95
- *
96
- * Used by `cascadeCompletion` to dispatch disjoint groups in parallel
97
- * while keeping shared-ancestor groups strictly sequential (concurrent
98
- * transitions on the same ancestor would race the "all children done?"
99
- * check).
100
- *
101
- * @param {Array<number>} parents
102
- * @param {import('../ITicketingProvider.js').ITicketingProvider} provider
103
- * @returns {Promise<Array<Array<number>>>} disjoint groups of parent ids.
104
- */
105
- export async function groupByAncestor(parents, provider) {
106
- if (!Array.isArray(parents) || parents.length === 0) return [];
107
-
108
- // Walk each parent's ancestor chain once, sharing a cache so a parent
109
- // that re-enters an already-walked subgraph reuses the cached set.
110
- const cache = new Map();
111
- const ancestorsByParent = new Map();
112
- for (const parentId of parents) {
113
- const chain = await walkAncestorChain(provider, parentId, cache);
114
- ancestorsByParent.set(parentId, chain);
115
- }
116
-
117
- return unionFindByAncestor(parents, ancestorsByParent);
118
- }
119
-
120
- /**
121
- * Union-Find over `parents`, joined whenever any two parents' ancestor
122
- * chains overlap on at least one id. Returns the parents bucketed by
123
- * representative, in input order both for the groups themselves and for
124
- * members within each group.
125
- *
126
- * Pulled out of {@link groupByAncestor} to keep that function's CRAP
127
- * under the v6 ceiling.
128
- *
129
- * @param {Array<number>} parents
130
- * @param {Map<number, Set<number>>} ancestorsByParent
131
- * @returns {Array<Array<number>>}
132
- */
133
- function unionFindByAncestor(parents, ancestorsByParent) {
134
- const parentIndex = new Map();
135
- parents.forEach((p, i) => {
136
- parentIndex.set(p, i);
137
- });
138
- const uf = parents.map((_, i) => i);
139
- const find = (i) => {
140
- while (uf[i] !== i) {
141
- uf[i] = uf[uf[i]];
142
- i = uf[i];
143
- }
144
- return i;
145
- };
146
- const union = (a, b) => {
147
- const ra = find(a);
148
- const rb = find(b);
149
- if (ra !== rb) uf[ra] = rb;
150
- };
151
-
152
- // For each ancestor id, collect parents whose chain hits it; union them.
153
- const ancestorToParents = new Map();
154
- for (const [parentId, chain] of ancestorsByParent) {
155
- for (const ancestorId of chain) {
156
- if (!ancestorToParents.has(ancestorId)) {
157
- ancestorToParents.set(ancestorId, []);
158
- }
159
- ancestorToParents.get(ancestorId).push(parentId);
160
- }
161
- }
162
- for (const sharing of ancestorToParents.values()) {
163
- if (sharing.length < 2) continue;
164
- const first = parentIndex.get(sharing[0]);
165
- for (let i = 1; i < sharing.length; i++) {
166
- union(first, parentIndex.get(sharing[i]));
167
- }
168
- }
169
-
170
- // Bucket parents by representative, preserving first-seen order for
171
- // both groups and within-group ordering.
172
- const repToGroup = new Map();
173
- const groupOrder = [];
174
- for (const parentId of parents) {
175
- const rep = find(parentIndex.get(parentId));
176
- if (!repToGroup.has(rep)) {
177
- repToGroup.set(rep, []);
178
- groupOrder.push(rep);
179
- }
180
- repToGroup.get(rep).push(parentId);
181
- }
182
-
183
- return groupOrder.map((rep) => repToGroup.get(rep));
184
- }
185
-
186
- /**
187
- * Dispatches cascade work across disjoint shared-ancestor groups in
188
- * parallel while running each within-group parent sequentially in input
189
- * order. Per-parent output is captured into a buffered logger so the
190
- * visible log stream is byte-identical to a serial baseline; the buffer
191
- * is flushed to `flushLogger` in the original `parsedParents` order
192
- * after every group resolves.
193
- *
194
- * The actual per-parent work is supplied by `processParent` so this
195
- * helper stays free of cascade-specific dependencies — its only job is
196
- * the parallel-dispatch + ordered-flush scaffolding.
197
- *
198
- * @template R
199
- * @param {Object} args
200
- * @param {Array<number>} args.parsedParents - Parent ids in their
201
- * original input order. Drives both the group-membership lookup and
202
- * the post-dispatch log flush order.
203
- * @param {Array<Array<number>>} args.groups - Disjoint groups returned
204
- * by {@link groupByAncestor}.
205
- * @param {(parentId: number, bufferedLogger: object) => Promise<R>} args.processParent
206
- * Per-parent worker. Receives the parent id and a buffered logger.
207
- * Its resolved value is collected into `args.parsedParents`-ordered
208
- * results.
209
- * @param {{ debug: Function, info: Function, warn: Function, error: Function }} args.flushLogger
210
- * Real logger that receives the buffered output after dispatch.
211
- * @returns {Promise<Array<R>>} per-parent results in `parsedParents`
212
- * order.
213
- */
214
- export async function dispatchCascadeGroups({
215
- parsedParents,
216
- groups,
217
- processParent,
218
- flushLogger,
219
- }) {
220
- const parentLoggers = new Map();
221
- const parentResults = new Map();
222
- for (const parentId of parsedParents) {
223
- parentLoggers.set(parentId, createBufferedLogger());
224
- }
225
-
226
- await Promise.all(
227
- groups.map(async (group) => {
228
- for (const parentId of group) {
229
- const logger = parentLoggers.get(parentId);
230
- const result = await processParent(parentId, logger);
231
- parentResults.set(parentId, result);
232
- }
233
- }),
234
- );
235
-
236
- const results = [];
237
- for (const parentId of parsedParents) {
238
- const lg = parentLoggers.get(parentId);
239
- if (lg) {
240
- for (const entry of lg.buffer) {
241
- flushLogger[entry.level](entry.message);
242
- }
243
- }
244
- const result = parentResults.get(parentId);
245
- if (result !== undefined) results.push(result);
246
- }
247
- return results;
248
- }
249
-
250
- /**
251
- * Buffered logger shaped like the public `Logger` surface. Stores every
252
- * emitted line in `buffer[]` instead of writing to the console. Callers
253
- * flush the buffer to a real logger after the buffered region completes
254
- * so the visible log output is byte-identical to a serial run.
255
- *
256
- * @returns {{ buffer: Array<{ level: 'debug'|'info'|'warn'|'error', message: string }>, debug: Function, info: Function, warn: Function, error: Function }}
257
- */
258
- export function createBufferedLogger() {
259
- const buffer = [];
260
- return {
261
- buffer,
262
- debug(message) {
263
- buffer.push({ level: 'debug', message });
264
- },
265
- info(message) {
266
- buffer.push({ level: 'info', message });
267
- },
268
- warn(message) {
269
- buffer.push({ level: 'warn', message });
270
- },
271
- error(message) {
272
- buffer.push({ level: 'error', message });
273
- },
274
- };
275
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * progress-reporter.js — facade module for the /epic-deliver progress
3
- * narrative. Story #1847 split the original 1158-LOC monolith into three
4
- * sibling sub-modules under `progress-reporter/`:
5
- *
6
- * - `composition.js` — structured-comment body builders and the pure
7
- * rendering helpers (the legacy ProgressReporter class used these).
8
- * - `transport.js` — the curated webhook emit surface (epic-started,
9
- * epic-progress, epic-blocked, epic-unblocked).
10
- * - `signals.js` — pure parse/aggregate over `story-run-progress` and
11
- * `phase-timings` structured comments + the shared state lookup
12
- * tables (PHASE_TO_STATE, PHASE_ORDER, STATE_EMOJI).
13
- *
14
- * Epic #2646 Story C (Task #2699) — the tick-based polling
15
- * `ProgressReporter` class that used to live here was deleted in favour
16
- * of the bus-driven `lifecycle/listeners/progress-reporter.js` which
17
- * already consumes `story.dispatch.end` + `wave.end` to compose the
18
- * `epic-run-progress` body. The webhook helpers and parse/aggregate
19
- * exports remain at this path so existing importers
20
- * (`epic-execute-record-wave.js`, `wave-record-notifications.js`,
21
- * `crap-remediation-1641.test.js`) keep resolving — only the periodic
22
- * emission shell went away.
23
- */
24
-
25
- import {
26
- deriveState as deriveStateFromComposition,
27
- renderProgressBody as renderProgressBodyFromComposition,
28
- truncate as truncateFromComposition,
29
- upsertEpicRunProgress as upsertEpicRunProgressFromComposition,
30
- } from './progress-reporter/composition.js';
31
- import {
32
- aggregatePhaseTimings as aggregatePhaseTimingsFromSignals,
33
- EPIC_RUN_PROGRESS_TYPE as EPIC_RUN_PROGRESS_TYPE_FROM_SIGNALS,
34
- PHASE_TIMINGS_TYPE as PHASE_TIMINGS_TYPE_FROM_SIGNALS,
35
- parsePhaseTimingsComment as parsePhaseTimingsCommentFromSignals,
36
- parseStoryRunProgressComment as parseStoryRunProgressCommentFromSignals,
37
- phaseToState as phaseToStateFromSignals,
38
- renderPhaseTimingsSection as renderPhaseTimingsSectionFromSignals,
39
- STORY_RUN_PROGRESS_TYPE as STORY_RUN_PROGRESS_TYPE_FROM_SIGNALS,
40
- } from './progress-reporter/signals.js';
41
- import {
42
- EPIC_PROGRESS_EVENT as EPIC_PROGRESS_EVENT_FROM_TRANSPORT,
43
- emitEpicBlocked as emitEpicBlockedFromTransport,
44
- emitEpicProgress as emitEpicProgressFromTransport,
45
- emitEpicStarted as emitEpicStartedFromTransport,
46
- emitEpicUnblocked as emitEpicUnblockedFromTransport,
47
- } from './progress-reporter/transport.js';
48
-
49
- // Re-exports — sub-module surfaces are aliased back to the parent path so
50
- // existing imports (epic-execute-record-wave.js,
51
- // wave-record-notifications.js) keep resolving.
52
- export const EPIC_RUN_PROGRESS_TYPE = EPIC_RUN_PROGRESS_TYPE_FROM_SIGNALS;
53
- export const PHASE_TIMINGS_TYPE = PHASE_TIMINGS_TYPE_FROM_SIGNALS;
54
- export const STORY_RUN_PROGRESS_TYPE = STORY_RUN_PROGRESS_TYPE_FROM_SIGNALS;
55
- export const EPIC_PROGRESS_EVENT = EPIC_PROGRESS_EVENT_FROM_TRANSPORT;
56
- export const emitEpicProgress = emitEpicProgressFromTransport;
57
- export const emitEpicStarted = emitEpicStartedFromTransport;
58
- export const emitEpicBlocked = emitEpicBlockedFromTransport;
59
- export const emitEpicUnblocked = emitEpicUnblockedFromTransport;
60
- export const parseStoryRunProgressComment =
61
- parseStoryRunProgressCommentFromSignals;
62
- export const parsePhaseTimingsComment = parsePhaseTimingsCommentFromSignals;
63
- export const aggregatePhaseTimings = aggregatePhaseTimingsFromSignals;
64
- export const renderPhaseTimingsSection = renderPhaseTimingsSectionFromSignals;
65
- export const phaseToState = phaseToStateFromSignals;
66
- export const upsertEpicRunProgress = upsertEpicRunProgressFromComposition;
67
- export const deriveState = deriveStateFromComposition;
68
- export const renderProgressBody = renderProgressBodyFromComposition;
69
- export const truncate = truncateFromComposition;
@@ -1,221 +0,0 @@
1
- /**
2
- * format-autofix-scoped.js — Story #2533: scoped biome-format auto-apply
3
- * inside story-close's pre-merge gate chain.
4
- *
5
- * Background. The whole-tree `runFormatAutofix` (sibling module) heals
6
- * drift across `.` before the gate chain runs. That step is intentionally
7
- * broad because it covers files (JSON / YAML / config) that lint-staged
8
- * does not glob. This module is the narrower companion: it scopes
9
- * `biome format --write` to the **changed-file set** between the Epic
10
- * branch and the Story branch and folds any auto-fixed paths into a
11
- * dedicated commit on the Story branch *before* `biome ci` runs in the
12
- * gate chain.
13
- *
14
- * Why scoped + warn-level. The Tech Spec (Epic #2527, Story 5) calls out
15
- * that format diffs introduced by Story commits should never surface to
16
- * Phase 3 close-validation. The whole-tree autofix already covers that,
17
- * but emits `info` so operators routinely miss it. This module emits
18
- * `Logger.warn` naming the auto-fixed files so the signal is visible in
19
- * the close transcript and downstream ledger.
20
- *
21
- * Dependencies are injected so unit tests pin behaviour without spawning
22
- * git or biome.
23
- */
24
-
25
- import { execFileSync } from 'node:child_process';
26
-
27
- import { diffNameOnly } from '../../changed-files.js';
28
- import { Logger as DefaultLogger } from '../../Logger.js';
29
- import {
30
- commitDirtyPaths,
31
- currentBranch,
32
- listDirtyPaths,
33
- resolveFormatterCmd,
34
- } from './format-autofix-shared.js';
35
-
36
- const TAG = '[format-autofix-scoped]';
37
-
38
- /**
39
- * List the files changed between `epicBranch` and `storyBranch` using the
40
- * three-dot merge-base diff. Delegates parsing to `diffNameOnly` from
41
- * `changed-files.js` so the stdout → path-list conversion lives in one place.
42
- *
43
- * The `git` parameter uses the caller's local interface:
44
- * `(args: string[], opts: object) => string`. A bridge adapter wraps it into
45
- * the `gitSpawn(cwd, ...args)` shape that `diffNameOnly` expects.
46
- *
47
- * @param {{ cwd: string, epicBranch: string, storyBranch: string, git: Function }} opts
48
- * @returns {string[]}
49
- */
50
- function listChangedFiles({ cwd, epicBranch, storyBranch, git }) {
51
- // Bridge the (args, opts) → string interface into gitSpawn(cwd, ...args).
52
- const gitSpawn = (_cwd, ...args) => {
53
- try {
54
- const stdout = git(args, {
55
- cwd: _cwd,
56
- encoding: 'utf8',
57
- stdio: ['ignore', 'pipe', 'ignore'],
58
- });
59
- return { status: 0, stdout: stdout ?? '', stderr: '' };
60
- } catch (err) {
61
- return {
62
- status: err.status ?? 1,
63
- stdout: err.stdout ?? '',
64
- stderr: err.stderr ?? err.message,
65
- };
66
- }
67
- };
68
- return diffNameOnly({
69
- range: `${epicBranch}...${storyBranch}`,
70
- cwd,
71
- gitSpawn,
72
- });
73
- }
74
-
75
- /**
76
- * Run `biome format --write <changedFiles>` on the Epic→Story diff. If
77
- * any file is modified, stage and commit the changes on the Story branch
78
- * with a conventional `fix(story-close):` subject and emit a
79
- * `Logger.warn` naming the auto-fixed files. Returns a structured
80
- * envelope so callers can log a single line.
81
- *
82
- * No-op envelopes:
83
- * - `{ ran: false, reason: 'no-changed-files' }` — empty diff.
84
- * - `{ ran: false, reason: 'dirty-tree' }` — refused to
85
- * absorb pre-existing edits.
86
- * - `{ ran: true, committed: false }` — formatter
87
- * was clean.
88
- *
89
- * **Worktree scope (Story #3907).** All git + formatter operations run in
90
- * `worktreePath` (the Story worktree where `story-<id>` is checked out), not
91
- * `cwd` (the main checkout). The earlier implementation ran every step
92
- * against `cwd`, so the `git add -u` + `git commit` could land an unreviewed
93
- * `fix(story-close):` commit on whatever branch the main checkout happened to
94
- * have out — including `main`. Before committing, the worktree's checked-out
95
- * branch is asserted to equal `storyBranch`; a mismatch refuses to commit and
96
- * returns `{ ran: true, committed: false, reason: 'wrong-branch' }` so a
97
- * stale-state checkout can never absorb the autofix into the wrong history.
98
- * `worktreePath` defaults to `cwd` for the resume/legacy callers that have no
99
- * separate worktree.
100
- *
101
- * @param {{
102
- * cwd: string,
103
- * worktreePath?: string,
104
- * storyId: number|string,
105
- * epicBranch: string,
106
- * storyBranch: string,
107
- * config?: object,
108
- * logger?: object,
109
- * spawnSync?: typeof execFileSync,
110
- * gitSync?: (args: string[], opts: object) => string,
111
- * }} opts
112
- * @returns {{
113
- * ran: boolean,
114
- * committed: boolean,
115
- * sha?: string,
116
- * modifiedPaths?: string[],
117
- * reason?: string,
118
- * }}
119
- */
120
- export function runScopedFormatAutofix({
121
- cwd,
122
- worktreePath,
123
- storyId,
124
- epicBranch,
125
- storyBranch,
126
- config,
127
- logger = DefaultLogger,
128
- spawnSync = execFileSync,
129
- gitSync,
130
- } = {}) {
131
- if (!cwd) throw new Error('runScopedFormatAutofix: cwd is required');
132
- if (!epicBranch)
133
- throw new Error('runScopedFormatAutofix: epicBranch is required');
134
- if (!storyBranch)
135
- throw new Error('runScopedFormatAutofix: storyBranch is required');
136
-
137
- // Story #3907 — the formatter writes + the commit must land in the Story
138
- // worktree, never the main checkout. Fall back to `cwd` only for callers
139
- // that do not run under worktree isolation.
140
- const workTree = worktreePath || cwd;
141
-
142
- const git = gitSync ?? ((args, opts) => spawnSync('git', args, opts));
143
-
144
- // Resolve the formatter base command (e.g. `npx biome format --write`).
145
- // We drop a trailing `.` so we can append the changed-file set explicitly.
146
- const { writeCmdString, writeCmd, writeArgs } = resolveFormatterCmd({
147
- commands: config?.project?.commands,
148
- dropTrailingDot: true,
149
- });
150
-
151
- const changed = listChangedFiles({
152
- cwd: workTree,
153
- epicBranch,
154
- storyBranch,
155
- git,
156
- });
157
- if (changed.length === 0) {
158
- logger.info?.(
159
- `${TAG} skipped — no changed files between ${epicBranch} and ${storyBranch}.`,
160
- );
161
- return { ran: false, committed: false, reason: 'no-changed-files' };
162
- }
163
-
164
- const dirtyBefore = listDirtyPaths(workTree, git);
165
- if (dirtyBefore.length) {
166
- logger.info?.(
167
- `${TAG} skipped — working tree dirty before scoped autofix (${dirtyBefore.length} paths).`,
168
- );
169
- return { ran: false, committed: false, reason: 'dirty-tree' };
170
- }
171
-
172
- // Run the formatter against the changed-file set. We tolerate non-zero
173
- // exit because the downstream check gate is the source of truth for
174
- // "did formatting succeed".
175
- try {
176
- spawnSync(writeCmd, [...writeArgs, ...changed], {
177
- cwd: workTree,
178
- stdio: ['ignore', 'pipe', 'pipe'],
179
- encoding: 'utf8',
180
- });
181
- } catch (err) {
182
- logger.warn?.(
183
- `${TAG} \`${writeCmdString}\` on ${changed.length} changed file(s) exited non-zero (${err?.status ?? 'unknown'}); falling through to the format check gate.`,
184
- );
185
- }
186
-
187
- const dirtyAfter = listDirtyPaths(workTree, git);
188
- if (!dirtyAfter.length) {
189
- logger.info?.(
190
- `${TAG} no format drift on ${changed.length} changed file(s).`,
191
- );
192
- return { ran: true, committed: false };
193
- }
194
-
195
- // Story #3907 — assert the worktree is actually on `storyBranch` before we
196
- // stage + commit. Without this guard a stale-state checkout (or a
197
- // mis-wired `cwd`) could absorb the autofix onto the wrong branch (incl.
198
- // `main`). A mismatch refuses to commit and leaves the format drift for the
199
- // downstream check gate to surface.
200
- const onBranch = currentBranch(workTree, git);
201
- if (onBranch !== storyBranch) {
202
- logger.warn?.(
203
- `${TAG} refusing to commit — worktree ${workTree} is on "${onBranch ?? 'unknown'}", expected "${storyBranch}". ` +
204
- `${dirtyAfter.length} format-drift path(s) left for the check gate.`,
205
- );
206
- return { ran: true, committed: false, reason: 'wrong-branch' };
207
- }
208
-
209
- // Stage every modified path and commit. Hooks must run; do not pass
210
- // --no-verify (project policy: never skip git hooks).
211
- const subject = `fix(story-close): auto-apply biome format in scoped lint (story #${storyId})`;
212
- const sha = commitDirtyPaths({ cwd: workTree, git, subject });
213
-
214
- // The warn-level emission is the Tech Spec contract — operators read
215
- // this line in the close transcript to know auto-fix landed in the
216
- // close commit, and downstream ledger inspectors filter on it.
217
- logger.warn?.(
218
- `${TAG} auto-applied biome format to ${dirtyAfter.length} path(s) on story #${storyId}: ${dirtyAfter.join(', ')}; committed as ${sha}.`,
219
- );
220
- return { ran: true, committed: true, sha, modifiedPaths: dirtyAfter };
221
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * format-autofix-shared.js — Story #3332 (Epic #3316): single-source the
3
- * git/formatter plumbing shared by the two format-autofix forks.
4
- *
5
- * `format-autofix.js` (whole-tree heal) and `format-autofix-scoped.js`
6
- * (Epic→Story changed-file heal) historically each carried their own copy
7
- * of the porcelain-status parse, the formatter-command resolution, and the
8
- * stage→commit→rev-parse sequence. The three forked helpers are
9
- * byte-for-byte equivalent apart from cosmetics, so a fix to (say) the
10
- * porcelain-line slice had to land twice. This module is the single home
11
- * for all three; the two forks now differ only in file-scope, commit
12
- * subject, and log level.
13
- *
14
- * Every helper takes an injected `git` runner (`(args, opts) => string`) so
15
- * unit tests pin behaviour without spawning git.
16
- */
17
-
18
- import { resolveFormatWriteCommand } from '../../close-validation.js';
19
-
20
- /**
21
- * Run `git status --porcelain` and return the list of changed paths.
22
- *
23
- * Porcelain lines are `XY <path>` — exactly two status chars, one space,
24
- * then the path. Leading whitespace inside the status pair is significant
25
- * (e.g. ` M file` for unstaged-modified) so we slice a fixed 3 chars off
26
- * the front rather than trimming.
27
- *
28
- * @param {string} cwd
29
- * @param {(args: string[], opts: object) => string} git
30
- * @returns {string[]}
31
- */
32
- export function listDirtyPaths(cwd, git) {
33
- const out = git(['status', '--porcelain'], {
34
- cwd,
35
- encoding: 'utf8',
36
- stdio: ['ignore', 'pipe', 'ignore'],
37
- });
38
- return out
39
- .split('\n')
40
- .filter((line) => line.length >= 4)
41
- .map((line) => line.slice(3));
42
- }
43
-
44
- /**
45
- * Resolve the formatter write command from `project.commands.formatWrite`
46
- * (falling back to the historical `npx biome format --write .`) and split
47
- * it into an executable + argv pair ready for `execFileSync`.
48
- *
49
- * The whole-tree fork runs the command verbatim (keeping the trailing `.`
50
- * so biome formats the entire tree). The scoped fork appends an explicit
51
- * changed-file set, so it passes `dropTrailingDot: true` to strip the `.`
52
- * before its file list.
53
- *
54
- * @param {{
55
- * commands?: object,
56
- * dropTrailingDot?: boolean,
57
- * }} [opts]
58
- * @returns {{ writeCmdString: string, writeCmd: string, writeArgs: string[] }}
59
- */
60
- export function resolveFormatterCmd({
61
- commands,
62
- dropTrailingDot = false,
63
- } = {}) {
64
- // `resolveFormatWriteCommand` reads `config.project.commands`; wrap the
65
- // caller-supplied `commands` map into that canonical shape.
66
- const writeCmdString = resolveFormatWriteCommand({ project: { commands } });
67
- const parts = writeCmdString.split(/\s+/).filter(Boolean);
68
- if (dropTrailingDot && parts[parts.length - 1] === '.') parts.pop();
69
- const [writeCmd, ...writeArgs] = parts;
70
- return { writeCmdString, writeCmd, writeArgs };
71
- }
72
-
73
- /**
74
- * Resolve the branch currently checked out at `cwd` via
75
- * `git rev-parse --abbrev-ref HEAD`. Returns the trimmed branch name, or
76
- * `null` when the call fails or the tree is in a detached-HEAD state
77
- * (`HEAD`). Used as the commit-target guard before
78
- * {@link commitDirtyPaths} writes a scoped-autofix commit, so the commit
79
- * can never land on the wrong branch (e.g. the main checkout's `main`).
80
- *
81
- * @param {string} cwd
82
- * @param {(args: string[], opts: object) => string} git
83
- * @returns {string|null}
84
- */
85
- export function currentBranch(cwd, git) {
86
- try {
87
- const out = git(['rev-parse', '--abbrev-ref', 'HEAD'], {
88
- cwd,
89
- encoding: 'utf8',
90
- stdio: ['ignore', 'pipe', 'ignore'],
91
- });
92
- const branch = (out ?? '').toString().trim();
93
- if (!branch || branch === 'HEAD') return null;
94
- return branch;
95
- } catch {
96
- return null;
97
- }
98
- }
99
-
100
- /**
101
- * Stage every modified path (`git add -u`), commit with the caller-supplied
102
- * `subject`, and return the short HEAD SHA. Hooks must run; we never pass
103
- * `--no-verify` (project policy: never skip git hooks).
104
- *
105
- * @param {{
106
- * cwd: string,
107
- * git: (args: string[], opts: object) => string,
108
- * subject: string,
109
- * }} opts
110
- * @returns {string} short HEAD SHA of the new commit
111
- */
112
- export function commitDirtyPaths({ cwd, git, subject }) {
113
- git(['add', '-u'], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
114
- git(['commit', '-m', subject], {
115
- cwd,
116
- stdio: ['ignore', 'pipe', 'pipe'],
117
- });
118
- return git(['rev-parse', '--short', 'HEAD'], {
119
- cwd,
120
- encoding: 'utf8',
121
- stdio: ['ignore', 'pipe', 'ignore'],
122
- }).trim();
123
- }