mandrel 2.65.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 (56) hide show
  1. package/.agents/agents/acceptance-critic.md +5 -5
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/execution-reference.md +27 -5
  6. package/.agents/instructions.md +10 -12
  7. package/.agents/rules/ci-remediation.md +3 -3
  8. package/.agents/rules/gherkin-standards.md +3 -2
  9. package/.agents/rules/git-conventions-reference.md +12 -3
  10. package/.agents/rules/git-conventions.md +9 -7
  11. package/.agents/rules/testing-standards.md +8 -7
  12. package/.agents/runtime-deps.json +1 -1
  13. package/.agents/scripts/bootstrap.js +94 -89
  14. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  15. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  16. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  17. package/.agents/scripts/lib/cli-args.js +26 -0
  18. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  19. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  20. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  21. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  22. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  23. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  24. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  25. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  26. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  27. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  28. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  34. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  35. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  36. package/.agents/scripts/lib/transpile.js +28 -3
  37. package/.agents/scripts/single-story-close.js +10 -2
  38. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  39. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  40. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  41. package/.agents/workflows/audit-architecture.md +5 -4
  42. package/.agents/workflows/audit-documentation.md +5 -5
  43. package/.agents/workflows/audit-performance.md +10 -10
  44. package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
  45. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  46. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  47. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  48. package/.agents/workflows/helpers/deliver-story.md +6 -1
  49. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  50. package/.agents/workflows/mandrel-deliver.md +1 -1
  51. package/.agents/workflows/mandrel-plan.md +6 -5
  52. package/docs/CHANGELOG.md +26 -0
  53. package/lib/cli/guarded-sync.js +87 -0
  54. package/lib/cli/sync-agents.js +9 -92
  55. package/lib/cli/sync-commands.js +9 -101
  56. package/package.json +2 -2
@@ -5,24 +5,33 @@
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { createRequire } from 'node:module';
7
7
  import path from 'node:path';
8
+ import { resolveDependencyVersion } from '../dependency-version.js';
8
9
 
9
10
  const DEFAULT_MIN_TOKENS = 50;
10
11
  const DEFAULT_FORMATS = Object.freeze(['javascript']);
11
12
 
12
13
  const require = createRequire(import.meta.url);
13
14
 
15
+ /** jscpd 5 is a Rust rewrite with no Node API; only major 4 exposes one. */
16
+ const SUPPORTED_JSCPD_MAJOR = '^4';
17
+
14
18
  /**
15
19
  * Lazy CJS load: jscpd's ESM entry has a broken transitive `colors/safe`
16
20
  * specifier under strict ESM resolution, and importers that never scan should
17
21
  * not pay the load.
18
22
  *
23
+ * No `detectClones` means an unsupported major, not a missing install.
24
+ *
25
+ * @param {NodeJS.Require} [requireFn] substitutes the module resolver
19
26
  * @returns {(opts: object) => Promise<Array<object>>}
20
27
  */
21
- export function resolveDetectClones() {
22
- const jscpd = require('jscpd');
23
- if (typeof jscpd.detectClones !== 'function') {
28
+ export function resolveDetectClones(requireFn = require) {
29
+ const jscpd = requireFn('jscpd');
30
+ if (typeof jscpd?.detectClones !== 'function') {
31
+ const version = resolveDependencyVersion('jscpd', requireFn) ?? 'unknown';
24
32
  throw new Error(
25
- "[Duplication] jscpd.detectClones is not available — run 'npm install'",
33
+ `[Duplication] jscpd ${version} exposes no detectClones Node API — ` +
34
+ `the duplication gate supports jscpd ${SUPPORTED_JSCPD_MAJOR}; install jscpd@${SUPPORTED_JSCPD_MAJOR}`,
26
35
  );
27
36
  }
28
37
  return jscpd.detectClones;
@@ -44,6 +53,9 @@ export function relativisePath(sourceId, cwd) {
44
53
  }
45
54
 
46
55
  /**
56
+ * A side with `end.line < start.line` counts nothing: its real span is
57
+ * unrecoverable, and widening it recorded hundreds of phantom lines.
58
+ *
47
59
  * @param {{ start?: { line?: number }, end?: { line?: number } }} dup
48
60
  * @returns {Array<number>} the 1-based line numbers the clone covers
49
61
  */
@@ -51,10 +63,8 @@ function cloneLineNumbers(dup) {
51
63
  const start = dup?.start?.line;
52
64
  const end = dup?.end?.line;
53
65
  if (!Number.isInteger(start) || !Number.isInteger(end)) return [];
54
- const lo = Math.min(start, end);
55
- const hi = Math.max(start, end);
56
66
  const lines = [];
57
- for (let n = lo; n <= hi; n += 1) lines.push(n);
67
+ for (let n = start; n <= end; n += 1) lines.push(n);
58
68
  return lines;
59
69
  }
60
70
 
@@ -80,6 +80,10 @@ function writeJson(p, obj, fsImpl = fs) {
80
80
  fsImpl.writeFileSync(p, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
81
81
  }
82
82
 
83
+ function agentRootOf(ctx) {
84
+ return ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents');
85
+ }
86
+
83
87
  /**
84
88
  * Node floor SSOT (`node:sqlite` stabilised at 22.22.1); import it, never
85
89
  * duplicate. Matches `package.json` `engines.node` (`>=22.22.1 <25`).
@@ -126,9 +130,36 @@ export function detectPackageManager(projectRoot, fsImpl = fs) {
126
130
  return detectPm(projectRoot, (p) => fsImpl.existsSync(p)) ?? 'npm';
127
131
  }
128
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
+
129
159
  /**
130
160
  * Ensure `package.json` carries the sync/prepare/bootstrap scripts. Never
131
161
  * touches `dependencies` — framework deps arrive transitively via `mandrel`.
162
+ * An operator-defined `bootstrap` script always wins.
132
163
  *
133
164
  * @param {object} ctx
134
165
  * @param {typeof fs} [ctx.fsImpl]
@@ -136,62 +167,32 @@ export function detectPackageManager(projectRoot, fsImpl = fs) {
136
167
  export function ensurePackageJson(ctx) {
137
168
  const { fsImpl = fs } = ctx;
138
169
  const pkgPath = path.join(ctx.projectRoot, 'package.json');
139
- const projectName = path.basename(path.resolve(ctx.projectRoot));
140
- const outcomes = {
141
- created: false,
142
- scriptsSyncCommands: 'already-present',
143
- scriptsSyncAgents: 'already-present',
144
- scriptsPrepare: 'already-present',
145
- 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',
146
176
  };
147
- let pkg = readJsonIfExists(pkgPath, fsImpl);
148
- if (!pkg) {
149
- pkg = {
150
- name: projectName,
151
- version: '0.0.0',
152
- private: true,
153
- type: 'module',
154
- };
155
- outcomes.created = true;
156
- }
157
177
  pkg.scripts = pkg.scripts ?? {};
158
- if (!pkg.scripts['sync:commands']) {
159
- pkg.scripts['sync:commands'] = SYNC_COMMAND;
160
- outcomes.scriptsSyncCommands = 'added';
161
- }
162
- if (!pkg.scripts['sync:agents']) {
163
- pkg.scripts['sync:agents'] = SYNC_AGENTS_COMMAND;
164
- outcomes.scriptsSyncAgents = 'added';
165
- }
166
- const prepare = pkg.scripts.prepare;
167
- if (!prepare) {
168
- pkg.scripts.prepare = `${SYNC_COMMAND} && ${SYNC_AGENTS_COMMAND}`;
169
- outcomes.scriptsPrepare = 'added';
170
- } else {
171
- // Append each projection independently so a partial prepare gains the other.
172
- let next = prepare;
173
- if (!next.includes('sync-claude-commands.js')) {
174
- next = `${next} && ${SYNC_COMMAND}`;
175
- }
176
- if (!next.includes('sync-claude-agents.js')) {
177
- next = `${next} && ${SYNC_AGENTS_COMMAND}`;
178
- }
179
- if (next !== prepare) {
180
- pkg.scripts.prepare = next;
181
- outcomes.scriptsPrepare = 'appended';
182
- }
183
- }
184
- // An operator-defined `bootstrap` script always wins.
185
- if (!pkg.scripts.bootstrap) {
186
- pkg.scripts.bootstrap = BOOTSTRAP_COMMAND;
187
- outcomes.scriptsBootstrap = 'added';
188
- }
189
- const mutated =
190
- outcomes.created ||
191
- outcomes.scriptsSyncCommands === 'added' ||
192
- outcomes.scriptsSyncAgents === 'added' ||
193
- outcomes.scriptsPrepare !== 'already-present' ||
194
- 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
+ );
195
196
  if (mutated) writeJson(pkgPath, pkg, fsImpl);
196
197
  return { ...outcomes, path: pkgPath, mutated };
197
198
  }
@@ -246,10 +247,7 @@ export function ensureAgentrc(ctx) {
246
247
  if (fsImpl.existsSync(target)) {
247
248
  return { action: 'already-present', path: target };
248
249
  }
249
- const starter = path.join(
250
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
251
- 'starter-agentrc.json',
252
- );
250
+ const starter = path.join(agentRootOf(ctx), 'starter-agentrc.json');
253
251
  if (!fsImpl.existsSync(starter)) {
254
252
  return { action: 'missing-starter', path: target };
255
253
  }
@@ -269,16 +267,28 @@ export function ensureAgentrc(ctx) {
269
267
  return { action: 'seeded', path: target, source: 'starter' };
270
268
  }
271
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
+
272
282
  /**
273
283
  * Validate `.agentrc.json` against the AJV schema; the caller decides whether to abort.
274
284
  *
275
285
  * @param {object} ctx
276
286
  * @param {typeof fs} [ctx.fsImpl]
277
287
  */
278
- export async function validateAgentrc(ctx) {
288
+ async function validateAgentrc(ctx) {
279
289
  const { fsImpl = fs } = ctx;
280
290
  const schemaModule = path.join(
281
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
291
+ agentRootOf(ctx),
282
292
  'scripts',
283
293
  'lib',
284
294
  'config-settings-schema.js',
@@ -286,16 +296,12 @@ export async function validateAgentrc(ctx) {
286
296
  if (!fsImpl.existsSync(schemaModule)) {
287
297
  return { ok: false, errors: ['config-settings-schema.js not found'] };
288
298
  }
289
- // pathToFileURL handles Windows drive letters and percent-encoding.
290
- const mod = await import(pathToFileURL(schemaModule).href);
291
- const validate = mod.getAgentrcValidator();
299
+ const validate = await loadAgentrcValidator(schemaModule);
292
300
  const data = readJsonIfExists(
293
301
  path.join(ctx.projectRoot, '.agentrc.json'),
294
302
  fsImpl,
295
303
  );
296
- if (!data) return { ok: false, errors: ['.agentrc.json missing'] };
297
- const ok = validate(data);
298
- return { ok: !!ok, errors: ok ? [] : (validate.errors ?? []) };
304
+ return agentrcVerdict(validate, data);
299
305
  }
300
306
 
301
307
  /**
@@ -347,12 +353,9 @@ function ensureIssueFormsPhase(ctx) {
347
353
  * @param {object} ctx
348
354
  * @param {typeof defaultSpawnSync} [ctx.spawnImpl]
349
355
  */
350
- export function runSyncCommands(ctx) {
356
+ function runSyncCommands(ctx) {
351
357
  const { spawnImpl = defaultSpawnSync } = ctx;
352
- const scriptsDir = path.join(
353
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
354
- 'scripts',
355
- );
358
+ const scriptsDir = path.join(agentRootOf(ctx), 'scripts');
356
359
  const projections = [
357
360
  { label: 'sync-claude-commands.js', script: 'sync-claude-commands.js' },
358
361
  { label: 'sync-claude-agents.js', script: 'sync-claude-agents.js' },
@@ -390,10 +393,7 @@ export function runSyncCommands(ctx) {
390
393
  */
391
394
  export function checkParity(ctx) {
392
395
  const { fsImpl = fs } = ctx;
393
- const workflowsDir = path.join(
394
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
395
- 'workflows',
396
- );
396
+ const workflowsDir = path.join(agentRootOf(ctx), 'workflows');
397
397
  const commandsDir = path.join(ctx.projectRoot, '.claude', 'commands');
398
398
  const list = (dir) =>
399
399
  fsImpl.existsSync(dir)
@@ -437,13 +437,13 @@ export function ensureSystemPromptWiring(ctx) {
437
437
  * @param {typeof fs} [ctx.fsImpl]
438
438
  * @param {typeof defaultSpawnSync} [ctx.spawnImpl]
439
439
  */
440
- export function checkWindowsGitPerf(ctx) {
440
+ function checkWindowsGitPerf(ctx) {
441
441
  const { fsImpl = fs, spawnImpl = defaultSpawnSync } = ctx;
442
- if (os.platform() !== 'win32') {
442
+ if ((ctx.platform ?? os.platform()) !== 'win32') {
443
443
  return { platform: process.platform, skipped: true };
444
444
  }
445
445
  const script = path.join(
446
- ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
446
+ agentRootOf(ctx),
447
447
  'scripts',
448
448
  'check-windows-git-perf.js',
449
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.',