mandrel 2.56.0 → 2.58.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/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* plan-critics.js — the /mandrel-plan
|
|
4
|
+
* plan-critics.js — the /mandrel-plan pre-mortem dispatch verdict CLI
|
|
5
|
+
* (Story #4592; pre-mortem only since Story #5312).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The operator runs this between Author and Persist when they want the
|
|
8
|
+
* pre-mortem — it is no longer a step of the `/mandrel-plan` spine. It
|
|
9
|
+
* evaluates the pre-mortem dispatch condition against the draft
|
|
10
|
+
* `stories.json` and prints the verdict as JSON on stdout so the workflow can
|
|
9
11
|
* act on it — dispatching a fresh-context critic sub-agent and folding its
|
|
10
12
|
* findings into a re-author round **before** the plan is persisted.
|
|
11
13
|
*
|
|
@@ -28,21 +30,15 @@
|
|
|
28
30
|
*
|
|
29
31
|
* CLI:
|
|
30
32
|
* --stories <file> Required. The draft Story ticket array (JSON).
|
|
31
|
-
* --tech-spec <file> Optional. Shared Tech Spec
|
|
32
|
-
*
|
|
33
|
-
* precondition reads.
|
|
33
|
+
* --tech-spec <file> Optional. Shared Tech Spec, folded into the plan
|
|
34
|
+
* text the external-dependency probe scans.
|
|
34
35
|
*
|
|
35
36
|
* stdout is reserved for the verdict JSON (Story #2278 discipline):
|
|
36
37
|
*
|
|
37
38
|
* {
|
|
38
|
-
* "
|
|
39
|
-
* "premortem": { "critic": "pre-mortem", "dispatch": true, "reasons": [...] },
|
|
40
|
-
* "textHygiene": { "critic": "text-hygiene", "findings": [...] }
|
|
39
|
+
* "premortem": { "critic": "pre-mortem", "dispatch": true, "reasons": [...] }
|
|
41
40
|
* }
|
|
42
41
|
*
|
|
43
|
-
* `textHygiene` (Story #4599) is advisory-only: deterministic body lints with
|
|
44
|
-
* no dispatch semantics — its findings fold into the re-author round.
|
|
45
|
-
*
|
|
46
42
|
* Human-readable log lines go to stderr, matching the sibling `plan-persist`.
|
|
47
43
|
*
|
|
48
44
|
* Exit codes: 0 success (any verdict); 1 usage/IO error.
|
|
@@ -192,11 +188,11 @@ export async function loadCriticArtifacts({
|
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
/**
|
|
195
|
-
* Log
|
|
191
|
+
* Log the decision and record a skip on the plan-metrics ledger. The
|
|
196
192
|
* ledger write is best-effort by `appendCriticSkip`'s own contract — it can
|
|
197
193
|
* never fail the plan step.
|
|
198
194
|
*
|
|
199
|
-
* @param {{
|
|
195
|
+
* @param {{ premortem: object }} verdict
|
|
200
196
|
* @param {object} config
|
|
201
197
|
* @param {{ append?: typeof appendCriticSkip }} [deps]
|
|
202
198
|
* @returns {Promise<void>}
|
|
@@ -206,48 +202,26 @@ export async function recordCriticSkips(
|
|
|
206
202
|
config,
|
|
207
203
|
{ append = appendCriticSkip } = {},
|
|
208
204
|
) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Text hygiene (Story #4599) is advisory-only — no dispatch semantics, so
|
|
228
|
-
// "skip" here means "zero findings". Recording that keeps the lint's
|
|
229
|
-
// fire/skip accounting on the same ledger as the dispatching critics.
|
|
230
|
-
const hygiene = verdict.textHygiene;
|
|
231
|
-
if (hygiene) {
|
|
232
|
-
const count = hygiene.findings.length;
|
|
233
|
-
Logger.info(
|
|
234
|
-
`[plan-critics] critic ${hygiene.critic}: ${count} finding(s) (advisory).`,
|
|
205
|
+
const decision = verdict.premortem;
|
|
206
|
+
Logger.info(
|
|
207
|
+
`[plan-critics] critic ${decision.critic}: ` +
|
|
208
|
+
`${decision.dispatch ? 'dispatch' : 'skip'} — ` +
|
|
209
|
+
decision.reasons.join('; '),
|
|
210
|
+
);
|
|
211
|
+
if (!decision.dispatch) {
|
|
212
|
+
await append(
|
|
213
|
+
{
|
|
214
|
+
critic: decision.critic,
|
|
215
|
+
reasons: decision.reasons,
|
|
216
|
+
cli: PLAN_CRITICS_CLI,
|
|
217
|
+
},
|
|
218
|
+
config,
|
|
235
219
|
);
|
|
236
|
-
if (count === 0) {
|
|
237
|
-
await append(
|
|
238
|
-
{
|
|
239
|
-
critic: hygiene.critic,
|
|
240
|
-
reasons: ['No text-hygiene findings over the draft stories.'],
|
|
241
|
-
cli: PLAN_CRITICS_CLI,
|
|
242
|
-
},
|
|
243
|
-
config,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
220
|
}
|
|
247
221
|
}
|
|
248
222
|
|
|
249
223
|
/**
|
|
250
|
-
* Load the artifacts, evaluate
|
|
224
|
+
* Load the artifacts, evaluate the pre-mortem, record a skip, and return the
|
|
251
225
|
* verdict. Exported as the CLI's whole body so tests drive it in-process with
|
|
252
226
|
* an explicit config and ledger seam.
|
|
253
227
|
*
|
|
@@ -262,7 +236,7 @@ export async function recordCriticSkips(
|
|
|
262
236
|
* manifests declare, forwarded to the pre-mortem external-dependency probe
|
|
263
237
|
* (Story #4700). `main()` resolves them via `collectRepoPackages`; tests may
|
|
264
238
|
* pass an explicit set or omit it (defaults to `[]`).
|
|
265
|
-
* @returns {Promise<{
|
|
239
|
+
* @returns {Promise<{ premortem: object }>}
|
|
266
240
|
*/
|
|
267
241
|
export async function evaluateCriticArtifacts({
|
|
268
242
|
storiesPath,
|
|
@@ -316,7 +290,7 @@ runAsCli(import.meta.url, main, {
|
|
|
316
290
|
invocation:
|
|
317
291
|
'node .agents/scripts/plan-critics.js --stories <file> [--tech-spec <file>]',
|
|
318
292
|
summary:
|
|
319
|
-
'
|
|
293
|
+
'Decide whether the maker-blind pre-mortem critic should run on an authored plan draft and print the verdict JSON on stdout.',
|
|
320
294
|
flags: [
|
|
321
295
|
['--stories <file>', 'Authored stories.json (required).'],
|
|
322
296
|
['--tech-spec <file>', 'Optional companion techspec.md.'],
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Given the author-written planning artifacts (`stories.json`, optional shared
|
|
8
8
|
* Tech Spec), this CLI validates and creates Story issue(s) directly:
|
|
9
9
|
*
|
|
10
|
-
* ticket validator / DAG
|
|
11
|
-
* split-policy partition → fold
|
|
10
|
+
* changes[] repair → ticket validator / DAG → reachability →
|
|
11
|
+
* split-policy partition → fold Spec into each Story body →
|
|
12
12
|
* createIssue(s) with type::story, resumably by plan fingerprint (NOT
|
|
13
13
|
* agent::ready) → story-plan-state on every Story;
|
|
14
14
|
* plan-summary on the primary → flip every Story to agent::ready →
|
|
@@ -34,31 +34,23 @@
|
|
|
34
34
|
* ids, for hand-driven runs. Each id must be
|
|
35
35
|
* claimed by exactly one Story's `supersedes[]`;
|
|
36
36
|
* they are commented on and closed as superseded
|
|
37
|
-
* --route-downgrade-reason <text>
|
|
38
|
-
* Audited planner downgrade (Story #4707): treat
|
|
39
|
-
* the envelope's `full` complexity verdict as
|
|
40
|
-
* `lite`, recording <text> on every Story's
|
|
41
|
-
* story-plan-state checkpoint. Absent this flag
|
|
42
|
-
* the deterministic verdict stands; the gate
|
|
43
|
-
* itself still fails toward `full`
|
|
44
37
|
* --no-close-superseded Keep the source tickets open (no comment, no
|
|
45
38
|
* close) — for a genuinely partial supersede
|
|
46
39
|
* --dry-run Assemble + validate without GitHub writes
|
|
47
|
-
* --chain-on-clean
|
|
48
|
-
* write-free dry-run first, and
|
|
49
|
-
* passes clean
|
|
50
|
-
*
|
|
51
|
-
* the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* its review round-trip (the chain declines, no
|
|
55
|
-
* writes). Ignored when `--dry-run` is also set
|
|
40
|
+
* --chain-on-clean Fast path (Story #4741; any plan since Story
|
|
41
|
+
* #5312): run the write-free dry-run first, and
|
|
42
|
+
* when it passes clean chain straight into the
|
|
43
|
+
* real persist in the SAME invocation, collapsing
|
|
44
|
+
* the two operator round-trips into one. A
|
|
45
|
+
* dry-run failure stops before any createIssue.
|
|
46
|
+
* Ignored when `--dry-run` is also set
|
|
56
47
|
* --force-review Operator-forced review stop before persist lands
|
|
57
|
-
* --allow-over-budget / --allow-large-fan-out
|
|
58
48
|
*
|
|
59
|
-
* Run `--dry-run` first. It exercises every gate —
|
|
60
|
-
*
|
|
61
|
-
*
|
|
49
|
+
* Run `--dry-run` first. It exercises every gate — the changes[] repair, the
|
|
50
|
+
* validator, DAG, reachability, split/supersede partition, Spec fold —
|
|
51
|
+
* write-free, and lists every warning (a footprint probe that disagrees with
|
|
52
|
+
* the base branch, an open question in a body) so an authoring mistake
|
|
53
|
+
* surfaces before a single issue exists.
|
|
62
54
|
*
|
|
63
55
|
* stdout is reserved for the JSON result (Story #2278 discipline, extended to
|
|
64
56
|
* this CLI by Story #4541): `routeAllOutputToStderr()` runs before any
|
|
@@ -116,14 +108,11 @@ const CLI_OPTIONS = {
|
|
|
116
108
|
'plan-context': { type: 'string' },
|
|
117
109
|
'plan-acceptance': { type: 'string' },
|
|
118
110
|
'source-tickets': { type: 'string' },
|
|
119
|
-
'route-downgrade-reason': { type: 'string' },
|
|
120
111
|
'close-superseded': { type: 'boolean', default: true },
|
|
121
112
|
'no-close-superseded': { type: 'boolean', default: false },
|
|
122
113
|
'dry-run': { type: 'boolean', default: false },
|
|
123
114
|
'chain-on-clean': { type: 'boolean', default: false },
|
|
124
115
|
'force-review': { type: 'boolean', default: false },
|
|
125
|
-
'allow-over-budget': { type: 'boolean', default: false },
|
|
126
|
-
'allow-large-fan-out': { type: 'boolean', default: false },
|
|
127
116
|
'epic-title': { type: 'string' },
|
|
128
117
|
'epic-goal': { type: 'string' },
|
|
129
118
|
epic: { type: 'string' },
|
|
@@ -134,9 +123,7 @@ const USAGE =
|
|
|
134
123
|
'[--tech-spec <file>] [--plan-dir <dir>] [--plan-context <file>] ' +
|
|
135
124
|
'[--plan-acceptance <file>] ' +
|
|
136
125
|
'[--source-tickets <ids>] [--no-close-superseded] ' +
|
|
137
|
-
'[--route-downgrade-reason <text>] ' +
|
|
138
126
|
'[--dry-run] [--chain-on-clean] [--force-review] ' +
|
|
139
|
-
'[--allow-over-budget] [--allow-large-fan-out] ' +
|
|
140
127
|
'[--epic-title <text> --epic-goal <text> | --epic <id>]';
|
|
141
128
|
|
|
142
129
|
async function readOptional(filePath, { required }) {
|
|
@@ -293,14 +280,11 @@ export function buildPersistOptions(values, paths, planContextEnvelope) {
|
|
|
293
280
|
|
|
294
281
|
return {
|
|
295
282
|
forceReview: values['force-review'],
|
|
296
|
-
allowOverBudget: values['allow-over-budget'],
|
|
297
|
-
allowLargeFanOut: values['allow-large-fan-out'],
|
|
298
283
|
dryRun: values['dry-run'],
|
|
299
284
|
planDir: paths.planDir,
|
|
300
285
|
skipCleanup: values['dry-run'],
|
|
301
286
|
sourceTicketIds: source.ids,
|
|
302
287
|
sourceTicketOrigin: source.origin,
|
|
303
|
-
routeDowngradeReason: values['route-downgrade-reason'] ?? null,
|
|
304
288
|
epic: resolveEpicRequest(values),
|
|
305
289
|
adoptEpicId: resolveEpicAdoptionId(values),
|
|
306
290
|
// Default-on: `--no-close-superseded` is the explicit escape and always
|
|
@@ -323,12 +307,6 @@ async function runPersistInvocation({
|
|
|
323
307
|
const paths = resolveInputPaths(values);
|
|
324
308
|
const effectiveDryRun =
|
|
325
309
|
typeof dryRun === 'boolean' ? dryRun : values['dry-run'] === true;
|
|
326
|
-
const settings = {
|
|
327
|
-
baseBranch: config.project?.baseBranch,
|
|
328
|
-
paths: config.project?.paths,
|
|
329
|
-
planning: config.planning,
|
|
330
|
-
docsContextFiles: config.project?.docsContextFiles,
|
|
331
|
-
};
|
|
332
310
|
|
|
333
311
|
return recordPlanInvocation(
|
|
334
312
|
{
|
|
@@ -341,7 +319,6 @@ async function runPersistInvocation({
|
|
|
341
319
|
provider,
|
|
342
320
|
artifacts,
|
|
343
321
|
config,
|
|
344
|
-
settings,
|
|
345
322
|
opts: {
|
|
346
323
|
...buildPersistOptions(values, paths, artifacts.planContextEnvelope),
|
|
347
324
|
dryRun: effectiveDryRun,
|
|
@@ -353,23 +330,23 @@ async function runPersistInvocation({
|
|
|
353
330
|
}
|
|
354
331
|
|
|
355
332
|
/**
|
|
356
|
-
*
|
|
357
|
-
* real persist in ONE operator invocation.
|
|
333
|
+
* Fast path (Story #4741 AC-1/AC-3; widened to any plan by Story #5312):
|
|
334
|
+
* chain a clean dry-run into the real persist in ONE operator invocation.
|
|
358
335
|
*
|
|
359
336
|
* Two passes over the **same** loaded artifacts:
|
|
360
337
|
*
|
|
361
338
|
* 1. A write-free dry-run. Every gate runs before any `createIssue` can
|
|
362
339
|
* happen, so a validation failure — which throws or returns reachability
|
|
363
340
|
* orphans — stops here, before a single issue exists (AC-3).
|
|
364
|
-
* 2. The real write, run
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
341
|
+
* 2. The real write, run when the dry-run passed clean. Because it replays
|
|
342
|
+
* the identical artifacts, the persisted output is byte-identical to
|
|
343
|
+
* what the dry-run validated (AC-1). The lite-route condition that used
|
|
344
|
+
* to gate this step went with the plan-side lite claim: a clean dry-run
|
|
345
|
+
* is the review the chain exists to fold.
|
|
369
346
|
*
|
|
370
|
-
* Exported for tests — this is where the round-trip collapse
|
|
371
|
-
*
|
|
372
|
-
*
|
|
347
|
+
* Exported for tests — this is where the round-trip collapse lives, so a
|
|
348
|
+
* regression here silently re-opens the second operator round-trip (or
|
|
349
|
+
* worse, persists a plan the dry-run never gated).
|
|
373
350
|
*
|
|
374
351
|
* @param {{ values: object, config: object, provider: object,
|
|
375
352
|
* artifacts: object, metricsSince: string }} args
|
|
@@ -391,20 +368,6 @@ export async function runPersistChain({
|
|
|
391
368
|
dryRun: true,
|
|
392
369
|
});
|
|
393
370
|
|
|
394
|
-
if (dryResult.route?.route !== 'lite') {
|
|
395
|
-
dryResult.chain = {
|
|
396
|
-
attempted: true,
|
|
397
|
-
persisted: false,
|
|
398
|
-
reason: 'route-not-lite',
|
|
399
|
-
};
|
|
400
|
-
Logger.info(
|
|
401
|
-
'[plan-persist] --chain-on-clean: dry-run clean but the plan did not ' +
|
|
402
|
-
'resolve to the lite route — declining the auto-persist; run persist ' +
|
|
403
|
-
'explicitly after review.',
|
|
404
|
-
);
|
|
405
|
-
return dryResult;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
371
|
const persistResult = await runPersistInvocation({
|
|
409
372
|
values,
|
|
410
373
|
config,
|
|
@@ -416,7 +379,7 @@ export async function runPersistChain({
|
|
|
416
379
|
persistResult.chain = {
|
|
417
380
|
attempted: true,
|
|
418
381
|
persisted: true,
|
|
419
|
-
reason: '
|
|
382
|
+
reason: 'dry-run-clean',
|
|
420
383
|
};
|
|
421
384
|
return persistResult;
|
|
422
385
|
}
|
|
@@ -540,10 +503,6 @@ runAsCli(import.meta.url, main, {
|
|
|
540
503
|
],
|
|
541
504
|
['--plan-acceptance <file>', 'Acceptance artifact to attach.'],
|
|
542
505
|
['--source-tickets <ids>', 'Ticket ids this plan supersedes.'],
|
|
543
|
-
[
|
|
544
|
-
'--route-downgrade-reason <text>',
|
|
545
|
-
'Why the authored route was downgraded.',
|
|
546
|
-
],
|
|
547
506
|
['--dry-run', 'Validate and report; create nothing.'],
|
|
548
507
|
['--chain-on-clean', 'Persist immediately when the dry run is clean.'],
|
|
549
508
|
['--no-close-superseded', 'Leave superseded source tickets open.'],
|
|
@@ -551,8 +510,6 @@ runAsCli(import.meta.url, main, {
|
|
|
551
510
|
'--force-review',
|
|
552
511
|
'Require the review gate even when it would be skipped.',
|
|
553
512
|
],
|
|
554
|
-
['--allow-over-budget', 'Permit a Spec over the context budget.'],
|
|
555
|
-
['--allow-large-fan-out', 'Permit a Story count above the fan-out gate.'],
|
|
556
513
|
[
|
|
557
514
|
'--epic-title <text>',
|
|
558
515
|
'Group the persisted Stories under a container Epic with this title (needs --epic-goal).',
|
|
@@ -249,9 +249,56 @@ export function mergeEnvelopes(
|
|
|
249
249
|
(crapEnvelope?.summary?.newViolations ?? 0),
|
|
250
250
|
},
|
|
251
251
|
cyclomaticFlag: flag,
|
|
252
|
+
// Story #5313: advisories ride the merge but never the exit code.
|
|
253
|
+
advisories: advisoriesOf(crapEnvelope),
|
|
252
254
|
};
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The cyclomatic advisories a CRAP envelope carries (Story #5313), or none.
|
|
259
|
+
*
|
|
260
|
+
* @param {{ cyclomaticAdvisories?: unknown } | null | undefined} crapEnvelope
|
|
261
|
+
* @returns {Array<{ file: string, method: string, startLine: number, cyclomatic: number }>}
|
|
262
|
+
*/
|
|
263
|
+
function advisoriesOf(crapEnvelope) {
|
|
264
|
+
const list = crapEnvelope?.cyclomaticAdvisories;
|
|
265
|
+
return Array.isArray(list) ? list : [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Print the advisories block when there is one (Story #5313). Split out of
|
|
270
|
+
* `emitReport` so that function's branching stays inside its committed
|
|
271
|
+
* cyclomatic budget.
|
|
272
|
+
*
|
|
273
|
+
* @param {Array<object>} advisories
|
|
274
|
+
* @param {{ write: (s: string) => void }} stdout
|
|
275
|
+
* @returns {void}
|
|
276
|
+
*/
|
|
277
|
+
function writeAdvisories(advisories, stdout) {
|
|
278
|
+
const block = renderAdvisories(advisories);
|
|
279
|
+
if (block) stdout.write(`\n${block}\n`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Render the cyclomatic advisories block (Story #5313), or `null` when the
|
|
284
|
+
* scan carried none. One line per method at or over the ceiling; the block
|
|
285
|
+
* says outright that it is advisory so a reader does not hunt for the exit
|
|
286
|
+
* code it did not change.
|
|
287
|
+
*
|
|
288
|
+
* @param {Array<{ file: string, method: string, startLine: number, cyclomatic: number }>} advisories
|
|
289
|
+
* @returns {string|null}
|
|
290
|
+
*/
|
|
291
|
+
export function renderAdvisories(advisories) {
|
|
292
|
+
const list = Array.isArray(advisories) ? advisories : [];
|
|
293
|
+
if (list.length === 0) return null;
|
|
294
|
+
return [
|
|
295
|
+
`Advisory — ${list.length} method(s) at cyclomatic 12 or above (reported, not a verdict; check-cyclomatic.js owns the ratchet):`,
|
|
296
|
+
...list.map(
|
|
297
|
+
(a) => ` - ${a.file}:${a.startLine} ${a.method} (c=${a.cyclomatic})`,
|
|
298
|
+
),
|
|
299
|
+
].join('\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
255
302
|
/**
|
|
256
303
|
* Render the named diagnostics a gate envelope carries, or `null` when it
|
|
257
304
|
* carries none (Story #4866).
|
|
@@ -286,6 +333,9 @@ export function renderDiagnostics(results) {
|
|
|
286
333
|
* Both signals are combined so a transient gate failure (e.g. JSON write
|
|
287
334
|
* error) still surfaces even if the violations array happens to be empty.
|
|
288
335
|
*
|
|
336
|
+
* Advisories (`merged.advisories`, Story #5313) are deliberately not read
|
|
337
|
+
* here: a method at cyclomatic 12 or above is reported and exits 0.
|
|
338
|
+
*
|
|
289
339
|
* @param {{ rows: Array<unknown>, totals: { miRegressions: number, crapViolations: number } }} merged
|
|
290
340
|
* @param {number} miExit
|
|
291
341
|
* @param {number} crapExit
|
|
@@ -484,6 +534,7 @@ function emitReport({
|
|
|
484
534
|
stdout.write('\n--- quality:preview ---\n');
|
|
485
535
|
stdout.write(stagedScopeLine({ staged, ref, cwd }));
|
|
486
536
|
stdout.write(`${renderTable(merged)}\n`);
|
|
537
|
+
writeAdvisories(merged.advisories, stdout);
|
|
487
538
|
const diagnostics = renderDiagnostics([miResult, crapResult]);
|
|
488
539
|
if (diagnostics) stdout.write(`\n${diagnostics}\n`);
|
|
489
540
|
if (miExit !== 0 || crapExit !== 0) {
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
* (`MAX_TARGET_CHARS`) and spawns one `node --test` process per chunk,
|
|
29
29
|
* aggregating the exit codes. On POSIX the far larger
|
|
30
30
|
* `POSIX_MAX_TARGET_CHARS` budget collapses every tier back into one spawn.
|
|
31
|
+
*
|
|
32
|
+
* Suite credit (Story #5313): a green full-tier run inside a `story-<id>`
|
|
33
|
+
* checkout deposits the `test` gate's evidence record — the one
|
|
34
|
+
* `single-story-close.js` reads — so a bare `npm test` in the worktree is
|
|
35
|
+
* credited at unchanged HEAD. See `lib/test-run-credit.js`.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
import { spawnSync } from 'node:child_process';
|
|
@@ -37,6 +42,7 @@ import { assertNoReservedIdStreams } from './check-test-temp-hygiene.js';
|
|
|
37
42
|
import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
|
|
38
43
|
import { runAsCli } from './lib/cli-utils.js';
|
|
39
44
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
45
|
+
import { reportTestRunCredit } from './lib/test-run-credit.js';
|
|
40
46
|
import {
|
|
41
47
|
resolveTestConcurrency,
|
|
42
48
|
runTierPreflight,
|
|
@@ -171,6 +177,7 @@ export function runTestSuite({
|
|
|
171
177
|
maxTargetChars = resolveMaxTargetChars(),
|
|
172
178
|
fixtureStreamGuard = assertNoReservedIdStreams,
|
|
173
179
|
preflight = runTierPreflight,
|
|
180
|
+
depositCredit = reportTestRunCredit,
|
|
174
181
|
} = {}) {
|
|
175
182
|
const { tier, rest } = parseTierArgv(argv);
|
|
176
183
|
|
|
@@ -185,6 +192,7 @@ export function runTestSuite({
|
|
|
185
192
|
const chunks = chunkTestTargets(targets, maxTargetChars);
|
|
186
193
|
|
|
187
194
|
const env = buildWebhookSafeTestEnv(process.env);
|
|
195
|
+
const startedAt = Date.now();
|
|
188
196
|
let status = 0;
|
|
189
197
|
let spawnError = null;
|
|
190
198
|
|
|
@@ -224,6 +232,10 @@ export function runTestSuite({
|
|
|
224
232
|
throw spawnError;
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
// Story #5313 — a green full run earns close's `test` credit; best-effort,
|
|
236
|
+
// never the exit code.
|
|
237
|
+
depositCredit({ cwd, tier, status, durationMs: Date.now() - startedAt });
|
|
238
|
+
|
|
227
239
|
return status;
|
|
228
240
|
}
|
|
229
241
|
|
|
@@ -55,12 +55,13 @@
|
|
|
55
55
|
* inFlight: number,
|
|
56
56
|
* cycleError: string | null,
|
|
57
57
|
* wedged: { reason, stories: [{ id, unmetBlockers }] } | null,
|
|
58
|
-
* inFlightReservation: { available, withheld: [{ id, blockedBy, reason, source, paths
|
|
59
|
-
* footprintGuard: { mode, withheld: [{ id, blockedBy, scope, source, paths
|
|
58
|
+
* inFlightReservation: { available, withheld: [{ id, blockedBy, reason, source, paths }], note },
|
|
59
|
+
* footprintGuard: { mode, withheld: [{ id, blockedBy, scope, source, paths }], advisory, note }
|
|
60
60
|
* }
|
|
61
61
|
*
|
|
62
62
|
* `inFlightReservation` reports the cross-beat half of the co-dispatch guard
|
|
63
|
-
* (Story #
|
|
63
|
+
* (Story #4950 made it reserve; Story #5313 narrowed the footprint back to
|
|
64
|
+
* the declared `changes[]`). Under
|
|
64
65
|
* `--probe-live` the in-flight Stories' own records are handed to the kernel,
|
|
65
66
|
* so a candidate sharing a CONCRETE path with a Story dispatched on an EARLIER
|
|
66
67
|
* beat is withheld and named here with its blocker and a `reason`
|
|
@@ -75,10 +76,10 @@
|
|
|
75
76
|
* reported nowhere: a same-beat overlap skip was a bare `continue` inside
|
|
76
77
|
* `planReadySet`, so the Story vanished from `ready[]` with no field anywhere
|
|
77
78
|
* naming the collision. Every entry in either report now also carries the
|
|
78
|
-
* colliding `paths` and a `source` tag — `declared-overlap
|
|
79
|
-
* `changes[]` named the path (intended
|
|
80
|
-
* rewrite the same generated baseline)
|
|
81
|
-
*
|
|
79
|
+
* colliding `paths` and a `source` tag — `declared-overlap`, the one class
|
|
80
|
+
* left since Story #5313: both Stories' `changes[]` named the path (intended
|
|
81
|
+
* serialization: two Stories really do rewrite the same generated baseline)
|
|
82
|
+
* or one declared a glob. `mode` names the `footprintGuard` config value;
|
|
82
83
|
* under `advisory` the collisions are detected and listed in `advisory[]` but
|
|
83
84
|
* dispatch follows the declared `depends_on` edges alone.
|
|
84
85
|
*
|
|
@@ -130,10 +131,7 @@ import { AGENT_LABELS } from './lib/label-constants.js';
|
|
|
130
131
|
import { parseIds } from './lib/orchestration/resolve-stories.js';
|
|
131
132
|
import { buildStoryAdjacency } from './lib/story-adjacency.js';
|
|
132
133
|
import { expandIdList } from './lib/util/parse-id-list.js';
|
|
133
|
-
import {
|
|
134
|
-
OVERLAP_SOURCES,
|
|
135
|
-
renderScrapeAttribution,
|
|
136
|
-
} from './lib/wave-runner/footprint.js';
|
|
134
|
+
import { OVERLAP_SOURCES } from './lib/wave-runner/footprint.js';
|
|
137
135
|
import {
|
|
138
136
|
createProbeContext,
|
|
139
137
|
probeLiveState,
|
|
@@ -240,10 +238,7 @@ Output envelope:
|
|
|
240
238
|
"blockedBy": 4949,
|
|
241
239
|
"reason": "in-flight-earlier-beat",
|
|
242
240
|
"source": "declared-overlap",
|
|
243
|
-
"paths": ["lib/shared.js"]
|
|
244
|
-
"attribution": [
|
|
245
|
-
{ "path": "lib/shared.js", "declared": true, "fields": [] }
|
|
246
|
-
]
|
|
241
|
+
"paths": ["lib/shared.js"]
|
|
247
242
|
}
|
|
248
243
|
],
|
|
249
244
|
"note": "..."
|
|
@@ -255,15 +250,8 @@ Output envelope:
|
|
|
255
250
|
"id": 4952,
|
|
256
251
|
"blockedBy": 4951,
|
|
257
252
|
"scope": "beat",
|
|
258
|
-
"source": "
|
|
259
|
-
"paths": ["lib/other.js"]
|
|
260
|
-
"attribution": [
|
|
261
|
-
{
|
|
262
|
-
"path": "lib/other.js",
|
|
263
|
-
"declared": false,
|
|
264
|
-
"fields": ["body:Verify"]
|
|
265
|
-
}
|
|
266
|
-
]
|
|
253
|
+
"source": "declared-overlap",
|
|
254
|
+
"paths": ["lib/other.js"]
|
|
267
255
|
}
|
|
268
256
|
],
|
|
269
257
|
"advisory": [],
|
|
@@ -280,11 +268,9 @@ report is { available: false } and selection de-conflicts within the beat only.
|
|
|
280
268
|
footprintGuard names each Story withheld from THIS beat by a peer already
|
|
281
269
|
admitted on it — the half that used to be an unreported skip — and every
|
|
282
270
|
entry in either report carries the colliding paths plus a source tag
|
|
283
|
-
(declared-overlap
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
— so a path that reached the comparison only because every Story RUNS it in
|
|
287
|
-
"## Verify" says so). Its "mode" echoes
|
|
271
|
+
(declared-overlap: both changes[] declarations named the path, or one declared
|
|
272
|
+
a glob — the text scrape that used to widen this was retired in Story #5313).
|
|
273
|
+
Its "mode" echoes
|
|
288
274
|
delivery.deliverRunner.footprintGuard: under "advisory" the collisions are
|
|
289
275
|
detected and listed in "advisory" but never withhold, and dispatch follows the
|
|
290
276
|
declared depends_on edges alone.
|
|
@@ -406,7 +392,6 @@ export function buildReservationReport(
|
|
|
406
392
|
: RESERVATION_REASONS.EARLIER_BEAT,
|
|
407
393
|
source: w.source ?? OVERLAP_SOURCES.DECLARED,
|
|
408
394
|
paths: w.paths ?? [],
|
|
409
|
-
attribution: w.attribution ?? [],
|
|
410
395
|
}));
|
|
411
396
|
return {
|
|
412
397
|
available: true,
|
|
@@ -438,16 +423,12 @@ export function buildReservationReport(
|
|
|
438
423
|
*/
|
|
439
424
|
export function buildFootprintGuardReport(footprintWithholds, mode) {
|
|
440
425
|
const ledger = Array.isArray(footprintWithholds) ? footprintWithholds : [];
|
|
441
|
-
const project = ({ id, blockedBy, scope, source, paths
|
|
426
|
+
const project = ({ id, blockedBy, scope, source, paths }) => ({
|
|
442
427
|
id,
|
|
443
428
|
blockedBy,
|
|
444
429
|
scope,
|
|
445
430
|
source,
|
|
446
431
|
paths,
|
|
447
|
-
// Story #5265: the per-path field attribution rides the entry itself, so
|
|
448
|
-
// a consumer reading the envelope never has to re-derive where a scraped
|
|
449
|
-
// path came from (and cannot get a different answer than the note did).
|
|
450
|
-
attribution: attribution ?? [],
|
|
451
432
|
});
|
|
452
433
|
const beat = ledger
|
|
453
434
|
.filter((w) => w.scope === WITHHOLD_SCOPES.BEAT && w.enforced)
|
|
@@ -474,22 +455,19 @@ export function buildFootprintGuardReport(footprintWithholds, mode) {
|
|
|
474
455
|
function footprintGuardNote(beat, advisory, mode) {
|
|
475
456
|
const detail = (entries) =>
|
|
476
457
|
entries
|
|
477
|
-
.map(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
})
|
|
458
|
+
.map(
|
|
459
|
+
(w) =>
|
|
460
|
+
`#${w.id} ← #${w.blockedBy} on ${w.paths.join(', ')} (${w.source})`,
|
|
461
|
+
)
|
|
482
462
|
.join('; ');
|
|
483
463
|
if (beat.length > 0) {
|
|
484
464
|
return (
|
|
485
465
|
`${beat.length} Story(ies) withheld from THIS beat because their file ` +
|
|
486
466
|
`footprint overlaps a peer already admitted on it — ${detail(beat)}. ` +
|
|
487
467
|
`Each is still eligible and re-admits on a later beat once its peer ` +
|
|
488
|
-
`lands.
|
|
489
|
-
`
|
|
490
|
-
`
|
|
491
|
-
`each such path was read out of, so a path only cited in '## Verify' ` +
|
|
492
|
-
`is distinguishable from an unpredicted edit target.`
|
|
468
|
+
`lands. Both Stories declared every colliding path in changes[] (or ` +
|
|
469
|
+
`one declared a glob): since Story #5313 the guard reads declarations ` +
|
|
470
|
+
`only, never the Story text.`
|
|
493
471
|
);
|
|
494
472
|
}
|
|
495
473
|
if (advisory.length > 0) {
|