mandrel 1.92.0 → 1.94.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.
Files changed (144) hide show
  1. package/.agents/agents/acceptance-critic.md +129 -0
  2. package/.agents/agents/retro.md +42 -0
  3. package/.agents/agents/story-worker.md +162 -0
  4. package/.agents/docs/configuration.md +7 -1
  5. package/.agents/docs/execution-reference.md +27 -2
  6. package/.agents/instructions.md +43 -33
  7. package/.agents/personas/engineer.md +26 -112
  8. package/.agents/personas/security-engineer.md +1 -2
  9. package/.agents/rules/git-conventions-reference.md +225 -0
  10. package/.agents/rules/git-conventions.md +25 -200
  11. package/.agents/rules/security-baseline.md +5 -0
  12. package/.agents/rules/testing-standards.md +106 -13
  13. package/.agents/schemas/agentrc.schema.json +31 -1
  14. package/.agents/schemas/lifecycle/slice.end.schema.json +21 -0
  15. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +20 -0
  16. package/.agents/schemas/lifecycle/slice.start.schema.json +17 -0
  17. package/.agents/scripts/acceptance-eval.js +62 -18
  18. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  19. package/.agents/scripts/bookkeeping-reconcile.js +117 -0
  20. package/.agents/scripts/check-context-budget.js +62 -5
  21. package/.agents/scripts/diagnose-friction.js +0 -6
  22. package/.agents/scripts/epic-deliver-prepare.js +272 -10
  23. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +56 -18
  24. package/.agents/scripts/lib/close-validation/gates.js +159 -21
  25. package/.agents/scripts/lib/config/acceptance-eval.js +52 -5
  26. package/.agents/scripts/lib/config/delivery-routing.js +87 -0
  27. package/.agents/scripts/lib/config/explain.js +2 -0
  28. package/.agents/scripts/lib/config-resolver.js +1 -1
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +37 -3
  30. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -0
  31. package/.agents/scripts/lib/doc-tiers.js +37 -2
  32. package/.agents/scripts/lib/observability/active-story-env.js +111 -2
  33. package/.agents/scripts/lib/observability/hook-heartbeat.js +219 -0
  34. package/.agents/scripts/lib/observability/tool-trace-hook.js +15 -4
  35. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +111 -0
  36. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +32 -4
  37. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +270 -0
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +141 -0
  39. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -124
  40. package/.agents/scripts/lib/orchestration/deliver-route.js +173 -0
  41. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +1 -1
  42. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +233 -0
  43. package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
  44. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +270 -0
  45. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +83 -2
  46. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +6 -0
  47. package/.agents/scripts/lib/orchestration/plan-context.js +189 -3
  48. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +3 -2
  49. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +38 -1
  51. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +16 -1
  52. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -0
  53. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +1 -0
  54. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -2
  55. package/.agents/scripts/lib/provider-factory.js +1 -1
  56. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  57. package/.agents/scripts/plan-context.js +28 -10
  58. package/.agents/scripts/post-structured-comment.js +38 -0
  59. package/.agents/scripts/slice-phase.js +361 -0
  60. package/.agents/scripts/sync-claude-agents.js +165 -0
  61. package/.agents/scripts/update-ticket-state.js +31 -0
  62. package/.agents/scripts/wave-tick.js +138 -9
  63. package/.agents/skills/core/api-and-interface-design/SKILL.md +5 -3
  64. package/.agents/skills/core/code-review-and-quality/SKILL.md +63 -7
  65. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +1 -1
  66. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +5 -5
  67. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +8 -8
  68. package/.agents/skills/core/epic-plan-premortem/SKILL.md +4 -4
  69. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +26 -56
  70. package/.agents/skills/core/gates-and-baselines/SKILL.md +149 -0
  71. package/.agents/skills/core/idea-refinement/SKILL.md +2 -8
  72. package/.agents/skills/core/qa-coverage-mapping/SKILL.md +7 -7
  73. package/.agents/skills/skills.index.json +11 -381
  74. package/.agents/workflows/deliver.md +47 -4
  75. package/.agents/workflows/helpers/acceptance-self-eval.md +38 -13
  76. package/.agents/workflows/helpers/deliver-epic-reference.md +18 -5
  77. package/.agents/workflows/helpers/deliver-epic-single.md +331 -0
  78. package/.agents/workflows/helpers/deliver-epic.md +51 -8
  79. package/.agents/workflows/helpers/deliver-stories.md +15 -5
  80. package/.agents/workflows/helpers/epic-deliver-story.md +12 -3
  81. package/.agents/workflows/helpers/mandrel-sync-config.md +1 -1
  82. package/.agents/workflows/helpers/plan-epic-reference.md +19 -8
  83. package/.agents/workflows/helpers/plan-epic.md +95 -27
  84. package/.agents/workflows/helpers/scope-triage-gate.md +9 -0
  85. package/.agents/workflows/mandrel-update.md +1 -1
  86. package/.agents/workflows/plan.md +16 -4
  87. package/docs/CHANGELOG.md +23 -0
  88. package/lib/cli/registry.js +95 -0
  89. package/package.json +4 -2
  90. package/.agents/personas/engineer-mobile.md +0 -120
  91. package/.agents/personas/engineer-web.md +0 -111
  92. package/.agents/personas/product.md +0 -94
  93. package/.agents/personas/refactorer.md +0 -113
  94. package/.agents/personas/sre.md +0 -86
  95. package/.agents/personas/ux-designer.md +0 -95
  96. package/.agents/scripts/epic-plan-decompose.js +0 -54
  97. package/.agents/scripts/epic-plan-spec.js +0 -64
  98. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +0 -109
  99. package/.agents/scripts/plan-critics.js +0 -227
  100. package/.agents/skills/core/baseline-refresh/SKILL.md +0 -181
  101. package/.agents/skills/core/ci-cd-and-automation/SKILL.md +0 -274
  102. package/.agents/skills/core/ci-cd-and-automation/examples.md +0 -211
  103. package/.agents/skills/core/code-simplification/SKILL.md +0 -389
  104. package/.agents/skills/core/context-engineering/SKILL.md +0 -309
  105. package/.agents/skills/core/context-engineering/examples.md +0 -58
  106. package/.agents/skills/core/deprecation-and-migration/SKILL.md +0 -250
  107. package/.agents/skills/core/frontend-ui-engineering/SKILL.md +0 -357
  108. package/.agents/skills/core/hydrate-context/SKILL.md +0 -123
  109. package/.agents/skills/core/idea-refinement/examples.md +0 -437
  110. package/.agents/skills/core/idea-refinement/frameworks.md +0 -135
  111. package/.agents/skills/core/incremental-implementation/SKILL.md +0 -271
  112. package/.agents/skills/core/introducing-a-baseline-gate/SKILL.md +0 -213
  113. package/.agents/skills/core/knowledge-transfer/SKILL.md +0 -180
  114. package/.agents/skills/core/mutation-survivor-remediation/SKILL.md +0 -117
  115. package/.agents/skills/core/performance-optimization/SKILL.md +0 -314
  116. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +0 -277
  117. package/.agents/skills/core/property-based-testing/SKILL.md +0 -148
  118. package/.agents/skills/core/refactoring-discipline/SKILL.md +0 -111
  119. package/.agents/skills/core/shipping-and-launch/SKILL.md +0 -328
  120. package/.agents/skills/core/spec-driven-development/SKILL.md +0 -252
  121. package/.agents/skills/core/test-driven-development/SKILL.md +0 -475
  122. package/.agents/skills/core/using-agent-skills/SKILL.md +0 -232
  123. package/.agents/skills/stack/architecture/monorepo-path-strategist/SKILL.md +0 -31
  124. package/.agents/skills/stack/architecture/structured-output-zod/SKILL.md +0 -51
  125. package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +0 -76
  126. package/.agents/skills/stack/backend/cloudflare-hono-architect/SKILL.md +0 -31
  127. package/.agents/skills/stack/backend/cloudflare-hono-architect/examples/route-template.ts +0 -33
  128. package/.agents/skills/stack/backend/cloudflare-queue-manager/SKILL.md +0 -31
  129. package/.agents/skills/stack/backend/cloudflare-workers/SKILL.md +0 -51
  130. package/.agents/skills/stack/backend/highlevel-crm/SKILL.md +0 -54
  131. package/.agents/skills/stack/backend/sqlite-drizzle-expert/SKILL.md +0 -29
  132. package/.agents/skills/stack/backend/sqlite-drizzle-expert/examples/schema-template.ts +0 -30
  133. package/.agents/skills/stack/backend/stripe-integration/SKILL.md +0 -57
  134. package/.agents/skills/stack/backend/stripe-integration/scripts/listen-stripe.sh +0 -9
  135. package/.agents/skills/stack/backend/turso-sqlite/SKILL.md +0 -48
  136. package/.agents/skills/stack/frontend/astro/SKILL.md +0 -62
  137. package/.agents/skills/stack/frontend/astro-react-island-strategist/SKILL.md +0 -30
  138. package/.agents/skills/stack/frontend/expo-react-native-developer/SKILL.md +0 -29
  139. package/.agents/skills/stack/frontend/google-analytics-v4/SKILL.md +0 -50
  140. package/.agents/skills/stack/frontend/tailwind-v4/SKILL.md +0 -58
  141. package/.agents/skills/stack/frontend/ui-accessibility-engineer/SKILL.md +0 -34
  142. package/.agents/skills/stack/qa/audit-accessibility/SKILL.md +0 -51
  143. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +0 -199
  144. package/.agents/skills/stack/security/backend-security-patterns/SKILL.md +0 -68
@@ -6,7 +6,12 @@
6
6
  * runner (`INDEPENDENT_GATE_NAMES` / `partitionGates`).
7
7
  */
8
8
 
9
+ import { existsSync } from 'node:fs';
10
+
11
+ import { _internals as baselineReaderInternals } from '../baselines/reader.js';
12
+ import { getQuality } from '../config/quality.js';
9
13
  import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
14
+ import { KNOWN_KINDS } from '../orchestration/check-baselines/phases/parse-args.js';
10
15
  import {
11
16
  buildFormatHint,
12
17
  FORMAT_CHECK_FALLBACK,
@@ -115,6 +120,106 @@ function buildTestGateEntry(coverageCaptureActive) {
115
120
  return [{ name: 'test', cmd: 'npm', args: ['test'] }];
116
121
  }
117
122
 
123
+ const CHECK_BASELINES_HINT =
124
+ 'Unified baselines gate breached. Inspect the JSON report (`node .agents/scripts/check-baselines.js`) to see which kind/component/axis fell below floor; remediate the underlying file(s) or — when the regression is intentional — refresh the relevant baseline through its per-kind update script and commit with a `baseline-refresh:` tagged subject.';
125
+
126
+ /**
127
+ * Baseline kinds the resolved config enables for the unified
128
+ * `check-baselines` gate. Mirrors `selectEnabledGates` in the check-baselines
129
+ * pipeline (a kind runs when its `gates.<kind>` block is present and not
130
+ * explicitly disabled) so the registration probe's view of "what will run"
131
+ * matches the gate's own view exactly.
132
+ *
133
+ * @param {object|undefined|null} config canonical resolved config
134
+ * @returns {string[]}
135
+ */
136
+ function enabledBaselineKinds(config) {
137
+ const gates = getQuality(config)?.gates ?? {};
138
+ return KNOWN_KINDS.filter((kind) => {
139
+ const block = gates[kind];
140
+ return block && typeof block === 'object' && block.enabled !== false;
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Whether the consumer opted into fail-closed baseline enforcement via
146
+ * `delivery.quality.requireBaselines: true`. Default false — a consumer that
147
+ * enables baseline gates but has not committed baseline artifacts gets a
148
+ * clean skip (see `probeBaselinesGate`) rather than a deterministic first-try
149
+ * close failure. Mirrors the `delivery.ci.requireChecks` escape hatch (#4472).
150
+ *
151
+ * @param {object|undefined|null} config
152
+ * @returns {boolean}
153
+ */
154
+ function baselinesRequiredByConfig(config) {
155
+ return config?.delivery?.quality?.requireBaselines === true;
156
+ }
157
+
158
+ function toKindSet(presentBaselines) {
159
+ if (presentBaselines instanceof Set) return presentBaselines;
160
+ if (Array.isArray(presentBaselines)) return new Set(presentBaselines);
161
+ return new Set();
162
+ }
163
+
164
+ /**
165
+ * Probe whether the `check-baselines` consumer contract is satisfied before
166
+ * registering the gate (#4495 — mirrors the #4473/#4480 coverage-capture
167
+ * remedy). The contract: every enabled baseline kind carries a committed
168
+ * baseline artifact on disk (the same path the gate's reader resolves).
169
+ *
170
+ * Decision shape:
171
+ * - `{ register: false, reason }` — skip (caller logs the reason). Baseline
172
+ * gates ARE enabled but none of the enabled kinds carry a committed
173
+ * baseline artifact and the consumer has not set `requireBaselines`. This
174
+ * is the bench/greenfield case: the gate would otherwise fail
175
+ * deterministically on first try reading a non-existent
176
+ * `baselines/<kind>.json`.
177
+ * - `{ register: true }` — at least one committed baseline artifact is
178
+ * present, OR no baseline kinds are enabled at all (the gate then self-
179
+ * skips every kind and exits a clean empty PASS — no failure to avoid, so
180
+ * the gate stays registered exactly as pre-#4495); run the gate.
181
+ * - `{ register: true, hint }` — baselines are required-by-config
182
+ * (`requireBaselines: true`) but absent; keep the gate registered so it
183
+ * fails, with a preflight hint naming the fix.
184
+ *
185
+ * @param {{ config?: object, cwd?: string, presentBaselines?: string[]|Set<string> }} opts
186
+ * `presentBaselines` injects the set of kinds whose baseline artifact
187
+ * exists (tests), short-circuiting the on-disk probe.
188
+ * @returns {{ register: boolean, reason?: string, hint?: string }}
189
+ */
190
+ function probeBaselinesGate({ config, cwd, presentBaselines } = {}) {
191
+ const enabled = enabledBaselineKinds(config);
192
+ if (enabled.length === 0) {
193
+ // No enabled baseline kinds → `check-baselines.js` self-skips every kind
194
+ // and exits clean (an empty PASS). There is no deterministic-failure risk
195
+ // to avoid, so keep the gate registered exactly as it was pre-#4495; the
196
+ // #4495 skip is confined strictly to the read-miss-would-fail case below.
197
+ return { register: true };
198
+ }
199
+ const injected =
200
+ presentBaselines != null ? toKindSet(presentBaselines) : null;
201
+ const present = enabled.filter((kind) =>
202
+ injected
203
+ ? injected.has(kind)
204
+ : existsSync(baselineReaderInternals.resolveBaselinePath(kind, { cwd })),
205
+ );
206
+ if (present.length > 0) return { register: true };
207
+ if (baselinesRequiredByConfig(config)) {
208
+ return {
209
+ register: true,
210
+ hint:
211
+ `Baselines are required (delivery.quality.requireBaselines) but no committed baseline artifact was found for enabled kind(s): ${enabled.join(', ')}. ` +
212
+ 'Generate the baseline(s) with the per-kind update script (e.g. `npm run crap:update`, `npm run maintainability:update`) and commit them, or unset requireBaselines to skip the gate until baselines exist.',
213
+ };
214
+ }
215
+ return {
216
+ register: false,
217
+ reason:
218
+ `check-baselines skipped — enabled kind(s) ${enabled.join(', ')} have no committed baseline artifact under baselines/ ` +
219
+ 'and delivery.quality.requireBaselines is not set. Commit baseline artifacts (or set requireBaselines to enforce them) to activate the gate.',
220
+ };
221
+ }
222
+
118
223
  /**
119
224
  * Build the canonical close-validation gate list.
120
225
  *
@@ -152,15 +257,30 @@ function buildTestGateEntry(coverageCaptureActive) {
152
257
  * instead of a deterministic close failure with no test gate at all. The
153
258
  * probe reads `package.json` at `cwd` (the gate execution directory).
154
259
  *
155
- * @param {{ config?: object, epicBranch?: string, cwd?: string, packageScripts?: Record<string, string> }} [opts]
260
+ * Story #4495 the unified `check-baselines` gate reads a committed
261
+ * `baselines/<kind>.json` for each enabled kind; a consumer that enables
262
+ * baseline gates but ships no `baselines/` tree (every bench sandbox, any
263
+ * greenfield consumer) failed the gate deterministically on first try. The
264
+ * gate is now registered only when its consumer contract is satisfied
265
+ * (`probeBaselinesGate`): at least one enabled kind carries a committed
266
+ * baseline, OR the consumer opted into fail-closed enforcement via
267
+ * `delivery.quality.requireBaselines`. When no baselines are committed and
268
+ * none are required, the gate is skipped with a logged reason (via `log`)
269
+ * instead of a blocking failure.
270
+ *
271
+ * @param {{ config?: object, epicBranch?: string, cwd?: string, packageScripts?: Record<string, string>, presentBaselines?: string[]|Set<string>, log?: (message: string) => void }} [opts]
156
272
  * `config` is the canonical resolved config (`{ project, delivery, ... }`);
157
273
  * gate commands resolve from `project.commands` and the CRAP toggle from
158
274
  * `delivery.quality.gates.crap.enabled`. `epicBranch` is the close run's
159
275
  * integration branch (`epic/<id>` for Epic-attached Stories, the base
160
276
  * branch for standalone Stories). `cwd` is where the `package.json`
161
- * coverage-script probe reads from (defaults to `process.cwd()`);
162
- * `packageScripts` injects the scripts map directly (tests) and short-
163
- * circuits the disk read.
277
+ * coverage-script probe and the `baselines/<kind>.json` presence probe
278
+ * read from (defaults to `process.cwd()`); `packageScripts` injects the
279
+ * scripts map directly (tests) and short-circuits the coverage-script disk
280
+ * read; `presentBaselines` injects the set of kinds whose baseline artifact
281
+ * exists (tests) and short-circuits the baseline-presence disk read; `log`
282
+ * receives the skip reason when the `check-baselines` gate is not
283
+ * registered.
164
284
  * @returns {Gate[]}
165
285
  */
166
286
  export function buildDefaultGates({
@@ -168,6 +288,8 @@ export function buildDefaultGates({
168
288
  epicBranch,
169
289
  cwd,
170
290
  packageScripts,
291
+ presentBaselines,
292
+ log,
171
293
  } = {}) {
172
294
  const scripts = packageScripts ?? readPackageScripts(cwd);
173
295
  const coverageCaptureActive =
@@ -186,6 +308,14 @@ export function buildDefaultGates({
186
308
  ? buildChangedFileScope(epicBranch)
187
309
  : null;
188
310
  const baselinesGateEnv = buildBaselinesGateEnv(epicBranch);
311
+ const baselinesDecision = probeBaselinesGate({
312
+ config,
313
+ cwd,
314
+ presentBaselines,
315
+ });
316
+ if (!baselinesDecision.register && baselinesDecision.reason) {
317
+ log?.(`[close-validation] ${baselinesDecision.reason}`);
318
+ }
189
319
  return [
190
320
  {
191
321
  name: 'typecheck',
@@ -218,23 +348,31 @@ export function buildDefaultGates({
218
348
  },
219
349
  ]
220
350
  : []),
221
- {
222
- // Story #2210 unified `check-baselines` gate is the only path for
223
- // per-kind regression enforcement. The legacy per-kind in-process
224
- // gates were retired because their regression-compare semantics are
225
- // fully subsumed by this gate's attribution-wired floor + tolerance +
226
- // schema enforcement, and running both paths in series was redundant
227
- // and conflict-prone.
228
- //
229
- // `check-baselines.js` self-skips per-kind gates whose
230
- // `enabled === false` is configured, so registering it
231
- // unconditionally is safe.
232
- name: 'check-baselines',
233
- cmd: 'node',
234
- args: ['.agents/scripts/check-baselines.js', '--format', 'text'],
235
- hint: 'Unified baselines gate breached. Inspect the JSON report (`node .agents/scripts/check-baselines.js`) to see which kind/component/axis fell below floor; remediate the underlying file(s) or — when the regression is intentional — refresh the relevant baseline through its per-kind update script and commit with a `baseline-refresh:` tagged subject.',
236
- ...(baselinesGateEnv ? { env: baselinesGateEnv } : {}),
237
- },
351
+ // Story #2210 — unified `check-baselines` gate is the only path for
352
+ // per-kind regression enforcement. The legacy per-kind in-process gates
353
+ // were retired because their regression-compare semantics are fully
354
+ // subsumed by this gate's attribution-wired floor + tolerance + schema
355
+ // enforcement, and running both paths in series was redundant and
356
+ // conflict-prone.
357
+ //
358
+ // `check-baselines.js` self-skips per-kind gates whose `enabled === false`
359
+ // is configured. Story #4495: it is now also skipped entirely when the
360
+ // consumer enables baseline gates but ships no committed baseline artifact
361
+ // (and has not set `delivery.quality.requireBaselines`) — otherwise the
362
+ // gate fails deterministically on first try reading a non-existent
363
+ // `baselines/<kind>.json` (`probeBaselinesGate`). When required-by-config
364
+ // but absent, it stays registered with a preflight hint naming the fix.
365
+ ...(baselinesDecision.register
366
+ ? [
367
+ {
368
+ name: 'check-baselines',
369
+ cmd: 'node',
370
+ args: ['.agents/scripts/check-baselines.js', '--format', 'text'],
371
+ hint: baselinesDecision.hint ?? CHECK_BASELINES_HINT,
372
+ ...(baselinesGateEnv ? { env: baselinesGateEnv } : {}),
373
+ },
374
+ ]
375
+ : []),
238
376
  ];
239
377
  }
240
378
 
@@ -38,6 +38,7 @@
38
38
  */
39
39
  export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
40
40
  maxRounds: 2,
41
+ clusterCeiling: 4,
41
42
  });
42
43
 
43
44
  /**
@@ -50,6 +51,22 @@ export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
50
51
  */
51
52
  export const ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING = 5;
52
53
 
54
+ /**
55
+ * Hard, undisableable ceiling on `clusterCeiling` — the max ACs one
56
+ * single-delivery acceptance critic scores in a single fresh-context pass
57
+ * (Epic #4475, M4-B, design §S2a). It is the **acceptance-dilution guard**:
58
+ * single delivery collapses the whole Epic into one session, so the only
59
+ * acceptance coverage left is the per-AC-cluster critic fan-out
60
+ * (`ceil(totalACs / clusterCeiling)` independent maker-blind passes). A
61
+ * pathologically large `clusterCeiling` would collapse that fan-out to a
62
+ * single critic scoring every AC at once — exactly the dilution the design
63
+ * forecloses — so a configured value above this ceiling is clamped down to
64
+ * it. Kept small on purpose.
65
+ *
66
+ * @type {number}
67
+ */
68
+ export const ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX = 8;
69
+
53
70
  /**
54
71
  * Clamp a candidate round count into the inviolable `[1, ceiling]` range.
55
72
  * Non-integer / non-finite inputs fall back to the documented default.
@@ -68,19 +85,43 @@ function clampRounds(value, fallback) {
68
85
  return candidate;
69
86
  }
70
87
 
88
+ /**
89
+ * Clamp a candidate cluster ceiling into the inviolable
90
+ * `[1, ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX]` range. Non-integer / non-finite
91
+ * inputs fall back to the documented default. Mirrors `clampRounds` — the
92
+ * anti-dilution guard cannot be disabled (`clusterCeiling: 0` or a negative
93
+ * clamps up to 1; an over-max value clamps down to the hard cap).
94
+ *
95
+ * @param {unknown} value
96
+ * @param {number} fallback
97
+ * @returns {number}
98
+ */
99
+ function clampClusterCeiling(value, fallback) {
100
+ const candidate =
101
+ typeof value === 'number' && Number.isInteger(value) ? value : fallback;
102
+ if (candidate < 1) return 1;
103
+ if (candidate > ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX) {
104
+ return ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX;
105
+ }
106
+ return candidate;
107
+ }
108
+
71
109
  /**
72
110
  * Read the merged acceptance-eval block. Returns the canonical shape:
73
111
  *
74
112
  * {
75
- * maxRounds: number, // clamped into [1, ceiling]
76
- * ceiling: number, // the undisableable hard cap
113
+ * maxRounds: number, // clamped into [1, roundsCeiling]
114
+ * ceiling: number, // the undisableable hard cap on rounds
115
+ * clusterCeiling: number, // clamped into [1, clusterCeilingMax]
116
+ * clusterCeilingMax: number // the undisableable hard cap on cluster size
77
117
  * }
78
118
  *
79
- * `maxRounds` is always a positive integer no greater than `ceiling`,
80
- * regardless of what the resolved config carried.
119
+ * `maxRounds` is always a positive integer no greater than `ceiling`;
120
+ * `clusterCeiling` is always a positive integer no greater than
121
+ * `clusterCeilingMax`, regardless of what the resolved config carried.
81
122
  *
82
123
  * @param {object | null | undefined} config
83
- * @returns {{ maxRounds: number, ceiling: number }}
124
+ * @returns {{ maxRounds: number, ceiling: number, clusterCeiling: number, clusterCeilingMax: number }}
84
125
  */
85
126
  export function getAcceptanceEval(config) {
86
127
  const user = config?.delivery?.acceptanceEval ?? {};
@@ -88,8 +129,14 @@ export function getAcceptanceEval(config) {
88
129
  user.maxRounds,
89
130
  ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
90
131
  );
132
+ const clusterCeiling = clampClusterCeiling(
133
+ user.clusterCeiling,
134
+ ACCEPTANCE_EVAL_DEFAULTS.clusterCeiling,
135
+ );
91
136
  return {
92
137
  maxRounds,
93
138
  ceiling: ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING,
139
+ clusterCeiling,
140
+ clusterCeilingMax: ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX,
94
141
  };
95
142
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `delivery.routing` accessor + framework defaults — Epic #4475 (M4-A),
3
+ * the single-delivery-as-default foundation, plus Epic #4478 (M7-B), the
4
+ * role-scoped-boot-context flip and the maker-checker sampling floor.
5
+ *
6
+ * `delivery.routing.singleDelivery` is the **global kill-switch** for the
7
+ * single-delivery route. It defaults to `true` (single-delivery is the
8
+ * default shape for epic-shaped work), and is shipped INERT in M4-A: the
9
+ * `deliver.md` router's single verdict currently falls through to the
10
+ * fan-out helper, so flipping this knob has no observable effect until
11
+ * M4-B wires `deliver-epic-single.md`.
12
+ *
13
+ * When set to `false`, `resolveEpicDeliveryRoute` forces EVERY Epic — even
14
+ * one carrying the `delivery::single` label or a `decompose.shape:"single"`
15
+ * checkpoint — down the fan-out path. This is the instant, per-consumer
16
+ * global revert that ships BEFORE the default flips: no code rollback, no
17
+ * re-plan, just a config edit.
18
+ *
19
+ * `delivery.routing.roleScopedAgents` is the **kill-switch for the role-scoped
20
+ * boot contexts** (Epic #4478, M7-B). It mirrors the `singleDelivery` shape
21
+ * exactly. It defaults to `true`: a converted spawn (`story-worker`,
22
+ * `acceptance-critic`) boots on its own `.claude/agents/<role>.md` system
23
+ * prompt instead of re-paying the full `CLAUDE.md` @-import closure, which is
24
+ * the whole payoff of the context diet (≈50KB → ≈8KB per spawn). When set to
25
+ * `false`, every converted spawn falls back to `subagent_type: general-purpose`
26
+ * — the instant, code-rollback-free per-consumer revert, and the universal
27
+ * escape for hosts that ignore `.claude/agents/`. Flipping it off never drops a
28
+ * gate: the fallback is the full-closure agent that ran before M7-B.
29
+ *
30
+ * `delivery.routing.freshCriticSampleRate` is the **maker-checker sampling
31
+ * floor** (Epic #4478, M7-B, Part 2). Risk-routed ceremony sends a low-risk
32
+ * acceptance cluster down the contract-identical *inline* critic path, but a
33
+ * fraction of low-risk clusters are still forced through a *fresh-context*
34
+ * critic so "low risk" never degrades to zero independent checking. The rate is
35
+ * clamped into `[0, 1]`; `0` disables the floor (pure risk routing), `1` forces
36
+ * every cluster fresh. The default is `0.2`. See `resolveCeremonyForRisk` in
37
+ * `lib/orchestration/ceremony-routing.js`.
38
+ *
39
+ * Framework-defaults pattern mirrors `lib/config/ci.js#getCiDelivery`.
40
+ */
41
+
42
+ export const DELIVERY_ROUTING_DEFAULTS = Object.freeze({
43
+ singleDelivery: true,
44
+ roleScopedAgents: true,
45
+ freshCriticSampleRate: 0.2,
46
+ });
47
+
48
+ /**
49
+ * Clamp a candidate sample rate into `[0, 1]`. Non-finite / non-number inputs
50
+ * fall back to the framework default so a degraded config never yields a
51
+ * NaN-driven or out-of-range floor.
52
+ *
53
+ * @param {unknown} value
54
+ * @returns {number}
55
+ */
56
+ function clampSampleRate(value) {
57
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
58
+ return DELIVERY_ROUTING_DEFAULTS.freshCriticSampleRate;
59
+ }
60
+ if (value < 0) return 0;
61
+ if (value > 1) return 1;
62
+ return value;
63
+ }
64
+
65
+ /**
66
+ * Read the merged `delivery.routing` block, applying framework defaults for
67
+ * any field the operator omitted. Accepts the full resolved config, the bare
68
+ * `delivery` bag, or the bare `routing` bag — mirroring `getCiDelivery`'s
69
+ * tolerant unwrap so callers can pass whichever shape they hold.
70
+ *
71
+ * @param {object | null | undefined} config
72
+ * @returns {{ singleDelivery: boolean, roleScopedAgents: boolean, freshCriticSampleRate: number }}
73
+ */
74
+ export function getDeliveryRouting(config) {
75
+ const routing = config?.delivery?.routing ?? config?.routing ?? config ?? {};
76
+ return {
77
+ singleDelivery:
78
+ typeof routing.singleDelivery === 'boolean'
79
+ ? routing.singleDelivery
80
+ : DELIVERY_ROUTING_DEFAULTS.singleDelivery,
81
+ roleScopedAgents:
82
+ typeof routing.roleScopedAgents === 'boolean'
83
+ ? routing.roleScopedAgents
84
+ : DELIVERY_ROUTING_DEFAULTS.roleScopedAgents,
85
+ freshCriticSampleRate: clampSampleRate(routing.freshCriticSampleRate),
86
+ };
87
+ }
@@ -204,6 +204,8 @@ const KEY_MEANINGS = Object.freeze({
204
204
  'Whether a dedicated refactor stage runs during delivery.',
205
205
  'delivery.acceptanceEval.maxRounds':
206
206
  'Redraft rounds the per-Story acceptance self-eval loop runs before escalating to agent::blocked (default 2; clamped to a hard ceiling that cannot be disabled).',
207
+ 'delivery.acceptanceEval.clusterCeiling':
208
+ 'Max acceptance criteria one single-delivery acceptance critic scores per fresh-context pass; the Epic ACs are split into ceil(totalACs / clusterCeiling) maker-blind critic clusters (default 4; clamped to [1, 8]).',
207
209
 
208
210
  // qa.*
209
211
  'qa.featureRoot': 'Root directory holding the QA harness .feature files.',
@@ -19,7 +19,7 @@
19
19
  * deleted from the resolver wrapper. Every internal call site reads the
20
20
  * canonical `project` / `github` / `planning` / `delivery` blocks
21
21
  * directly; consumers upgrade in lockstep with the framework bump
22
- * (see `.agents/rules/git-conventions.md#contract-cutovers-—-no-shim-layer`).
22
+ * (see `.agents/rules/git-conventions-reference.md#contract-cutovers-—-no-shim-layer`).
23
23
  */
24
24
 
25
25
  import fs from 'node:fs';
@@ -233,6 +233,33 @@ const EPIC_AUDIT_SCHEMA = {
233
233
  additionalProperties: false,
234
234
  };
235
235
 
236
+ // Epic #4475 (M4-A) — single-delivery routing kill-switch.
237
+ // `delivery.routing.singleDelivery` (default true via getDeliveryRouting) is
238
+ // the global kill-switch for the single-delivery route. When true (the
239
+ // default), an Epic marked `delivery::single` at plan time routes to the
240
+ // single-delivery helper; when false, EVERY Epic — even a single-marked one —
241
+ // is forced down the fan-out path (the instant, code-rollback-free per-consumer
242
+ // revert). Shipped INERT in M4-A: the router's single verdict falls through to
243
+ // fan-out until M4-B wires the executor, so the knob has no observable effect
244
+ // yet.
245
+ // Epic #4478 (M7-B) — role-scoped-agent kill-switch + maker-checker floor.
246
+ // `delivery.routing.roleScopedAgents` (default true via getDeliveryRouting)
247
+ // flips converted delivery spawns onto their `.claude/agents/<role>.md` boot
248
+ // context; false falls back to `subagent_type: general-purpose` (the instant
249
+ // per-consumer revert + the escape for hosts that ignore `.claude/agents/`).
250
+ // `delivery.routing.freshCriticSampleRate` (default 0.2, clamped [0, 1]) is the
251
+ // maker-checker sampling floor forcing a fraction of low-risk acceptance
252
+ // clusters through a fresh critic.
253
+ const ROUTING_SCHEMA = {
254
+ type: 'object',
255
+ properties: {
256
+ singleDelivery: { type: 'boolean' },
257
+ roleScopedAgents: { type: 'boolean' },
258
+ freshCriticSampleRate: { type: 'number', minimum: 0, maximum: 1 },
259
+ },
260
+ additionalProperties: false,
261
+ };
262
+
236
263
  // Story #2899 (Epic #2880) — performance defaults + preflight (F13).
237
264
  // `delivery.ci.skipForStoryPushes` (default true via getCiDelivery): when
238
265
  // true, pre-push tooling appends a `[skip ci]` trailer to Story-branch
@@ -296,8 +323,8 @@ const PREFLIGHT_SCHEMA = {
296
323
  * checkpoint wired into story-deliver (Story #3430, Epic #3418). Strictly
297
324
  * additive and default-OFF: when `enabled` is unset or `false`, story-deliver
298
325
  * behaves exactly as before. When `true`, the worker runs an advisory
299
- * post-green refactor pass (the `refactorer` persona +
300
- * `core/refactoring-discipline` skill) after the suite is green. The stage is
326
+ * post-green refactor pass (the `core/code-review-and-quality` skill's
327
+ * Post-Green Refactor Pass) after the suite is green. The stage is
301
328
  * advisory only — it never changes existing close-validation gate semantics.
302
329
  */
303
330
  const REFACTOR_STAGE_SCHEMA = {
@@ -306,7 +333,7 @@ const REFACTOR_STAGE_SCHEMA = {
306
333
  enabled: {
307
334
  type: 'boolean',
308
335
  description:
309
- 'When true, story-deliver runs an advisory post-green refactor stage (refactorer persona + core/refactoring-discipline skill) after the suite is green. Default false — when unset the stage is skipped and close-validation gate semantics are unchanged.',
336
+ 'When true, story-deliver runs an advisory post-green refactor stage (core/code-review-and-quality skill, Post-Green Refactor Pass) after the suite is green. Default false — when unset the stage is skipped and close-validation gate semantics are unchanged.',
310
337
  },
311
338
  },
312
339
  additionalProperties: false,
@@ -337,6 +364,12 @@ const ACCEPTANCE_EVAL_SCHEMA = {
337
364
  description:
338
365
  'Maximum number of redraft rounds the acceptance self-eval loop runs before escalating to agent::blocked when criteria remain unmet. Default 2; clamped into [1, hard ceiling] by the resolver so the cap can never be disabled.',
339
366
  },
367
+ clusterCeiling: {
368
+ type: 'integer',
369
+ minimum: 1,
370
+ description:
371
+ 'Epic #4475 (M4-B). Max acceptance criteria one single-delivery acceptance critic scores in a single fresh-context pass. Single delivery clusters the Epic ## Acceptance Table ACs into ceil(totalACs / clusterCeiling) groups and spawns one maker-blind critic per cluster, restoring the distributed acceptance coverage the per-Story critic fan-out gave for free. Default 4; clamped into [1, 8] by the resolver so a large value cannot collapse the fan-out to a single diluted critic. Ignored on the fan-out route.',
372
+ },
340
373
  },
341
374
  additionalProperties: false,
342
375
  };
@@ -384,6 +417,7 @@ export const DELIVERY_SCHEMA = {
384
417
  acceptanceEval: ACCEPTANCE_EVAL_SCHEMA,
385
418
  feedbackLoop: FEEDBACK_LOOP_SCHEMA,
386
419
  ci: CI_DELIVERY_SCHEMA,
420
+ routing: ROUTING_SCHEMA,
387
421
  preflight: PREFLIGHT_SCHEMA,
388
422
  // Cross-Story concurrency-hazard gate (Story #2297). When true,
389
423
  // `epic-deliver-prepare` refuses to flip the Epic to
@@ -96,6 +96,15 @@ export const QUALITY_SCHEMA = {
96
96
  codingGuardrails: CODING_GUARDRAILS_SCHEMA,
97
97
  autoRefresh: AUTO_REFRESH_SCHEMA,
98
98
  baselineEpsilon: BASELINE_EPSILON_SCHEMA,
99
+ // Story #4495. Fail-closed baseline-enforcement policy for the unified
100
+ // check-baselines close-validation gate. Default false: a consumer that
101
+ // enables baseline gates but has not committed baseline artifacts under
102
+ // baselines/ gets a clean skip-with-reason from buildDefaultGates rather
103
+ // than a deterministic first-try close failure. Set true to keep the gate
104
+ // registered so an absent baseline artifact fails close-validation with a
105
+ // preflight hint (the fail-closed posture, analogous to
106
+ // delivery.ci.requireChecks).
107
+ requireBaselines: { type: 'boolean' },
99
108
  // Navigability lens + post-wave integration gate config (Epic #4131,
100
109
  // F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and the
101
110
  // deliver-epic.md Phase 6.5 gate (journey suite). Opt-in: absent or empty
@@ -24,10 +24,16 @@
24
24
  * - `onDemand` — the on-demand `.agents/rules/*.md` set (instructions.md
25
25
  * § 1.F): every rule file that is **not** part of the
26
26
  * always-on core already captured in `alwaysLoaded`.
27
+ * - `agentBoot` — the role-scoped boot contexts `.agents/agents/*.md`
28
+ * (issue #4478). Each is a standalone system prompt a
29
+ * converted spawn boots on **instead of** the always-loaded
30
+ * closure, so it is budgeted independently (per-file ≤8KB
31
+ * ceiling gated by `check-context-budget.js`).
27
32
  *
28
33
  * A file that could appear in more than one tier is kept in its **highest**
29
34
  * tier only (alwaysLoaded > mandatoryRead > digestVisible > onDemand), so the
30
- * four arrays partition the doc set with no double-counting.
35
+ * arrays partition the doc set with no double-counting. `agentBoot` is disjoint
36
+ * from the read-tiers (it lives under `.agents/agents/`, not the doc/rules set).
31
37
  *
32
38
  * The closure is discovered by parsing `@`-import references and following
33
39
  * them recursively (cycle-safe via a visited set). A candidate `@`-token only
@@ -216,6 +222,7 @@ export function docsContextPaths(config) {
216
222
  * mandatoryRead: Array<{ path: string, bytes: number }>,
217
223
  * digestVisible: Array<{ path: string, bytes: number }>,
218
224
  * onDemand: Array<{ path: string, bytes: number }>,
225
+ * agentBoot: Array<{ path: string, bytes: number }>,
219
226
  * } }}
220
227
  */
221
228
  export function resolveDocTiers(
@@ -254,7 +261,35 @@ export function resolveDocTiers(
254
261
  // (the always-on ones already live in the alwaysLoaded closure).
255
262
  const onDemand = collect(listOnDemandRules(root, fs));
256
263
 
257
- return { tiers: { alwaysLoaded, mandatoryRead, digestVisible, onDemand } };
264
+ // 5. agent-boot: role-scoped boot contexts .agents/agents/*.md (#4478). These
265
+ // are standalone system prompts, disjoint from the doc read-tiers.
266
+ const agentBoot = collect(listAgentDefs(root, fs));
267
+
268
+ return {
269
+ tiers: { alwaysLoaded, mandatoryRead, digestVisible, onDemand, agentBoot },
270
+ };
271
+ }
272
+
273
+ /**
274
+ * List the role-scoped agent-boot defs (repo-relative posix): every
275
+ * `.agents/agents/*.md`. Returns [] when the directory is absent.
276
+ *
277
+ * @param {string} root absolute repo root
278
+ * @param {FsLike} fs
279
+ * @returns {string[]}
280
+ */
281
+ function listAgentDefs(root, fs) {
282
+ const agentsDir = path.resolve(root, '.agents', 'agents');
283
+ let names;
284
+ try {
285
+ names = fs.readdirSync(agentsDir);
286
+ } catch {
287
+ return [];
288
+ }
289
+ return names
290
+ .filter((n) => n.endsWith('.md'))
291
+ .map((n) => path.posix.join('.agents', 'agents', n))
292
+ .sort();
258
293
  }
259
294
 
260
295
  /**