mandrel 2.25.0 → 2.26.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/agents/acceptance-critic.md +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `/deliver` story-list path.
|
|
6
6
|
*
|
|
7
7
|
* Thin **adapter** over the path-agnostic ready-set scheduling core
|
|
8
|
-
* (`lib/wave-runner/ready-set.js#
|
|
8
|
+
* (`lib/wave-runner/ready-set.js#planReadySet`). It emits the set of
|
|
9
9
|
* Stories safe to dispatch **on this beat** — a Story becomes dispatchable
|
|
10
10
|
* the instant its own dependencies are done, under the same global
|
|
11
11
|
* concurrency cap and the same file-overlap co-dispatch guard
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* The previous static wave-batch plan (group N must fully drain before
|
|
16
16
|
* group N+1 opens, via `Graph.js#assignLayers`) is gone. The scheduling
|
|
17
17
|
* kernel — adjacency derivation, the done-predicate classifier, the
|
|
18
|
-
* eligibility rule, and the overlap guard — lives once in `
|
|
18
|
+
* eligibility rule, and the overlap guard — lives once in `planReadySet`;
|
|
19
19
|
* this file only gathers input, resolves the cap, and renders the envelope.
|
|
20
20
|
*
|
|
21
21
|
* **Two modes, one kernel.**
|
|
@@ -54,9 +54,22 @@
|
|
|
54
54
|
* concurrencyCap: number,
|
|
55
55
|
* inFlight: number,
|
|
56
56
|
* cycleError: string | null,
|
|
57
|
-
* wedged: { reason, stories: [{ id, unmetBlockers }] } | null
|
|
57
|
+
* wedged: { reason, stories: [{ id, unmetBlockers }] } | null,
|
|
58
|
+
* inFlightReservation: { available, withheld: [{ id, blockedBy, reason }], note }
|
|
58
59
|
* }
|
|
59
60
|
*
|
|
61
|
+
* `inFlightReservation` reports the cross-beat half of the co-dispatch guard
|
|
62
|
+
* (Story #4875 widened the footprint; Story #4950 made it reserve). Under
|
|
63
|
+
* `--probe-live` the in-flight Stories' own records are handed to the kernel,
|
|
64
|
+
* so a candidate sharing a CONCRETE path with a Story dispatched on an EARLIER
|
|
65
|
+
* beat is withheld and named here with its blocker and a `reason`
|
|
66
|
+
* (`in-flight-earlier-beat` or `foreign-lease`). A glob / UNKNOWN footprint
|
|
67
|
+
* reserves nothing across beats — it would withhold the whole run for one
|
|
68
|
+
* blocker's entire implementation window — while still serializing its own
|
|
69
|
+
* beat (Story #4960). Flag mode carries a count and no records, so it reports
|
|
70
|
+
* `available: false` rather than an empty — and therefore indistinguishable —
|
|
71
|
+
* result.
|
|
72
|
+
*
|
|
60
73
|
* Probe mode adds fields the caller can no longer compute for itself:
|
|
61
74
|
* `done: number[]` (the resolved done set, in-set ∪ satisfied foreign
|
|
62
75
|
* blockers), `epilogueDue: boolean` (true exactly when every listed Story
|
|
@@ -104,12 +117,13 @@ import { Logger } from './lib/Logger.js';
|
|
|
104
117
|
import { AGENT_LABELS } from './lib/label-constants.js';
|
|
105
118
|
import { parseIds } from './lib/orchestration/resolve-stories.js';
|
|
106
119
|
import { buildStoryAdjacency } from './lib/story-adjacency.js';
|
|
120
|
+
import { expandIdList } from './lib/util/parse-id-list.js';
|
|
107
121
|
import {
|
|
108
122
|
createProbeContext,
|
|
109
123
|
probeLiveState,
|
|
110
124
|
validateProbeFlags,
|
|
111
125
|
} from './lib/wave-runner/live-probe.js';
|
|
112
|
-
import {
|
|
126
|
+
import { planReadySet } from './lib/wave-runner/ready-set.js';
|
|
113
127
|
|
|
114
128
|
/**
|
|
115
129
|
* Exit code for a wedged run — deliberately distinct from the cycle exit (2)
|
|
@@ -135,7 +149,7 @@ const HELP = `Usage:
|
|
|
135
149
|
Continuous ready-set planner for standalone Story delivery. Emits the set of
|
|
136
150
|
Stories safe to dispatch on this beat — a Story is dispatchable the instant
|
|
137
151
|
its own dependencies are done — plus the resolved per-beat concurrency cap
|
|
138
|
-
and the same file-overlap guard as
|
|
152
|
+
and the same file-overlap guard as planReadySet.
|
|
139
153
|
|
|
140
154
|
Two modes:
|
|
141
155
|
--probe-live Resolve the graph and derive done / in-flight from LIVE state
|
|
@@ -153,9 +167,10 @@ Each entry must include:
|
|
|
153
167
|
dependsOn - Array of Story IDs that must complete before this Story runs
|
|
154
168
|
|
|
155
169
|
Options:
|
|
156
|
-
--stories <csv> Story ids to deliver (probe mode).
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
--stories <csv> Story ids to deliver (probe mode). Singles or inclusive
|
|
171
|
+
A-B ranges (101,104-107). The graph, the done set, and
|
|
172
|
+
the in-flight count are resolved from live state — no
|
|
173
|
+
--done / --in-flight bookkeeping.
|
|
159
174
|
--probe-live Enable probe mode. Requires --stories.
|
|
160
175
|
--dispatched <csv> Probe mode only. Ids you have SPAWNED this run. Unioned
|
|
161
176
|
into the live-derived in-flight set, then filtered by
|
|
@@ -196,9 +211,20 @@ Output envelope:
|
|
|
196
211
|
},
|
|
197
212
|
"inFlight": 0,
|
|
198
213
|
"cycleError": null,
|
|
199
|
-
"wedged": null
|
|
214
|
+
"wedged": null,
|
|
215
|
+
"inFlightReservation": {
|
|
216
|
+
"available": true,
|
|
217
|
+
"withheld": [{ "id": 4951, "blockedBy": 4949 }],
|
|
218
|
+
"note": "..."
|
|
219
|
+
}
|
|
200
220
|
}
|
|
201
221
|
|
|
222
|
+
inFlightReservation names each Story withheld this beat because its file
|
|
223
|
+
footprint overlaps one still IN FLIGHT from an earlier beat, together with the
|
|
224
|
+
blocking id — so an unfilled slot is explained rather than mysterious. It needs
|
|
225
|
+
the in-flight Stories' footprints, which only --probe-live has: under --dag the
|
|
226
|
+
report is { available: false } and selection de-conflicts within the beat only.
|
|
227
|
+
|
|
202
228
|
Exit codes:
|
|
203
229
|
0 - Success, ready set emitted
|
|
204
230
|
1 - Invalid input (missing/malformed DAG, invalid --concurrency/--in-flight/--done)
|
|
@@ -231,12 +257,124 @@ function inputErrorResult(message, concurrencyCap = null, inFlightValue = 0) {
|
|
|
231
257
|
inFlight: inFlightValue,
|
|
232
258
|
cycleError: null,
|
|
233
259
|
wedged: null,
|
|
260
|
+
inFlightReservation: null,
|
|
234
261
|
inputError: message,
|
|
235
262
|
},
|
|
236
263
|
exitCode: 1,
|
|
237
264
|
};
|
|
238
265
|
}
|
|
239
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Why a reservation withheld a Story. Machine-readable companion to the
|
|
269
|
+
* operator-facing `note`, so a consumer never has to parse prose to tell an
|
|
270
|
+
* earlier-beat blocker from a foreign-lease one (Story #4960).
|
|
271
|
+
*/
|
|
272
|
+
const RESERVATION_REASONS = Object.freeze({
|
|
273
|
+
EARLIER_BEAT: 'in-flight-earlier-beat',
|
|
274
|
+
FOREIGN_LEASE: 'foreign-lease',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Describe this beat's in-flight footprint reservation for the envelope
|
|
279
|
+
* (Story #4950).
|
|
280
|
+
*
|
|
281
|
+
* The reservation needs the in-flight Stories' **records** — their footprints
|
|
282
|
+
* — not just how many there are, so its availability is a property of the
|
|
283
|
+
* mode rather than of the run:
|
|
284
|
+
*
|
|
285
|
+
* - **Probe mode** hands over the records `live-probe.js` already fetched,
|
|
286
|
+
* so `inFlightRecords` is an array (possibly empty) and reservation is
|
|
287
|
+
* `available: true`.
|
|
288
|
+
* - **Flag mode** (`--dag` + `--in-flight <n>`) supplies a graph and a
|
|
289
|
+
* count; no node carries a label, so nothing there can even classify as
|
|
290
|
+
* in-flight. There is no footprint to reserve against, and selection is
|
|
291
|
+
* unchanged from before #4950. Saying so explicitly is the point: a
|
|
292
|
+
* silently-absent guard reads exactly like a guard that found nothing.
|
|
293
|
+
*
|
|
294
|
+
* A reservation is held either by a Story **this run** dispatched on an
|
|
295
|
+
* earlier beat or by one **another operator's lease** holds — `live-probe.js`
|
|
296
|
+
* folds both into the in-flight set, and they reserve identically but read
|
|
297
|
+
* very differently to an operator. Reporting a foreign-held peer as "still in
|
|
298
|
+
* flight from an earlier beat" is simply false: this run never dispatched it
|
|
299
|
+
* and no later beat of this run will clear it. `foreignHeldIds` splits the two
|
|
300
|
+
* so each carries its own reason (Story #4960).
|
|
301
|
+
*
|
|
302
|
+
* @param {object[]|null|undefined} inFlightRecords
|
|
303
|
+
* @param {Array<{id: number, blockedBy: number}>} withheld
|
|
304
|
+
* @param {Iterable<number>} [foreignHeldIds] Ids held by a foreign lease.
|
|
305
|
+
* @returns {{ available: boolean, withheld: Array<{id: number, blockedBy: number, reason: string}>, note: string|null }}
|
|
306
|
+
*/
|
|
307
|
+
export function buildReservationReport(
|
|
308
|
+
inFlightRecords,
|
|
309
|
+
withheld,
|
|
310
|
+
foreignHeldIds = [],
|
|
311
|
+
) {
|
|
312
|
+
if (!Array.isArray(inFlightRecords)) {
|
|
313
|
+
return {
|
|
314
|
+
available: false,
|
|
315
|
+
withheld: [],
|
|
316
|
+
note:
|
|
317
|
+
'In-flight footprint reservation is UNAVAILABLE this beat: flag mode ' +
|
|
318
|
+
'supplies a dependency graph and an --in-flight count, never the ' +
|
|
319
|
+
'in-flight Stories themselves, so there are no footprints to reserve ' +
|
|
320
|
+
'against. Selection is unchanged (same-beat de-confliction only). ' +
|
|
321
|
+
'Use --probe-live to reserve in-flight footprints.',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (withheld.length === 0) {
|
|
325
|
+
return { available: true, withheld: [], note: null };
|
|
326
|
+
}
|
|
327
|
+
const foreign = new Set(foreignHeldIds);
|
|
328
|
+
const classified = withheld.map((w) => ({
|
|
329
|
+
...w,
|
|
330
|
+
reason: foreign.has(w.blockedBy)
|
|
331
|
+
? RESERVATION_REASONS.FOREIGN_LEASE
|
|
332
|
+
: RESERVATION_REASONS.EARLIER_BEAT,
|
|
333
|
+
}));
|
|
334
|
+
return {
|
|
335
|
+
available: true,
|
|
336
|
+
withheld: classified,
|
|
337
|
+
note: reservationNote(classified),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Render the operator-facing reservation note, one sentence per reason class
|
|
343
|
+
* present. Neither class is a failure or a wedge, but they clear by different
|
|
344
|
+
* events, so each names its own.
|
|
345
|
+
*
|
|
346
|
+
* @param {Array<{id: number, blockedBy: number, reason: string}>} withheld
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
function reservationNote(withheld) {
|
|
350
|
+
const detail = (entries) =>
|
|
351
|
+
entries.map((w) => `#${w.id} ← #${w.blockedBy}`).join('; ');
|
|
352
|
+
const byBeat = withheld.filter(
|
|
353
|
+
(w) => w.reason === RESERVATION_REASONS.EARLIER_BEAT,
|
|
354
|
+
);
|
|
355
|
+
const byLease = withheld.filter(
|
|
356
|
+
(w) => w.reason === RESERVATION_REASONS.FOREIGN_LEASE,
|
|
357
|
+
);
|
|
358
|
+
const parts = [];
|
|
359
|
+
if (byBeat.length > 0) {
|
|
360
|
+
parts.push(
|
|
361
|
+
`${byBeat.length} Story(ies) withheld because their file footprint ` +
|
|
362
|
+
`overlaps a Story still in flight from an earlier beat — ${detail(byBeat)}. ` +
|
|
363
|
+
`Each re-admits automatically on a later beat, once the Story ` +
|
|
364
|
+
`reserving its files leaves the in-flight set.`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (byLease.length > 0) {
|
|
368
|
+
parts.push(
|
|
369
|
+
`${byLease.length} Story(ies) withheld because their file footprint ` +
|
|
370
|
+
`overlaps a Story another operator's lease holds — ${detail(byLease)}. ` +
|
|
371
|
+
`No beat of THIS run clears that: the peer is the holder's work, and ` +
|
|
372
|
+
`each re-admits once their lease clears (see foreignHeldReason).`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return `${parts.join(' ')} Neither is a wedge and neither is a failure.`;
|
|
376
|
+
}
|
|
377
|
+
|
|
240
378
|
/**
|
|
241
379
|
* Parse and validate the raw DAG input array.
|
|
242
380
|
*
|
|
@@ -308,33 +446,22 @@ export function parseDag(raw) {
|
|
|
308
446
|
}
|
|
309
447
|
|
|
310
448
|
/**
|
|
311
|
-
* Parse a comma-separated list of Story IDs
|
|
312
|
-
* integers. Empty / absent input yields an
|
|
313
|
-
* is not a positive integer
|
|
314
|
-
*
|
|
449
|
+
* Parse a comma-separated list of Story IDs — singles or `A-B` dash ranges —
|
|
450
|
+
* into a deduped set of positive integers. Empty / absent input yields an
|
|
451
|
+
* empty set. Rejects any token that is not a positive integer or a valid
|
|
452
|
+
* range, so a typo never silently drops a dependency gate (`--done`) or a
|
|
453
|
+
* held dispatch slot (`--dispatched`).
|
|
454
|
+
*
|
|
455
|
+
* Ranges are accepted here for the same reason `--stories` accepts them: an
|
|
456
|
+
* operator delivering `4922-4926` writes the dispatched set back the same way.
|
|
315
457
|
*
|
|
316
458
|
* @param {string|undefined} raw
|
|
317
459
|
* @param {string} flag Flag name, for the error message.
|
|
318
460
|
* @returns {{ ids: Set<number>|null, error: string|null }}
|
|
319
461
|
*/
|
|
320
462
|
export function parseIdCsv(raw, flag) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const ids = new Set();
|
|
325
|
-
for (const token of String(raw).split(',')) {
|
|
326
|
-
const trimmed = token.trim();
|
|
327
|
-
if (trimmed === '') continue;
|
|
328
|
-
const num = Number(trimmed);
|
|
329
|
-
if (!Number.isInteger(num) || num <= 0) {
|
|
330
|
-
return {
|
|
331
|
-
ids: null,
|
|
332
|
-
error: `${flag} must be a comma-separated list of positive integers, got "${trimmed}"`,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
ids.add(num);
|
|
336
|
-
}
|
|
337
|
-
return { ids, error: null };
|
|
463
|
+
const { ids, error } = expandIdList(raw, { flag });
|
|
464
|
+
return error ? { ids: null, error } : { ids: new Set(ids), error: null };
|
|
338
465
|
}
|
|
339
466
|
|
|
340
467
|
/**
|
|
@@ -480,7 +607,7 @@ export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
|
480
607
|
* understands (`{ id, dependsOn }`), tags any node already in the done set
|
|
481
608
|
* as `agent::done` so the core's classifier excludes it from the dispatch
|
|
482
609
|
* set **and** counts it as a satisfied dependency, then delegates the
|
|
483
|
-
* scheduling decision to `
|
|
610
|
+
* scheduling decision to `planReadySet`. A cyclic operator DAG is a
|
|
484
611
|
* planning error (the core would silently never schedule the cycle), so we
|
|
485
612
|
* detect it up front via the shared `detectCycle` kernel and short-circuit
|
|
486
613
|
* with a `cycleError` and exit code 2.
|
|
@@ -492,6 +619,14 @@ export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
|
492
619
|
* record explaining which source set the cap.
|
|
493
620
|
* @param {Set<number>} [args.doneIds] Story IDs already completed this run.
|
|
494
621
|
* @param {number} [args.inFlight] Stories already occupying a slot.
|
|
622
|
+
* @param {object[]|null} [args.inFlightRecords] Records for the Stories
|
|
623
|
+
* already in flight, so the kernel reserves their footprints instead of
|
|
624
|
+
* merely counting them (Story #4950). `null` (flag mode) means reservation
|
|
625
|
+
* is structurally unavailable — see {@link buildReservationReport}.
|
|
626
|
+
* @param {number[]} [args.foreignHeldIds] Ids among `inFlightRecords` that a
|
|
627
|
+
* **foreign operator's lease** holds rather than this run's own earlier
|
|
628
|
+
* beat, so a withholding against one is reported for what it is
|
|
629
|
+
* (Story #4960). Flag mode has no lease view and passes none.
|
|
495
630
|
* @returns {{
|
|
496
631
|
* envelope: {
|
|
497
632
|
* kind: 'stories-ready-set',
|
|
@@ -506,7 +641,14 @@ export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
|
506
641
|
*/
|
|
507
642
|
export function buildReadySetEnvelope(
|
|
508
643
|
nodes,
|
|
509
|
-
{
|
|
644
|
+
{
|
|
645
|
+
concurrencyCap,
|
|
646
|
+
capPrecedence = null,
|
|
647
|
+
doneIds = new Set(),
|
|
648
|
+
inFlight = 0,
|
|
649
|
+
inFlightRecords = null,
|
|
650
|
+
foreignHeldIds = [],
|
|
651
|
+
},
|
|
510
652
|
) {
|
|
511
653
|
const totalStories = nodes.length;
|
|
512
654
|
|
|
@@ -522,6 +664,10 @@ export function buildReadySetEnvelope(
|
|
|
522
664
|
inFlight,
|
|
523
665
|
cycleError: null,
|
|
524
666
|
wedged: null,
|
|
667
|
+
// Whether this beat could reserve the in-flight Stories' footprints, and
|
|
668
|
+
// which Stories a reservation withheld (Story #4950). Never omitted on a
|
|
669
|
+
// resolved beat: an absent report reads exactly like an empty one.
|
|
670
|
+
inFlightReservation: buildReservationReport(inFlightRecords, []),
|
|
525
671
|
};
|
|
526
672
|
|
|
527
673
|
if (totalStories === 0) {
|
|
@@ -531,7 +677,7 @@ export function buildReadySetEnvelope(
|
|
|
531
677
|
// Cycle detection before scheduling — a cycle is a planning error the
|
|
532
678
|
// operator must fix. dropForeign:false preserves the operator-DAG contract
|
|
533
679
|
// (a dependency on an id outside the supplied set is honored, not pruned),
|
|
534
|
-
// matching the same builder seam
|
|
680
|
+
// matching the same builder seam planReadySet uses internally.
|
|
535
681
|
const adjacency = buildStoryAdjacency(nodes, { dropForeign: false });
|
|
536
682
|
const cycle = detectCycle(adjacency);
|
|
537
683
|
if (cycle) {
|
|
@@ -568,12 +714,22 @@ export function buildReadySetEnvelope(
|
|
|
568
714
|
return rec;
|
|
569
715
|
});
|
|
570
716
|
|
|
571
|
-
const
|
|
717
|
+
const { selected, withheldByInFlight } = planReadySet({
|
|
572
718
|
stories: records,
|
|
573
719
|
doneIds,
|
|
574
720
|
inFlight,
|
|
575
721
|
globalCap: concurrencyCap,
|
|
576
|
-
|
|
722
|
+
// Flag mode has no in-flight records at all; `?? []` keeps the kernel's
|
|
723
|
+
// contract (an array) while `base.inFlightReservation` reports that the
|
|
724
|
+
// reservation itself was unavailable rather than merely empty.
|
|
725
|
+
inFlightRecords: inFlightRecords ?? [],
|
|
726
|
+
});
|
|
727
|
+
const ready = selected.map((rec) => rec.id);
|
|
728
|
+
const reservation = buildReservationReport(
|
|
729
|
+
inFlightRecords,
|
|
730
|
+
withheldByInFlight,
|
|
731
|
+
foreignHeldIds,
|
|
732
|
+
);
|
|
577
733
|
|
|
578
734
|
// Wedge detection (Story #4540). `ready: []` is normal while work is in
|
|
579
735
|
// flight — the loop is simply waiting. But ready-empty AND nothing in
|
|
@@ -589,12 +745,25 @@ export function buildReadySetEnvelope(
|
|
|
589
745
|
const wedge = detectWedge({ nodes, doneIds, ready, inFlight });
|
|
590
746
|
if (wedge) {
|
|
591
747
|
return {
|
|
592
|
-
envelope: {
|
|
748
|
+
envelope: {
|
|
749
|
+
...base,
|
|
750
|
+
ready,
|
|
751
|
+
wedged: wedge,
|
|
752
|
+
inFlightReservation: reservation,
|
|
753
|
+
},
|
|
593
754
|
exitCode: WEDGED_EXIT_CODE,
|
|
594
755
|
};
|
|
595
756
|
}
|
|
596
757
|
|
|
597
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
envelope: {
|
|
760
|
+
...base,
|
|
761
|
+
ready,
|
|
762
|
+
wedged: null,
|
|
763
|
+
inFlightReservation: reservation,
|
|
764
|
+
},
|
|
765
|
+
exitCode: 0,
|
|
766
|
+
};
|
|
598
767
|
}
|
|
599
768
|
|
|
600
769
|
/**
|
|
@@ -775,7 +944,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
775
944
|
|
|
776
945
|
let ids;
|
|
777
946
|
try {
|
|
778
|
-
ids = parseIds(stories);
|
|
947
|
+
ids = parseIds(stories, '--stories');
|
|
779
948
|
} catch (err) {
|
|
780
949
|
return inputErrorResult(err.message);
|
|
781
950
|
}
|
|
@@ -819,12 +988,19 @@ export async function runProbedStoriesWaveTick({
|
|
|
819
988
|
inFlight,
|
|
820
989
|
blockedIds = [],
|
|
821
990
|
foreignHeld = [],
|
|
991
|
+
inFlightRecords = [],
|
|
822
992
|
} = probed;
|
|
823
993
|
const { envelope, exitCode } = buildReadySetEnvelope(nodes, {
|
|
824
994
|
concurrencyCap,
|
|
825
995
|
capPrecedence,
|
|
826
996
|
doneIds,
|
|
827
997
|
inFlight,
|
|
998
|
+
// Probe mode is the only mode that HAS the in-flight Stories' records, so
|
|
999
|
+
// it is the only mode that can reserve their footprints (Story #4950).
|
|
1000
|
+
inFlightRecords,
|
|
1001
|
+
// ...and the only mode that can tell a foreign lease-holder apart from
|
|
1002
|
+
// this run's own earlier-beat dispatch (Story #4960).
|
|
1003
|
+
foreignHeldIds: foreignHeld.map((h) => h.id),
|
|
828
1004
|
});
|
|
829
1005
|
|
|
830
1006
|
const done = [...doneIds].sort((a, b) => a - b);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// cli-opt-out: top-level main()-driven CLI invoked via npm run coverage:update; no runAsCli() wrapper required.
|
|
3
2
|
/**
|
|
4
3
|
* Refresh `baselines/coverage.json` from the most recent
|
|
5
4
|
* `coverage/coverage-final.json`. Run this when you intentionally add,
|
|
@@ -22,6 +21,7 @@ import { createRequire } from 'node:module';
|
|
|
22
21
|
import path from 'node:path';
|
|
23
22
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
24
23
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
24
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
25
25
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
26
26
|
import {
|
|
27
27
|
buildScopePredicate,
|
|
@@ -31,6 +31,32 @@ import {
|
|
|
31
31
|
} from './lib/coverage-baseline.js';
|
|
32
32
|
import { Logger } from './lib/Logger.js';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Usage block for `--help`. This CLI *writes* on invocation, so the help
|
|
36
|
+
* branch must short-circuit before `main` runs rather than inside it —
|
|
37
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
38
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
39
|
+
*/
|
|
40
|
+
const USAGE = {
|
|
41
|
+
invocation:
|
|
42
|
+
'node .agents/scripts/update-coverage-baseline.js [--full-scope | --diff-scope <ref>]',
|
|
43
|
+
summary:
|
|
44
|
+
'Score → write the coverage baseline from the coverage-final.json already on disk. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
45
|
+
flags: [
|
|
46
|
+
[
|
|
47
|
+
'--full-scope',
|
|
48
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
'--diff-scope <ref>',
|
|
52
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
notes: [
|
|
56
|
+
'Run `npm run test:coverage` first — this script never runs the suite itself.',
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
34
60
|
const require = createRequire(import.meta.url);
|
|
35
61
|
|
|
36
62
|
function loadC8Scope(cwd) {
|
|
@@ -123,7 +149,11 @@ function main() {
|
|
|
123
149
|
});
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
runAsCli(import.meta.url, main, {
|
|
153
|
+
source: 'coverage-baseline',
|
|
154
|
+
usage: USAGE,
|
|
155
|
+
onError: (err) => {
|
|
156
|
+
Logger.error(`[Coverage] ❌ Fatal error: ${err?.message ?? err}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
},
|
|
129
159
|
});
|