mandrel 2.24.0 → 2.25.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 +13 -19
- 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/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/workflows.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +5 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/scripts/acceptance-eval.js +35 -9
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-test-temp-hygiene.js +83 -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/lib/audit-to-stories/audit-label-taxonomy.js +177 -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/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +396 -51
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/writer.js +7 -3
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +97 -19
- package/.agents/scripts/lib/crap-utils.js +73 -13
- package/.agents/scripts/lib/env-loader.js +46 -16
- 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/label-constants.js +17 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- 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/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-persist/run-plan-persist.js +7 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +13 -1
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -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 +96 -34
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +50 -13
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +73 -9
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +6 -3
- package/.agents/scripts/lib/workers/crap-worker.js +6 -3
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/quality-preview.js +25 -0
- package/.agents/scripts/run-tests.js +10 -0
- package/.agents/scripts/single-story-close.js +6 -0
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +95 -10
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +81 -95
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/deliver-reference.md +18 -3
- package/.agents/workflows/helpers/deliver-story-reference.md +127 -46
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +34 -0
- package/.agents/workflows/plan.md +60 -76
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +1 -1
|
@@ -191,6 +191,29 @@ export function mergeEnvelopes(miEnvelope, crapEnvelope) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Render the named diagnostics a gate envelope carries, or `null` when it
|
|
196
|
+
* carries none (Story #4866).
|
|
197
|
+
*
|
|
198
|
+
* A diagnostic is what a gate emits *instead of* per-method verdicts when it
|
|
199
|
+
* has established that no verdict it could produce would be meaningful — an
|
|
200
|
+
* incomparable baseline, or a comparison basis whose drifted-row ratio proves
|
|
201
|
+
* the two sides disagree on line coordinates. It must reach the operator
|
|
202
|
+
* verbatim: the gate exits 0, so silence would read as a clean run.
|
|
203
|
+
*
|
|
204
|
+
* @param {Array<{ envelope: { diagnostics?: Array<{name: string, message: string}> } | null }>} results
|
|
205
|
+
* @returns {string | null}
|
|
206
|
+
*/
|
|
207
|
+
export function renderDiagnostics(results) {
|
|
208
|
+
const lines = [];
|
|
209
|
+
for (const { envelope } of results ?? []) {
|
|
210
|
+
for (const d of envelope?.diagnostics ?? []) {
|
|
211
|
+
lines.push(`[${d.name}] ${d.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return lines.length === 0 ? null : lines.join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
/**
|
|
195
218
|
* Compute the CLI exit code from a merge result + per-gate exit codes. Pure.
|
|
196
219
|
*
|
|
@@ -320,6 +343,8 @@ export async function runCli({
|
|
|
320
343
|
: `scope=diff ref=${ref}\n\n`,
|
|
321
344
|
);
|
|
322
345
|
stdout.write(`${renderTable(merged)}\n`);
|
|
346
|
+
const diagnostics = renderDiagnostics([miResult, crapResult]);
|
|
347
|
+
if (diagnostics) stdout.write(`\n${diagnostics}\n`);
|
|
323
348
|
if (miExit !== 0 || crapExit !== 0) {
|
|
324
349
|
stderr.write(
|
|
325
350
|
`\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
|
|
@@ -32,6 +32,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { assertNoReservedIdStreams } from './check-test-temp-hygiene.js';
|
|
35
36
|
import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
|
|
36
37
|
import { runAsCli } from './lib/cli-utils.js';
|
|
37
38
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
@@ -165,6 +166,7 @@ export function runTestSuite({
|
|
|
165
166
|
cleanup = cleanupRepoTestTempArtifacts,
|
|
166
167
|
listTargets = listTestFilesForTier,
|
|
167
168
|
maxTargetChars = resolveMaxTargetChars(),
|
|
169
|
+
fixtureStreamGuard = assertNoReservedIdStreams,
|
|
168
170
|
} = {}) {
|
|
169
171
|
const { tier, rest } = parseTierArgv(argv);
|
|
170
172
|
const targets = listTargets(tier, cwd);
|
|
@@ -192,6 +194,14 @@ export function runTestSuite({
|
|
|
192
194
|
|
|
193
195
|
cleanup({ repoRoot: cwd });
|
|
194
196
|
|
|
197
|
+
// A test that spawns a real CLI at the repository root inherits the real
|
|
198
|
+
// state directory, so its fixture telemetry can land in the operator's live
|
|
199
|
+
// signals tree — where the retro graduator counts it as recurrence evidence
|
|
200
|
+
// and files a ticket citing a fixture Story id. Fail the run that caused it
|
|
201
|
+
// rather than discovering it from that ticket.
|
|
202
|
+
const polluted = fixtureStreamGuard({ cwd });
|
|
203
|
+
if (polluted !== 0 && status === 0) status = polluted;
|
|
204
|
+
|
|
195
205
|
if (spawnError) {
|
|
196
206
|
throw spawnError;
|
|
197
207
|
}
|
|
@@ -90,6 +90,7 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
90
90
|
import { formatCliError } from './lib/error-redactor.js';
|
|
91
91
|
import { Logger } from './lib/Logger.js';
|
|
92
92
|
import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
|
|
93
|
+
import { resolveRunScopedConfig } from './lib/orchestration/run-scoped-config.js';
|
|
93
94
|
import {
|
|
94
95
|
failedTerminalFor,
|
|
95
96
|
gatesForFailedPhase,
|
|
@@ -123,12 +124,17 @@ export const enableAutoMerge = enableAutoMergeWith;
|
|
|
123
124
|
// `gatesForFailedPhase` now lives beside the envelope it feeds
|
|
124
125
|
// (`single-story-close/failed-terminal.js`); it is re-exported here so the
|
|
125
126
|
// CLI's public surface is unchanged by that move.
|
|
127
|
+
// `resolveRunScopedConfig` (Story #4891) is the run-scoped config pin the
|
|
128
|
+
// pipeline reads before its first phase; it is part of this CLI's surface for
|
|
129
|
+
// the same reason the sync helpers are — the runner reaches it only through a
|
|
130
|
+
// dynamic import, so this file is where it is statically visible.
|
|
126
131
|
export {
|
|
127
132
|
buildStoryReviewCrossRefBody,
|
|
128
133
|
buildSyncFailureCommentBody,
|
|
129
134
|
gatesForFailedPhase,
|
|
130
135
|
handleSyncFailure,
|
|
131
136
|
parsePrNumber,
|
|
137
|
+
resolveRunScopedConfig,
|
|
132
138
|
runStoryScopeReview,
|
|
133
139
|
};
|
|
134
140
|
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
planFastForward,
|
|
62
62
|
} from './lib/orchestration/git-cleanup/phases/fast-forward.js';
|
|
63
63
|
import { verifyRemote } from './lib/orchestration/remote-verifier.js';
|
|
64
|
+
import { pinRunScopedConfig } from './lib/orchestration/run-scoped-config.js';
|
|
64
65
|
import {
|
|
65
66
|
acquireStoryLease,
|
|
66
67
|
releaseStoryLease,
|
|
@@ -735,6 +736,11 @@ export async function runSingleStoryInit({
|
|
|
735
736
|
standalone: true,
|
|
736
737
|
storyBranch,
|
|
737
738
|
baseBranch,
|
|
739
|
+
// The write half of the run-scoped config pin. `baseBranch` above is the
|
|
740
|
+
// legacy field close still reads as a fallback; this block is the
|
|
741
|
+
// registry-driven form a second run-scoped key joins without a second
|
|
742
|
+
// mechanism (`lib/orchestration/run-scoped-config.js`).
|
|
743
|
+
runScopedConfig: pinRunScopedConfig(config),
|
|
738
744
|
storyTitle: story.title,
|
|
739
745
|
worktreeEnabled: runtime.worktreeEnabled,
|
|
740
746
|
workCwd,
|
|
@@ -805,6 +811,7 @@ export function renderSingleStoryInitComment(result) {
|
|
|
805
811
|
standalone: true,
|
|
806
812
|
storyBranch: result.storyBranch,
|
|
807
813
|
baseBranch: result.baseBranch,
|
|
814
|
+
runScopedConfig: result.runScopedConfig,
|
|
808
815
|
worktreeEnabled: result.worktreeEnabled,
|
|
809
816
|
workCwd: result.workCwd,
|
|
810
817
|
worktreeCreated: result.worktreeCreated,
|
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
* `/deliver` uses — `resolveConfig` + `getRunners` reading
|
|
76
76
|
* `delivery.deliverRunner.concurrencyCap` (default 3) — so a
|
|
77
77
|
* `.agentrc.local.json` override is honored. A `--concurrency <n>` CLI flag
|
|
78
|
-
* overrides the config-resolved value for that run only
|
|
78
|
+
* overrides the config-resolved value for that run only, and the envelope's
|
|
79
|
+
* `capPrecedence` names which source won so the override is never silent
|
|
80
|
+
* (Story #4875). This shares one
|
|
79
81
|
* deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
|
|
80
82
|
* one scheduling kernel with every `/deliver` multi-Story invocation.
|
|
81
83
|
*
|
|
@@ -167,7 +169,10 @@ Options:
|
|
|
167
169
|
--concurrency <n> Override the per-beat concurrency cap for this run only.
|
|
168
170
|
Must be a positive integer. When omitted, the cap is
|
|
169
171
|
resolved from delivery.deliverRunner.concurrencyCap in
|
|
170
|
-
.agentrc.json / .agentrc.local.json (default 3).
|
|
172
|
+
.agentrc.json / .agentrc.local.json (default 3). The flag
|
|
173
|
+
WINS over the configured value, and the envelope's
|
|
174
|
+
capPrecedence records that it did — including when the
|
|
175
|
+
request exceeds the configured cap.
|
|
171
176
|
--done <csv> Comma-separated Story IDs already completed this run.
|
|
172
177
|
Their dependents become eligible; they are never
|
|
173
178
|
re-dispatched. Defaults to empty.
|
|
@@ -181,6 +186,14 @@ Output envelope:
|
|
|
181
186
|
"ready": [101],
|
|
182
187
|
"totalStories": 2,
|
|
183
188
|
"concurrencyCap": 3,
|
|
189
|
+
"capPrecedence": {
|
|
190
|
+
"cap": 3,
|
|
191
|
+
"source": "config",
|
|
192
|
+
"configuredCap": 3,
|
|
193
|
+
"requestedCap": null,
|
|
194
|
+
"exceedsConfigured": false,
|
|
195
|
+
"note": "..."
|
|
196
|
+
},
|
|
184
197
|
"inFlight": 0,
|
|
185
198
|
"cycleError": null,
|
|
186
199
|
"wedged": null
|
|
@@ -214,6 +227,7 @@ function inputErrorResult(message, concurrencyCap = null, inFlightValue = 0) {
|
|
|
214
227
|
ready: [],
|
|
215
228
|
totalStories: 0,
|
|
216
229
|
concurrencyCap,
|
|
230
|
+
capPrecedence: null,
|
|
217
231
|
inFlight: inFlightValue,
|
|
218
232
|
cycleError: null,
|
|
219
233
|
wedged: null,
|
|
@@ -394,13 +408,69 @@ export function parseConcurrencyOverride(raw) {
|
|
|
394
408
|
* `--concurrency`; wins over config.
|
|
395
409
|
* @returns {number} The resolved positive-integer concurrency cap.
|
|
396
410
|
*/
|
|
397
|
-
export function resolveConcurrencyCap(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
export function resolveConcurrencyCap(opts = {}) {
|
|
412
|
+
return resolveCapPrecedence(opts).cap;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Resolve the per-beat cap **and the precedence that produced it** (Story
|
|
417
|
+
* #4875).
|
|
418
|
+
*
|
|
419
|
+
* `--concurrency` wins over `delivery.deliverRunner.concurrencyCap`, and that
|
|
420
|
+
* is the intended contract — a flag an operator typed for one run should not be
|
|
421
|
+
* outranked by a checked-in default. What was wrong is that it won *silently*:
|
|
422
|
+
* the envelope reported a single `concurrencyCap` number with no record of
|
|
423
|
+
* which source set it, so a run at 8 when the project configured 3 was
|
|
424
|
+
* indistinguishable from a project configured at 8. A reader could not tell an
|
|
425
|
+
* override from a default, and an override that ran the repo above its own
|
|
426
|
+
* configured ceiling left no trace at all.
|
|
427
|
+
*
|
|
428
|
+
* So the flag still wins, but never quietly: the source is named, the
|
|
429
|
+
* configured value is carried alongside the requested one, and a request that
|
|
430
|
+
* exceeds the configured cap is called out as such. Reporting rather than
|
|
431
|
+
* refusing is deliberate — the configured cap is a project default, not a
|
|
432
|
+
* safety limit, and refusing a deliberate operator escalation would trade a
|
|
433
|
+
* silent override for a silent stall.
|
|
434
|
+
*
|
|
435
|
+
* @param {object} [opts]
|
|
436
|
+
* @param {string} [opts.cwd] Repo root for config resolution.
|
|
437
|
+
* @param {object} [opts.config] Pre-resolved config (test injection).
|
|
438
|
+
* @param {number} [opts.override] Validated positive integer from
|
|
439
|
+
* `--concurrency`.
|
|
440
|
+
* @returns {{
|
|
441
|
+
* cap: number,
|
|
442
|
+
* source: 'flag'|'config',
|
|
443
|
+
* configuredCap: number,
|
|
444
|
+
* requestedCap: number|null,
|
|
445
|
+
* exceedsConfigured: boolean,
|
|
446
|
+
* note: string,
|
|
447
|
+
* }}
|
|
448
|
+
*/
|
|
449
|
+
export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
401
450
|
const resolved = config ?? resolveConfig({ cwd });
|
|
402
451
|
const { deliverRunner } = getRunners(resolved);
|
|
403
|
-
|
|
452
|
+
const configuredCap = deliverRunner.concurrencyCap;
|
|
453
|
+
if (override == null) {
|
|
454
|
+
return {
|
|
455
|
+
cap: configuredCap,
|
|
456
|
+
source: 'config',
|
|
457
|
+
configuredCap,
|
|
458
|
+
requestedCap: null,
|
|
459
|
+
exceedsConfigured: false,
|
|
460
|
+
note: `cap ${configuredCap} from delivery.deliverRunner.concurrencyCap (no --concurrency given)`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const exceedsConfigured = override > configuredCap;
|
|
464
|
+
return {
|
|
465
|
+
cap: override,
|
|
466
|
+
source: 'flag',
|
|
467
|
+
configuredCap,
|
|
468
|
+
requestedCap: override,
|
|
469
|
+
exceedsConfigured,
|
|
470
|
+
note: exceedsConfigured
|
|
471
|
+
? `cap ${override} from --concurrency, which OVERRIDES and EXCEEDS the configured delivery.deliverRunner.concurrencyCap ${configuredCap} — this run is deliberately above the project default`
|
|
472
|
+
: `cap ${override} from --concurrency, which overrides the configured delivery.deliverRunner.concurrencyCap ${configuredCap}`,
|
|
473
|
+
};
|
|
404
474
|
}
|
|
405
475
|
|
|
406
476
|
/**
|
|
@@ -418,6 +488,8 @@ export function resolveConcurrencyCap({ cwd, config, override } = {}) {
|
|
|
418
488
|
* @param {Array<{id: number, dependsOn: number[]}>} nodes
|
|
419
489
|
* @param {object} args
|
|
420
490
|
* @param {number} args.concurrencyCap Resolved per-beat concurrency cap.
|
|
491
|
+
* @param {object|null} [args.capPrecedence] The {@link resolveCapPrecedence}
|
|
492
|
+
* record explaining which source set the cap.
|
|
421
493
|
* @param {Set<number>} [args.doneIds] Story IDs already completed this run.
|
|
422
494
|
* @param {number} [args.inFlight] Stories already occupying a slot.
|
|
423
495
|
* @returns {{
|
|
@@ -434,7 +506,7 @@ export function resolveConcurrencyCap({ cwd, config, override } = {}) {
|
|
|
434
506
|
*/
|
|
435
507
|
export function buildReadySetEnvelope(
|
|
436
508
|
nodes,
|
|
437
|
-
{ concurrencyCap, doneIds = new Set(), inFlight = 0 },
|
|
509
|
+
{ concurrencyCap, capPrecedence = null, doneIds = new Set(), inFlight = 0 },
|
|
438
510
|
) {
|
|
439
511
|
const totalStories = nodes.length;
|
|
440
512
|
|
|
@@ -443,6 +515,10 @@ export function buildReadySetEnvelope(
|
|
|
443
515
|
ready: [],
|
|
444
516
|
totalStories,
|
|
445
517
|
concurrencyCap,
|
|
518
|
+
// Which source set `concurrencyCap`, and whether it outranks the project's
|
|
519
|
+
// configured value (Story #4875). Never omitted on a resolved beat: a
|
|
520
|
+
// missing precedence record is what made a silent override possible.
|
|
521
|
+
capPrecedence,
|
|
446
522
|
inFlight,
|
|
447
523
|
cycleError: null,
|
|
448
524
|
wedged: null,
|
|
@@ -484,6 +560,11 @@ export function buildReadySetEnvelope(
|
|
|
484
560
|
labels: doneIds.has(node.id) ? [AGENT_LABELS.DONE] : (node.labels ?? []),
|
|
485
561
|
};
|
|
486
562
|
if (node.files !== undefined) rec.files = node.files;
|
|
563
|
+
// Probe-mode nodes carry the Story body so the overlap guard can widen a
|
|
564
|
+
// declared footprint from the paths the Story's own text names (Story
|
|
565
|
+
// #4875). Flag-mode nodes carry none — `parseDag` accepts no body — so
|
|
566
|
+
// this is inert there and the legacy contract is unchanged.
|
|
567
|
+
if (typeof node.body === 'string') rec.body = node.body;
|
|
487
568
|
return rec;
|
|
488
569
|
});
|
|
489
570
|
|
|
@@ -600,7 +681,8 @@ export function runStoriesWaveTick({
|
|
|
600
681
|
return inputErrorResult(doneError, null, inFlightValue);
|
|
601
682
|
}
|
|
602
683
|
|
|
603
|
-
const
|
|
684
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
685
|
+
const concurrencyCap = capPrecedence.cap;
|
|
604
686
|
|
|
605
687
|
let rawJson;
|
|
606
688
|
|
|
@@ -642,6 +724,7 @@ export function runStoriesWaveTick({
|
|
|
642
724
|
|
|
643
725
|
return buildReadySetEnvelope(nodes, {
|
|
644
726
|
concurrencyCap,
|
|
727
|
+
capPrecedence,
|
|
645
728
|
doneIds,
|
|
646
729
|
inFlight: inFlightValue,
|
|
647
730
|
});
|
|
@@ -705,7 +788,8 @@ export async function runProbedStoriesWaveTick({
|
|
|
705
788
|
return inputErrorResult(dispatchedError);
|
|
706
789
|
}
|
|
707
790
|
|
|
708
|
-
const
|
|
791
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
792
|
+
const concurrencyCap = capPrecedence.cap;
|
|
709
793
|
|
|
710
794
|
let probed;
|
|
711
795
|
try {
|
|
@@ -738,6 +822,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
738
822
|
} = probed;
|
|
739
823
|
const { envelope, exitCode } = buildReadySetEnvelope(nodes, {
|
|
740
824
|
concurrencyCap,
|
|
825
|
+
capPrecedence,
|
|
741
826
|
doneIds,
|
|
742
827
|
inFlight,
|
|
743
828
|
});
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
rankDuplicateCandidates,
|
|
33
33
|
readTechStackSummary,
|
|
34
34
|
shouldRefine,
|
|
35
|
+
synthesizeContractSections,
|
|
35
36
|
validateStoryBody,
|
|
36
37
|
} from './lib/story-plan.js';
|
|
37
38
|
|
|
@@ -40,7 +41,8 @@ Usage:
|
|
|
40
41
|
story-plan.js --emit-context (--seed "<seed>" | --seed-file <file>) \\
|
|
41
42
|
[--refine | --no-refine] [--pretty]
|
|
42
43
|
|
|
43
|
-
story-plan.js --body <file> [--
|
|
44
|
+
story-plan.js --body <file> [--acceptance <file>] [--verify <file>] \\
|
|
45
|
+
[--dry-run]
|
|
44
46
|
|
|
45
47
|
story-plan.js --help
|
|
46
48
|
|
|
@@ -48,9 +50,18 @@ Modes:
|
|
|
48
50
|
--emit-context Build the host-LLM authoring envelope and print it as
|
|
49
51
|
JSON on stdout. Use this first; the host LLM authors a
|
|
50
52
|
draft body using the envelope and the body template.
|
|
51
|
-
--body <file> Persist a pre-authored body. Validates shape (
|
|
52
|
-
|
|
53
|
-
calls \`gh issue create\` with
|
|
53
|
+
--body <file> Persist a pre-authored body. Validates shape (## Goal
|
|
54
|
+
and ## Changes present, no Epic: ref, an acceptance +
|
|
55
|
+
verify contract) and calls \`gh issue create\` with
|
|
56
|
+
type::story.
|
|
57
|
+
--acceptance <file>
|
|
58
|
+
JSON string[] of the Story's top-level acceptance
|
|
59
|
+
criteria. Authored once here — NOT mirrored into the
|
|
60
|
+
body by hand; the \`## Acceptance\` section is
|
|
61
|
+
synthesized from it, matching the story-author prompt.
|
|
62
|
+
--verify <file> JSON string[] of the Story's top-level verify entries,
|
|
63
|
+
each ending in a (unit|contract|e2e|validate) tier.
|
|
64
|
+
Synthesized into \`## Verify\` the same way.
|
|
54
65
|
--dry-run With --body: print the body and the gh argv that would
|
|
55
66
|
be invoked, then exit 0. No GitHub mutations.
|
|
56
67
|
|
|
@@ -101,8 +112,13 @@ async function fetchOpenStories(provider) {
|
|
|
101
112
|
* array; the caller decides whether to execute it (persist) or print
|
|
102
113
|
* it (--dry-run).
|
|
103
114
|
*/
|
|
104
|
-
function renderGhArgv({ title, bodyPath, labels }) {
|
|
105
|
-
|
|
115
|
+
function renderGhArgv({ title, bodyPath, labels, body = null }) {
|
|
116
|
+
// `--body-file` streams the authored file untouched; an inline `--body` is
|
|
117
|
+
// used only when persist synthesized sections into it (Story #4874), so the
|
|
118
|
+
// file on disk is no longer what gets created.
|
|
119
|
+
const source =
|
|
120
|
+
typeof body === 'string' ? ['--body', body] : ['--body-file', bodyPath];
|
|
121
|
+
const argv = ['issue', 'create', '--title', title, ...source];
|
|
106
122
|
for (const label of labels) {
|
|
107
123
|
argv.push('--label', label);
|
|
108
124
|
}
|
|
@@ -180,6 +196,31 @@ async function runEmitContext({
|
|
|
180
196
|
write(`${json}\n`);
|
|
181
197
|
}
|
|
182
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Read an optional top-level contract array (`--acceptance` / `--verify`)
|
|
201
|
+
* from a JSON file. Absent flag → an empty list, so the flags stay optional
|
|
202
|
+
* for a body that already carries its own sections.
|
|
203
|
+
*
|
|
204
|
+
* @param {string|undefined} filePath
|
|
205
|
+
* @param {'acceptance'|'verify'} label
|
|
206
|
+
* @returns {Promise<string[]>}
|
|
207
|
+
*/
|
|
208
|
+
async function readContractList(filePath, label) {
|
|
209
|
+
if (!filePath) return [];
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Cannot read --${label} file "${filePath}": ${err.message}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!Array.isArray(parsed) || parsed.some((v) => typeof v !== 'string')) {
|
|
219
|
+
throw new Error(`--${label} file "${filePath}" must be a JSON string[].`);
|
|
220
|
+
}
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
async function runPersist({
|
|
184
225
|
values,
|
|
185
226
|
provider,
|
|
@@ -193,17 +234,30 @@ async function runPersist({
|
|
|
193
234
|
if (!bodyPath) {
|
|
194
235
|
throw new Error('--body <file> is required in persist mode.');
|
|
195
236
|
}
|
|
196
|
-
const
|
|
197
|
-
const
|
|
237
|
+
const authored = await readFile(bodyPath, 'utf8');
|
|
238
|
+
const contract = {
|
|
239
|
+
acceptance: await readContractList(values.acceptance, 'acceptance'),
|
|
240
|
+
verify: await readContractList(values.verify, 'verify'),
|
|
241
|
+
};
|
|
242
|
+
const validation = validateStoryBody(authored, contract);
|
|
198
243
|
if (!validation.ok) {
|
|
199
244
|
throw new Error(
|
|
200
245
|
`Drafted body failed validation:\n - ${validation.errors.join('\n - ')}`,
|
|
201
246
|
);
|
|
202
247
|
}
|
|
203
248
|
|
|
249
|
+
// Story #4874: the author writes acceptance[] / verify[] once, at the
|
|
250
|
+
// ticket's top level, and persist renders their sections — never the
|
|
251
|
+
// author by hand.
|
|
252
|
+
const body = synthesizeContractSections(authored, contract);
|
|
204
253
|
const title = extractTitle(body);
|
|
205
254
|
const labels = [TYPE_LABELS.STORY];
|
|
206
|
-
const argv = renderGhArgv({
|
|
255
|
+
const argv = renderGhArgv({
|
|
256
|
+
title,
|
|
257
|
+
bodyPath,
|
|
258
|
+
labels,
|
|
259
|
+
body: body === authored ? null : body,
|
|
260
|
+
});
|
|
207
261
|
|
|
208
262
|
if (dryRun) {
|
|
209
263
|
Logger.info('--- DRY RUN ---');
|
|
@@ -255,6 +309,8 @@ async function main() {
|
|
|
255
309
|
seed: { type: 'string' },
|
|
256
310
|
'seed-file': { type: 'string' },
|
|
257
311
|
body: { type: 'string' },
|
|
312
|
+
acceptance: { type: 'string' },
|
|
313
|
+
verify: { type: 'string' },
|
|
258
314
|
refine: { type: 'boolean', default: false },
|
|
259
315
|
'no-refine': { type: 'boolean', default: false },
|
|
260
316
|
pretty: { type: 'boolean', default: false },
|
|
@@ -5,6 +5,7 @@ import './lib/runtime-deps/ensure-installed.js';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
7
7
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
8
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
8
9
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
9
10
|
import {
|
|
10
11
|
getBaselines,
|
|
@@ -44,6 +45,40 @@ import { Logger } from './lib/Logger.js';
|
|
|
44
45
|
* "no baseline yet".
|
|
45
46
|
*/
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Usage block for `--help` (Story #4872). This CLI *writes* on invocation, so
|
|
50
|
+
* the help branch must short-circuit before `main` runs rather than inside it —
|
|
51
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
52
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
53
|
+
*/
|
|
54
|
+
const USAGE = {
|
|
55
|
+
invocation:
|
|
56
|
+
'node .agents/scripts/update-crap-baseline.js [--baseline <path>] [--coverage <path>] [--full-scope | --diff-scope <ref>]',
|
|
57
|
+
summary:
|
|
58
|
+
'Scan → score → write the CRAP baseline. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
59
|
+
flags: [
|
|
60
|
+
[
|
|
61
|
+
'--baseline <path>',
|
|
62
|
+
'Write to this path instead of `delivery.quality.baselines.crap.path`.',
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'--coverage <path>',
|
|
66
|
+
'Read coverage from this path (default `coverage/coverage-final.json`).',
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'--full-scope',
|
|
70
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'--diff-scope <ref>',
|
|
74
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
notes: [
|
|
78
|
+
'Run `npm run test:coverage` first — without a coverage artifact every file is skipped.',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
47
82
|
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
48
83
|
const out = {
|
|
49
84
|
baselinePath: undefined,
|
|
@@ -183,8 +218,11 @@ async function main() {
|
|
|
183
218
|
);
|
|
184
219
|
}
|
|
185
220
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
runAsCli(import.meta.url, main, {
|
|
222
|
+
source: 'crap-baseline',
|
|
223
|
+
usage: USAGE,
|
|
224
|
+
onError: (err) => {
|
|
225
|
+
Logger.error(`[CRAP] ❌ Fatal error: ${err?.stack ?? err?.message ?? err}`);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
},
|
|
190
228
|
});
|
|
@@ -43,10 +43,34 @@ import './lib/runtime-deps/ensure-installed.js';
|
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
45
45
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
46
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
46
47
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
47
48
|
import { getBaselines, resolveConfig } from './lib/config-resolver.js';
|
|
48
49
|
import { Logger } from './lib/Logger.js';
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Usage block for `--help` (Story #4872). This CLI *writes* on invocation, so
|
|
53
|
+
* the help branch must short-circuit before `main` runs rather than inside it —
|
|
54
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
55
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
56
|
+
*/
|
|
57
|
+
const USAGE = {
|
|
58
|
+
invocation:
|
|
59
|
+
'node .agents/scripts/update-maintainability-baseline.js [--full-scope | --diff-scope <ref>]',
|
|
60
|
+
summary:
|
|
61
|
+
'Score → write the maintainability baseline. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
62
|
+
flags: [
|
|
63
|
+
[
|
|
64
|
+
'--full-scope',
|
|
65
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'--diff-scope <ref>',
|
|
69
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
70
|
+
],
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
50
74
|
/**
|
|
51
75
|
* Parse `--full-scope` (boolean opt-out flag).
|
|
52
76
|
*
|
|
@@ -119,8 +143,11 @@ async function main() {
|
|
|
119
143
|
);
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
runAsCli(import.meta.url, main, {
|
|
147
|
+
source: 'maintainability-baseline',
|
|
148
|
+
usage: USAGE,
|
|
149
|
+
onError: (err) => {
|
|
150
|
+
Logger.error(`[Maintainability] ❌ Fatal error: ${err.message}`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
},
|
|
126
153
|
});
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# {{title}}
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Goal
|
|
4
4
|
|
|
5
|
-
{{
|
|
5
|
+
{{goal}}
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Spec
|
|
8
8
|
|
|
9
|
-
{{
|
|
9
|
+
{{spec}}
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Changes
|
|
12
12
|
|
|
13
|
-
{{
|
|
13
|
+
{{changes}}
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Non-Goals
|
|
16
16
|
|
|
17
|
-
{{
|
|
17
|
+
{{nonGoals}}
|
|
18
|
+
|
|
19
|
+
<!--
|
|
20
|
+
`acceptance[]` and `verify[]` are the ticket's top-level machine contract.
|
|
21
|
+
Author each list ONCE, at the top level (`--acceptance` / `--verify`); do
|
|
22
|
+
NOT mirror them into this body — persist synthesizes the `## Acceptance`
|
|
23
|
+
and `## Verify` sections from those arrays so the issue still reads as a
|
|
24
|
+
complete executable document.
|
|
25
|
+
-->
|