mandrel 2.56.0 → 2.57.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 -34
- 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/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/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/plan-context.js +181 -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/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -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 +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- 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 +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -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 +266 -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 +41 -57
- 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 +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- 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
|
@@ -327,145 +327,21 @@
|
|
|
327
327
|
},
|
|
328
328
|
"planning": {
|
|
329
329
|
"type": "object",
|
|
330
|
-
"description": "Inputs to `/mandrel-plan`:
|
|
330
|
+
"description": "Inputs to `/mandrel-plan`: the memory-hygiene advisory ceiling and the opt-in navigability reachability gate.",
|
|
331
331
|
"properties": {
|
|
332
|
-
"riskHeuristics": {
|
|
333
|
-
"oneOf": [
|
|
334
|
-
{
|
|
335
|
-
"type": "array",
|
|
336
|
-
"items": {
|
|
337
|
-
"type": "string"
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
"type": "object",
|
|
342
|
-
"properties": {
|
|
343
|
-
"append": {
|
|
344
|
-
"type": "array",
|
|
345
|
-
"items": {
|
|
346
|
-
"type": "string"
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
"prepend": {
|
|
350
|
-
"type": "array",
|
|
351
|
-
"items": {
|
|
352
|
-
"type": "string"
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
},
|
|
356
|
-
"additionalProperties": false
|
|
357
|
-
}
|
|
358
|
-
],
|
|
359
|
-
"description": "Prose heuristics the planner escalates a Story against. A plain array replaces the framework list; the `{ append, prepend }` extender form deep-merges with it.",
|
|
360
|
-
"default": [
|
|
361
|
-
"Destructive or irreversible data mutations (dropping tables, deleting rows without soft-delete or backup, truncating production state).",
|
|
362
|
-
"Modifications to shared security or auth infrastructure (IAM policies, auth middleware, session or token handling, secret rotation).",
|
|
363
|
-
"Changes to CI/CD, deployment pipelines, or release gating that could disable safety checks or ship unverified code to production.",
|
|
364
|
-
"Monorepo-wide AST or text replacements touching overlapping files in parallel (catastrophic merge-conflict risk across concurrent agents).",
|
|
365
|
-
"Schema migrations that rewrite existing rows or drop columns without a backfill or rollback plan."
|
|
366
|
-
]
|
|
367
|
-
},
|
|
368
|
-
"complexityGate": {
|
|
369
|
-
"type": "object",
|
|
370
|
-
"description": "Shape-derived ceremony-lite complexity routing. A lite claim is validated against the authored Story shape at persist and re-derived from the Story body at dispatch; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.",
|
|
371
|
-
"properties": {
|
|
372
|
-
"enabled": {
|
|
373
|
-
"type": "boolean",
|
|
374
|
-
"description": "Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true."
|
|
375
|
-
},
|
|
376
|
-
"maxArtifacts": {
|
|
377
|
-
"type": "integer",
|
|
378
|
-
"minimum": 0,
|
|
379
|
-
"description": "Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1."
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
"additionalProperties": false
|
|
383
|
-
},
|
|
384
332
|
"memoryPool": {
|
|
385
333
|
"type": "object",
|
|
386
|
-
"description": "
|
|
334
|
+
"description": "Threshold for the memory-hygiene advisory `/mandrel-plan` surfaces at Gate #1. Advisory only: it recommends `/memory-consolidate` and never gates, reroutes, or mutates the memory pool.",
|
|
387
335
|
"properties": {
|
|
388
|
-
"staleAfterDays": {
|
|
389
|
-
"type": "integer",
|
|
390
|
-
"minimum": 1,
|
|
391
|
-
"description": "Recommend a consolidation pass once the pool's stamp is older than this many days. Default 30.",
|
|
392
|
-
"default": 30
|
|
393
|
-
},
|
|
394
|
-
"growthDelta": {
|
|
395
|
-
"type": "integer",
|
|
396
|
-
"minimum": 1,
|
|
397
|
-
"description": "Recommend a consolidation pass once this many entries have been written since the last one. Measured against the entry count the last pass stamped, so a stamp predating that field leaves growth unmeasured and only the age threshold applies. Default 25.",
|
|
398
|
-
"default": 25
|
|
399
|
-
},
|
|
400
336
|
"indexByteCeiling": {
|
|
401
337
|
"type": "integer",
|
|
402
338
|
"minimum": 1,
|
|
403
|
-
"description": "Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes.
|
|
339
|
+
"description": "Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. The harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself.",
|
|
404
340
|
"default": 24576
|
|
405
341
|
}
|
|
406
342
|
},
|
|
407
343
|
"additionalProperties": false
|
|
408
344
|
},
|
|
409
|
-
"failOnSharedEditors": {
|
|
410
|
-
"type": "boolean",
|
|
411
|
-
"description": "When true, upgrade shared-editor conflict findings to hard errors (default false — advisory soft findings only).",
|
|
412
|
-
"default": false
|
|
413
|
-
},
|
|
414
|
-
"requireExplicitCrossStoryDeps": {
|
|
415
|
-
"type": "boolean",
|
|
416
|
-
"description": "When true, upgrade implicit cross-Story dependency findings to hard errors (default false — advisory soft findings only).",
|
|
417
|
-
"default": false
|
|
418
|
-
},
|
|
419
|
-
"crossCuttingRegistries": {
|
|
420
|
-
"oneOf": [
|
|
421
|
-
{
|
|
422
|
-
"type": "array",
|
|
423
|
-
"items": {
|
|
424
|
-
"type": "string"
|
|
425
|
-
}
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
"type": "object",
|
|
429
|
-
"properties": {
|
|
430
|
-
"append": {
|
|
431
|
-
"type": "array",
|
|
432
|
-
"items": {
|
|
433
|
-
"type": "string"
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
"prepend": {
|
|
437
|
-
"type": "array",
|
|
438
|
-
"items": {
|
|
439
|
-
"type": "string"
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
"additionalProperties": false
|
|
444
|
-
}
|
|
445
|
-
],
|
|
446
|
-
"description": "Registry path patterns whose concurrent edits across Stories are flagged as conflicts. Defaults to the framework listener/handler index patterns when omitted.",
|
|
447
|
-
"default": [
|
|
448
|
-
"lib/orchestration/lifecycle/listeners/index.js",
|
|
449
|
-
"**/listeners/index.js",
|
|
450
|
-
"**/handlers/index.js"
|
|
451
|
-
]
|
|
452
|
-
},
|
|
453
|
-
"failOnRegistryConflicts": {
|
|
454
|
-
"type": "boolean",
|
|
455
|
-
"description": "When true, upgrade cross-cutting registry conflict findings to hard errors (default false).",
|
|
456
|
-
"default": false
|
|
457
|
-
},
|
|
458
|
-
"failOnLargeFanOut": {
|
|
459
|
-
"type": "boolean",
|
|
460
|
-
"description": "When true, upgrade fan-out-warning findings (delete blast radius) to hard errors (default false — soft advisory).",
|
|
461
|
-
"default": false
|
|
462
|
-
},
|
|
463
|
-
"largeFanOutThreshold": {
|
|
464
|
-
"type": "integer",
|
|
465
|
-
"minimum": 0,
|
|
466
|
-
"description": "Call-site count above which a Story that deletes a module emits a fan-out-warning. Counts base-branch references to the deleted path basename. Soft by default; does not size or reject Stories. Default 10.",
|
|
467
|
-
"default": 10
|
|
468
|
-
},
|
|
469
345
|
"navigation": {
|
|
470
346
|
"type": "object",
|
|
471
347
|
"description": "Opt-in navigability reachability gate. Absent or empty routeGlobs is a silent no-op.",
|
|
@@ -494,7 +370,7 @@
|
|
|
494
370
|
},
|
|
495
371
|
"delivery": {
|
|
496
372
|
"type": "object",
|
|
497
|
-
"description": "Everything `/mandrel-deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness,
|
|
373
|
+
"description": "Everything `/mandrel-deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness, quality gates, merge/CI watch, review ceremony, and the feedback loop.",
|
|
498
374
|
"properties": {
|
|
499
375
|
"execution": {
|
|
500
376
|
"type": "object",
|
|
@@ -671,39 +547,6 @@
|
|
|
671
547
|
}
|
|
672
548
|
]
|
|
673
549
|
},
|
|
674
|
-
"signals": {
|
|
675
|
-
"type": "object",
|
|
676
|
-
"description": "Detector thresholds for the surviving performance-signal categories. Each block is shallow-merged by the resolver.",
|
|
677
|
-
"properties": {
|
|
678
|
-
"rework": {
|
|
679
|
-
"type": "object",
|
|
680
|
-
"description": "Rework detector — repeated edits to one file in a run.",
|
|
681
|
-
"properties": {
|
|
682
|
-
"editsPerFile": {
|
|
683
|
-
"type": "integer",
|
|
684
|
-
"minimum": 1,
|
|
685
|
-
"description": "Edits to a single file within one run that trip the rework signal.",
|
|
686
|
-
"default": 5
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
"additionalProperties": false
|
|
690
|
-
},
|
|
691
|
-
"retry": {
|
|
692
|
-
"type": "object",
|
|
693
|
-
"description": "Retry detector — the same command failing repeatedly.",
|
|
694
|
-
"properties": {
|
|
695
|
-
"repeatCount": {
|
|
696
|
-
"type": "integer",
|
|
697
|
-
"minimum": 1,
|
|
698
|
-
"description": "Repeats of an identical failing command that trip the retry signal.",
|
|
699
|
-
"default": 3
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
"additionalProperties": false
|
|
703
|
-
}
|
|
704
|
-
},
|
|
705
|
-
"additionalProperties": false
|
|
706
|
-
},
|
|
707
550
|
"quality": {
|
|
708
551
|
"type": "object",
|
|
709
552
|
"description": "Quality-gate configuration. Every gate lives under `gates.<tier>` and shares the same `{ enabled, baselinePath, tolerance, floors, components }` base; shared scoping lives at this block root.",
|
|
@@ -1610,12 +1453,6 @@
|
|
|
1610
1453
|
"description": "Cyclomatic complexity at which a new or changed method is flagged for a refactor look.",
|
|
1611
1454
|
"default": 8
|
|
1612
1455
|
},
|
|
1613
|
-
"cyclomaticMustFix": {
|
|
1614
|
-
"type": "integer",
|
|
1615
|
-
"minimum": 1,
|
|
1616
|
-
"description": "Cyclomatic complexity at which a new or changed method must be decomposed before the diff closes.",
|
|
1617
|
-
"default": 12
|
|
1618
|
-
},
|
|
1619
1456
|
"requireSiblingTest": {
|
|
1620
1457
|
"type": "boolean",
|
|
1621
1458
|
"description": "When true, a new source file with no colocated sibling test is reported by the guardrails pass.",
|
|
@@ -1857,12 +1694,6 @@
|
|
|
1857
1694
|
"description": "Maximum auto-fix retry attempts per finding in /mandrel-deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.",
|
|
1858
1695
|
"default": 3
|
|
1859
1696
|
},
|
|
1860
|
-
"maxFixScopeFiles": {
|
|
1861
|
-
"type": "integer",
|
|
1862
|
-
"minimum": 1,
|
|
1863
|
-
"description": "Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5.",
|
|
1864
|
-
"default": 5
|
|
1865
|
-
},
|
|
1866
1697
|
"autoFixSeverity": {
|
|
1867
1698
|
"type": "string",
|
|
1868
1699
|
"enum": ["high", "medium"],
|
|
@@ -1898,12 +1729,12 @@
|
|
|
1898
1729
|
},
|
|
1899
1730
|
"acceptanceEval": {
|
|
1900
1731
|
"type": "object",
|
|
1901
|
-
"description": "Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds, then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the
|
|
1732
|
+
"description": "Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds (0 = scored once, no redraft), then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the scoring pass is a hard cutover (always on).",
|
|
1902
1733
|
"properties": {
|
|
1903
1734
|
"maxRounds": {
|
|
1904
1735
|
"type": "integer",
|
|
1905
|
-
"minimum":
|
|
1906
|
-
"description": "Maximum number of redraft rounds before escalation. Default 2;
|
|
1736
|
+
"minimum": 0,
|
|
1737
|
+
"description": "Maximum number of redraft rounds before escalation. Default 2; 0 means the verdict is scored once with no redraft round (Story #5313 dropped the hard ceiling and the floor-of-one clamp).",
|
|
1907
1738
|
"default": 2
|
|
1908
1739
|
}
|
|
1909
1740
|
},
|
|
@@ -2012,24 +1843,17 @@
|
|
|
2012
1843
|
},
|
|
2013
1844
|
"routing": {
|
|
2014
1845
|
"type": "object",
|
|
2015
|
-
"description": "v2 delivery-spawn routing: role-scoped boot contexts and
|
|
1846
|
+
"description": "v2 delivery-spawn routing: role-scoped boot contexts and the ceremony profile. The v1 singleDelivery epic-route kill-switch was removed in Stage 6; the freshCriticSampleRate sampling floor was retired in Story #5313.",
|
|
2016
1847
|
"properties": {
|
|
2017
1848
|
"roleScopedAgents": {
|
|
2018
1849
|
"type": "boolean",
|
|
2019
1850
|
"description": "Epic #4478 (M7-B). Kill-switch for the role-scoped boot contexts. When true (default), a converted delivery spawn (`story-worker`, `acceptance-critic`) boots on its own `.claude/agents/<role>.md` system prompt instead of re-paying the full CLAUDE.md @-import closure. When false, every converted spawn falls back to `subagent_type: general-purpose` — the instant, code-rollback-free per-consumer revert, and the universal escape for hosts that ignore `.claude/agents/`. The fallback is the full-closure agent that ran before M7-B, so flipping it off never drops a gate.",
|
|
2020
1851
|
"default": true
|
|
2021
1852
|
},
|
|
2022
|
-
"freshCriticSampleRate": {
|
|
2023
|
-
"type": "number",
|
|
2024
|
-
"minimum": 0,
|
|
2025
|
-
"maximum": 1,
|
|
2026
|
-
"description": "Epic #4478 (M7-B, Part 2). Maker-checker sampling floor. Under the standard profile, a change set touching no sensitive path routes its acceptance clusters down the contract-identical inline critic path, but this fraction of them is still forced through a fresh-context critic so a low derived level never means zero independent checking. Clamped to [0, 1]; 0 disables the floor, 1 forces every cluster fresh. Consumed by resolveCeremonyForRisk (lib/orchestration/ceremony-routing.js).",
|
|
2027
|
-
"default": 0.2
|
|
2028
|
-
},
|
|
2029
1853
|
"ceremonyProfile": {
|
|
2030
1854
|
"type": "string",
|
|
2031
1855
|
"enum": ["minimal", "standard", "strict"],
|
|
2032
|
-
"description": "Acceptance-ceremony depth. minimal = always inline critic; strict = always fresh-context critic; standard (default) = routed off the change level derived from the Story diff
|
|
1856
|
+
"description": "Acceptance-ceremony depth. minimal = always inline critic; strict = always fresh-context critic; standard (default) = routed off the change level derived from the Story diff: high or underivable → fresh, low → inline.",
|
|
2033
1857
|
"default": "standard"
|
|
2034
1858
|
},
|
|
2035
1859
|
"closeAndLand": {
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"type": "array",
|
|
162
162
|
"minItems": 1,
|
|
163
163
|
"items": { "type": "string", "minLength": 1 },
|
|
164
|
-
"description": "The gate's reasons verbatim — the same strings the
|
|
164
|
+
"description": "The gate's reasons verbatim — the same strings the gate logs, so an escalation explains itself identically on every surface."
|
|
165
165
|
},
|
|
166
166
|
"created": {
|
|
167
167
|
"type": "object",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* 1. Validate the verdict file against the verdict JSON Schema (a
|
|
18
18
|
* malformed verdict is a hard error — the loop refuses to guess).
|
|
19
19
|
* 2. Decide `proceed | redraft | block` from the per-criterion verdicts
|
|
20
|
-
* and the resolved
|
|
21
|
-
*
|
|
20
|
+
* and the resolved redraft budget (`delivery.acceptanceEval.maxRounds`;
|
|
21
|
+
* `0` means the verdict is scored once with no redraft round).
|
|
22
22
|
* 3. Emit one per-criterion `acceptance-eval` signal into the retro /
|
|
23
23
|
* feedback substrate so the retro and `/mandrel-plan` Phase 0 feedback
|
|
24
24
|
* fetch can see which acceptance items needed rework and the round
|
|
@@ -46,14 +46,19 @@
|
|
|
46
46
|
* the caller into ONE verdict — `criteria[]` in acceptance-array order — and
|
|
47
47
|
* scored here exactly once. Invoking the gate per cluster instead would burn
|
|
48
48
|
* one Story-level round per cluster (distinct fingerprints defeat the replay
|
|
49
|
-
* guard) and race the `signals.ndjson` round ledger.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* guard) and race the `signals.ndjson` round ledger. The gate reads the
|
|
50
|
+
* Story's own `acceptance[]` count off its body (Story #5313) and rejects a
|
|
51
|
+
* verdict whose `criteria[]` length differs **before** scoring, so the
|
|
52
|
+
* mistake costs no round. `--expected-criteria` is still accepted but is
|
|
53
|
+
* redundant with the derived count: when both are known they must agree.
|
|
54
|
+
* An inline-owned verdict is one file scored in one call — the cluster
|
|
55
|
+
* merge applies only to fresh critics.
|
|
52
56
|
*
|
|
53
57
|
* CLI:
|
|
54
58
|
* --story <id> Story ID (required).
|
|
55
59
|
* --verdict <path> Path to the round's verdict JSON (required).
|
|
56
|
-
* --expected-criteria <n>
|
|
60
|
+
* --expected-criteria <n> Optional; must equal the Story's acceptance[]
|
|
61
|
+
* count when the body is readable.
|
|
57
62
|
* --no-signal Suppress the signal emit (tests).
|
|
58
63
|
*
|
|
59
64
|
* Stdout: a single JSON envelope
|
|
@@ -93,6 +98,8 @@ import {
|
|
|
93
98
|
FULL_SUITE_SHAPE_WARNING,
|
|
94
99
|
isFullSuiteCommand,
|
|
95
100
|
} from './lib/orchestration/verify-credit.js';
|
|
101
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
102
|
+
import { parse as parseStoryBody } from './lib/story-body/story-body.js';
|
|
96
103
|
|
|
97
104
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
98
105
|
|
|
@@ -181,10 +188,84 @@ const MERGE_CONTRACT =
|
|
|
181
188
|
"merge every cluster's records into a single criteria[] in acceptance[] order, " +
|
|
182
189
|
'one per acceptance item, before scoring.';
|
|
183
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Read the Story's `acceptance[]` count off its body (Story #5313), so the
|
|
193
|
+
* coverage assertion no longer depends on the caller restating a number it
|
|
194
|
+
* already read. Total: any failure — no provider, an unreadable ticket, an
|
|
195
|
+
* unparseable body — yields `null`, which routes the caller to the flag (when
|
|
196
|
+
* passed) and otherwise to a stated, logged skip. It never manufactures a
|
|
197
|
+
* count.
|
|
198
|
+
*
|
|
199
|
+
* @param {{ storyId: number, config: object }} args
|
|
200
|
+
* @param {{ createProviderFn?: typeof createProvider, parseBodyFn?: typeof parseStoryBody }} [deps]
|
|
201
|
+
* @returns {Promise<number|null>}
|
|
202
|
+
*/
|
|
203
|
+
export async function readStoryAcceptanceCount(
|
|
204
|
+
{ storyId, config },
|
|
205
|
+
{ createProviderFn = createProvider, parseBodyFn = parseStoryBody } = {},
|
|
206
|
+
) {
|
|
207
|
+
try {
|
|
208
|
+
const ticket = await createProviderFn(config).getTicket(storyId);
|
|
209
|
+
const body = typeof ticket?.body === 'string' ? ticket.body : null;
|
|
210
|
+
if (body === null) return null;
|
|
211
|
+
const acceptance = parseBodyFn(body)?.body?.acceptance;
|
|
212
|
+
return Array.isArray(acceptance) && acceptance.length > 0
|
|
213
|
+
? acceptance.length
|
|
214
|
+
: null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Reconcile the body-derived count with the optional flag (Story #5313): the
|
|
222
|
+
* derived count is authoritative; a flag that disagrees with it is a wiring
|
|
223
|
+
* error worth failing on rather than a value to prefer silently.
|
|
224
|
+
*
|
|
225
|
+
* @param {{ derived: number|null, flagged: number|null }} args
|
|
226
|
+
* @returns {number|null}
|
|
227
|
+
*/
|
|
228
|
+
export function reconcileExpectedCriteria({ derived, flagged }) {
|
|
229
|
+
if (derived === null) return flagged;
|
|
230
|
+
if (flagged !== null && flagged !== derived) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`acceptance-eval: --expected-criteria ${flagged} disagrees with the Story's acceptance[] count (${derived}); drop the flag — the gate reads the count itself.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return derived;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The count the coverage assertion runs against: the body-derived count,
|
|
240
|
+
* reconciled with the optional flag, with a stated skip when neither is
|
|
241
|
+
* readable (Story #5313). Split out of `runAcceptanceEvalCli` so the CLI
|
|
242
|
+
* body's own branching stays inside its committed cyclomatic budget.
|
|
243
|
+
*
|
|
244
|
+
* @param {{ storyId: number, config: object, flagged: number|null, readAcceptanceCountImpl: typeof readStoryAcceptanceCount, logger: { warn?: Function } }} args
|
|
245
|
+
* @returns {Promise<number|null>}
|
|
246
|
+
*/
|
|
247
|
+
async function resolveExpectedCriteriaCount({
|
|
248
|
+
storyId,
|
|
249
|
+
config,
|
|
250
|
+
flagged,
|
|
251
|
+
readAcceptanceCountImpl,
|
|
252
|
+
logger,
|
|
253
|
+
}) {
|
|
254
|
+
const derived = await readAcceptanceCountImpl({ storyId, config });
|
|
255
|
+
const expected = reconcileExpectedCriteria({ derived, flagged });
|
|
256
|
+
if (expected === null) {
|
|
257
|
+
logger.warn?.(
|
|
258
|
+
'[acceptance-eval] ⚠ the Story acceptance[] count could not be read and no --expected-criteria was passed — the coverage assertion is skipped for this round.',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return expected;
|
|
262
|
+
}
|
|
263
|
+
|
|
184
264
|
/**
|
|
185
265
|
* Resolve the optional `--expected-criteria` flag to a positive integer, or
|
|
186
|
-
* `null` when the flag is absent
|
|
187
|
-
*
|
|
266
|
+
* `null` when the flag is absent. Since Story #5313 the gate derives the
|
|
267
|
+
* count from the Story body; the flag remains accepted for callers that
|
|
268
|
+
* still pass it and is checked against the derived value.
|
|
188
269
|
*
|
|
189
270
|
* Exported for tests.
|
|
190
271
|
*
|
|
@@ -225,7 +306,7 @@ export function assertCriteriaCoverage(verdict, expectedCriteria) {
|
|
|
225
306
|
const actual = Array.isArray(verdict?.criteria) ? verdict.criteria.length : 0;
|
|
226
307
|
if (actual === expectedCriteria) return;
|
|
227
308
|
throw new Error(
|
|
228
|
-
`acceptance-eval: verdict covers ${actual} criteria but
|
|
309
|
+
`acceptance-eval: verdict covers ${actual} criteria but the Story's acceptance[] count is ${expectedCriteria}. ` +
|
|
229
310
|
`${MERGE_CONTRACT} No round was consumed.`,
|
|
230
311
|
);
|
|
231
312
|
}
|
|
@@ -409,6 +490,7 @@ export async function runAcceptanceEval(
|
|
|
409
490
|
* resolveConfigImpl?: typeof resolveConfig,
|
|
410
491
|
* validateVerdictImpl?: typeof validateVerdict,
|
|
411
492
|
* runAcceptanceEvalImpl?: typeof runAcceptanceEval,
|
|
493
|
+
* readAcceptanceCountImpl?: typeof readStoryAcceptanceCount,
|
|
412
494
|
* logger?: { info: Function, warn?: Function },
|
|
413
495
|
* }} [deps]
|
|
414
496
|
* @returns {Promise<object>} the emitted envelope.
|
|
@@ -422,11 +504,12 @@ export async function runAcceptanceEvalCli(
|
|
|
422
504
|
resolveConfigImpl = resolveConfig,
|
|
423
505
|
validateVerdictImpl = validateVerdict,
|
|
424
506
|
runAcceptanceEvalImpl = runAcceptanceEval,
|
|
507
|
+
readAcceptanceCountImpl = readStoryAcceptanceCount,
|
|
425
508
|
logger = Logger,
|
|
426
509
|
} = deps;
|
|
427
510
|
const { storyId, verdictPath, expectedCriteria, emitSignal } =
|
|
428
511
|
parseCliArgs(argv);
|
|
429
|
-
const
|
|
512
|
+
const flagged = resolveExpectedCriteria(expectedCriteria);
|
|
430
513
|
|
|
431
514
|
if (!storyId) {
|
|
432
515
|
throw new Error(
|
|
@@ -461,9 +544,17 @@ export async function runAcceptanceEvalCli(
|
|
|
461
544
|
|
|
462
545
|
const verdict = validateVerdictImpl(parsed);
|
|
463
546
|
|
|
464
|
-
// Story #4951: a merged verdict must cover every acceptance[] item
|
|
465
|
-
//
|
|
466
|
-
// a free mistake.
|
|
547
|
+
// Story #4951 / #5313: a merged verdict must cover every acceptance[] item,
|
|
548
|
+
// and the count comes from the Story body itself. This runs before the
|
|
549
|
+
// round ledger is touched, so a partial cluster verdict is a free mistake.
|
|
550
|
+
const config = resolveConfigImpl();
|
|
551
|
+
const expected = await resolveExpectedCriteriaCount({
|
|
552
|
+
storyId,
|
|
553
|
+
config,
|
|
554
|
+
flagged,
|
|
555
|
+
readAcceptanceCountImpl,
|
|
556
|
+
logger,
|
|
557
|
+
});
|
|
467
558
|
assertCriteriaCoverage(verdict, expected);
|
|
468
559
|
|
|
469
560
|
// A verdict whose embedded storyId disagrees with the CLI flag is a
|
|
@@ -479,7 +570,6 @@ export async function runAcceptanceEvalCli(
|
|
|
479
570
|
// caller that injects its own scorer.
|
|
480
571
|
warnOnFullSuiteVerify(verdict, logger);
|
|
481
572
|
|
|
482
|
-
const config = resolveConfigImpl();
|
|
483
573
|
const { envelope, exitCode } = await runAcceptanceEvalImpl({
|
|
484
574
|
storyId,
|
|
485
575
|
verdict,
|
|
@@ -522,9 +612,9 @@ runAsCli(import.meta.url, main, {
|
|
|
522
612
|
['--verdict <path>', 'Path to the authored verdict JSON (required).'],
|
|
523
613
|
[
|
|
524
614
|
'--expected-criteria <n>',
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
'
|
|
615
|
+
"Optional and redundant since Story #5313: the gate reads the Story's " +
|
|
616
|
+
'acceptance[] count itself and rejects a shorter verdict before ' +
|
|
617
|
+
'scoring. When passed it must agree with that count.',
|
|
528
618
|
],
|
|
529
619
|
[
|
|
530
620
|
'--no-signal',
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ceremony-derive.js — derive a Story's acceptance ceremony from ground truth
|
|
5
|
+
* (Story #5313).
|
|
6
|
+
*
|
|
7
|
+
* The deliver digest used to hand the worker a three-module import block to
|
|
8
|
+
* paste into `node --input-type=module -e` — compute the change set, derive
|
|
9
|
+
* the level, resolve the ceremony — and every hand-carried incantation is a
|
|
10
|
+
* transcription risk (the object-vs-string `derivedLevel` slip alone routed a
|
|
11
|
+
* whole class of Stories to the null fail-safe). This CLI is that block,
|
|
12
|
+
* scripted: one invocation, one JSON object, computed from the branch rather
|
|
13
|
+
* than recalled.
|
|
14
|
+
*
|
|
15
|
+
* 1. Compute the change set ONCE for `<base>...story-<id>`
|
|
16
|
+
* (`lib/orchestration/change-set.js`).
|
|
17
|
+
* 2. Derive the change level and the sensitive-path classes from it
|
|
18
|
+
* (`lib/orchestration/review-depth.js#deriveChangeLevel`).
|
|
19
|
+
* 3. Resolve the ceremony for the configured profile
|
|
20
|
+
* (`lib/orchestration/ceremony-routing.js#resolveCeremonyForRisk`).
|
|
21
|
+
*
|
|
22
|
+
* Stdout: a single JSON object —
|
|
23
|
+
* { storyId, baseRef, headRef, files, enumerated, level, classes, profile,
|
|
24
|
+
* mode, reason, verdictOwner }
|
|
25
|
+
*
|
|
26
|
+
* `files` is the one change set every acceptance critic must be handed; the
|
|
27
|
+
* caller never lets a critic re-enumerate it. `files: null` means the diff
|
|
28
|
+
* could not be enumerated, which routes to the fail-safe fresh critic.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 on a derived decision (including the `null` fail-safe — an
|
|
31
|
+
* unenumerable diff is a decision, not an error), 1 on a usage error.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { parseArgs } from 'node:util';
|
|
35
|
+
|
|
36
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
37
|
+
import { getDeliveryRouting } from './lib/config/delivery-routing.js';
|
|
38
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
39
|
+
import { resolveCeremonyForRisk } from './lib/orchestration/ceremony-routing.js';
|
|
40
|
+
import { computeChangeSet } from './lib/orchestration/change-set.js';
|
|
41
|
+
import { deriveChangeLevel } from './lib/orchestration/review-depth.js';
|
|
42
|
+
|
|
43
|
+
const USAGE = {
|
|
44
|
+
invocation:
|
|
45
|
+
'node .agents/scripts/ceremony-derive.js --story <id> [--base <ref>] [--cwd <path>]',
|
|
46
|
+
summary:
|
|
47
|
+
'Compute the Story change set once, derive its change level and sensitive-path classes, resolve the acceptance ceremony, and print one JSON object.',
|
|
48
|
+
flags: [
|
|
49
|
+
['--story <id>', 'Story issue number; the head ref is story-<id>.'],
|
|
50
|
+
[
|
|
51
|
+
'--base <ref>',
|
|
52
|
+
'Base ref for the three-dot diff (default: project.baseBranch, normally main).',
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
'--cwd <path>',
|
|
56
|
+
'Checkout to run the diff in (default: the current directory).',
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse the CLI argv. Exported for tests.
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} argv
|
|
65
|
+
* @returns {{ storyId: number|null, base: string|null, cwd: string|null }}
|
|
66
|
+
*/
|
|
67
|
+
export function parseArgv(argv) {
|
|
68
|
+
const { values } = parseArgs({
|
|
69
|
+
args: argv,
|
|
70
|
+
options: {
|
|
71
|
+
story: { type: 'string' },
|
|
72
|
+
base: { type: 'string' },
|
|
73
|
+
cwd: { type: 'string' },
|
|
74
|
+
},
|
|
75
|
+
strict: false,
|
|
76
|
+
});
|
|
77
|
+
const storyId = Number.parseInt(values.story ?? '', 10);
|
|
78
|
+
return {
|
|
79
|
+
storyId: Number.isInteger(storyId) && storyId > 0 ? storyId : null,
|
|
80
|
+
base: values.base ?? null,
|
|
81
|
+
cwd: values.cwd ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The derivation itself — pure over its injected collaborators, so tests can
|
|
87
|
+
* pin the envelope without spawning git or reading a config.
|
|
88
|
+
*
|
|
89
|
+
* @param {{ storyId: number, baseRef: string, cwd: string, ceremonyProfile: string }} input
|
|
90
|
+
* @param {{
|
|
91
|
+
* computeChangeSetImpl?: typeof computeChangeSet,
|
|
92
|
+
* deriveChangeLevelImpl?: typeof deriveChangeLevel,
|
|
93
|
+
* resolveCeremonyImpl?: typeof resolveCeremonyForRisk,
|
|
94
|
+
* }} [deps]
|
|
95
|
+
* @returns {{
|
|
96
|
+
* storyId: number, baseRef: string, headRef: string,
|
|
97
|
+
* files: string[]|null, enumerated: boolean,
|
|
98
|
+
* level: 'low'|'high'|null, classes: string[],
|
|
99
|
+
* profile: string, mode: 'fresh'|'inline', reason: string,
|
|
100
|
+
* verdictOwner: 'fresh-critic'|'inline-self-eval',
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
export function deriveCeremony(
|
|
104
|
+
{ storyId, baseRef, cwd, ceremonyProfile },
|
|
105
|
+
deps = {},
|
|
106
|
+
) {
|
|
107
|
+
const {
|
|
108
|
+
computeChangeSetImpl = computeChangeSet,
|
|
109
|
+
deriveChangeLevelImpl = deriveChangeLevel,
|
|
110
|
+
resolveCeremonyImpl = resolveCeremonyForRisk,
|
|
111
|
+
} = deps;
|
|
112
|
+
const headRef = `story-${storyId}`;
|
|
113
|
+
const changeSet = computeChangeSetImpl({ baseRef, headRef, cwd });
|
|
114
|
+
const { level, classes } = deriveChangeLevelImpl({
|
|
115
|
+
changedFiles: changeSet.files,
|
|
116
|
+
});
|
|
117
|
+
// `derivedLevel` is the level STRING — handing the `{ level, classes }`
|
|
118
|
+
// object here was the transcription slip this CLI exists to retire.
|
|
119
|
+
const ceremony = resolveCeremonyImpl({
|
|
120
|
+
derivedLevel: level,
|
|
121
|
+
clusterIndex: 0,
|
|
122
|
+
ceremonyProfile,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
storyId,
|
|
126
|
+
baseRef,
|
|
127
|
+
headRef,
|
|
128
|
+
files: changeSet.files,
|
|
129
|
+
enumerated: changeSet.enumerated,
|
|
130
|
+
level,
|
|
131
|
+
classes,
|
|
132
|
+
profile: ceremony.profile,
|
|
133
|
+
mode: ceremony.mode,
|
|
134
|
+
reason: ceremony.reason,
|
|
135
|
+
verdictOwner: ceremony.verdictOwner,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* CLI shell: resolve the config-backed defaults, derive, print.
|
|
141
|
+
*
|
|
142
|
+
* @param {string[]} [argv]
|
|
143
|
+
* @param {{
|
|
144
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
145
|
+
* stdout?: { write: (s: string) => void },
|
|
146
|
+
* cwd?: string,
|
|
147
|
+
* } & Parameters<typeof deriveCeremony>[1]} [deps]
|
|
148
|
+
* @returns {Promise<object>} the printed envelope
|
|
149
|
+
*/
|
|
150
|
+
export async function runCeremonyDeriveCli(
|
|
151
|
+
argv = process.argv.slice(2),
|
|
152
|
+
deps = {},
|
|
153
|
+
) {
|
|
154
|
+
const {
|
|
155
|
+
resolveConfigImpl = resolveConfig,
|
|
156
|
+
stdout = process.stdout,
|
|
157
|
+
cwd: defaultCwd = process.cwd(),
|
|
158
|
+
...derivationDeps
|
|
159
|
+
} = deps;
|
|
160
|
+
const { storyId, base, cwd } = parseArgv(argv);
|
|
161
|
+
if (!storyId) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'ceremony-derive: --story <id> is required (a positive integer).',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const workCwd = cwd ?? defaultCwd;
|
|
167
|
+
const config = resolveConfigImpl({ cwd: workCwd });
|
|
168
|
+
const envelope = deriveCeremony(
|
|
169
|
+
{
|
|
170
|
+
storyId,
|
|
171
|
+
baseRef: base ?? config?.project?.baseBranch ?? 'main',
|
|
172
|
+
cwd: workCwd,
|
|
173
|
+
ceremonyProfile: getDeliveryRouting(config).ceremonyProfile,
|
|
174
|
+
},
|
|
175
|
+
derivationDeps,
|
|
176
|
+
);
|
|
177
|
+
stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
178
|
+
return envelope;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
await runCeremonyDeriveCli();
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
runAsCli(import.meta.url, main, {
|
|
187
|
+
source: 'ceremony-derive',
|
|
188
|
+
propagateExitCode: true,
|
|
189
|
+
errorPrefix: '[ceremony-derive] ❌ Fatal error',
|
|
190
|
+
usage: USAGE,
|
|
191
|
+
});
|