@remits/remits-cli 0.1.157 → 0.1.158
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/index.js
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
- L4591 Auth And Component Staging
|
|
13
13
|
- L5223 Component Summaries, Status, And Sync Gates
|
|
14
14
|
- L7547 Branches, Promotion, Commit, And Test Runs
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
15
|
+
- L9790 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11307 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L13806 Agent And Ticket Workflows
|
|
18
|
+
- L17982 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -112,7 +112,7 @@ let LOCAL_ACTOR_CONTEXT = null;
|
|
|
112
112
|
|
|
113
113
|
// Flags that are legitimately repeatable accumulate into an array instead of last-wins. Every other
|
|
114
114
|
// flag keeps last-wins so existing callers are unaffected.
|
|
115
|
-
const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names', 'tag', 'tags', 'key', 'keys', 'case-key', 'score', 'scores', 'lower-is-better']);
|
|
115
|
+
const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names', 'tag', 'tags', 'key', 'keys', 'case-key', 'score', 'scores', 'lower-is-better', 'param', 'params']);
|
|
116
116
|
|
|
117
117
|
// `-m "msg"` is how git spells a commit message, and agents type it from habit. Before this the parser took a
|
|
118
118
|
// single-dash token as the VALUE of whatever flag preceded it: `--json -m "x"` silently turned JSON mode
|
|
@@ -9156,7 +9156,9 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
|
9156
9156
|
const data = JSON.parse(msg.body);
|
|
9157
9157
|
if (data.type === 'TestSuite') {
|
|
9158
9158
|
const payload = data.payload || {};
|
|
9159
|
-
|
|
9159
|
+
// A parallel run is followed by every shard's task id.
|
|
9160
|
+
const followed = Array.isArray(taskId) ? taskId : (taskId ? [taskId] : []);
|
|
9161
|
+
if (payload.taskId && followed.length && !followed.includes(payload.taskId)) {
|
|
9160
9162
|
return;
|
|
9161
9163
|
}
|
|
9162
9164
|
if (payload.tests || payload.name || payload.total != null) {
|
|
@@ -9179,6 +9181,82 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
|
9179
9181
|
};
|
|
9180
9182
|
}
|
|
9181
9183
|
|
|
9184
|
+
// `--param name=value` (repeatable) -> the map a suite reads with runParams(). A value that is a JSON number,
|
|
9185
|
+
// boolean, null, array or object is passed as that JSON; anything else is the literal text, so `--param model=x`
|
|
9186
|
+
// needs no quoting. A name without `=` is refused rather than guessed at.
|
|
9187
|
+
function parseRunParams(raw) {
|
|
9188
|
+
const values = raw === undefined || raw === null || raw === true ? [] : (Array.isArray(raw) ? raw : [raw]);
|
|
9189
|
+
const params = {};
|
|
9190
|
+
values.forEach((entry) => {
|
|
9191
|
+
const text = String(entry);
|
|
9192
|
+
const eq = text.indexOf('=');
|
|
9193
|
+
if (eq <= 0) throw new Error('--param expects name=value (got "' + text + '")');
|
|
9194
|
+
const name = text.slice(0, eq).trim();
|
|
9195
|
+
const value = text.slice(eq + 1);
|
|
9196
|
+
let parsed = value;
|
|
9197
|
+
const trimmed = value.trim();
|
|
9198
|
+
if (/^(-?\d+(\.\d+)?([eE][+-]?\d+)?|true|false|null)$/.test(trimmed) || /^(\[[\s\S]*\]|\{[\s\S]*\})$/.test(trimmed)) {
|
|
9199
|
+
try {
|
|
9200
|
+
parsed = JSON.parse(trimmed);
|
|
9201
|
+
} catch (err) {
|
|
9202
|
+
parsed = value;
|
|
9203
|
+
}
|
|
9204
|
+
}
|
|
9205
|
+
params[name] = parsed;
|
|
9206
|
+
});
|
|
9207
|
+
return params;
|
|
9208
|
+
}
|
|
9209
|
+
|
|
9210
|
+
// How `test run` labels, parameterises, repeats and shards a run, from its flags. Validation of the values is the
|
|
9211
|
+
// platform's (Test.normalizeRunOptions); this only shapes the request and checks what the CLI itself acts on.
|
|
9212
|
+
function testRunOptionsFromFlags(flags = {}) {
|
|
9213
|
+
const label = flags.label === undefined || flags.label === true ? undefined : String(flags.label);
|
|
9214
|
+
const params = parseRunParams(flags.param !== undefined ? flags.param : flags.params);
|
|
9215
|
+
const repeat = flags.repeat === undefined ? undefined : parsePositiveInt(flags.repeat, NaN);
|
|
9216
|
+
if (flags.repeat !== undefined && !Number.isFinite(repeat)) throw new Error('--repeat expects a whole number of runs per case (got "' + flags.repeat + '")');
|
|
9217
|
+
const parallel = flags.parallel === undefined ? 1 : parsePositiveInt(flags.parallel, NaN);
|
|
9218
|
+
if (!Number.isFinite(parallel)) throw new Error('--parallel expects a whole number of shards (got "' + flags.parallel + '")');
|
|
9219
|
+
return {
|
|
9220
|
+
label,
|
|
9221
|
+
params: Object.keys(params).length ? params : undefined,
|
|
9222
|
+
repeat: repeat && repeat > 1 ? repeat : undefined,
|
|
9223
|
+
parallel
|
|
9224
|
+
};
|
|
9225
|
+
}
|
|
9226
|
+
|
|
9227
|
+
// How long a later shard of a parallel run waits for a free suite slot before the launch gives up.
|
|
9228
|
+
const SHARD_ADMISSION_WAIT_MS = 30 * 60 * 1000;
|
|
9229
|
+
|
|
9230
|
+
// A start the platform refused because every suite slot on the instance is taken (safe to repeat: no work began).
|
|
9231
|
+
function shardStartBusy(err) {
|
|
9232
|
+
const response = err && err.response;
|
|
9233
|
+
return !!(response && response.status === 429 && response.data && response.data.code === 'WORKLOAD_BUSY');
|
|
9234
|
+
}
|
|
9235
|
+
|
|
9236
|
+
// One start request per shard of a `--parallel N` run, all under one group id: shard i of N runs every Nth
|
|
9237
|
+
// selected case. The platform reads the group id as ONE run in status, corpus runs/compare/gate.
|
|
9238
|
+
function shardRunPayloads(runOptions = {}, groupId) {
|
|
9239
|
+
const count = Math.max(1, Number(runOptions.parallel || 1));
|
|
9240
|
+
const common = { label: runOptions.label, params: runOptions.params, repeat: runOptions.repeat };
|
|
9241
|
+
if (count <= 1) return [common];
|
|
9242
|
+
return Array.from({ length: count }, (_, i) => Object.assign({}, common, {
|
|
9243
|
+
runGroupId: groupId,
|
|
9244
|
+
shard: { index: i + 1, count }
|
|
9245
|
+
}));
|
|
9246
|
+
}
|
|
9247
|
+
|
|
9248
|
+
// What a launch asked for beyond the suite itself: label, params, repetitions and shards.
|
|
9249
|
+
function testRunLaunchLines(start = {}, runOptions = {}) {
|
|
9250
|
+
const lines = [];
|
|
9251
|
+
if (runOptions.label) lines.push('Label: ' + runOptions.label);
|
|
9252
|
+
if (runOptions.params) lines.push('Params: ' + Object.keys(runOptions.params).map((k) => k + '=' + JSON.stringify(runOptions.params[k])).join(' '));
|
|
9253
|
+
if (runOptions.repeat) lines.push('Repeat: every selected case runs ' + runOptions.repeat + ' times');
|
|
9254
|
+
if (start.runGroupId) {
|
|
9255
|
+
lines.push('Parallel: ' + (start.shardTaskIds || []).length + ' shards (' + (start.shardTaskIds || []).join(', ') + '), read as ONE run by ' + start.runGroupId);
|
|
9256
|
+
}
|
|
9257
|
+
return lines;
|
|
9258
|
+
}
|
|
9259
|
+
|
|
9182
9260
|
async function testCommand(flags) {
|
|
9183
9261
|
const cwd = process.cwd();
|
|
9184
9262
|
ensureLocalState(cwd);
|
|
@@ -9207,6 +9285,9 @@ async function testCommand(flags) {
|
|
|
9207
9285
|
? []
|
|
9208
9286
|
: (Array.isArray(flags.names) ? flags.names : [flags.names]);
|
|
9209
9287
|
const names = parseTestCaseSelectors(rawNames);
|
|
9288
|
+
// --label / --param / --repeat / --parallel: what this run varies, and how it is executed.
|
|
9289
|
+
const runOptions = testRunOptionsFromFlags(flags);
|
|
9290
|
+
const runGroupId = runOptions.parallel > 1 ? 'grp-' + crypto.randomUUID() : undefined;
|
|
9210
9291
|
|
|
9211
9292
|
const api = buildAxios(baseUrl, session.token);
|
|
9212
9293
|
// --as-account runs the Test AS a (descendant) subscriber account, so that account's relationship
|
|
@@ -9243,7 +9324,7 @@ async function testCommand(flags) {
|
|
|
9243
9324
|
});
|
|
9244
9325
|
});
|
|
9245
9326
|
|
|
9246
|
-
const
|
|
9327
|
+
const startRequest = {
|
|
9247
9328
|
token: session.token,
|
|
9248
9329
|
accountId,
|
|
9249
9330
|
asAccountId,
|
|
@@ -9255,11 +9336,43 @@ async function testCommand(flags) {
|
|
|
9255
9336
|
testId: /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
|
|
9256
9337
|
testName: /^\d+$/.test(String(testRef)) ? undefined : String(testRef),
|
|
9257
9338
|
tests: names
|
|
9258
|
-
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9339
|
+
};
|
|
9340
|
+
// Shards are started one after another (each start returns at once) and then run side by side on the platform.
|
|
9341
|
+
const shardStarts = [];
|
|
9342
|
+
// The platform admits only a few suites per instance at once and refuses the rest as WORKLOAD_BUSY. Once one
|
|
9343
|
+
// of our shards is running it holds a slot it will give back, so a later shard keeps waiting for one (bounded)
|
|
9344
|
+
// instead of giving up after the usual retries and leaving a group that can never finish.
|
|
9345
|
+
const shardWaitDeadline = Date.now() + SHARD_ADMISSION_WAIT_MS;
|
|
9346
|
+
for (const shardOptions of shardRunPayloads(runOptions, runGroupId)) {
|
|
9347
|
+
let shardStart;
|
|
9348
|
+
for (;;) {
|
|
9349
|
+
try {
|
|
9350
|
+
shardStart = await loggedPost(api, cwd, '/cli/test', Object.assign({}, startRequest, shardOptions)).then((r) => r.data);
|
|
9351
|
+
break;
|
|
9352
|
+
} catch (err) {
|
|
9353
|
+
const data = err && err.response && err.response.data;
|
|
9354
|
+
if (shardStarts.length && shardStartBusy(err) && Date.now() < shardWaitDeadline) {
|
|
9355
|
+
console.error('Shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel + ' is waiting for a free suite slot ' +
|
|
9356
|
+
'(this platform instance runs a limited number of suites at once; the shards already running free them).');
|
|
9357
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
9358
|
+
continue;
|
|
9359
|
+
}
|
|
9360
|
+
const started = shardStarts.length ? ' (shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel +
|
|
9361
|
+
' never started; the shards already started keep running: ' + shardStarts.map((st) => st.taskId).join(', ') + ')' : '';
|
|
9362
|
+
if (data && data.message) throw new Error('Failed to start test run: ' + data.message + started);
|
|
9363
|
+
throw err;
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
if (!shardStart.success) {
|
|
9367
|
+
throw new Error('Failed to start test run' + (shardStarts.length ? ' (shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel +
|
|
9368
|
+
'; the shards already started keep running: ' + shardStarts.map((st) => st.taskId).join(', ') + ')' : ''));
|
|
9369
|
+
}
|
|
9370
|
+
shardStarts.push(shardStart);
|
|
9262
9371
|
}
|
|
9372
|
+
// One run to follow: the single run, or the group whose status the platform merges from every shard.
|
|
9373
|
+
const start = runGroupId
|
|
9374
|
+
? Object.assign({}, shardStarts[0], { taskId: runGroupId, runGroupId, shardTaskIds: shardStarts.map((s) => s.taskId) })
|
|
9375
|
+
: shardStarts[0];
|
|
9263
9376
|
|
|
9264
9377
|
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
9265
9378
|
printSessionResolutionWarning(sessionContext);
|
|
@@ -9281,6 +9394,7 @@ async function testCommand(flags) {
|
|
|
9281
9394
|
gitHead: safeGitValue(cwd, 'git rev-parse HEAD')
|
|
9282
9395
|
}).forEach((line) => console.log(line));
|
|
9283
9396
|
console.log('Test run started:', start.taskId);
|
|
9397
|
+
testRunLaunchLines(start, runOptions).forEach((line) => console.log(line));
|
|
9284
9398
|
});
|
|
9285
9399
|
runtimeState.currentTestTaskId = start.taskId;
|
|
9286
9400
|
|
|
@@ -9330,7 +9444,7 @@ async function testCommand(flags) {
|
|
|
9330
9444
|
if (flags.watch !== 'false') {
|
|
9331
9445
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
9332
9446
|
if (topic) {
|
|
9333
|
-
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
|
|
9447
|
+
stopWs = watchWebsocket(baseUrl, topic, start.shardTaskIds || start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
|
|
9334
9448
|
}
|
|
9335
9449
|
}
|
|
9336
9450
|
|
|
@@ -10345,6 +10459,7 @@ function corpusAiLabel(row = {}) {
|
|
|
10345
10459
|
|
|
10346
10460
|
function corpusScoreSummary(scores = {}) {
|
|
10347
10461
|
return Object.keys(scores).map((k) => k + ' ' + (scores[k] && scores[k].mean !== undefined ? scores[k].mean : scores[k]) +
|
|
10462
|
+
(scores[k] && scores[k].stderr !== undefined ? ' \u00b1' + scores[k].stderr : '') +
|
|
10348
10463
|
(scores[k] && scores[k].cases !== undefined ? '/' + scores[k].cases : '')).join(', ');
|
|
10349
10464
|
}
|
|
10350
10465
|
|
|
@@ -10352,7 +10467,8 @@ function corpusScoreDeltas(scores = {}) {
|
|
|
10352
10467
|
return Object.keys(scores).map((k) => {
|
|
10353
10468
|
const s = scores[k] || {};
|
|
10354
10469
|
const delta = s.delta !== undefined && s.delta !== null ? ' (' + (Number(s.delta) >= 0 ? '+' : '') + s.delta + ')' : '';
|
|
10355
|
-
|
|
10470
|
+
const noise = s.stderr !== undefined ? ' [se ' + s.stderr + (s.withinNoise ? ', within noise' : '') + ']' : '';
|
|
10471
|
+
return k + ' ' + (s.base !== undefined ? s.base : '?') + ' -> ' + (s.head !== undefined ? s.head : '?') + delta + noise + (s.regressed ? ' REGRESSED' : '');
|
|
10356
10472
|
}).join(', ');
|
|
10357
10473
|
}
|
|
10358
10474
|
|
|
@@ -10364,16 +10480,22 @@ function corpusLines(sub, data = {}) {
|
|
|
10364
10480
|
cases.forEach((c) => lines.push('- ' + c.caseKey + (c.split ? ' [' + c.split + ']' : '') + (c.tags && c.tags.length ? ' tags ' + c.tags.join(',') : '') +
|
|
10365
10481
|
' expected ' + (c.hasExpected ? (c.expectedStatus || 'present') : 'none') + ' artifacts ' + (c.artifacts || []).map((a) => a.name + ':' + a.bytes + 'b').join(' ')));
|
|
10366
10482
|
} else if (sub === 'results') {
|
|
10367
|
-
(data.results || []).forEach((r) => lines.push('- ' + r.caseKey + ' ' + (r.outcome || '?') + (r.score !== undefined ? ' score ' + r.score : '') +
|
|
10368
|
-
' run ' + r.taskId + ' ' + corpusAiLabel(r) + (r.durationMs ? ' ' + formatDurationMs(r.durationMs) : '') +
|
|
10483
|
+
(data.results || []).forEach((r) => lines.push('- ' + r.caseKey + (r.repeatIndex ? ' #' + r.repeatIndex : '') + ' ' + (r.outcome || '?') + (r.score !== undefined ? ' score ' + r.score : '') +
|
|
10484
|
+
' run ' + (r.runGroupId || r.taskId) + (r.runLabel ? ' (' + r.runLabel + ')' : '') + (r.split ? ' [' + r.split + ']' : '') + ' ' + corpusAiLabel(r) + (r.durationMs ? ' ' + formatDurationMs(r.durationMs) : '') +
|
|
10369
10485
|
' metrics ' + String(r.metricsHash || '').slice(0, 10) + ' ' + formatTime(r.recordedAt)));
|
|
10370
10486
|
} else if (sub === 'runs') {
|
|
10371
|
-
(data.runs || []).forEach((r) => lines.push('- ' + r.taskId + ' ' + r.
|
|
10487
|
+
(data.runs || []).forEach((r) => lines.push('- ' + r.taskId + (r.label ? ' "' + r.label + '"' : '') + ' ' + r.cases + ' case(s)' +
|
|
10488
|
+
(r.repeats ? ' x' + r.repeats + ' repeats' : '') + (r.shards ? ' ' + r.shards + ' shards' : '') + ' ' +
|
|
10489
|
+
Object.keys(r.outcomes || {}).map((k) => k + ' ' + r.outcomes[k]).join(', ') + (r.passRate !== undefined ? ' pass rate ' + r.passRate : '') +
|
|
10372
10490
|
' ' + corpusAiLabel(r) + ' ' + (r.dataMode || '?') + ' ' + (r.branchName || '?') + (r.workspace ? ' [ws:' + r.workspace + ']' : '') +
|
|
10373
10491
|
(r.laneContentHash ? ' lane ' + String(r.laneContentHash).slice(0, 12) : '') + ' ' + formatTime(r.recordedAt)));
|
|
10374
10492
|
(data.runs || []).forEach((r) => {
|
|
10375
10493
|
const extra = [];
|
|
10494
|
+
if (r.params && Object.keys(r.params).length) extra.push('params ' + Object.keys(r.params).map((k) => k + '=' + JSON.stringify(r.params[k])).join(' '));
|
|
10376
10495
|
if (r.scores && Object.keys(r.scores).length) extra.push('scores ' + corpusScoreSummary(r.scores));
|
|
10496
|
+
if (r.bySplit) extra.push('by split ' + Object.keys(r.bySplit).map((sp) => sp + ': ' + r.bySplit[sp].cases + ' case(s)' +
|
|
10497
|
+
(r.bySplit[sp].passRate !== undefined ? ' pass ' + r.bySplit[sp].passRate : '') +
|
|
10498
|
+
(r.bySplit[sp].scores ? ' ' + corpusScoreSummary(r.bySplit[sp].scores) : '')).join('; '));
|
|
10377
10499
|
if (r.review) extra.push('graded ' + r.review.graded + ', spot-checked ' + r.review.reviewed + (r.review.agreement !== undefined ? ' (agreement ' + r.review.agreement + ')' : ''));
|
|
10378
10500
|
if (extra.length) lines.push(' ' + r.taskId + ': ' + extra.join(' '));
|
|
10379
10501
|
});
|
|
@@ -10388,6 +10510,7 @@ function corpusLines(sub, data = {}) {
|
|
|
10388
10510
|
if (cmp.scores && Object.keys(cmp.scores).length) lines.push('Scores (cases both runs measured): ' + corpusScoreDeltas(cmp.scores));
|
|
10389
10511
|
(cmp.cases || []).filter((c) => c.direction !== 'same' || c.changedScores).forEach((c) => {
|
|
10390
10512
|
lines.push('- ' + c.caseKey + ' ' + c.direction + (c.base ? ' ' + c.base.outcome : '') + (c.head ? ' -> ' + c.head.outcome : '') +
|
|
10513
|
+
(c.base && c.base.passRate !== undefined ? ' pass ' + c.base.passRate + ' -> ' + (c.head && c.head.passRate !== undefined ? c.head.passRate : '?') : '') +
|
|
10391
10514
|
(c.expectedChanged ? ' (EXPECTED ANSWER CHANGED between runs)' : ''));
|
|
10392
10515
|
(c.changedMetrics || []).slice(0, 8).forEach((m) => lines.push(' ' + m.metric + ': ' + JSON.stringify(m.base) + ' -> ' + JSON.stringify(m.head)));
|
|
10393
10516
|
Object.keys(c.changedScores || {}).slice(0, 8).forEach((k) => lines.push(' score ' + k + ': ' + c.changedScores[k].base + ' -> ' + c.changedScores[k].head));
|
|
@@ -10404,9 +10527,11 @@ function corpusLines(sub, data = {}) {
|
|
|
10404
10527
|
} else if (sub === 'gate') {
|
|
10405
10528
|
const gate = data.gate || {};
|
|
10406
10529
|
lines.push('Gate ' + (gate.baseTaskId || '?') + ' -> ' + (gate.headTaskId || '?') + ': ' + (gate.passed ? 'PASSED' : 'DID NOT PASS') +
|
|
10407
|
-
(gate.tolerance !== undefined ? ' (tolerance ' + gate.tolerance + ')' : ''))
|
|
10530
|
+
(gate.tolerance !== undefined ? ' (tolerance ' + gate.tolerance + ')' : '') + (gate.noiseSigma !== undefined ? ' (noise sigma ' + gate.noiseSigma + ')' : '') +
|
|
10531
|
+
(gate.split ? ' split ' + gate.split : ''));
|
|
10408
10532
|
if (gate.scores && Object.keys(gate.scores).length) lines.push('Scores: ' + corpusScoreDeltas(gate.scores));
|
|
10409
10533
|
(gate.reasons || []).forEach((r) => lines.push('- ' + r));
|
|
10534
|
+
(gate.notes || []).forEach((n) => lines.push('Note: ' + n));
|
|
10410
10535
|
if (gate.expectedChanged && gate.expectedChanged.length) lines.push('Left out (expected answer changed between runs): ' + gate.expectedChanged.join(', '));
|
|
10411
10536
|
(gate.changedCases || []).slice(0, 15).forEach((c) => lines.push(' ' + c.caseKey + ' ' + c.direction +
|
|
10412
10537
|
Object.keys(c.changedScores || {}).map((k) => ' ' + k + ' ' + c.changedScores[k].base + '->' + c.changedScores[k].head).join('')));
|
|
@@ -10428,7 +10553,7 @@ function corpusLines(sub, data = {}) {
|
|
|
10428
10553
|
lines.push('Case ' + con.caseKey + ': ' + con.runs + ' run(s), ' + (con.stable ? 'STABLE (identical metrics every run)' : (con.variants || []).length + ' distinct metric results') +
|
|
10429
10554
|
(con.aiModes && con.aiModes.length ? ' AI ' + con.aiModes.join('+') : ''));
|
|
10430
10555
|
if (con.note) lines.push('WARNING: ' + con.note);
|
|
10431
|
-
(con.variants || []).forEach((v) => lines.push('- metrics ' + String(v.metricsHash).slice(0, 10) + ' runs ' + (v.runs || []).map((r) => r.taskId + ':' + r.outcome + (r.aiMode && r.aiMode !== 'none' ? '/' + r.aiMode : '')).join(', ')));
|
|
10556
|
+
(con.variants || []).forEach((v) => lines.push('- metrics ' + String(v.metricsHash).slice(0, 10) + ' runs ' + (v.runs || []).map((r) => r.taskId + (r.repeatIndex ? '#' + r.repeatIndex : '') + ':' + r.outcome + (r.aiMode && r.aiMode !== 'none' ? '/' + r.aiMode : '')).join(', ')));
|
|
10432
10557
|
}
|
|
10433
10558
|
return lines;
|
|
10434
10559
|
}
|
|
@@ -10538,12 +10663,16 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10538
10663
|
const keys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
|
|
10539
10664
|
data = await post({ command: 'cases', corpus: corpusName, split: flags.split, tags: tags.length ? tags : undefined, keys: keys.length ? keys : undefined, includeValues: flagEnabled(flags['include-values']), includeRetired: flagEnabled(flags['include-retired']), limit: flags.limit });
|
|
10540
10665
|
} else if (sub === 'results') {
|
|
10541
|
-
|
|
10666
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
10667
|
+
data = await post({ command: 'results', corpus: corpusName, caseKey: flags.case || flags['case-key'], taskId: flags['task-id'], label: flags.label,
|
|
10668
|
+
split: flags.split, tags: tags.length ? tags : undefined, limit: flags.limit || 50 });
|
|
10542
10669
|
} else if (sub === 'runs') {
|
|
10543
|
-
|
|
10670
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
10671
|
+
data = await post({ command: 'runs', corpus: corpusName, label: flags.label, split: flags.split, tags: tags.length ? tags : undefined, limit: flags.limit || 20 });
|
|
10544
10672
|
} else if (sub === 'compare') {
|
|
10545
10673
|
if (!flags.base || !flags.head) throw new Error('corpus compare requires --base <taskId> --head <taskId>');
|
|
10546
|
-
|
|
10674
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
10675
|
+
data = await post({ command: 'compare', corpus: corpusName, base: flags.base, head: flags.head, split: flags.split, tags: tags.length ? tags : undefined });
|
|
10547
10676
|
} else if (sub === 'consistency') {
|
|
10548
10677
|
const caseKey = flags.case || flags['case-key'];
|
|
10549
10678
|
if (!caseKey) throw new Error('corpus consistency requires --case <caseKey>');
|
|
@@ -10556,10 +10685,13 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10556
10685
|
} else if (sub === 'gate') {
|
|
10557
10686
|
const scores = parseCorpusFilterValues(flags, ['score', 'scores']);
|
|
10558
10687
|
const lowerIsBetter = parseCorpusFilterValues(flags, ['lower-is-better']);
|
|
10688
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
10559
10689
|
data = await post({ command: 'gate', corpus: corpusName, base: flags.base, head: flags.head, tolerance: flags.tolerance,
|
|
10560
10690
|
scores: scores.length ? scores : undefined,
|
|
10561
10691
|
lowerIsBetter: lowerIsBetter.length ? lowerIsBetter : undefined,
|
|
10562
|
-
allowModeChange: flagEnabled(flags['allow-mode-change'])
|
|
10692
|
+
allowModeChange: flagEnabled(flags['allow-mode-change']),
|
|
10693
|
+
baseLabel: flags['base-label'], headLabel: flags['head-label'], split: flags.split, tags: tags.length ? tags : undefined,
|
|
10694
|
+
minCases: flags['min-cases'], noiseSigma: flags['noise-sigma'] });
|
|
10563
10695
|
const gate = (data && data.gate) || {};
|
|
10564
10696
|
if (!gate.passed) process.exitCode = 1;
|
|
10565
10697
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -10569,7 +10701,8 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10569
10701
|
world: Object.assign({}, (data && data.world) || {}, { host: normalizeBaseUrl(baseUrl), branchName, workspace, dataMode }),
|
|
10570
10702
|
revision: collectVerificationSource(cwd, flags),
|
|
10571
10703
|
evidenceCategories: ['corpus_gate'],
|
|
10572
|
-
corpusGate: { corpus: corpusName, baseTaskId: gate.baseTaskId, headTaskId: gate.headTaskId, passed: !!gate.passed, scores: gate.scores, reasons: gate.reasons
|
|
10704
|
+
corpusGate: { corpus: corpusName, baseTaskId: gate.baseTaskId, headTaskId: gate.headTaskId, passed: !!gate.passed, scores: gate.scores, reasons: gate.reasons,
|
|
10705
|
+
split: gate.split, noiseSigma: gate.noiseSigma, minCases: gate.minCases, notes: gate.notes }
|
|
10573
10706
|
}, { quiet: jsonOutput });
|
|
10574
10707
|
} else if (sub === 'review') {
|
|
10575
10708
|
const taskId = flags['task-id'];
|
|
@@ -10577,7 +10710,7 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10577
10710
|
if (flags.verdict) {
|
|
10578
10711
|
const caseKey = flags.case || flags['case-key'];
|
|
10579
10712
|
if (!caseKey) throw new Error('corpus review --verdict requires --case <caseKey>');
|
|
10580
|
-
data = await post({ command: 'review', corpus: corpusName, taskId, caseKey, verdict: flags.verdict, score: flags.score, note: flags.note });
|
|
10713
|
+
data = await post({ command: 'review', corpus: corpusName, taskId, caseKey, verdict: flags.verdict, score: flags.score, note: flags.note, repeatIndex: flags.repeat });
|
|
10581
10714
|
} else {
|
|
10582
10715
|
data = await post({ command: 'review', corpus: corpusName, taskId, size: flags.sample || flags.size });
|
|
10583
10716
|
}
|
|
@@ -18179,7 +18312,7 @@ function printComponentsHelp(subcommand) {
|
|
|
18179
18312
|
}
|
|
18180
18313
|
|
|
18181
18314
|
function printTestHelp() {
|
|
18182
|
-
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--json]');
|
|
18315
|
+
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--label TEXT] [--param name=value ...] [--repeat N] [--parallel N] [--json]');
|
|
18183
18316
|
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
18184
18317
|
console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
|
|
18185
18318
|
console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
|
|
@@ -18210,13 +18343,14 @@ function printTestHelp() {
|
|
|
18210
18343
|
function printCorpusHelp() {
|
|
18211
18344
|
console.log('Usage: remits-cli corpus import --manifest corpus-manifest.json [--corpus <name>] [--as-account ID] [--data-mode test|prod --confirm-prod] [--json]');
|
|
18212
18345
|
console.log(' remits-cli corpus cases --corpus <name> [--split dev] [--tag name] [--key caseKey] [--include-values] [--include-retired] [--json]');
|
|
18213
|
-
console.log(' remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--limit 50] [--json]');
|
|
18214
|
-
console.log(' remits-cli corpus runs --corpus <name> [--limit 20] [--json]');
|
|
18215
|
-
console.log(' remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--json]');
|
|
18346
|
+
console.log(' remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId|groupId>] [--label L] [--split S] [--tag T] [--limit 50] [--json]');
|
|
18347
|
+
console.log(' remits-cli corpus runs --corpus <name> [--label L] [--split S] [--tag T] [--limit 20] [--json]');
|
|
18348
|
+
console.log(' remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--split S] [--tag T] [--json]');
|
|
18216
18349
|
console.log(' remits-cli corpus consistency --corpus <name> --case <caseKey> [--limit 20] [--json]');
|
|
18217
18350
|
console.log(' remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json]');
|
|
18218
|
-
console.log(' remits-cli corpus gate --corpus <name> [--base <taskId>] [--head <taskId>
|
|
18219
|
-
console.log('
|
|
18351
|
+
console.log(' remits-cli corpus gate --corpus <name> [--base <taskId> | --base-label L] [--head <taskId> | --head-label L] [--split holdout] [--tag T]');
|
|
18352
|
+
console.log(' [--score answer --score passRate] [--tolerance 0.02] [--lower-is-better <name>] [--min-cases N] [--noise-sigma 2] [--allow-mode-change] [--json]');
|
|
18353
|
+
console.log(' remits-cli corpus review --corpus <name> --task-id <taskId> [--sample 5] | --case <caseKey> [--repeat N] --verdict agree|disagree [--score n] [--note "..."]');
|
|
18220
18354
|
console.log('');
|
|
18221
18355
|
console.log('A corpus is a named, durable set of cases an evaluation-style Test runs again and again (front stage:');
|
|
18222
18356
|
console.log('corpus(name)). Cases live in a platform table owned by one account and visible to the accounts beneath');
|
|
@@ -18477,7 +18611,7 @@ async function main() {
|
|
|
18477
18611
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
18478
18612
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
18479
18613
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
18480
|
-
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--json]');
|
|
18614
|
+
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--label TEXT] [--param name=value ...] [--repeat N] [--parallel N] [--json]');
|
|
18481
18615
|
console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
|
|
18482
18616
|
console.log(' remits-cli test compare --base <taskId> --head <taskId>');
|
|
18483
18617
|
console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
|
package/package.json
CHANGED
|
@@ -46,6 +46,7 @@ reference below, load it before you act, not after something surprises you.
|
|
|
46
46
|
| work from a non-trunk checkout, or promote a branch | `branch-variants.md` | what a variant is, which world your checkout resolves, `--as-account` vs `--variant-branch`, tombstones, the promotion loop and its phases, subscribe/retire |
|
|
47
47
|
| touch a support ticket | `support-tickets.md` | the ticket record and its verbs, routed vs claimed vs owned, autonomous workers, the repository edit lease, ask/answer/resume, delivery |
|
|
48
48
|
| register this session as an agent, or ask why a routed ticket never started | `agent-sessions.md` | `serve` vs `register`, what registration does, worker spawning per agent kind, routing order, the control center, multi-session auth |
|
|
49
|
+
| tune, improve or "fine-tune" a workflow with evals, or compare two variants of one | `development-loop.md` → *Evaluation runs* | `--label`/`--param`/`--repeat`/`--parallel`, the baseline → candidate → gate loop, the holdout rule; the full playbook is `guides/test-components.md` → *Tuning A Workflow With Evals* |
|
|
49
50
|
| build or evaluate cited Q&A, review learnings, or investigate answer quality | `ai-quality.md` | front-stage guide routing, interaction/session pivots, baseline vs proposal measurements, human checks and explicit corpus gates |
|
|
50
51
|
| investigate live behavior | `investigation.md` | which tool reads which record, correlation keys, reading a record's `content`, HTTP audits, AI activity, node/`localMode`, the production support flows |
|
|
51
52
|
| call any `mcp_*` tool | `tool-reference.md` | every tool's parameters, semantics, and traps — read the tool's entry before building its input |
|
|
@@ -61,14 +61,20 @@ search written just for the evaluator may measure something the answering Agent
|
|
|
61
61
|
2. Make the source/prompt change, or enable `includeProposed` in the Test's Agent/retrieval path.
|
|
62
62
|
Preview includes all relevant proposals in scope, not one selected id. Keep the proposal set
|
|
63
63
|
stable. Learning records are shared across data lanes; a workspace does not isolate them.
|
|
64
|
-
3. Re-stage and run the candidate under an explicit `withAiBudget` in the Test
|
|
65
|
-
per-case `scores: [retrieval: ..., answer: ...]`, grader
|
|
64
|
+
3. Re-stage and run the candidate under an explicit `withAiBudget` in the Test, with the same flags as
|
|
65
|
+
the baseline and a different `--label`. Keep per-case `scores: [retrieval: ..., answer: ...]`, grader
|
|
66
|
+
results and retrieval diagnostics.
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
69
|
remits-cli components stage --workset --account-id 1 --base-url http://localhost:8080 --data-mode test
|
|
69
|
-
remits-cli test run --test "Support Answer Evaluation" --account-id 1 --base-url http://localhost:8080 --data-mode test --json
|
|
70
|
+
remits-cli test run --test "Support Answer Evaluation" --label baseline --repeat 3 --parallel 2 --account-id 1 --base-url http://localhost:8080 --data-mode test --json
|
|
71
|
+
# make the change (or pass it with --param name=value), re-stage, then:
|
|
72
|
+
remits-cli test run --test "Support Answer Evaluation" --label candidate --repeat 3 --parallel 2 --account-id 1 --base-url http://localhost:8080 --data-mode test --json
|
|
70
73
|
```
|
|
71
74
|
|
|
75
|
+
`--repeat 3` runs each question three times. Live answers and grades vary from run to run, and the repeats are
|
|
76
|
+
what give each score its `±` noise estimate. `--parallel 2` runs the cases in two shards under one `grp-…` id.
|
|
77
|
+
|
|
72
78
|
The suite name and account above are examples: create/use your application's suite and resolved account.
|
|
73
79
|
Repeat after the candidate edit and stage. `--test` selects a suite; `--names` selects exact case names
|
|
74
80
|
(or a `|`-separated list), not substrings or a regex. Check `total` and outcomes; a zero-case run proves
|
|
@@ -85,19 +91,23 @@ verify unreleased platform changes, even with the same account and test lane.
|
|
|
85
91
|
With the same explicit host/account/data-mode options as the runs:
|
|
86
92
|
|
|
87
93
|
```bash
|
|
94
|
+
remits-cli corpus runs --corpus support-golden --limit 2 # each run's id (grp-...), label, scores ± noise
|
|
88
95
|
remits-cli corpus compare --corpus support-golden --base BASE_TASK_ID --head CANDIDATE_TASK_ID
|
|
89
96
|
remits-cli corpus review --corpus support-golden --task-id CANDIDATE_TASK_ID --sample 5
|
|
90
97
|
remits-cli corpus review --corpus support-golden --task-id CANDIDATE_TASK_ID --case CASE_KEY --verdict agree --note "Checked against the source"
|
|
91
|
-
remits-cli corpus gate --corpus support-golden --base
|
|
98
|
+
remits-cli corpus gate --corpus support-golden --base-label baseline --head-label candidate --split holdout --score retrieval --score answer --noise-sigma 2
|
|
92
99
|
remits-cli evidence
|
|
93
100
|
```
|
|
94
101
|
|
|
95
|
-
Replace uppercase ids with the
|
|
96
|
-
|
|
102
|
+
Replace uppercase ids with the ids `corpus runs` prints. A parallel run's `grp-…` id is accepted wherever a task
|
|
103
|
+
id is. The gate can select runs by label instead of id. Run it on `holdout`, the split you did not tune against. Each repeated `--score` is a required metric;
|
|
104
|
+
`--scores retrieval,answer` is equivalent. Name the baseline and candidate by id or by label; never rely on the gate's "latest two runs" default.
|
|
97
105
|
The gate exits nonzero for regressions, lost/absent measurements or an unaccepted mocked/live mode
|
|
98
106
|
change. It records `corpus_gate` evidence. Inspect measured case counts and excluded changed expected
|
|
99
107
|
answers; also assess the application's minimum accuracy and the human spot checks. A non-regression
|
|
100
|
-
gate
|
|
108
|
+
gate does not establish an absolute quality target. With `--noise-sigma 2` (both runs launched with `--repeat`),
|
|
109
|
+
a move only counts as a regression when it is larger than the run-to-run noise. Without repeats, the gate's notes
|
|
110
|
+
say that a small difference may be noise.
|
|
101
111
|
|
|
102
112
|
Stop a release workflow on a failing gate. Approval and component sync do not automatically invoke it.
|
|
103
113
|
Human review preserves the model grade; it does not replace it. Approve/reject through the application's
|
|
@@ -259,18 +259,18 @@ remits-cli components branch <name> --copy-to <newBranch> [--dry-run] [--force]
|
|
|
259
259
|
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge] # make an account resolve this branch
|
|
260
260
|
remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
|
|
261
261
|
remits-cli components branch <name> --retire [--force] # delete the branch's overlays
|
|
262
|
-
remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>] [--json]
|
|
263
|
-
remits-cli test status --task-id <taskId> [--branch <stagingScope>] [--data-mode test|prod] [--json] # falls back to the DURABLE run record once the live status expires
|
|
262
|
+
remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a|b"] [--watch true|false] [--wait true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>] [--label <text>] [--param name=value ...] [--repeat N] [--parallel N] [--json]
|
|
263
|
+
remits-cli test status --task-id <taskId|groupId> [--branch <stagingScope>] [--data-mode test|prod] [--json] # falls back to the DURABLE run record once the live status expires
|
|
264
264
|
remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--all-lanes] [--as-account <ID>] [--json] # durable run history: pass counts, live AI cost, lane, content hash
|
|
265
265
|
remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case improved / regressed / changed, cost and world deltas
|
|
266
266
|
remits-cli corpus import --manifest corpus-manifest.json [--corpus <name>] [--as-account <ID>] [--data-mode test|prod --confirm-prod] [--json] # seed an evaluation corpus: cases + immutable artifacts; idempotent by caseKey
|
|
267
267
|
remits-cli corpus cases --corpus <name> [--split S] [--tag T|--tags T,U] [--key K|--keys K,L] [--include-values] [--include-retired] [--limit N] [--json]
|
|
268
|
-
remits-cli corpus runs --corpus <name> [--limit 20] [--json]
|
|
269
|
-
remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--json]
|
|
270
|
-
remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--json]
|
|
271
|
-
remits-cli corpus gate --corpus <name> --base <taskId> --head <taskId> [--score retrieval --score
|
|
268
|
+
remits-cli corpus runs --corpus <name> [--label L] [--split S] [--tag T] [--limit 20] [--json] # runs that measured the corpus: label, repeats, shards, pass rate, scores ± noise, per split, AI mode (live|MOCKED|none), cost, world
|
|
269
|
+
remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId|groupId>] [--label L] [--split S] [--tag T] [--json]
|
|
270
|
+
remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--split S] [--tag T] [--json] # per-case direction, pass rate, metric deltas, expectedChanged
|
|
271
|
+
remits-cli corpus gate --corpus <name> [--base <taskId> | --base-label L] [--head <taskId> | --head-label L] [--split holdout] [--tag T] [--score retrieval --score passRate] [--lower-is-better <name>] [--tolerance 0.02] [--min-cases N] [--noise-sigma 2] [--allow-mode-change] [--json]
|
|
272
272
|
remits-cli corpus review --corpus <name> --task-id <taskId> [--sample 5] [--json]
|
|
273
|
-
remits-cli corpus review --corpus <name> --task-id <taskId> --case <key> --verdict agree|disagree [--score n] [--note "..."]
|
|
273
|
+
remits-cli corpus review --corpus <name> --task-id <taskId> --case <key> [--repeat N] --verdict agree|disagree [--score n] [--note "..."]
|
|
274
274
|
remits-cli corpus consistency --corpus <name> --case <caseKey> [--json] # did the case produce the same metrics every run?
|
|
275
275
|
remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json] # drop a case from future runs; past measurements stay readable
|
|
276
276
|
```
|
|
@@ -328,8 +328,12 @@ For the baseline → proposed learning → human review workflow, see `ai-qualit
|
|
|
328
328
|
`--score retrieval --score answer` requires both metrics (`--scores retrieval,answer` is equivalent).
|
|
329
329
|
`interactions()` and `memories()` are component DSL APIs, not top-level CLI subcommands.
|
|
330
330
|
|
|
331
|
-
`corpus gate` exits nonzero for a score regression, missing measurements,
|
|
332
|
-
|
|
331
|
+
`corpus gate` exits nonzero for a score regression, missing measurements, too few shared cases
|
|
332
|
+
(`--min-cases`), a missing noise estimate (`--noise-sigma` without `--repeat` on both runs), or an unaccepted
|
|
333
|
+
AI-mode change, and records `corpus_gate` evidence. `passRate`, `liveAiCost`, `liveAiCalls` and `durationMs` are
|
|
334
|
+
built-in scores every case carries; the last three are lower-is-better without `--lower-is-better`.
|
|
335
|
+
`--base-label`/`--head-label` select the latest run with that `test run --label`. Every corpus command accepts a
|
|
336
|
+
parallel run's `grp-…` id wherever it accepts a task id. Run it explicitly before sync or approving a learning;
|
|
333
337
|
neither operation invokes it automatically. Prefer explicit run ids and check their world.
|
|
334
338
|
`corpus review` samples graded cases or records a human spot check; it preserves the model grade.
|
|
335
339
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [Stage your workset, not the whole repo](#stage-your-workset-not-the-whole-repo)
|
|
21
21
|
- [Three numbers, three questions](#three-numbers-three-questions)
|
|
22
22
|
- [Step 4: Verify the Change](#step-4-verify-the-change)
|
|
23
|
+
- [Evaluation runs](#evaluation-runs)
|
|
23
24
|
- [Many failures are usually few causes](#many-failures-are-usually-few-causes)
|
|
24
25
|
- [Step 5: Iterate If Needed](#step-5-iterate-if-needed)
|
|
25
26
|
- [Step 6: Update Documentation](#step-6-update-documentation)
|
|
@@ -430,11 +431,38 @@ about a different world.
|
|
|
430
431
|
|
|
431
432
|
The platform launch is asynchronous. By default `remits-cli test run` waits for its task to finish, and any
|
|
432
433
|
cases selected by `--names "a|b"` execute sequentially inside that suite task. If several slow cases are
|
|
433
|
-
independent,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
independent, use `--parallel N`. It splits the selected cases across N shards that run side by side, and the run
|
|
435
|
+
keeps ONE id (`grp-…`) for `test status` and every corpus command. An instance runs only a few suites at once
|
|
436
|
+
(two by default), so shards beyond that wait for a slot. Ask for 2–4. Starting separate
|
|
437
|
+
`test run --names "<case>" --wait false` commands also works for one-off proofs, but every such run is a separate
|
|
438
|
+
run: a corpus cannot compare or gate them as one. Do not parallelize cases that mutate the same fixture, rely on
|
|
439
|
+
shared suite setup state, or make undeclared live AI/provider calls.
|
|
440
|
+
|
|
441
|
+
##### Evaluation runs
|
|
442
|
+
|
|
443
|
+
When the job is to improve a workflow ("tune it", "make it more accurate", "compare these two prompts"), the unit
|
|
444
|
+
of evidence is a measured corpus run, not a green suite. Follow this loop:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
remits-cli test run --test "<eval suite>" --label baseline --repeat 3 --parallel 4 # live, inside withAiBudget
|
|
448
|
+
remits-cli corpus runs --corpus <name> --split dev # pass rate, scores ± noise
|
|
449
|
+
remits-cli corpus results --corpus <name> --label baseline --split dev # failing cases -> trace ids
|
|
450
|
+
# change ONE thing (or pass the variant with --param name=value), re-stage, then:
|
|
451
|
+
remits-cli test run --test "<eval suite>" --label candidate --repeat 3 --parallel 4
|
|
452
|
+
remits-cli corpus runs --corpus <name> --limit 2 # the two runs' grp-ids
|
|
453
|
+
remits-cli corpus compare --corpus <name> --base <baseline grp-id> --head <candidate grp-id>
|
|
454
|
+
remits-cli corpus gate --corpus <name> --base-label baseline --head-label candidate --split holdout --noise-sigma 2
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Rules:
|
|
458
|
+
|
|
459
|
+
- Tune against `dev`, and gate on `holdout` only once you are done.
|
|
460
|
+
- Keep the lane, the AI mode and the corpus identical between the two runs.
|
|
461
|
+
- Read a failing case's AI session before you change a prompt.
|
|
462
|
+
- Report the gate's numbers, its `±` and its notes, not "it passed".
|
|
463
|
+
|
|
464
|
+
The full playbook, `runParams()` and `compareFields(...)` are in `guides/test-components.md` →
|
|
465
|
+
*Tuning A Workflow With Evals*.
|
|
438
466
|
|
|
439
467
|
If cases fail, read the printed summary and pivots first: each case has an `outcome`
|
|
440
468
|
(`failed`, `error`, `budget_exceeded`, `provider_unavailable`, …) with its reason, timing, trace id, AI usage
|