mandrel 2.10.0 → 2.12.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/docs/configuration.md +35 -33
- package/.agents/rules/orchestration-error-handling.md +9 -1
- package/.agents/schemas/agentrc.schema.json +13 -8
- package/.agents/scripts/acceptance-eval.js +9 -5
- package/.agents/scripts/lib/audit-suite/audit-rules-reader.js +48 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -26
- package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
- package/.agents/scripts/lib/config/quality.js +13 -0
- package/.agents/scripts/lib/config-settings-schema.js +12 -16
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +561 -184
- package/.agents/scripts/lib/orchestration/plan-context.js +69 -10
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +117 -60
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +21 -15
- package/.agents/scripts/lib/orchestration/resolve-stories.js +28 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +186 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +21 -3
- package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
- package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
- package/.agents/scripts/plan-context.js +23 -5
- package/.agents/scripts/resolve-stories.js +2 -0
- package/.agents/workflows/deliver.md +28 -28
- package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
- package/.agents/workflows/helpers/deliver-digest.md +126 -0
- package/.agents/workflows/helpers/deliver-reference.md +30 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +38 -12
- package/.agents/workflows/helpers/deliver-story.md +34 -37
- package/.agents/workflows/helpers/plan-reference.md +79 -44
- package/.agents/workflows/plan.md +11 -10
- package/docs/CHANGELOG.md +31 -0
- package/lib/cli/registry.js +31 -14
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
- package/package.json +1 -1
|
@@ -102,10 +102,9 @@ top-level keys are validation errors.
|
|
|
102
102
|
| `codebaseSnapshot.include` | No | `array<string>` | — | — |
|
|
103
103
|
| `codebaseSnapshot.exclude` | No | `array<string>` | — | — |
|
|
104
104
|
| `codebaseSnapshot.recentCommitWindow` | No | `integer` | — | — |
|
|
105
|
-
| `complexityGate` | No | `object` | — |
|
|
106
|
-
| `complexityGate.enabled` | No | `boolean` | — | Master switch. When false,
|
|
107
|
-
| `complexityGate.
|
|
108
|
-
| `complexityGate.maxArtifacts` | No | `integer` | — | Enumerated-artifact ceiling for the lite path. A seed enumerating more than this many candidate artifacts is multi-capability and takes the full path. Default 1. |
|
|
105
|
+
| `complexityGate` | No | `object` | — | 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. |
|
|
106
|
+
| `complexityGate.enabled` | No | `boolean` | — | Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true. |
|
|
107
|
+
| `complexityGate.maxArtifacts` | No | `integer` | — | Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1. |
|
|
109
108
|
| `failOnSharedEditors` | No | `boolean` | — | When true, upgrade shared-editor conflict findings to hard errors (default false — advisory soft findings only). |
|
|
110
109
|
| `requireExplicitCrossStoryDeps` | No | `boolean` | — | When true, upgrade implicit cross-Story dependency findings to hard errors (default false — advisory soft findings only). |
|
|
111
110
|
| `failOnRegistryConflicts` | No | `boolean` | — | When true, upgrade cross-cutting registry conflict findings to hard errors (default false). |
|
|
@@ -189,6 +188,7 @@ top-level keys are validation errors.
|
|
|
189
188
|
| `quality.gates.maintainability.floors` | No | `object<map>` | — | — |
|
|
190
189
|
| `quality.gates.maintainability.components` | No | `object<map>` | — | — |
|
|
191
190
|
| `quality.gates.maintainability.targetDirs` | No | `string[]` or `{ append?, prepend? }` | — | Directories whose JS sources the maintainability gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/` plus `tests/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal. |
|
|
191
|
+
| `quality.gates.maintainability.refreshTag` | No | `string` | — | — |
|
|
192
192
|
| `quality.gates.maintainability.refreshTimeoutMs` | No | `integer` | — | Bounded timeout (ms) for `npm run maintainability:update` spawned by the baseline-attribution refresh path. Mirrors `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked`. Default 60000 (Story #2165). |
|
|
193
193
|
| `quality.gates.maintainability.ignoreGlobs` | No | `array<string>` | — | Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from MI discovery before scoring. Orthogonal to `components` (grouping) — a file excluded here never appears in any component bucket. Absent or empty preserves the existing IGNORED_DIRS-only behaviour (Story #3217). |
|
|
194
194
|
| `quality.gates.mutation` | No | `object` | — | Nested configuration block. |
|
|
@@ -351,36 +351,38 @@ scans `.agents/scripts/**`, `src/**`, `lib/**`, `app/**`, `packages/**` and
|
|
|
351
351
|
`exclude` drops `node_modules`, build dirs, and test files. Override `include`
|
|
352
352
|
only when the project's source layout differs.
|
|
353
353
|
|
|
354
|
-
- **`complexityGate`.**
|
|
355
|
-
|
|
356
|
-
work but imposes a large fixed
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
`
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
354
|
+
- **`complexityGate`.** Shape-derived ceremony-lite routing (Story #4722,
|
|
355
|
+
superseding the word-count gate of Stories #4683/#4707). The full ceremony
|
|
356
|
+
buys measurable quality on capability-sized work but imposes a large fixed
|
|
357
|
+
cost premium on genuinely trivial scopes — and seed word count is the wrong
|
|
358
|
+
proxy in both directions (a detailed prompt can describe trivial work, a
|
|
359
|
+
terse one complex work), so `maxSeedWords` was **removed** in the hard
|
|
360
|
+
cutover (a config still setting it is rejected as an additional property).
|
|
361
|
+
Routing is now staged on the objective shape of the work: `/plan`'s context
|
|
362
|
+
envelope emits advisory `complexitySignals` (enumerated-artifact count,
|
|
363
|
+
risk-heuristic hits, repo state of predicted paths, sensitive-path classes)
|
|
364
|
+
with **no routing authority**; the planner authors the trivial-vs-standard
|
|
365
|
+
verdict via `plan-persist.js --route-downgrade-reason "<why>"` (recorded on
|
|
366
|
+
every created Story's `story-plan-state` checkpoint); persist validates a
|
|
367
|
+
lite claim against each authored Story's own shape (`changes[]` count,
|
|
368
|
+
acceptance count, creates-vs-refactors mix, sensitive-path classes — the
|
|
369
|
+
framework constants `STORY_SHAPE_CEILINGS`) and **fails closed to `full`**
|
|
370
|
+
when the shape exceeds the ceilings; and `/deliver` re-derives the route
|
|
371
|
+
from the fetched Story body via the same shape function at dispatch. The
|
|
372
|
+
`route::lite` label is a human-visible hint only — a lost label cannot
|
|
373
|
+
misroute delivery. A lite-shaped Story executes inline (no story-worker or
|
|
374
|
+
acceptance-critic sub-agent fan-out); sensitivity always wins — a footprint
|
|
375
|
+
intersecting a sensitive-path class routes `full` and keeps its fresh
|
|
376
|
+
critic. The lite path **never** relaxes a non-negotiable: it still produces
|
|
377
|
+
a Story ticket, still lands via a PR to `main`, still runs every repo
|
|
378
|
+
quality gate, and still honours `rules/security-baseline.md` — those gates
|
|
379
|
+
run in `single-story-close.js` regardless of route. **Knobs:** `enabled`
|
|
380
|
+
(default `true`; `false` disables lite routing everywhere) and
|
|
381
|
+
`maxArtifacts` (default `1` — a signal threshold, not a router). Defaults
|
|
382
|
+
live on `DEFAULT_COMPLEXITY_GATE` in
|
|
375
383
|
[`lib/orchestration/complexity-gate.js`](../scripts/lib/orchestration/complexity-gate.js);
|
|
376
|
-
a malformed or negative
|
|
377
|
-
the lite path.
|
|
378
|
-
planner may downgrade a `full` verdict to `lite` only via
|
|
379
|
-
`plan-persist.js --route-downgrade-reason "<why>"` — the reason is recorded
|
|
380
|
-
on every created Story's `story-plan-state` checkpoint, so the judgment is
|
|
381
|
-
auditable and never a silent gate change. A lite-routed plan's Stories carry
|
|
382
|
-
the runtime-derived `route::lite` label, which `/deliver` reads to execute
|
|
383
|
-
the Story inline (no sub-agent fan-out) with every close gate unchanged.
|
|
384
|
+
a malformed or negative value falls back to the default rather than
|
|
385
|
+
widening the lite path.
|
|
384
386
|
|
|
385
387
|
### `delivery`
|
|
386
388
|
|
|
@@ -46,8 +46,16 @@ success output is a per-turn tax on the whole session — not a one-time cost.
|
|
|
46
46
|
their drivers — emit them single-line (`JSON.stringify(x)`), never
|
|
47
47
|
pretty-printed (`null, 2` only adds resident bytes). Pretty output is
|
|
48
48
|
reserved for explicit opt-in flags (`--pretty`).
|
|
49
|
+
- **Streamed child output counts too (Story #4736).** A script that pipes a
|
|
50
|
+
child process's stdout/stderr through to the caller is emitting that output
|
|
51
|
+
as its own. `single-story-close.js` streamed every close-validation gate —
|
|
52
|
+
the whole of `npm test` included — and blew the budget by ~25× on a *passing*
|
|
53
|
+
close. Capture it to an artifact instead
|
|
54
|
+
([`single-story-close/gate-log.js`](../scripts/lib/orchestration/single-story-close/gate-log.js)),
|
|
55
|
+
emit the digest, and **replay the tail inline on failure** — the bound is a
|
|
56
|
+
success-path bound, and a red gate's evidence belongs in front of the caller.
|
|
49
57
|
- **Escape hatch.** `MANDREL_RESULT_DETAIL=inline` restores inline full
|
|
50
58
|
detail for interactive debugging; scripts using `emitTerseResult` honor it
|
|
51
|
-
automatically.
|
|
59
|
+
automatically. `AGENT_LOG_LEVEL=verbose` restores live gate streaming.
|
|
52
60
|
- **stdout purity is unchanged.** Scripts whose stdout is a machine contract
|
|
53
61
|
(Story #2278) keep logs on stderr; the digest is the *only* stdout line.
|
|
@@ -329,21 +329,16 @@
|
|
|
329
329
|
},
|
|
330
330
|
"complexityGate": {
|
|
331
331
|
"type": "object",
|
|
332
|
-
"description": "
|
|
332
|
+
"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.",
|
|
333
333
|
"properties": {
|
|
334
334
|
"enabled": {
|
|
335
335
|
"type": "boolean",
|
|
336
|
-
"description": "Master switch. When false,
|
|
337
|
-
},
|
|
338
|
-
"maxSeedWords": {
|
|
339
|
-
"type": "integer",
|
|
340
|
-
"minimum": 0,
|
|
341
|
-
"description": "Seed prose word ceiling for the lite path. A seed above this many words is not trivial and takes the full path. Default 150."
|
|
336
|
+
"description": "Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true."
|
|
342
337
|
},
|
|
343
338
|
"maxArtifacts": {
|
|
344
339
|
"type": "integer",
|
|
345
340
|
"minimum": 0,
|
|
346
|
-
"description": "Enumerated-artifact
|
|
341
|
+
"description": "Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1."
|
|
347
342
|
}
|
|
348
343
|
},
|
|
349
344
|
"additionalProperties": false
|
|
@@ -844,6 +839,16 @@
|
|
|
844
839
|
"$ref": "#/$defs/listOrExtenderOfStrings",
|
|
845
840
|
"description": "Directories whose JS sources the maintainability gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/` plus `tests/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal."
|
|
846
841
|
},
|
|
842
|
+
"refreshTag": {
|
|
843
|
+
"allOf": [
|
|
844
|
+
{
|
|
845
|
+
"$ref": "#/$defs/safeString"
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"minLength": 1
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
},
|
|
847
852
|
"refreshTimeoutMs": {
|
|
848
853
|
"type": "integer",
|
|
849
854
|
"minimum": 1,
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* acceptance-eval.js — bounded per-Story acceptance self-eval gate (Story #3819).
|
|
5
5
|
*
|
|
6
|
-
* The Story-implementation phase runs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The Story-implementation phase runs ONE verdict-owner per acceptance
|
|
7
|
+
* cluster (Story #4723) — the fresh-context critic when
|
|
8
|
+
* `ceremony-routing.js` sensitivity-routes the cluster fresh, the
|
|
9
|
+
* contract-identical inline self-eval otherwise — which scores the
|
|
10
|
+
* caller-injected change set against each inline `acceptance[]` item and
|
|
11
|
+
* emits one verdict file per round
|
|
9
12
|
* (`.agents/schemas/acceptance-eval-verdict.schema.json`). This CLI is the
|
|
10
|
-
* deterministic
|
|
11
|
-
*
|
|
13
|
+
* deterministic SCORER of that single authored verdict — it validates and
|
|
14
|
+
* decides, it never re-scores the criteria as an independent additional
|
|
15
|
+
* pass — turning the verdict into the loop's next action:
|
|
12
16
|
*
|
|
13
17
|
* 1. Validate the verdict file against the verdict JSON Schema (a
|
|
14
18
|
* malformed verdict is a hard error — the loop refuses to guess).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-suite/audit-rules-reader.js — the one synchronous reader of the
|
|
3
|
+
* `audit-rules.json` manifest, memoized for the process lifetime.
|
|
4
|
+
*
|
|
5
|
+
* The manifest is shipped framework configuration resolved from one fixed
|
|
6
|
+
* path per process, and the shape-derivation path reads it once per Story at
|
|
7
|
+
* resolve AND persist — an un-memoized read is pure repeated I/O (measured
|
|
8
|
+
* 221 µs/op raw vs 5.5 µs seamed on the run adhoc-4722-4723 audit). Only a
|
|
9
|
+
* successful parse is cached: a read failure stays a per-call throw so a
|
|
10
|
+
* caller can observe a manifest that becomes readable later. Tests never
|
|
11
|
+
* reach this read — they inject fixture rules through the callers'
|
|
12
|
+
* `injectedRules` seam.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
|
|
18
|
+
|
|
19
|
+
/** Process-lifetime memo of the parsed manifest (successful parses only). */
|
|
20
|
+
let auditRulesCache = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read and parse the `audit-rules.json` manifest synchronously from the
|
|
24
|
+
* project's configured `schemasRoot`. Shared by the synchronous, ticket-free
|
|
25
|
+
* readers (`resolveLensTier`, `selectLocalLenses`,
|
|
26
|
+
* `selectSensitivePathClasses`) so the path resolution and read-failure
|
|
27
|
+
* handling live in one place rather than being duplicated per reader.
|
|
28
|
+
*
|
|
29
|
+
* @returns {{ audits?: Record<string, object> }} Parsed manifest.
|
|
30
|
+
* @throws {Error} When the manifest cannot be read or parsed.
|
|
31
|
+
*/
|
|
32
|
+
export function readAuditRulesSync() {
|
|
33
|
+
if (auditRulesCache !== null) return auditRulesCache;
|
|
34
|
+
const config = resolveConfig();
|
|
35
|
+
const rulesPath = path.join(
|
|
36
|
+
PROJECT_ROOT,
|
|
37
|
+
getPaths(config).schemasRoot,
|
|
38
|
+
'audit-rules.json',
|
|
39
|
+
);
|
|
40
|
+
try {
|
|
41
|
+
auditRulesCache = JSON.parse(readFileSync(rulesPath, 'utf8'));
|
|
42
|
+
return auditRulesCache;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`audit-suite: failed to read audit-rules from ${rulesPath}: ${err.message}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -26,6 +26,7 @@ import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
|
|
|
26
26
|
import { softFailOrThrow } from '../degraded-mode.js';
|
|
27
27
|
import { gitSpawn } from '../git-utils.js';
|
|
28
28
|
import { withTimeout } from '../util/with-timeout.js';
|
|
29
|
+
import { readAuditRulesSync } from './audit-rules-reader.js';
|
|
29
30
|
|
|
30
31
|
const DEFAULT_GIT_TIMEOUT_MS = 30000;
|
|
31
32
|
|
|
@@ -101,32 +102,6 @@ export const LENS_TIERS = Object.freeze(['local', 'cumulative', 'global']);
|
|
|
101
102
|
* manifest cannot be read, or the registered entry carries a scope outside
|
|
102
103
|
* {@link LENS_TIERS}.
|
|
103
104
|
*/
|
|
104
|
-
/**
|
|
105
|
-
* Read and parse the `audit-rules.json` manifest synchronously from the
|
|
106
|
-
* project's configured `schemasRoot`. Shared by the synchronous, ticket-free
|
|
107
|
-
* readers ({@link resolveLensTier}, {@link selectLocalLenses}) so the path
|
|
108
|
-
* resolution and read-failure handling live in one place rather than being
|
|
109
|
-
* duplicated per reader.
|
|
110
|
-
*
|
|
111
|
-
* @returns {{ audits?: Record<string, object> }} Parsed manifest.
|
|
112
|
-
* @throws {Error} When the manifest cannot be read or parsed.
|
|
113
|
-
*/
|
|
114
|
-
function readAuditRulesSync() {
|
|
115
|
-
const config = resolveConfig();
|
|
116
|
-
const rulesPath = path.join(
|
|
117
|
-
PROJECT_ROOT,
|
|
118
|
-
getPaths(config).schemasRoot,
|
|
119
|
-
'audit-rules.json',
|
|
120
|
-
);
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(readFileSync(rulesPath, 'utf8'));
|
|
123
|
-
} catch (err) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`audit-suite: failed to read audit-rules from ${rulesPath}: ${err.message}`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
105
|
export function resolveLensTier(lens) {
|
|
131
106
|
const rulesData = readAuditRulesSync();
|
|
132
107
|
|
|
@@ -117,6 +117,39 @@ export function resolveBundleSizeEnvOverrides(env) {
|
|
|
117
117
|
return { acknowledged, overrides };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Pure helper: resolve the one-shot maintainability refresh/acknowledge flag
|
|
122
|
+
* (Story #4731). This is the env-parity sibling of
|
|
123
|
+
* `resolveBundleSizeEnvOverrides`: `MAINTAINABILITY_REFRESH=1` (or `true`,
|
|
124
|
+
* case-insensitive) tells `check-baselines --gate maintainability` to demote
|
|
125
|
+
* this run's head-vs-base maintainability regressions to `unchanged` for this
|
|
126
|
+
* invocation only. Floors still apply — an acknowledged run can still fail on
|
|
127
|
+
* an absolute floor breach (e.g. a row below `min` 70); only the
|
|
128
|
+
* ratchet-vs-base regression comparison is suspended.
|
|
129
|
+
*
|
|
130
|
+
* Unlike bundle-size, maintainability also has a **commit-tagged** trigger
|
|
131
|
+
* (a `baseline-refresh:`-tagged commit in the compared range that touches the
|
|
132
|
+
* maintainability baseline file) resolved in the evaluate phase — this env
|
|
133
|
+
* flag is the manual override the two share by shape. Neither is persisted:
|
|
134
|
+
* the next run without the flag / tag re-enforces the ratchet at full
|
|
135
|
+
* strength automatically.
|
|
136
|
+
*
|
|
137
|
+
* Accepted truthy values: `1`, `true` (case-insensitive). Anything else
|
|
138
|
+
* (including unset/empty) resolves to `acknowledged: false`.
|
|
139
|
+
*
|
|
140
|
+
* @param {NodeJS.ProcessEnv} env
|
|
141
|
+
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
142
|
+
*/
|
|
143
|
+
export function resolveMaintainabilityRefreshOverrides(env) {
|
|
144
|
+
const raw = env?.MAINTAINABILITY_REFRESH;
|
|
145
|
+
const acknowledged =
|
|
146
|
+
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
147
|
+
const overrides = acknowledged
|
|
148
|
+
? [`acknowledged=true (MAINTAINABILITY_REFRESH=${raw})`]
|
|
149
|
+
: [];
|
|
150
|
+
return { acknowledged, overrides };
|
|
151
|
+
}
|
|
152
|
+
|
|
120
153
|
/**
|
|
121
154
|
* Pure helper: resolve the effective MI tolerance by layering precedence:
|
|
122
155
|
* 1. `CRAP_TOLERANCE` env-var (CI override — the baseline-refresh-
|
|
Binary file
|
|
@@ -263,6 +263,11 @@ export async function runCrapPreview({
|
|
|
263
263
|
const requireCoverage = crap.requireCoverage !== false;
|
|
264
264
|
const coveragePath = crap.coveragePath ?? 'coverage/coverage-final.json';
|
|
265
265
|
const coverage = loadCoverage(path.resolve(cwd, coveragePath));
|
|
266
|
+
// Story #4731 (AC-3) — feed the CRAP regression compare the *configured*
|
|
267
|
+
// crap tolerance (env override → `gates.crap.tolerance` → framework default)
|
|
268
|
+
// so `compareCrap` demotes positive deltas at or under tolerance rather than
|
|
269
|
+
// failing on any positive delta; over-tolerance deltas still fail. This keeps
|
|
270
|
+
// the pre-commit/pre-push preview aligned with the authoritative gate.
|
|
266
271
|
const { newMethodCeiling, tolerance } = resolveCrapEnvOverrides(
|
|
267
272
|
crap,
|
|
268
273
|
process.env,
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/* node:coverage ignore file -- AJV schema declaration (data-as-code) */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
GATE_BASE,
|
|
5
|
+
LIST_OR_EXTENDER_OF_STRINGS,
|
|
6
|
+
SAFE_STRING,
|
|
7
|
+
} from './shared.js';
|
|
4
8
|
|
|
5
9
|
export const MAINTAINABILITY_GATE = {
|
|
6
10
|
type: 'object',
|
|
7
11
|
properties: {
|
|
8
12
|
...GATE_BASE,
|
|
9
13
|
targetDirs: LIST_OR_EXTENDER_OF_STRINGS,
|
|
14
|
+
// Story #4731 — commit-subject substring that acknowledges a deliberate
|
|
15
|
+
// maintainability baseline refresh in the compared range. Mirrors the CRAP
|
|
16
|
+
// gate's `refreshTag`; a range commit carrying it that touches the baseline
|
|
17
|
+
// file demotes head-vs-base regressions (floors still enforced).
|
|
18
|
+
refreshTag: { ...SAFE_STRING, minLength: 1 },
|
|
10
19
|
// Story #2165 — bounded timeout for `npm run maintainability:update`
|
|
11
20
|
// spawned by the baseline-attribution refresh path. Mirrors
|
|
12
21
|
// `coverage.timeoutMs` (Story #2142).
|
|
@@ -110,6 +110,13 @@ export const MAINTAINABILITY_GATE_DEFAULTS = Object.freeze({
|
|
|
110
110
|
tolerance: DEFAULT_MI_TOLERANCE,
|
|
111
111
|
floors: DEFAULT_MI_FLOORS,
|
|
112
112
|
targetDirs: Object.freeze([]),
|
|
113
|
+
// Story #4731 — the commit-subject substring that acknowledges a deliberate
|
|
114
|
+
// maintainability baseline refresh in the compared range. Mirrors the CRAP
|
|
115
|
+
// gate's `refreshTag`; the evaluate phase demotes head-vs-base regressions
|
|
116
|
+
// (floors still enforced) when a range commit carrying this tag touches the
|
|
117
|
+
// baseline file. Kept identical to the CRAP default so one refresh commit
|
|
118
|
+
// can acknowledge both gates.
|
|
119
|
+
refreshTag: 'baseline-refresh:',
|
|
113
120
|
// Story #2165 — bounded timeout (ms) for `npm run maintainability:update`
|
|
114
121
|
// spawned by the baseline-attribution refresh path. Defaults to 60 s.
|
|
115
122
|
refreshTimeoutMs: 60_000,
|
|
@@ -154,6 +161,7 @@ const MI_GATE_KEYS = new Set([
|
|
|
154
161
|
'tolerance',
|
|
155
162
|
'floors',
|
|
156
163
|
'targetDirs',
|
|
164
|
+
'refreshTag',
|
|
157
165
|
'refreshTimeoutMs',
|
|
158
166
|
'ignoreGlobs',
|
|
159
167
|
]);
|
|
@@ -268,6 +276,7 @@ function resolveMaintainabilityQuality(userBlock, gateScoping) {
|
|
|
268
276
|
if (userBlock == null || typeof userBlock !== 'object') {
|
|
269
277
|
return {
|
|
270
278
|
targetDirs: [...defaults.targetDirs],
|
|
279
|
+
refreshTag: defaults.refreshTag,
|
|
271
280
|
refreshTimeoutMs: defaults.refreshTimeoutMs,
|
|
272
281
|
ignoreGlobs: [...defaults.ignoreGlobs],
|
|
273
282
|
defaultScope: scoping.defaultScope,
|
|
@@ -277,6 +286,10 @@ function resolveMaintainabilityQuality(userBlock, gateScoping) {
|
|
|
277
286
|
warnUnknownKeys(userBlock, MI_GATE_KEYS, 'quality.gates.maintainability');
|
|
278
287
|
const out = {
|
|
279
288
|
targetDirs: resolveListValue(defaults.targetDirs, userBlock.targetDirs),
|
|
289
|
+
refreshTag:
|
|
290
|
+
typeof userBlock.refreshTag === 'string' && userBlock.refreshTag.length
|
|
291
|
+
? userBlock.refreshTag
|
|
292
|
+
: defaults.refreshTag,
|
|
280
293
|
refreshTimeoutMs: resolvePositiveIntegerMs(
|
|
281
294
|
userBlock.refreshTimeoutMs,
|
|
282
295
|
defaults.refreshTimeoutMs,
|
|
@@ -278,34 +278,30 @@ const PLANNING_SCHEMA = {
|
|
|
278
278
|
properties: {
|
|
279
279
|
riskHeuristics: LIST_OR_EXTENDER_OF_STRINGS,
|
|
280
280
|
codebaseSnapshot: CODEBASE_SNAPSHOT_SCHEMA,
|
|
281
|
-
// Story #
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
281
|
+
// Story #4722 (superseding #4683's word-count gate) — shape-derived
|
|
282
|
+
// ceremony-lite routing. Complexity routes on the objective shape of the
|
|
283
|
+
// authored work (changes[] count, acceptance count, creates-vs-refactors
|
|
284
|
+
// mix, sensitive-path classes), never on seed word count: `maxSeedWords`
|
|
285
|
+
// was removed in the hard cutover and is rejected as an additional
|
|
286
|
+
// property. The lite path never relaxes a non-negotiable (Story ticket,
|
|
287
|
+
// PR-to-main, repo gates, security baseline). Defaults live on
|
|
288
|
+
// DEFAULT_COMPLEXITY_GATE in `lib/orchestration/complexity-gate.js`;
|
|
289
|
+
// shape ceilings are the framework constants STORY_SHAPE_CEILINGS.
|
|
288
290
|
complexityGate: {
|
|
289
291
|
type: 'object',
|
|
290
292
|
description:
|
|
291
|
-
'
|
|
293
|
+
'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.',
|
|
292
294
|
properties: {
|
|
293
295
|
enabled: {
|
|
294
296
|
type: 'boolean',
|
|
295
297
|
description:
|
|
296
|
-
'Master switch. When false,
|
|
297
|
-
},
|
|
298
|
-
maxSeedWords: {
|
|
299
|
-
type: 'integer',
|
|
300
|
-
minimum: 0,
|
|
301
|
-
description:
|
|
302
|
-
'Seed prose word ceiling for the lite path. A seed above this many words is not trivial and takes the full path. Default 150.',
|
|
298
|
+
'Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true.',
|
|
303
299
|
},
|
|
304
300
|
maxArtifacts: {
|
|
305
301
|
type: 'integer',
|
|
306
302
|
minimum: 0,
|
|
307
303
|
description:
|
|
308
|
-
'Enumerated-artifact
|
|
304
|
+
'Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1.',
|
|
309
305
|
},
|
|
310
306
|
},
|
|
311
307
|
additionalProperties: false,
|
|
@@ -35,6 +35,19 @@
|
|
|
35
35
|
* sub-agent. This module takes the cluster index as an INPUT and returns a
|
|
36
36
|
* decision for that one cluster; it has no way to add or remove clusters.
|
|
37
37
|
*
|
|
38
|
+
* ## One verdict-owner per cluster (Story #4723)
|
|
39
|
+
*
|
|
40
|
+
* The resolved decision names the cluster's **single verdict owner** via
|
|
41
|
+
* `verdictOwner`: `'fresh-critic'` when the mode is `fresh`,
|
|
42
|
+
* `'inline-self-eval'` when the mode is `inline`. Exactly one pass authors
|
|
43
|
+
* the cluster's verdict — the fresh maker-blind critic OR the
|
|
44
|
+
* contract-identical inline self-eval, never both, and never an additional
|
|
45
|
+
* pre-pass self-assessment before the owner runs. `acceptance-eval.js` is
|
|
46
|
+
* the deterministic SCORER of that one authored verdict (schema validation,
|
|
47
|
+
* round cap, proceed/redraft/block) — it is not a third pass over the
|
|
48
|
+
* criteria. This removes a redundant pass only; it never removes a
|
|
49
|
+
* cluster's verdict (the M4-B floor above holds).
|
|
50
|
+
*
|
|
38
51
|
* ## Tier rules (per cluster, `standard` profile)
|
|
39
52
|
*
|
|
40
53
|
* - `high` level → `fresh` (a sensitive path was touched — a
|
|
@@ -63,10 +76,23 @@
|
|
|
63
76
|
* `undefined` / malformed inputs degrade to `fresh` + `full` ceremony.
|
|
64
77
|
*
|
|
65
78
|
* @typedef {'fresh'|'inline'} CeremonyMode
|
|
79
|
+
* @typedef {'fresh-critic'|'inline-self-eval'} VerdictOwner
|
|
66
80
|
* @typedef {import('./review-depth.js').ChangeLevel} ChangeLevel
|
|
67
81
|
* @typedef {'minimal'|'standard'|'strict'} CeremonyProfile
|
|
68
82
|
*/
|
|
69
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Map a resolved ceremony mode to the cluster's single verdict owner
|
|
86
|
+
* (Story #4723). Total: any non-`fresh` value maps to the inline
|
|
87
|
+
* self-eval owner, mirroring how the mode itself degrades.
|
|
88
|
+
*
|
|
89
|
+
* @param {CeremonyMode} mode
|
|
90
|
+
* @returns {VerdictOwner}
|
|
91
|
+
*/
|
|
92
|
+
export function verdictOwnerForMode(mode) {
|
|
93
|
+
return mode === 'fresh' ? 'fresh-critic' : 'inline-self-eval';
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
/** @type {readonly CeremonyProfile[]} */
|
|
71
97
|
export const CEREMONY_PROFILES = Object.freeze([
|
|
72
98
|
'minimal',
|
|
@@ -133,9 +159,28 @@ export function sampledFresh(clusterIndex, rate) {
|
|
|
133
159
|
* reason: string,
|
|
134
160
|
* sampled: boolean,
|
|
135
161
|
* profile: CeremonyProfile,
|
|
162
|
+
* verdictOwner: VerdictOwner,
|
|
136
163
|
* }}
|
|
137
164
|
*/
|
|
138
165
|
export function resolveCeremonyForRisk(input = {}) {
|
|
166
|
+
const decision = resolveCeremonyDecision(input);
|
|
167
|
+
return { ...decision, verdictOwner: verdictOwnerForMode(decision.mode) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Internal mode/reason resolution — the tier rules and sampling floor.
|
|
172
|
+
* `resolveCeremonyForRisk` decorates the result with the single
|
|
173
|
+
* `verdictOwner` derived from the mode (Story #4723).
|
|
174
|
+
*
|
|
175
|
+
* @param {Parameters<typeof resolveCeremonyForRisk>[0]} [input]
|
|
176
|
+
* @returns {{
|
|
177
|
+
* mode: CeremonyMode,
|
|
178
|
+
* reason: string,
|
|
179
|
+
* sampled: boolean,
|
|
180
|
+
* profile: CeremonyProfile,
|
|
181
|
+
* }}
|
|
182
|
+
*/
|
|
183
|
+
function resolveCeremonyDecision(input = {}) {
|
|
139
184
|
const derivedLevel =
|
|
140
185
|
input && typeof input === 'object' ? input.derivedLevel : undefined;
|
|
141
186
|
const clusterIndex =
|