mandrel 2.24.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 +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -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 +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- 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-context-budget.js +95 -14
- 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 +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -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-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -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 +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- 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 -7
- 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/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- 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/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -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/acceptance-eval-decision.js +110 -17
- 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/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- 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/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- 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 +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- 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/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- 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/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- 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/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- 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/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- 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/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -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 +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- 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/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- 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 +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- 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
|
|
@@ -75,7 +88,9 @@
|
|
|
75
88
|
* `/deliver` uses — `resolveConfig` + `getRunners` reading
|
|
76
89
|
* `delivery.deliverRunner.concurrencyCap` (default 3) — so a
|
|
77
90
|
* `.agentrc.local.json` override is honored. A `--concurrency <n>` CLI flag
|
|
78
|
-
* overrides the config-resolved value for that run only
|
|
91
|
+
* overrides the config-resolved value for that run only, and the envelope's
|
|
92
|
+
* `capPrecedence` names which source won so the override is never silent
|
|
93
|
+
* (Story #4875). This shares one
|
|
79
94
|
* deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
|
|
80
95
|
* one scheduling kernel with every `/deliver` multi-Story invocation.
|
|
81
96
|
*
|
|
@@ -102,12 +117,13 @@ import { Logger } from './lib/Logger.js';
|
|
|
102
117
|
import { AGENT_LABELS } from './lib/label-constants.js';
|
|
103
118
|
import { parseIds } from './lib/orchestration/resolve-stories.js';
|
|
104
119
|
import { buildStoryAdjacency } from './lib/story-adjacency.js';
|
|
120
|
+
import { expandIdList } from './lib/util/parse-id-list.js';
|
|
105
121
|
import {
|
|
106
122
|
createProbeContext,
|
|
107
123
|
probeLiveState,
|
|
108
124
|
validateProbeFlags,
|
|
109
125
|
} from './lib/wave-runner/live-probe.js';
|
|
110
|
-
import {
|
|
126
|
+
import { planReadySet } from './lib/wave-runner/ready-set.js';
|
|
111
127
|
|
|
112
128
|
/**
|
|
113
129
|
* Exit code for a wedged run — deliberately distinct from the cycle exit (2)
|
|
@@ -133,7 +149,7 @@ const HELP = `Usage:
|
|
|
133
149
|
Continuous ready-set planner for standalone Story delivery. Emits the set of
|
|
134
150
|
Stories safe to dispatch on this beat — a Story is dispatchable the instant
|
|
135
151
|
its own dependencies are done — plus the resolved per-beat concurrency cap
|
|
136
|
-
and the same file-overlap guard as
|
|
152
|
+
and the same file-overlap guard as planReadySet.
|
|
137
153
|
|
|
138
154
|
Two modes:
|
|
139
155
|
--probe-live Resolve the graph and derive done / in-flight from LIVE state
|
|
@@ -151,9 +167,10 @@ Each entry must include:
|
|
|
151
167
|
dependsOn - Array of Story IDs that must complete before this Story runs
|
|
152
168
|
|
|
153
169
|
Options:
|
|
154
|
-
--stories <csv> Story ids to deliver (probe mode).
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
157
174
|
--probe-live Enable probe mode. Requires --stories.
|
|
158
175
|
--dispatched <csv> Probe mode only. Ids you have SPAWNED this run. Unioned
|
|
159
176
|
into the live-derived in-flight set, then filtered by
|
|
@@ -167,7 +184,10 @@ Options:
|
|
|
167
184
|
--concurrency <n> Override the per-beat concurrency cap for this run only.
|
|
168
185
|
Must be a positive integer. When omitted, the cap is
|
|
169
186
|
resolved from delivery.deliverRunner.concurrencyCap in
|
|
170
|
-
.agentrc.json / .agentrc.local.json (default 3).
|
|
187
|
+
.agentrc.json / .agentrc.local.json (default 3). The flag
|
|
188
|
+
WINS over the configured value, and the envelope's
|
|
189
|
+
capPrecedence records that it did — including when the
|
|
190
|
+
request exceeds the configured cap.
|
|
171
191
|
--done <csv> Comma-separated Story IDs already completed this run.
|
|
172
192
|
Their dependents become eligible; they are never
|
|
173
193
|
re-dispatched. Defaults to empty.
|
|
@@ -181,11 +201,30 @@ Output envelope:
|
|
|
181
201
|
"ready": [101],
|
|
182
202
|
"totalStories": 2,
|
|
183
203
|
"concurrencyCap": 3,
|
|
204
|
+
"capPrecedence": {
|
|
205
|
+
"cap": 3,
|
|
206
|
+
"source": "config",
|
|
207
|
+
"configuredCap": 3,
|
|
208
|
+
"requestedCap": null,
|
|
209
|
+
"exceedsConfigured": false,
|
|
210
|
+
"note": "..."
|
|
211
|
+
},
|
|
184
212
|
"inFlight": 0,
|
|
185
213
|
"cycleError": null,
|
|
186
|
-
"wedged": null
|
|
214
|
+
"wedged": null,
|
|
215
|
+
"inFlightReservation": {
|
|
216
|
+
"available": true,
|
|
217
|
+
"withheld": [{ "id": 4951, "blockedBy": 4949 }],
|
|
218
|
+
"note": "..."
|
|
219
|
+
}
|
|
187
220
|
}
|
|
188
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
|
+
|
|
189
228
|
Exit codes:
|
|
190
229
|
0 - Success, ready set emitted
|
|
191
230
|
1 - Invalid input (missing/malformed DAG, invalid --concurrency/--in-flight/--done)
|
|
@@ -214,15 +253,128 @@ function inputErrorResult(message, concurrencyCap = null, inFlightValue = 0) {
|
|
|
214
253
|
ready: [],
|
|
215
254
|
totalStories: 0,
|
|
216
255
|
concurrencyCap,
|
|
256
|
+
capPrecedence: null,
|
|
217
257
|
inFlight: inFlightValue,
|
|
218
258
|
cycleError: null,
|
|
219
259
|
wedged: null,
|
|
260
|
+
inFlightReservation: null,
|
|
220
261
|
inputError: message,
|
|
221
262
|
},
|
|
222
263
|
exitCode: 1,
|
|
223
264
|
};
|
|
224
265
|
}
|
|
225
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
|
+
|
|
226
378
|
/**
|
|
227
379
|
* Parse and validate the raw DAG input array.
|
|
228
380
|
*
|
|
@@ -294,33 +446,22 @@ export function parseDag(raw) {
|
|
|
294
446
|
}
|
|
295
447
|
|
|
296
448
|
/**
|
|
297
|
-
* Parse a comma-separated list of Story IDs
|
|
298
|
-
* integers. Empty / absent input yields an
|
|
299
|
-
* is not a positive integer
|
|
300
|
-
*
|
|
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.
|
|
301
457
|
*
|
|
302
458
|
* @param {string|undefined} raw
|
|
303
459
|
* @param {string} flag Flag name, for the error message.
|
|
304
460
|
* @returns {{ ids: Set<number>|null, error: string|null }}
|
|
305
461
|
*/
|
|
306
462
|
export function parseIdCsv(raw, flag) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
const ids = new Set();
|
|
311
|
-
for (const token of String(raw).split(',')) {
|
|
312
|
-
const trimmed = token.trim();
|
|
313
|
-
if (trimmed === '') continue;
|
|
314
|
-
const num = Number(trimmed);
|
|
315
|
-
if (!Number.isInteger(num) || num <= 0) {
|
|
316
|
-
return {
|
|
317
|
-
ids: null,
|
|
318
|
-
error: `${flag} must be a comma-separated list of positive integers, got "${trimmed}"`,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
ids.add(num);
|
|
322
|
-
}
|
|
323
|
-
return { ids, error: null };
|
|
463
|
+
const { ids, error } = expandIdList(raw, { flag });
|
|
464
|
+
return error ? { ids: null, error } : { ids: new Set(ids), error: null };
|
|
324
465
|
}
|
|
325
466
|
|
|
326
467
|
/**
|
|
@@ -394,13 +535,69 @@ export function parseConcurrencyOverride(raw) {
|
|
|
394
535
|
* `--concurrency`; wins over config.
|
|
395
536
|
* @returns {number} The resolved positive-integer concurrency cap.
|
|
396
537
|
*/
|
|
397
|
-
export function resolveConcurrencyCap(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
538
|
+
export function resolveConcurrencyCap(opts = {}) {
|
|
539
|
+
return resolveCapPrecedence(opts).cap;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Resolve the per-beat cap **and the precedence that produced it** (Story
|
|
544
|
+
* #4875).
|
|
545
|
+
*
|
|
546
|
+
* `--concurrency` wins over `delivery.deliverRunner.concurrencyCap`, and that
|
|
547
|
+
* is the intended contract — a flag an operator typed for one run should not be
|
|
548
|
+
* outranked by a checked-in default. What was wrong is that it won *silently*:
|
|
549
|
+
* the envelope reported a single `concurrencyCap` number with no record of
|
|
550
|
+
* which source set it, so a run at 8 when the project configured 3 was
|
|
551
|
+
* indistinguishable from a project configured at 8. A reader could not tell an
|
|
552
|
+
* override from a default, and an override that ran the repo above its own
|
|
553
|
+
* configured ceiling left no trace at all.
|
|
554
|
+
*
|
|
555
|
+
* So the flag still wins, but never quietly: the source is named, the
|
|
556
|
+
* configured value is carried alongside the requested one, and a request that
|
|
557
|
+
* exceeds the configured cap is called out as such. Reporting rather than
|
|
558
|
+
* refusing is deliberate — the configured cap is a project default, not a
|
|
559
|
+
* safety limit, and refusing a deliberate operator escalation would trade a
|
|
560
|
+
* silent override for a silent stall.
|
|
561
|
+
*
|
|
562
|
+
* @param {object} [opts]
|
|
563
|
+
* @param {string} [opts.cwd] Repo root for config resolution.
|
|
564
|
+
* @param {object} [opts.config] Pre-resolved config (test injection).
|
|
565
|
+
* @param {number} [opts.override] Validated positive integer from
|
|
566
|
+
* `--concurrency`.
|
|
567
|
+
* @returns {{
|
|
568
|
+
* cap: number,
|
|
569
|
+
* source: 'flag'|'config',
|
|
570
|
+
* configuredCap: number,
|
|
571
|
+
* requestedCap: number|null,
|
|
572
|
+
* exceedsConfigured: boolean,
|
|
573
|
+
* note: string,
|
|
574
|
+
* }}
|
|
575
|
+
*/
|
|
576
|
+
export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
401
577
|
const resolved = config ?? resolveConfig({ cwd });
|
|
402
578
|
const { deliverRunner } = getRunners(resolved);
|
|
403
|
-
|
|
579
|
+
const configuredCap = deliverRunner.concurrencyCap;
|
|
580
|
+
if (override == null) {
|
|
581
|
+
return {
|
|
582
|
+
cap: configuredCap,
|
|
583
|
+
source: 'config',
|
|
584
|
+
configuredCap,
|
|
585
|
+
requestedCap: null,
|
|
586
|
+
exceedsConfigured: false,
|
|
587
|
+
note: `cap ${configuredCap} from delivery.deliverRunner.concurrencyCap (no --concurrency given)`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const exceedsConfigured = override > configuredCap;
|
|
591
|
+
return {
|
|
592
|
+
cap: override,
|
|
593
|
+
source: 'flag',
|
|
594
|
+
configuredCap,
|
|
595
|
+
requestedCap: override,
|
|
596
|
+
exceedsConfigured,
|
|
597
|
+
note: exceedsConfigured
|
|
598
|
+
? `cap ${override} from --concurrency, which OVERRIDES and EXCEEDS the configured delivery.deliverRunner.concurrencyCap ${configuredCap} — this run is deliberately above the project default`
|
|
599
|
+
: `cap ${override} from --concurrency, which overrides the configured delivery.deliverRunner.concurrencyCap ${configuredCap}`,
|
|
600
|
+
};
|
|
404
601
|
}
|
|
405
602
|
|
|
406
603
|
/**
|
|
@@ -410,7 +607,7 @@ export function resolveConcurrencyCap({ cwd, config, override } = {}) {
|
|
|
410
607
|
* understands (`{ id, dependsOn }`), tags any node already in the done set
|
|
411
608
|
* as `agent::done` so the core's classifier excludes it from the dispatch
|
|
412
609
|
* set **and** counts it as a satisfied dependency, then delegates the
|
|
413
|
-
* scheduling decision to `
|
|
610
|
+
* scheduling decision to `planReadySet`. A cyclic operator DAG is a
|
|
414
611
|
* planning error (the core would silently never schedule the cycle), so we
|
|
415
612
|
* detect it up front via the shared `detectCycle` kernel and short-circuit
|
|
416
613
|
* with a `cycleError` and exit code 2.
|
|
@@ -418,8 +615,18 @@ export function resolveConcurrencyCap({ cwd, config, override } = {}) {
|
|
|
418
615
|
* @param {Array<{id: number, dependsOn: number[]}>} nodes
|
|
419
616
|
* @param {object} args
|
|
420
617
|
* @param {number} args.concurrencyCap Resolved per-beat concurrency cap.
|
|
618
|
+
* @param {object|null} [args.capPrecedence] The {@link resolveCapPrecedence}
|
|
619
|
+
* record explaining which source set the cap.
|
|
421
620
|
* @param {Set<number>} [args.doneIds] Story IDs already completed this run.
|
|
422
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.
|
|
423
630
|
* @returns {{
|
|
424
631
|
* envelope: {
|
|
425
632
|
* kind: 'stories-ready-set',
|
|
@@ -434,7 +641,14 @@ export function resolveConcurrencyCap({ cwd, config, override } = {}) {
|
|
|
434
641
|
*/
|
|
435
642
|
export function buildReadySetEnvelope(
|
|
436
643
|
nodes,
|
|
437
|
-
{
|
|
644
|
+
{
|
|
645
|
+
concurrencyCap,
|
|
646
|
+
capPrecedence = null,
|
|
647
|
+
doneIds = new Set(),
|
|
648
|
+
inFlight = 0,
|
|
649
|
+
inFlightRecords = null,
|
|
650
|
+
foreignHeldIds = [],
|
|
651
|
+
},
|
|
438
652
|
) {
|
|
439
653
|
const totalStories = nodes.length;
|
|
440
654
|
|
|
@@ -443,9 +657,17 @@ export function buildReadySetEnvelope(
|
|
|
443
657
|
ready: [],
|
|
444
658
|
totalStories,
|
|
445
659
|
concurrencyCap,
|
|
660
|
+
// Which source set `concurrencyCap`, and whether it outranks the project's
|
|
661
|
+
// configured value (Story #4875). Never omitted on a resolved beat: a
|
|
662
|
+
// missing precedence record is what made a silent override possible.
|
|
663
|
+
capPrecedence,
|
|
446
664
|
inFlight,
|
|
447
665
|
cycleError: null,
|
|
448
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, []),
|
|
449
671
|
};
|
|
450
672
|
|
|
451
673
|
if (totalStories === 0) {
|
|
@@ -455,7 +677,7 @@ export function buildReadySetEnvelope(
|
|
|
455
677
|
// Cycle detection before scheduling — a cycle is a planning error the
|
|
456
678
|
// operator must fix. dropForeign:false preserves the operator-DAG contract
|
|
457
679
|
// (a dependency on an id outside the supplied set is honored, not pruned),
|
|
458
|
-
// matching the same builder seam
|
|
680
|
+
// matching the same builder seam planReadySet uses internally.
|
|
459
681
|
const adjacency = buildStoryAdjacency(nodes, { dropForeign: false });
|
|
460
682
|
const cycle = detectCycle(adjacency);
|
|
461
683
|
if (cycle) {
|
|
@@ -484,15 +706,30 @@ export function buildReadySetEnvelope(
|
|
|
484
706
|
labels: doneIds.has(node.id) ? [AGENT_LABELS.DONE] : (node.labels ?? []),
|
|
485
707
|
};
|
|
486
708
|
if (node.files !== undefined) rec.files = node.files;
|
|
709
|
+
// Probe-mode nodes carry the Story body so the overlap guard can widen a
|
|
710
|
+
// declared footprint from the paths the Story's own text names (Story
|
|
711
|
+
// #4875). Flag-mode nodes carry none — `parseDag` accepts no body — so
|
|
712
|
+
// this is inert there and the legacy contract is unchanged.
|
|
713
|
+
if (typeof node.body === 'string') rec.body = node.body;
|
|
487
714
|
return rec;
|
|
488
715
|
});
|
|
489
716
|
|
|
490
|
-
const
|
|
717
|
+
const { selected, withheldByInFlight } = planReadySet({
|
|
491
718
|
stories: records,
|
|
492
719
|
doneIds,
|
|
493
720
|
inFlight,
|
|
494
721
|
globalCap: concurrencyCap,
|
|
495
|
-
|
|
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
|
+
);
|
|
496
733
|
|
|
497
734
|
// Wedge detection (Story #4540). `ready: []` is normal while work is in
|
|
498
735
|
// flight — the loop is simply waiting. But ready-empty AND nothing in
|
|
@@ -508,12 +745,25 @@ export function buildReadySetEnvelope(
|
|
|
508
745
|
const wedge = detectWedge({ nodes, doneIds, ready, inFlight });
|
|
509
746
|
if (wedge) {
|
|
510
747
|
return {
|
|
511
|
-
envelope: {
|
|
748
|
+
envelope: {
|
|
749
|
+
...base,
|
|
750
|
+
ready,
|
|
751
|
+
wedged: wedge,
|
|
752
|
+
inFlightReservation: reservation,
|
|
753
|
+
},
|
|
512
754
|
exitCode: WEDGED_EXIT_CODE,
|
|
513
755
|
};
|
|
514
756
|
}
|
|
515
757
|
|
|
516
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
envelope: {
|
|
760
|
+
...base,
|
|
761
|
+
ready,
|
|
762
|
+
wedged: null,
|
|
763
|
+
inFlightReservation: reservation,
|
|
764
|
+
},
|
|
765
|
+
exitCode: 0,
|
|
766
|
+
};
|
|
517
767
|
}
|
|
518
768
|
|
|
519
769
|
/**
|
|
@@ -600,7 +850,8 @@ export function runStoriesWaveTick({
|
|
|
600
850
|
return inputErrorResult(doneError, null, inFlightValue);
|
|
601
851
|
}
|
|
602
852
|
|
|
603
|
-
const
|
|
853
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
854
|
+
const concurrencyCap = capPrecedence.cap;
|
|
604
855
|
|
|
605
856
|
let rawJson;
|
|
606
857
|
|
|
@@ -642,6 +893,7 @@ export function runStoriesWaveTick({
|
|
|
642
893
|
|
|
643
894
|
return buildReadySetEnvelope(nodes, {
|
|
644
895
|
concurrencyCap,
|
|
896
|
+
capPrecedence,
|
|
645
897
|
doneIds,
|
|
646
898
|
inFlight: inFlightValue,
|
|
647
899
|
});
|
|
@@ -692,7 +944,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
692
944
|
|
|
693
945
|
let ids;
|
|
694
946
|
try {
|
|
695
|
-
ids = parseIds(stories);
|
|
947
|
+
ids = parseIds(stories, '--stories');
|
|
696
948
|
} catch (err) {
|
|
697
949
|
return inputErrorResult(err.message);
|
|
698
950
|
}
|
|
@@ -705,7 +957,8 @@ export async function runProbedStoriesWaveTick({
|
|
|
705
957
|
return inputErrorResult(dispatchedError);
|
|
706
958
|
}
|
|
707
959
|
|
|
708
|
-
const
|
|
960
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
961
|
+
const concurrencyCap = capPrecedence.cap;
|
|
709
962
|
|
|
710
963
|
let probed;
|
|
711
964
|
try {
|
|
@@ -735,11 +988,19 @@ export async function runProbedStoriesWaveTick({
|
|
|
735
988
|
inFlight,
|
|
736
989
|
blockedIds = [],
|
|
737
990
|
foreignHeld = [],
|
|
991
|
+
inFlightRecords = [],
|
|
738
992
|
} = probed;
|
|
739
993
|
const { envelope, exitCode } = buildReadySetEnvelope(nodes, {
|
|
740
994
|
concurrencyCap,
|
|
995
|
+
capPrecedence,
|
|
741
996
|
doneIds,
|
|
742
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),
|
|
743
1004
|
});
|
|
744
1005
|
|
|
745
1006
|
const done = [...doneIds].sort((a, b) => a - b);
|