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.
- package/.agents/agents/acceptance-critic.md +5 -5
- package/.agents/agents/auditor.md +17 -18
- package/.agents/agents/plan-critic.md +5 -5
- package/.agents/agents/story-worker.md +5 -5
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/instructions.md +10 -12
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +12 -3
- package/.agents/rules/git-conventions.md +9 -7
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/scripts/bootstrap.js +94 -89
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
- package/.agents/scripts/lib/cli/standard-args.js +60 -76
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/config/gates/shared.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
- package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/orchestration/code-review.js +22 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
- package/.agents/workflows/audit-architecture.md +5 -4
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-performance.md +10 -10
- package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +3 -1
- package/.agents/workflows/helpers/deliver-story.md +6 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/mandrel-deliver.md +1 -1
- package/.agents/workflows/mandrel-plan.md +6 -5
- package/docs/CHANGELOG.md +26 -0
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- 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 =
|
|
23
|
-
if (typeof jscpd
|
|
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
|
-
|
|
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 =
|
|
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
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
288
|
+
async function validateAgentrc(ctx) {
|
|
279
289
|
const { fsImpl = fs } = ctx;
|
|
280
290
|
const schemaModule = path.join(
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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: ${
|
|
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
|
|
152
|
-
|
|
153
|
-
for (const
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
211
|
+
buildDefineFlagsSpec(entries),
|
|
227
212
|
argv,
|
|
228
213
|
);
|
|
229
|
-
const values = normaliseValues(raw,
|
|
230
|
-
enforceRequired(values,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.',
|