mandrel 2.64.0 → 2.66.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 (105) hide show
  1. package/.agents/agents/acceptance-critic.md +8 -7
  2. package/.agents/agents/auditor.md +20 -20
  3. package/.agents/agents/plan-critic.md +8 -7
  4. package/.agents/agents/story-worker.md +7 -7
  5. package/.agents/audit-checklists/quality.md +3 -0
  6. package/.agents/docs/agentrc-reference.json +1 -9
  7. package/.agents/docs/configuration.md +8 -7
  8. package/.agents/docs/execution-reference.md +27 -5
  9. package/.agents/instructions.md +10 -12
  10. package/.agents/rules/ci-remediation.md +3 -3
  11. package/.agents/rules/gherkin-standards.md +3 -2
  12. package/.agents/rules/git-conventions-reference.md +12 -3
  13. package/.agents/rules/git-conventions.md +9 -7
  14. package/.agents/rules/testing-standards.md +8 -7
  15. package/.agents/runtime-deps.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +6 -13
  17. package/.agents/schemas/audit-rules.schema.json +1 -1
  18. package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
  19. package/.agents/scripts/bootstrap.js +102 -91
  20. package/.agents/scripts/check-context-budget.js +1 -1
  21. package/.agents/scripts/lib/ITicketingProvider.js +1 -3
  22. package/.agents/scripts/lib/audit-suite/findings.js +1 -17
  23. package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
  24. package/.agents/scripts/lib/audit-suite/index.js +0 -6
  25. package/.agents/scripts/lib/audit-suite/selector.js +0 -31
  26. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  27. package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
  28. package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
  29. package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
  30. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +91 -107
  31. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  32. package/.agents/scripts/lib/cli-args.js +26 -0
  33. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  34. package/.agents/scripts/lib/config/review-chain-default.js +13 -0
  35. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  36. package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
  37. package/.agents/scripts/lib/doc-tiers.js +25 -6
  38. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  39. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  40. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  41. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  42. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  43. package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
  44. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  45. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  46. package/.agents/scripts/lib/orchestration/code-review.js +33 -6
  47. package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
  48. package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
  49. package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
  50. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  51. package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
  52. package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
  53. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +29 -4
  54. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
  55. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  56. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
  57. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -12
  58. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +370 -268
  59. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  60. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
  61. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  62. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +393 -313
  63. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
  64. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
  65. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  66. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  67. package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
  68. package/.agents/scripts/lib/transpile.js +28 -3
  69. package/.agents/scripts/providers/github/issues.js +14 -23
  70. package/.agents/scripts/single-story-close.js +10 -2
  71. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  72. package/.agents/scripts/sync-claude-agents.js +1 -1
  73. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  74. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  75. package/.agents/workflows/audit-architecture.md +5 -4
  76. package/.agents/workflows/audit-documentation.md +5 -5
  77. package/.agents/workflows/audit-performance.md +10 -10
  78. package/.agents/workflows/audit-quality.md +42 -7
  79. package/.agents/workflows/helpers/acceptance-self-eval.md +9 -9
  80. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  81. package/.agents/workflows/helpers/code-review.md +15 -38
  82. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  83. package/.agents/workflows/helpers/deliver-reference.md +7 -3
  84. package/.agents/workflows/helpers/deliver-story.md +9 -1
  85. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  86. package/.agents/workflows/helpers/plan-reference.md +9 -8
  87. package/.agents/workflows/mandrel-deliver.md +3 -2
  88. package/.agents/workflows/mandrel-plan.md +11 -7
  89. package/.agents/workflows/mandrel-update.md +5 -3
  90. package/docs/CHANGELOG.md +57 -0
  91. package/lib/cli/claude-code-version.js +73 -0
  92. package/lib/cli/doctor.js +2 -2
  93. package/lib/cli/guarded-sync.js +87 -0
  94. package/lib/cli/registry.js +9 -0
  95. package/lib/cli/sync-agents.js +9 -92
  96. package/lib/cli/sync-commands.js +9 -101
  97. package/lib/cli/uninstall.js +37 -9
  98. package/lib/migrations/index.js +2 -0
  99. package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
  100. package/package.json +3 -2
  101. package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
  102. package/.agents/scripts/lib/audit-suite/runner.js +0 -205
  103. package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
  104. package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
  105. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
@@ -11,6 +11,12 @@ import path from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { isCommandExcluded } from '../command-header.js';
13
13
  import { detectPackageManager as detectPm } from '../detect-package-manager.js';
14
+ import {
15
+ SYSTEM_PROMPT_BLOCK,
16
+ SYSTEM_PROMPT_ENTRY_DOC,
17
+ SYSTEM_PROMPT_IMPORT,
18
+ wireEntryDoc,
19
+ } from './agents-md-fold.js';
14
20
  import { LEDGER_RELATIVE_PATH } from './install-ledger.js';
15
21
  import { ensureIssueForms } from './issue-forms-template.js';
16
22
  import { PHASE_GROUPS } from './manifest.js';
@@ -23,17 +29,10 @@ export const SYNC_AGENTS_COMMAND = 'node .agents/scripts/sync-claude-agents.js';
23
29
 
24
30
  export const BOOTSTRAP_COMMAND = 'node .agents/scripts/bootstrap.js';
25
31
 
26
- /** `CLAUDE.md` wiring keys idempotence off this exact import path. */
27
- export const SYSTEM_PROMPT_IMPORT = '@.agents/instructions.md';
32
+ export { SYSTEM_PROMPT_BLOCK, SYSTEM_PROMPT_IMPORT };
28
33
 
29
- export const SYSTEM_PROMPT_BLOCK = `## System Prompt
30
-
31
- ${SYSTEM_PROMPT_IMPORT}
32
- `;
33
-
34
- export const SYSTEM_PROMPT_CLAUDE_MD = `# Agent Protocols
35
-
36
- ${SYSTEM_PROMPT_BLOCK}`;
34
+ /** Install template for a fresh AGENTS.md (also the legacy CLAUDE.md one). */
35
+ export const SYSTEM_PROMPT_AGENTS_MD = SYSTEM_PROMPT_ENTRY_DOC;
37
36
 
38
37
  export const GITIGNORE_BLOCKS = Object.freeze({
39
38
  commands: {
@@ -81,6 +80,10 @@ function writeJson(p, obj, fsImpl = fs) {
81
80
  fsImpl.writeFileSync(p, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
82
81
  }
83
82
 
83
+ function agentRootOf(ctx) {
84
+ return ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents');
85
+ }
86
+
84
87
  /**
85
88
  * Node floor SSOT (`node:sqlite` stabilised at 22.22.1); import it, never
86
89
  * duplicate. Matches `package.json` `engines.node` (`>=22.22.1 <25`).
@@ -127,9 +130,36 @@ export function detectPackageManager(projectRoot, fsImpl = fs) {
127
130
  return detectPm(projectRoot, (p) => fsImpl.existsSync(p)) ?? 'npm';
128
131
  }
129
132
 
133
+ /** @returns {'added'|'already-present'} */
134
+ function ensureScript(scripts, key, command) {
135
+ if (scripts[key]) return 'already-present';
136
+ scripts[key] = command;
137
+ return 'added';
138
+ }
139
+
140
+ /** Append each projection independently so a partial prepare gains the other. */
141
+ function ensurePrepareScript(scripts) {
142
+ const prepare = scripts.prepare;
143
+ if (!prepare) {
144
+ scripts.prepare = `${SYNC_COMMAND} && ${SYNC_AGENTS_COMMAND}`;
145
+ return 'added';
146
+ }
147
+ let next = prepare;
148
+ if (!next.includes('sync-claude-commands.js')) {
149
+ next = `${next} && ${SYNC_COMMAND}`;
150
+ }
151
+ if (!next.includes('sync-claude-agents.js')) {
152
+ next = `${next} && ${SYNC_AGENTS_COMMAND}`;
153
+ }
154
+ if (next === prepare) return 'already-present';
155
+ scripts.prepare = next;
156
+ return 'appended';
157
+ }
158
+
130
159
  /**
131
160
  * Ensure `package.json` carries the sync/prepare/bootstrap scripts. Never
132
161
  * touches `dependencies` — framework deps arrive transitively via `mandrel`.
162
+ * An operator-defined `bootstrap` script always wins.
133
163
  *
134
164
  * @param {object} ctx
135
165
  * @param {typeof fs} [ctx.fsImpl]
@@ -137,62 +167,32 @@ export function detectPackageManager(projectRoot, fsImpl = fs) {
137
167
  export function ensurePackageJson(ctx) {
138
168
  const { fsImpl = fs } = ctx;
139
169
  const pkgPath = path.join(ctx.projectRoot, 'package.json');
140
- const projectName = path.basename(path.resolve(ctx.projectRoot));
141
- const outcomes = {
142
- created: false,
143
- scriptsSyncCommands: 'already-present',
144
- scriptsSyncAgents: 'already-present',
145
- scriptsPrepare: 'already-present',
146
- scriptsBootstrap: 'already-present',
170
+ const existing = readJsonIfExists(pkgPath, fsImpl);
171
+ const pkg = existing || {
172
+ name: path.basename(path.resolve(ctx.projectRoot)),
173
+ version: '0.0.0',
174
+ private: true,
175
+ type: 'module',
147
176
  };
148
- let pkg = readJsonIfExists(pkgPath, fsImpl);
149
- if (!pkg) {
150
- pkg = {
151
- name: projectName,
152
- version: '0.0.0',
153
- private: true,
154
- type: 'module',
155
- };
156
- outcomes.created = true;
157
- }
158
177
  pkg.scripts = pkg.scripts ?? {};
159
- if (!pkg.scripts['sync:commands']) {
160
- pkg.scripts['sync:commands'] = SYNC_COMMAND;
161
- outcomes.scriptsSyncCommands = 'added';
162
- }
163
- if (!pkg.scripts['sync:agents']) {
164
- pkg.scripts['sync:agents'] = SYNC_AGENTS_COMMAND;
165
- outcomes.scriptsSyncAgents = 'added';
166
- }
167
- const prepare = pkg.scripts.prepare;
168
- if (!prepare) {
169
- pkg.scripts.prepare = `${SYNC_COMMAND} && ${SYNC_AGENTS_COMMAND}`;
170
- outcomes.scriptsPrepare = 'added';
171
- } else {
172
- // Append each projection independently so a partial prepare gains the other.
173
- let next = prepare;
174
- if (!next.includes('sync-claude-commands.js')) {
175
- next = `${next} && ${SYNC_COMMAND}`;
176
- }
177
- if (!next.includes('sync-claude-agents.js')) {
178
- next = `${next} && ${SYNC_AGENTS_COMMAND}`;
179
- }
180
- if (next !== prepare) {
181
- pkg.scripts.prepare = next;
182
- outcomes.scriptsPrepare = 'appended';
183
- }
184
- }
185
- // An operator-defined `bootstrap` script always wins.
186
- if (!pkg.scripts.bootstrap) {
187
- pkg.scripts.bootstrap = BOOTSTRAP_COMMAND;
188
- outcomes.scriptsBootstrap = 'added';
189
- }
190
- const mutated =
191
- outcomes.created ||
192
- outcomes.scriptsSyncCommands === 'added' ||
193
- outcomes.scriptsSyncAgents === 'added' ||
194
- outcomes.scriptsPrepare !== 'already-present' ||
195
- outcomes.scriptsBootstrap === 'added';
178
+ const outcomes = {
179
+ created: !existing,
180
+ scriptsSyncCommands: ensureScript(
181
+ pkg.scripts,
182
+ 'sync:commands',
183
+ SYNC_COMMAND,
184
+ ),
185
+ scriptsSyncAgents: ensureScript(
186
+ pkg.scripts,
187
+ 'sync:agents',
188
+ SYNC_AGENTS_COMMAND,
189
+ ),
190
+ scriptsPrepare: ensurePrepareScript(pkg.scripts),
191
+ scriptsBootstrap: ensureScript(pkg.scripts, 'bootstrap', BOOTSTRAP_COMMAND),
192
+ };
193
+ const mutated = Object.values(outcomes).some(
194
+ (v) => v === true || v === 'added' || v === 'appended',
195
+ );
196
196
  if (mutated) writeJson(pkgPath, pkg, fsImpl);
197
197
  return { ...outcomes, path: pkgPath, mutated };
198
198
  }
@@ -247,10 +247,7 @@ export function ensureAgentrc(ctx) {
247
247
  if (fsImpl.existsSync(target)) {
248
248
  return { action: 'already-present', path: target };
249
249
  }
250
- const starter = path.join(
251
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
252
- 'starter-agentrc.json',
253
- );
250
+ const starter = path.join(agentRootOf(ctx), 'starter-agentrc.json');
254
251
  if (!fsImpl.existsSync(starter)) {
255
252
  return { action: 'missing-starter', path: target };
256
253
  }
@@ -270,16 +267,28 @@ export function ensureAgentrc(ctx) {
270
267
  return { action: 'seeded', path: target, source: 'starter' };
271
268
  }
272
269
 
270
+ async function loadAgentrcValidator(schemaModule) {
271
+ // pathToFileURL handles Windows drive letters and percent-encoding.
272
+ const mod = await import(pathToFileURL(schemaModule).href);
273
+ return mod.getAgentrcValidator();
274
+ }
275
+
276
+ function agentrcVerdict(validate, data) {
277
+ if (!data) return { ok: false, errors: ['.agentrc.json missing'] };
278
+ const ok = validate(data);
279
+ return { ok: !!ok, errors: ok ? [] : (validate.errors ?? []) };
280
+ }
281
+
273
282
  /**
274
283
  * Validate `.agentrc.json` against the AJV schema; the caller decides whether to abort.
275
284
  *
276
285
  * @param {object} ctx
277
286
  * @param {typeof fs} [ctx.fsImpl]
278
287
  */
279
- export async function validateAgentrc(ctx) {
288
+ async function validateAgentrc(ctx) {
280
289
  const { fsImpl = fs } = ctx;
281
290
  const schemaModule = path.join(
282
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
291
+ agentRootOf(ctx),
283
292
  'scripts',
284
293
  'lib',
285
294
  'config-settings-schema.js',
@@ -287,16 +296,12 @@ export async function validateAgentrc(ctx) {
287
296
  if (!fsImpl.existsSync(schemaModule)) {
288
297
  return { ok: false, errors: ['config-settings-schema.js not found'] };
289
298
  }
290
- // pathToFileURL handles Windows drive letters and percent-encoding.
291
- const mod = await import(pathToFileURL(schemaModule).href);
292
- const validate = mod.getAgentrcValidator();
299
+ const validate = await loadAgentrcValidator(schemaModule);
293
300
  const data = readJsonIfExists(
294
301
  path.join(ctx.projectRoot, '.agentrc.json'),
295
302
  fsImpl,
296
303
  );
297
- if (!data) return { ok: false, errors: ['.agentrc.json missing'] };
298
- const ok = validate(data);
299
- return { ok: !!ok, errors: ok ? [] : (validate.errors ?? []) };
304
+ return agentrcVerdict(validate, data);
300
305
  }
301
306
 
302
307
  /**
@@ -348,12 +353,9 @@ function ensureIssueFormsPhase(ctx) {
348
353
  * @param {object} ctx
349
354
  * @param {typeof defaultSpawnSync} [ctx.spawnImpl]
350
355
  */
351
- export function runSyncCommands(ctx) {
356
+ function runSyncCommands(ctx) {
352
357
  const { spawnImpl = defaultSpawnSync } = ctx;
353
- const scriptsDir = path.join(
354
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
355
- 'scripts',
356
- );
358
+ const scriptsDir = path.join(agentRootOf(ctx), 'scripts');
357
359
  const projections = [
358
360
  { label: 'sync-claude-commands.js', script: 'sync-claude-commands.js' },
359
361
  { label: 'sync-claude-agents.js', script: 'sync-claude-agents.js' },
@@ -391,10 +393,7 @@ export function runSyncCommands(ctx) {
391
393
  */
392
394
  export function checkParity(ctx) {
393
395
  const { fsImpl = fs } = ctx;
394
- const workflowsDir = path.join(
395
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
396
- 'workflows',
397
- );
396
+ const workflowsDir = path.join(agentRootOf(ctx), 'workflows');
398
397
  const commandsDir = path.join(ctx.projectRoot, '.claude', 'commands');
399
398
  const list = (dir) =>
400
399
  fsImpl.existsSync(dir)
@@ -420,30 +419,15 @@ export function checkParity(ctx) {
420
419
  }
421
420
 
422
421
  /**
423
- * Ensure `CLAUDE.md` imports the system prompt — without it the framework
424
- * never loads on cold start. Creates, appends, or no-ops.
422
+ * Ensure `AGENTS.md` imports the system prompt — without it the framework
423
+ * never loads on cold start. A pre-existing `CLAUDE.md` is folded in first
424
+ * (the host would otherwise read it instead). Creates, appends, or no-ops.
425
425
  *
426
426
  * @param {object} ctx
427
427
  * @param {typeof fs} [ctx.fsImpl]
428
428
  */
429
429
  export function ensureSystemPromptWiring(ctx) {
430
- const { fsImpl = fs } = ctx;
431
- const target = path.join(ctx.projectRoot, 'CLAUDE.md');
432
- if (!fsImpl.existsSync(target)) {
433
- fsImpl.writeFileSync(target, SYSTEM_PROMPT_CLAUDE_MD, 'utf8');
434
- return { action: 'created', path: target };
435
- }
436
- const existing = fsImpl.readFileSync(target, 'utf8');
437
- if (existing.includes(SYSTEM_PROMPT_IMPORT)) {
438
- return { action: 'already-present', path: target };
439
- }
440
- const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
441
- fsImpl.writeFileSync(
442
- target,
443
- `${existing}${separator}\n${SYSTEM_PROMPT_BLOCK}`,
444
- 'utf8',
445
- );
446
- return { action: 'appended', path: target };
430
+ return wireEntryDoc(ctx.projectRoot, ctx.fsImpl);
447
431
  }
448
432
 
449
433
  /**
@@ -453,13 +437,13 @@ export function ensureSystemPromptWiring(ctx) {
453
437
  * @param {typeof fs} [ctx.fsImpl]
454
438
  * @param {typeof defaultSpawnSync} [ctx.spawnImpl]
455
439
  */
456
- export function checkWindowsGitPerf(ctx) {
440
+ function checkWindowsGitPerf(ctx) {
457
441
  const { fsImpl = fs, spawnImpl = defaultSpawnSync } = ctx;
458
- if (os.platform() !== 'win32') {
442
+ if ((ctx.platform ?? os.platform()) !== 'win32') {
459
443
  return { platform: process.platform, skipped: true };
460
444
  }
461
445
  const script = path.join(
462
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
446
+ agentRootOf(ctx),
463
447
  'scripts',
464
448
  'check-windows-git-perf.js',
465
449
  );
@@ -24,13 +24,26 @@ const SUPPORTED_FLAGS = Object.freeze({
24
24
 
25
25
  const FLAG_NAMES = Object.keys(SUPPORTED_FLAGS);
26
26
 
27
- const SUPPORTED_EXTRAS_TYPES = new Set([
28
- 'string',
29
- 'boolean',
30
- 'ticket',
31
- 'integer',
32
- 'string-multi',
33
- ]);
27
+ const isNullish = (v) => v === null || v === undefined;
28
+
29
+ /** Per-type rules shared by built-in and extra flags. */
30
+ const FLAG_TYPES = Object.freeze({
31
+ string: {
32
+ normalise: (v, orDefault) =>
33
+ typeof v === 'string' && v.length > 0 ? v : orDefault(null),
34
+ isAbsent: (v) => isNullish(v) || v === '',
35
+ },
36
+ boolean: { normalise: (v) => v === true, isAbsent: (v) => !v },
37
+ ticket: { normalise: (v) => parseTicketId(v), isAbsent: isNullish },
38
+ integer: {
39
+ normalise: (v, orDefault) => (v === undefined ? orDefault(undefined) : v),
40
+ isAbsent: (v) => v === undefined || Number.isNaN(v),
41
+ },
42
+ 'string-multi': {
43
+ normalise: (v, orDefault) => (Array.isArray(v) ? v : orDefault([])),
44
+ isAbsent: (v) => !Array.isArray(v) || v.length === 0,
45
+ },
46
+ });
34
47
 
35
48
  function camelCase(name) {
36
49
  return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
@@ -50,16 +63,33 @@ function normaliseCallSignature(opts) {
50
63
  return { argv: argv ?? [], schema, extras };
51
64
  }
52
65
 
53
- function buildDefineFlagsSpec(extras) {
66
+ function flagEntries(schema, extras) {
67
+ const entries = Object.entries(SUPPORTED_FLAGS).map(
68
+ ([flag, { key, type }]) => ({
69
+ flag,
70
+ key,
71
+ type,
72
+ def: {},
73
+ required: schema?.[flag]?.required === true,
74
+ }),
75
+ );
76
+ for (const [flag, def] of Object.entries(extras ?? {})) {
77
+ entries.push({
78
+ flag,
79
+ key: def.alias ?? camelCase(flag),
80
+ type: def.type,
81
+ def,
82
+ required: def.required === true,
83
+ });
84
+ }
85
+ return entries;
86
+ }
87
+
88
+ function buildDefineFlagsSpec(entries) {
54
89
  const spec = {};
55
- for (const [flag, { key, type }] of Object.entries(SUPPORTED_FLAGS)) {
90
+ for (const { flag, key, type, def } of entries) {
56
91
  spec[flag] = { type, alias: key };
57
- }
58
- if (!extras) return spec;
59
- for (const [flag, def] of Object.entries(extras)) {
60
- const entry = { type: def.type, alias: def.alias ?? camelCase(flag) };
61
- if ('default' in def) entry.default = def.default;
62
- spec[flag] = entry;
92
+ if ('default' in def) spec[flag].default = def.default;
63
93
  }
64
94
  return spec;
65
95
  }
@@ -119,10 +149,10 @@ function validateExtras(extras) {
119
149
  `parseStandardCliArgs: extras["${flag}"] must be an object`,
120
150
  );
121
151
  }
122
- if (!SUPPORTED_EXTRAS_TYPES.has(def.type)) {
152
+ if (!Object.hasOwn(FLAG_TYPES, def.type)) {
123
153
  const err = new Error(
124
154
  `parseStandardCliArgs: extras["${flag}"].type "${def.type}" is unsupported. ` +
125
- `Supported: ${[...SUPPORTED_EXTRAS_TYPES].join(', ')}.`,
155
+ `Supported: ${Object.keys(FLAG_TYPES).join(', ')}.`,
126
156
  );
127
157
  err.code = 'UNKNOWN_EXTRAS_TYPE';
128
158
  throw err;
@@ -130,15 +160,6 @@ function validateExtras(extras) {
130
160
  }
131
161
  }
132
162
 
133
- function isAbsent(type, cur) {
134
- if (type === 'ticket') return cur === null || cur === undefined;
135
- if (type === 'string') return cur === null || cur === undefined || cur === '';
136
- if (type === 'boolean') return !cur;
137
- if (type === 'integer') return cur === undefined || Number.isNaN(cur);
138
- if (type === 'string-multi') return !Array.isArray(cur) || cur.length === 0;
139
- return cur === undefined || cur === null;
140
- }
141
-
142
163
  function throwMissing(flag) {
143
164
  const err = new Error(
144
165
  `parseStandardCliArgs: missing required flag --${flag}`,
@@ -148,56 +169,19 @@ function throwMissing(flag) {
148
169
  throw err;
149
170
  }
150
171
 
151
- function enforceRequired(values, schema) {
152
- if (!schema) return;
153
- for (const [flag, rule] of Object.entries(schema)) {
154
- if (!rule || rule.required !== true) continue;
155
- const meta = SUPPORTED_FLAGS[flag];
156
- if (isAbsent(meta.type, values[meta.key])) throwMissing(flag);
157
- }
158
- }
159
-
160
- function enforceExtrasRequired(values, extras) {
161
- if (!extras) return;
162
- for (const [flag, def] of Object.entries(extras)) {
163
- if (!def || def.required !== true) continue;
164
- const key = def.alias ?? camelCase(flag);
165
- if (isAbsent(def.type, values[key])) throwMissing(flag);
172
+ function normaliseValues(raw, entries) {
173
+ const out = {};
174
+ for (const { key, type, def } of entries) {
175
+ const orDefault = (empty) => ('default' in def ? def.default : empty);
176
+ out[key] = FLAG_TYPES[type].normalise(raw[key], orDefault);
166
177
  }
178
+ return out;
167
179
  }
168
180
 
169
- /** Absent strings become `null` and booleans strict `false`. */
170
- function normaliseValues(raw, extras) {
171
- const out = {};
172
- for (const [, { key, type }] of Object.entries(SUPPORTED_FLAGS)) {
173
- let v = raw[key];
174
- if (type === 'ticket') {
175
- v = parseTicketId(v);
176
- } else if (type === 'string') {
177
- v = typeof v === 'string' && v.length > 0 ? v : null;
178
- } else if (type === 'boolean') {
179
- v = v === true;
180
- }
181
- out[key] = v;
181
+ function enforceRequired(values, entries) {
182
+ for (const { flag, key, type, required } of entries) {
183
+ if (required && FLAG_TYPES[type].isAbsent(values[key])) throwMissing(flag);
182
184
  }
183
- if (!extras) return out;
184
- for (const [flag, def] of Object.entries(extras)) {
185
- const key = def.alias ?? camelCase(flag);
186
- let v = raw[key];
187
- if (def.type === 'boolean') {
188
- v = v === true;
189
- } else if (def.type === 'string') {
190
- if (v === undefined) v = 'default' in def ? def.default : null;
191
- } else if (def.type === 'ticket') {
192
- v = parseTicketId(v);
193
- } else if (def.type === 'string-multi') {
194
- if (!Array.isArray(v)) v = 'default' in def ? def.default : [];
195
- } else if (def.type === 'integer') {
196
- if (v === undefined && 'default' in def) v = def.default;
197
- }
198
- out[key] = v;
199
- }
200
- return out;
201
185
  }
202
186
 
203
187
  /**
@@ -222,13 +206,13 @@ export function parseStandardCliArgs(opts = {}) {
222
206
  err.flag = unknown;
223
207
  throw err;
224
208
  }
209
+ const entries = flagEntries(schema, extras);
225
210
  const { values: raw, positionals } = defineFlags(
226
- buildDefineFlagsSpec(extras),
211
+ buildDefineFlagsSpec(entries),
227
212
  argv,
228
213
  );
229
- const values = normaliseValues(raw, extras);
230
- enforceRequired(values, schema);
231
- enforceExtrasRequired(values, extras);
214
+ const values = normaliseValues(raw, entries);
215
+ enforceRequired(values, entries);
232
216
  return { values, positionals };
233
217
  }
234
218
 
@@ -133,6 +133,30 @@ function tolerantOverrideReviewBlock(value) {
133
133
  }
134
134
  }
135
135
 
136
+ /**
137
+ * Best-effort: absent or junk is `null` plus a warning, never a throw.
138
+ *
139
+ * @param {unknown} value
140
+ * @returns {{ tokens: number|null, warning: string|null }}
141
+ */
142
+ export function parseWorkerTokens(value) {
143
+ if (value == null) {
144
+ return {
145
+ tokens: null,
146
+ warning:
147
+ '--worker-tokens not supplied; telemetry.workerTokens records null.',
148
+ };
149
+ }
150
+ const raw = String(value).trim();
151
+ if (/^\d+$/.test(raw) && Number.isSafeInteger(Number(raw))) {
152
+ return { tokens: Number(raw), warning: null };
153
+ }
154
+ return {
155
+ tokens: null,
156
+ warning: `--worker-tokens must be a non-negative integer (got ${JSON.stringify(value)}); telemetry.workerTokens records null.`,
157
+ };
158
+ }
159
+
136
160
  /**
137
161
  * Throws when a validating flag parser rejects a value; error handlers use
138
162
  * {@link parseSprintArgsTolerant} instead of re-calling this.
@@ -164,6 +188,7 @@ export function parseSprintArgs(
164
188
  'merge-watch-mode': { type: 'string' },
165
189
  'rerun-advisory': { type: 'string' },
166
190
  'override-review-block': { type: 'string' },
191
+ 'worker-tokens': { type: 'string' },
167
192
  executor: { type: 'string' },
168
193
  cwd: { type: 'string' },
169
194
  'recut-of': { type: 'string' },
@@ -194,6 +219,7 @@ export function parseSprintArgs(
194
219
  overrideReviewBlock: tolerant
195
220
  ? tolerantOverrideReviewBlock(values['override-review-block'])
196
221
  : parseOverrideReviewBlock(values['override-review-block']),
222
+ workerTokens: values['worker-tokens'],
197
223
  executor: values.executor ?? null,
198
224
  cwd:
199
225
  (typeof values.cwd === 'string' && values.cwd.trim()) ||
@@ -23,7 +23,7 @@ export const LIST_OR_EXTENDER_OF_STRINGS = {
23
23
  ],
24
24
  };
25
25
 
26
- export const TOLERANCE_SCHEMA = {
26
+ const TOLERANCE_SCHEMA = {
27
27
  type: 'object',
28
28
  description:
29
29
  'How much a rollup may drift from the committed baseline before the gate reports a regression.',
@@ -47,7 +47,7 @@ export const TOLERANCE_SCHEMA = {
47
47
  /**
48
48
  * `"*"` is optional: the resolver injects the framework default when absent.
49
49
  */
50
- export const FLOORS_SCHEMA = {
50
+ const FLOORS_SCHEMA = {
51
51
  type: 'object',
52
52
  description:
53
53
  'Workspace-keyed absolute floors: `{ "<workspace>": { "<metric>": number } }`. `"*"` is the project-wide catch-all; the metric keyset is open so per-rollup keys flow through without each gate enumerating them. Floors are absolute — unlike `tolerance`, they are enforced regardless of the baseline.',
@@ -57,7 +57,7 @@ export const FLOORS_SCHEMA = {
57
57
  },
58
58
  };
59
59
 
60
- export const COMPONENTS_SCHEMA = {
60
+ const COMPONENTS_SCHEMA = {
61
61
  type: 'object',
62
62
  description:
63
63
  'Per-gate component map — component name to the glob list whose files roll up under it. Defaults to `{ "*": ["**"] }` at the resolver layer.',
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The one default review chain: the factory's unset/empty fallback and the
3
+ * runtime schema's declared default. `code-review` is `optional` so hosts
4
+ * without the `claude` CLI close on `native` alone.
5
+ */
6
+ export const DEFAULT_REVIEW_PROVIDERS = Object.freeze([
7
+ Object.freeze({ name: 'native' }),
8
+ Object.freeze({
9
+ name: 'code-review',
10
+ scopes: Object.freeze(['story']),
11
+ optional: true,
12
+ }),
13
+ ]);
@@ -140,7 +140,7 @@ const WORKTREE_ISOLATION_SCHEMA = {
140
140
  const MERGE_WATCH_SCHEMA = {
141
141
  type: 'object',
142
142
  description:
143
- "Knobs consumed by the close-and-land merge wait (Story #4543). `mode` (Story #4698) selects the close-time merge posture. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. The 30s poll cadence and the cap of 3 behind-the-base updates are fixed.",
143
+ "Knobs consumed by the close-and-land merge wait (Story #4543). `mode` (Story #4698) selects the close-time merge posture. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. The poll cadence is internal (at most 30s while checks run, at most 10s once they are green and the PR is unmerged); it and the cap of 3 behind-the-base updates are not configurable.",
144
144
  properties: {
145
145
  mode: {
146
146
  type: 'string',
@@ -173,7 +173,7 @@ const ROUTING_SCHEMA = {
173
173
  roleScopedAgents: {
174
174
  type: 'boolean',
175
175
  description:
176
- '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.',
176
+ '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 entry-doc (AGENTS.md or 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.',
177
177
  default: DELIVERY_ROUTING_DEFAULTS.roleScopedAgents,
178
178
  },
179
179
  ceremonyProfile: {
@@ -4,6 +4,7 @@
4
4
  // live in `config/gates/`.
5
5
 
6
6
  import { GATES_SCHEMA } from './config/gates/index.js';
7
+ import { DEFAULT_REVIEW_PROVIDERS } from './config/review-chain-default.js';
7
8
  import { DEFAULT_CODE_REVIEW } from './config/runners.js';
8
9
 
9
10
  const AUTO_REFRESH_SCHEMA = {
@@ -80,9 +81,15 @@ export const CODE_REVIEW_SCHEMA = {
80
81
  properties: {
81
82
  name: {
82
83
  type: 'string',
83
- enum: ['native', 'codex', 'security-review', 'ultrareview'],
84
+ enum: [
85
+ 'native',
86
+ 'code-review',
87
+ 'codex',
88
+ 'security-review',
89
+ 'ultrareview',
90
+ ],
84
91
  description:
85
- 'Registered provider key. Inline: native, codex, security-review. Manual-prompt: ultrareview.',
92
+ 'Registered provider key. Inline: native, code-review, codex, security-review. Manual-prompt: ultrareview.',
86
93
  },
87
94
  scopes: {
88
95
  type: 'array',
@@ -125,17 +132,8 @@ export const CODE_REVIEW_SCHEMA = {
125
132
  additionalProperties: false,
126
133
  },
127
134
  description:
128
- 'Review-provider chain (Story #2871). When unset or empty, defaults to [{ name: "native" }]. The orchestrator iterates inline entries in declaration order and merges their Finding[] before posting one structured comment; manual-prompt entries (e.g. ultrareview) contribute a trailing \'Manual review suggestions\' section. Selecting an adapter whose probe fails hard-fails at factory construction unless declared `optional: true` in the chain.',
129
- default: [
130
- { name: 'native' },
131
- { name: 'security-review', scopes: ['story'], optional: true },
132
- {
133
- name: 'ultrareview',
134
- scopes: ['story'],
135
- manualPrompt: true,
136
- when: { label: 'risk::high' },
137
- },
138
- ],
135
+ "Review-provider chain (Story #2871). When unset or empty, falls back to this default: `native` (scoped lint + MI) then an optional story-scoped `code-review` — a low-effort `claude --print` bug review whose every finding is critical and halts close before auto-merge; hosts without the `claude` CLI skip it. `security-review` and `ultrareview` are opt-in. The orchestrator iterates inline entries in declaration order and merges their Finding[] before posting one structured comment; manual-prompt entries (e.g. ultrareview) contribute a trailing 'Manual review suggestions' section. Selecting an adapter whose probe fails hard-fails at factory construction unless declared `optional: true` in the chain.",
136
+ default: DEFAULT_REVIEW_PROVIDERS,
139
137
  },
140
138
  autoFixSeverity: {
141
139
  type: 'string',