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.
- package/.agents/README.md +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +241 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +16 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +21 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- 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
|
-
}
|