@remits/remits-cli 0.1.156 → 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 +234 -27
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +7 -6
- package/skills/remits-cli/references/ai-quality.md +128 -0
- package/skills/remits-cli/references/command-reference.md +26 -7
- package/skills/remits-cli/references/development-loop.md +37 -5
- package/skills/remits-cli/references/investigation.md +9 -1
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']);
|
|
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
|
|
|
@@ -10343,6 +10457,21 @@ function corpusAiLabel(row = {}) {
|
|
|
10343
10457
|
return 'no AI';
|
|
10344
10458
|
}
|
|
10345
10459
|
|
|
10460
|
+
function corpusScoreSummary(scores = {}) {
|
|
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 : '') +
|
|
10463
|
+
(scores[k] && scores[k].cases !== undefined ? '/' + scores[k].cases : '')).join(', ');
|
|
10464
|
+
}
|
|
10465
|
+
|
|
10466
|
+
function corpusScoreDeltas(scores = {}) {
|
|
10467
|
+
return Object.keys(scores).map((k) => {
|
|
10468
|
+
const s = scores[k] || {};
|
|
10469
|
+
const delta = s.delta !== undefined && s.delta !== null ? ' (' + (Number(s.delta) >= 0 ? '+' : '') + s.delta + ')' : '';
|
|
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' : '');
|
|
10472
|
+
}).join(', ');
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10346
10475
|
function corpusLines(sub, data = {}) {
|
|
10347
10476
|
const lines = ['Corpus: ' + (data.corpus || '?') + ' (owner account ' + (data.ownerAccountId || '?') + ')'];
|
|
10348
10477
|
if (sub === 'cases') {
|
|
@@ -10351,13 +10480,25 @@ function corpusLines(sub, data = {}) {
|
|
|
10351
10480
|
cases.forEach((c) => lines.push('- ' + c.caseKey + (c.split ? ' [' + c.split + ']' : '') + (c.tags && c.tags.length ? ' tags ' + c.tags.join(',') : '') +
|
|
10352
10481
|
' expected ' + (c.hasExpected ? (c.expectedStatus || 'present') : 'none') + ' artifacts ' + (c.artifacts || []).map((a) => a.name + ':' + a.bytes + 'b').join(' ')));
|
|
10353
10482
|
} else if (sub === 'results') {
|
|
10354
|
-
(data.results || []).forEach((r) => lines.push('- ' + r.caseKey + ' ' + (r.outcome || '?') + (r.score !== undefined ? ' score ' + r.score : '') +
|
|
10355
|
-
' 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) : '') +
|
|
10356
10485
|
' metrics ' + String(r.metricsHash || '').slice(0, 10) + ' ' + formatTime(r.recordedAt)));
|
|
10357
10486
|
} else if (sub === 'runs') {
|
|
10358
|
-
(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 : '') +
|
|
10359
10490
|
' ' + corpusAiLabel(r) + ' ' + (r.dataMode || '?') + ' ' + (r.branchName || '?') + (r.workspace ? ' [ws:' + r.workspace + ']' : '') +
|
|
10360
10491
|
(r.laneContentHash ? ' lane ' + String(r.laneContentHash).slice(0, 12) : '') + ' ' + formatTime(r.recordedAt)));
|
|
10492
|
+
(data.runs || []).forEach((r) => {
|
|
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(' '));
|
|
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('; '));
|
|
10499
|
+
if (r.review) extra.push('graded ' + r.review.graded + ', spot-checked ' + r.review.reviewed + (r.review.agreement !== undefined ? ' (agreement ' + r.review.agreement + ')' : ''));
|
|
10500
|
+
if (extra.length) lines.push(' ' + r.taskId + ': ' + extra.join(' '));
|
|
10501
|
+
});
|
|
10361
10502
|
if ((data.runs || []).some((r) => r.aiMode === 'mocked')) {
|
|
10362
10503
|
lines.push('Runs marked AI MOCKED replayed every AI turn from aiMock - they measure the harness, not the model.');
|
|
10363
10504
|
}
|
|
@@ -10366,10 +10507,13 @@ function corpusLines(sub, data = {}) {
|
|
|
10366
10507
|
lines.push('Base ' + cmp.baseTaskId + ' -> head ' + cmp.headTaskId + ': ' + Object.keys(cmp.counts || {}).map((k) => k + ' ' + cmp.counts[k]).join(', '));
|
|
10367
10508
|
lines.push('AI mode: base ' + (cmp.baseAiMode || '?') + ', head ' + (cmp.headAiMode || '?'));
|
|
10368
10509
|
if (cmp.aiModeChanged) lines.push('WARNING: ' + cmp.aiModeChanged);
|
|
10369
|
-
(cmp.
|
|
10510
|
+
if (cmp.scores && Object.keys(cmp.scores).length) lines.push('Scores (cases both runs measured): ' + corpusScoreDeltas(cmp.scores));
|
|
10511
|
+
(cmp.cases || []).filter((c) => c.direction !== 'same' || c.changedScores).forEach((c) => {
|
|
10370
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 : '?') : '') +
|
|
10371
10514
|
(c.expectedChanged ? ' (EXPECTED ANSWER CHANGED between runs)' : ''));
|
|
10372
10515
|
(c.changedMetrics || []).slice(0, 8).forEach((m) => lines.push(' ' + m.metric + ': ' + JSON.stringify(m.base) + ' -> ' + JSON.stringify(m.head)));
|
|
10516
|
+
Object.keys(c.changedScores || {}).slice(0, 8).forEach((k) => lines.push(' score ' + k + ': ' + c.changedScores[k].base + ' -> ' + c.changedScores[k].head));
|
|
10373
10517
|
if (c.head && c.head.outcomeReason && c.head.outcome !== 'passed') lines.push(' reason: ' + String(c.head.outcomeReason).slice(0, 300));
|
|
10374
10518
|
});
|
|
10375
10519
|
} else if (sub === 'retire') {
|
|
@@ -10380,17 +10524,41 @@ function corpusLines(sub, data = {}) {
|
|
|
10380
10524
|
if ((data.cases || []).some((c) => c.status === 'retired' || c.status === 'restored')) {
|
|
10381
10525
|
lines.push('Retired cases stay readable in results and compare; they are just no longer handed to runs. `corpus cases --include-retired` lists them, `corpus retire --restore` puts them back.');
|
|
10382
10526
|
}
|
|
10527
|
+
} else if (sub === 'gate') {
|
|
10528
|
+
const gate = data.gate || {};
|
|
10529
|
+
lines.push('Gate ' + (gate.baseTaskId || '?') + ' -> ' + (gate.headTaskId || '?') + ': ' + (gate.passed ? 'PASSED' : 'DID NOT PASS') +
|
|
10530
|
+
(gate.tolerance !== undefined ? ' (tolerance ' + gate.tolerance + ')' : '') + (gate.noiseSigma !== undefined ? ' (noise sigma ' + gate.noiseSigma + ')' : '') +
|
|
10531
|
+
(gate.split ? ' split ' + gate.split : ''));
|
|
10532
|
+
if (gate.scores && Object.keys(gate.scores).length) lines.push('Scores: ' + corpusScoreDeltas(gate.scores));
|
|
10533
|
+
(gate.reasons || []).forEach((r) => lines.push('- ' + r));
|
|
10534
|
+
(gate.notes || []).forEach((n) => lines.push('Note: ' + n));
|
|
10535
|
+
if (gate.expectedChanged && gate.expectedChanged.length) lines.push('Left out (expected answer changed between runs): ' + gate.expectedChanged.join(', '));
|
|
10536
|
+
(gate.changedCases || []).slice(0, 15).forEach((c) => lines.push(' ' + c.caseKey + ' ' + c.direction +
|
|
10537
|
+
Object.keys(c.changedScores || {}).map((k) => ' ' + k + ' ' + c.changedScores[k].base + '->' + c.changedScores[k].head).join('')));
|
|
10538
|
+
} else if (sub === 'review') {
|
|
10539
|
+
if (data.review) {
|
|
10540
|
+
lines.push('Recorded: ' + data.review.caseKey + ' in run ' + data.review.taskId + ': ' + data.review.verdict + (data.review.score !== undefined && data.review.score !== null ? ' (score ' + data.review.score + ')' : ''));
|
|
10541
|
+
} else {
|
|
10542
|
+
const sample = data.sample || [];
|
|
10543
|
+
lines.push('Spot-check sample: ' + sample.length + ' graded case(s), unreviewed first');
|
|
10544
|
+
sample.forEach((c) => {
|
|
10545
|
+
const g = c.grade || {};
|
|
10546
|
+
lines.push('- ' + c.caseKey + ' grade ' + (g.verdict || '?') + ' ' + (g.score !== undefined ? g.score : '') + (c.review ? ' reviewed: ' + c.review.verdict : ' NOT REVIEWED'));
|
|
10547
|
+
if (g.reasons) lines.push(' ' + String(g.reasons).slice(0, 300));
|
|
10548
|
+
});
|
|
10549
|
+
if (sample.length) lines.push('Record a check: remits-cli corpus review --corpus ' + (data.corpus || '<name>') + ' --task-id <taskId> --case <caseKey> --verdict agree|disagree [--score n] [--note "..."]');
|
|
10550
|
+
}
|
|
10383
10551
|
} else if (sub === 'consistency') {
|
|
10384
10552
|
const con = data.consistency || {};
|
|
10385
10553
|
lines.push('Case ' + con.caseKey + ': ' + con.runs + ' run(s), ' + (con.stable ? 'STABLE (identical metrics every run)' : (con.variants || []).length + ' distinct metric results') +
|
|
10386
10554
|
(con.aiModes && con.aiModes.length ? ' AI ' + con.aiModes.join('+') : ''));
|
|
10387
10555
|
if (con.note) lines.push('WARNING: ' + con.note);
|
|
10388
|
-
(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(', ')));
|
|
10389
10557
|
}
|
|
10390
10558
|
return lines;
|
|
10391
10559
|
}
|
|
10392
10560
|
|
|
10393
|
-
// `corpus import|cases|results|runs|compare|consistency`: test corpora through the platform's own corpus(name).
|
|
10561
|
+
// `corpus import|cases|results|runs|compare|consistency|retire|gate|review`: test corpora through the platform's own corpus(name).
|
|
10394
10562
|
async function corpusCommand(flags, subcommand) {
|
|
10395
10563
|
const cwd = process.cwd();
|
|
10396
10564
|
ensureLocalState(cwd);
|
|
@@ -10495,12 +10663,16 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10495
10663
|
const keys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
|
|
10496
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 });
|
|
10497
10665
|
} else if (sub === 'results') {
|
|
10498
|
-
|
|
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 });
|
|
10499
10669
|
} else if (sub === 'runs') {
|
|
10500
|
-
|
|
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 });
|
|
10501
10672
|
} else if (sub === 'compare') {
|
|
10502
10673
|
if (!flags.base || !flags.head) throw new Error('corpus compare requires --base <taskId> --head <taskId>');
|
|
10503
|
-
|
|
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 });
|
|
10504
10676
|
} else if (sub === 'consistency') {
|
|
10505
10677
|
const caseKey = flags.case || flags['case-key'];
|
|
10506
10678
|
if (!caseKey) throw new Error('corpus consistency requires --case <caseKey>');
|
|
@@ -10510,8 +10682,40 @@ async function corpusCommand(flags, subcommand) {
|
|
|
10510
10682
|
if (!retireKeys.length) throw new Error('corpus retire requires --case <caseKey> (repeatable) or --keys a,b,c');
|
|
10511
10683
|
data = await post({ command: 'retire', corpus: corpusName, keys: retireKeys, restore: flagEnabled(flags.restore) });
|
|
10512
10684
|
if (data && data.success === false) process.exitCode = 1;
|
|
10685
|
+
} else if (sub === 'gate') {
|
|
10686
|
+
const scores = parseCorpusFilterValues(flags, ['score', 'scores']);
|
|
10687
|
+
const lowerIsBetter = parseCorpusFilterValues(flags, ['lower-is-better']);
|
|
10688
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
10689
|
+
data = await post({ command: 'gate', corpus: corpusName, base: flags.base, head: flags.head, tolerance: flags.tolerance,
|
|
10690
|
+
scores: scores.length ? scores : undefined,
|
|
10691
|
+
lowerIsBetter: lowerIsBetter.length ? lowerIsBetter : undefined,
|
|
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'] });
|
|
10695
|
+
const gate = (data && data.gate) || {};
|
|
10696
|
+
if (!gate.passed) process.exitCode = 1;
|
|
10697
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
10698
|
+
type: 'corpus_gate',
|
|
10699
|
+
success: !!gate.passed,
|
|
10700
|
+
claim: 'Corpus ' + corpusName + ' gate ' + (gate.baseTaskId || '?') + ' -> ' + (gate.headTaskId || '?') + ': ' + (gate.passed ? 'passed' : 'did not pass'),
|
|
10701
|
+
world: Object.assign({}, (data && data.world) || {}, { host: normalizeBaseUrl(baseUrl), branchName, workspace, dataMode }),
|
|
10702
|
+
revision: collectVerificationSource(cwd, flags),
|
|
10703
|
+
evidenceCategories: ['corpus_gate'],
|
|
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 }
|
|
10706
|
+
}, { quiet: jsonOutput });
|
|
10707
|
+
} else if (sub === 'review') {
|
|
10708
|
+
const taskId = flags['task-id'];
|
|
10709
|
+
if (!taskId) throw new Error('corpus review requires --task-id <taskId> (and --case <caseKey> --verdict agree|disagree to record a check)');
|
|
10710
|
+
if (flags.verdict) {
|
|
10711
|
+
const caseKey = flags.case || flags['case-key'];
|
|
10712
|
+
if (!caseKey) throw new Error('corpus review --verdict requires --case <caseKey>');
|
|
10713
|
+
data = await post({ command: 'review', corpus: corpusName, taskId, caseKey, verdict: flags.verdict, score: flags.score, note: flags.note, repeatIndex: flags.repeat });
|
|
10714
|
+
} else {
|
|
10715
|
+
data = await post({ command: 'review', corpus: corpusName, taskId, size: flags.sample || flags.size });
|
|
10716
|
+
}
|
|
10513
10717
|
} else {
|
|
10514
|
-
throw new Error('Unknown corpus subcommand: ' + sub + ' (use import, cases, results, runs, compare, consistency or
|
|
10718
|
+
throw new Error('Unknown corpus subcommand: ' + sub + ' (use import, cases, results, runs, compare, consistency, retire, gate or review)');
|
|
10515
10719
|
}
|
|
10516
10720
|
if (jsonOutput) {
|
|
10517
10721
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -18108,7 +18312,7 @@ function printComponentsHelp(subcommand) {
|
|
|
18108
18312
|
}
|
|
18109
18313
|
|
|
18110
18314
|
function printTestHelp() {
|
|
18111
|
-
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]');
|
|
18112
18316
|
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
18113
18317
|
console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
|
|
18114
18318
|
console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
|
|
@@ -18139,11 +18343,14 @@ function printTestHelp() {
|
|
|
18139
18343
|
function printCorpusHelp() {
|
|
18140
18344
|
console.log('Usage: remits-cli corpus import --manifest corpus-manifest.json [--corpus <name>] [--as-account ID] [--data-mode test|prod --confirm-prod] [--json]');
|
|
18141
18345
|
console.log(' remits-cli corpus cases --corpus <name> [--split dev] [--tag name] [--key caseKey] [--include-values] [--include-retired] [--json]');
|
|
18142
|
-
console.log(' remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--limit 50] [--json]');
|
|
18143
|
-
console.log(' remits-cli corpus runs --corpus <name> [--limit 20] [--json]');
|
|
18144
|
-
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]');
|
|
18145
18349
|
console.log(' remits-cli corpus consistency --corpus <name> --case <caseKey> [--limit 20] [--json]');
|
|
18146
18350
|
console.log(' remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json]');
|
|
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 "..."]');
|
|
18147
18354
|
console.log('');
|
|
18148
18355
|
console.log('A corpus is a named, durable set of cases an evaluation-style Test runs again and again (front stage:');
|
|
18149
18356
|
console.log('corpus(name)). Cases live in a platform table owned by one account and visible to the accounts beneath');
|
|
@@ -18404,7 +18611,7 @@ async function main() {
|
|
|
18404
18611
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
18405
18612
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
18406
18613
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
18407
|
-
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]');
|
|
18408
18615
|
console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
|
|
18409
18616
|
console.log(' remits-cli test compare --base <taskId> --head <taskId>');
|
|
18410
18617
|
console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
|
package/package.json
CHANGED
|
@@ -46,6 +46,8 @@ 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* |
|
|
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 |
|
|
49
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 |
|
|
50
52
|
| call any `mcp_*` tool | `tool-reference.md` | every tool's parameters, semantics, and traps — read the tool's entry before building its input |
|
|
51
53
|
| run, describe, poll, or interrupt an Action from the CLI | `tool-reference.md` → `mcp_run_action` | Action execution through `remits-cli tool --name mcp_run_action`; there is no separate top-level `remits-cli action` / `run action` wrapper |
|
|
@@ -158,12 +160,11 @@ reference named after it.
|
|
|
158
160
|
(`component-resolution.md`, `command-reference.md`)
|
|
159
161
|
- **Host and data mode are two independent decisions.** `--base-url` picks the Remits host,
|
|
160
162
|
`--data-mode` picks the data segment on it. Neither implies the other. (`command-reference.md`)
|
|
161
|
-
- **
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
server, while `test run` still defaults to `--data-mode test`. (`command-reference.md`)
|
|
163
|
+
- **Use a reachable host that has the implementation you are verifying.** For component work against
|
|
164
|
+
existing capabilities, localhost being down need not block verification: pass the intended deployed
|
|
165
|
+
host with `--base-url`, preserving account, branch, workspace and data-mode facts. For unreleased
|
|
166
|
+
platform capabilities, a deployed old binary is not a substitute; use the updated local/test runtime
|
|
167
|
+
and follow that repository's startup/restart instructions. (`command-reference.md`, `ai-quality.md`)
|
|
167
168
|
- **`test run` ignores the stored session data lane.** It defaults to `test` even when `whoami` shows
|
|
168
169
|
the session parked on prod; production Test runs require explicit prod provenance (`--data-mode prod`)
|
|
169
170
|
or Test source declaring `dataMode 'prod'` / `[dataMode:'prod']`. Check returned `dataModeSource`
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Answer Quality And Reviewed Learning
|
|
2
|
+
|
|
3
|
+
Load this when building cited Q&A, investigating a wrong answer, changing retrieval or prompts,
|
|
4
|
+
or measuring a proposed learning before review. Component APIs belong in the account's front-stage
|
|
5
|
+
guides; this reference owns the CLI investigation and measurement workflow.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Find the right guide](#find-the-right-guide)
|
|
10
|
+
- [Investigate one answer](#investigate-one-answer)
|
|
11
|
+
- [Measure a change](#measure-a-change)
|
|
12
|
+
- [Review and gate](#review-and-gate)
|
|
13
|
+
- [Keep the regression](#keep-the-regression)
|
|
14
|
+
|
|
15
|
+
## Find the right guide
|
|
16
|
+
|
|
17
|
+
| Building or changing | Read in the account repository |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Search Tool → citations → feedback/review page | `guides/features/ai-interactions.md` → *Wiring a question-to-review workflow* |
|
|
20
|
+
| Learning proposals, review, retirement, Agent memory | `guides/features/agent-memory.md` → *Reviewing learnings* |
|
|
21
|
+
| Rewrite, learned wording, rerank, answer spans, retrieval misses | `guides/features/schema-vertex-ai-extension.md` → *Retrieval quality* |
|
|
22
|
+
| Named retrieval/answer scores, rubric grader, baseline/candidate design | `guides/test-components.md` → *Measuring a proposed learning* and *Human Review And Release Gates* |
|
|
23
|
+
| Per-question guidance and timeout recovery | `guides/agent-components.md` → *ai.turnContext* and *Wiring tools to an agent* |
|
|
24
|
+
|
|
25
|
+
Use the built-in interaction and learning stores. `interactions()` and `memories()` are component DSL
|
|
26
|
+
builders, not top-level CLI commands. Call them from an application's Action/review UI or a Test;
|
|
27
|
+
do not bypass their scope and review behavior with SQL. The CLI has `corpus review` and `corpus gate`.
|
|
28
|
+
|
|
29
|
+
## Investigate one answer
|
|
30
|
+
|
|
31
|
+
Start from the answer's `interactionId`, its sources and feedback. `interactions().trace(id)` gives
|
|
32
|
+
its provider rounds and tool calls; `sessionId` and `traceId` pivot into `mcp_ai_session_search`
|
|
33
|
+
(`investigation.md`, then that tool's entry in `tool-reference.md`). Inspect what the model actually
|
|
34
|
+
received before changing a prompt. For normalization, `unstructured()` also returns `aiSessionId`
|
|
35
|
+
when a model was called, plus `traceId`.
|
|
36
|
+
|
|
37
|
+
Decide what failed before choosing a remedy:
|
|
38
|
+
|
|
39
|
+
- Expected source missing: inspect the actual retrieval and `index_explain` diagnostics in the
|
|
40
|
+
authorized data lane. Test documents are not indexed. Diagnose eligibility/indexing before tuning
|
|
41
|
+
wording; `not_reached` describes a bounded search window, not proof that a document is irrelevant.
|
|
42
|
+
- Source retrieved but answer wrong: inspect the offered passages, per-turn instructions and answer
|
|
43
|
+
trace. A citation is not proof that the source supports the claim.
|
|
44
|
+
- Coverage absent: include a should-decline corpus case; a grounded decline can be the correct result.
|
|
45
|
+
- Terminology mismatch: propose `kind: 'phrasing'` with indexed wording in `details` and a source
|
|
46
|
+
document id in `subjectRef`, then measure it. Feedback alone does not alter retrieval.
|
|
47
|
+
|
|
48
|
+
`quality.rerank.applied: false` is fallback, not successful reranking. `indexSearchMock` bypasses
|
|
49
|
+
rewriting, learned retrieval and ranking; it proves how the caller uses supplied search results.
|
|
50
|
+
Use live-provider evidence for claims about those steps. Ranking charges are outside chat-token budgets.
|
|
51
|
+
|
|
52
|
+
## Measure a change
|
|
53
|
+
|
|
54
|
+
Use the existing account Test/corpus workflow. Keep durable regression components in the account
|
|
55
|
+
repository, and stage the workset in a dedicated workspace. Establish host, execution account, source
|
|
56
|
+
layer and data lane before running (`components status`). Compare the same cases, expected answers,
|
|
57
|
+
source inputs and model settings. Retrieve and score through the actual application path; a parallel
|
|
58
|
+
search written just for the evaluator may measure something the answering Agent never saw.
|
|
59
|
+
|
|
60
|
+
1. Capture a baseline with approved memories only. Keep each question in a fresh Agent session.
|
|
61
|
+
2. Make the source/prompt change, or enable `includeProposed` in the Test's Agent/retrieval path.
|
|
62
|
+
Preview includes all relevant proposals in scope, not one selected id. Keep the proposal set
|
|
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, with the same flags as
|
|
65
|
+
the baseline and a different `--label`. Keep per-case `scores: [retrieval: ..., answer: ...]`, grader
|
|
66
|
+
results and retrieval diagnostics.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
remits-cli components stage --workset --account-id 1 --base-url http://localhost:8080 --data-mode test
|
|
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
|
|
73
|
+
```
|
|
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
|
+
|
|
78
|
+
The suite name and account above are examples: create/use your application's suite and resolved account.
|
|
79
|
+
Repeat after the candidate edit and stage. `--test` selects a suite; `--names` selects exact case names
|
|
80
|
+
(or a `|`-separated list), not substrings or a regex. Check `total` and outcomes; a zero-case run proves
|
|
81
|
+
nothing. `--wait false` only launches; finish with `test status --task-id <id>`.
|
|
82
|
+
|
|
83
|
+
Use deterministic search/AI mocks for protocol regressions and live models against pinned corpus
|
|
84
|
+
artifacts for answer quality. Real index measurements need an explicitly authorized production data
|
|
85
|
+
lane and a read-only application path; changing the lane of a workflow with writes is not a shortcut.
|
|
86
|
+
The intended host must also have the implementation being verified: a deployed old binary cannot
|
|
87
|
+
verify unreleased platform changes, even with the same account and test lane.
|
|
88
|
+
|
|
89
|
+
## Review and gate
|
|
90
|
+
|
|
91
|
+
With the same explicit host/account/data-mode options as the runs:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
remits-cli corpus runs --corpus support-golden --limit 2 # each run's id (grp-...), label, scores ± noise
|
|
95
|
+
remits-cli corpus compare --corpus support-golden --base BASE_TASK_ID --head CANDIDATE_TASK_ID
|
|
96
|
+
remits-cli corpus review --corpus support-golden --task-id CANDIDATE_TASK_ID --sample 5
|
|
97
|
+
remits-cli corpus review --corpus support-golden --task-id CANDIDATE_TASK_ID --case CASE_KEY --verdict agree --note "Checked against the source"
|
|
98
|
+
remits-cli corpus gate --corpus support-golden --base-label baseline --head-label candidate --split holdout --score retrieval --score answer --noise-sigma 2
|
|
99
|
+
remits-cli evidence
|
|
100
|
+
```
|
|
101
|
+
|
|
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.
|
|
105
|
+
The gate exits nonzero for regressions, lost/absent measurements or an unaccepted mocked/live mode
|
|
106
|
+
change. It records `corpus_gate` evidence. Inspect measured case counts and excluded changed expected
|
|
107
|
+
answers; also assess the application's minimum accuracy and the human spot checks. A non-regression
|
|
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.
|
|
111
|
+
|
|
112
|
+
Stop a release workflow on a failing gate. Approval and component sync do not automatically invoke it.
|
|
113
|
+
Human review preserves the model grade; it does not replace it. Approve/reject through the application's
|
|
114
|
+
`memories()` review path after the decision, and keep the relevant task ids in the review note. Editing
|
|
115
|
+
an approved memory affects later answers immediately; use a proposal when a replacement needs measurement.
|
|
116
|
+
|
|
117
|
+
## Keep the regression
|
|
118
|
+
|
|
119
|
+
Use `auxiliary: false` for durable Tests and their helper components. Set `regression: true` for suites
|
|
120
|
+
the account expects in its normal regression, including explicitly budgeted provider suites when
|
|
121
|
+
their cost/dependency is intentional. Otherwise leave costly/exploratory measurements opt-in.
|
|
122
|
+
Describe the proof boundary in the sidecar and README: mocked protocol, live provider smoke,
|
|
123
|
+
measured corpus quality, or UI journey. Retire/reject synthetic memories in cleanup.
|
|
124
|
+
|
|
125
|
+
Read `component-integrity.md` before landing. Source files alone do not update the platform's durable
|
|
126
|
+
regression selection: use normal account commit/push/sync when authorized, or stage for local runs.
|
|
127
|
+
Read `evidence` before reporting results; distinguish checked-in/staged source, measured behavior,
|
|
128
|
+
human approval and deployment.
|
|
@@ -65,8 +65,10 @@ Do not assume `--data-mode prod` implies the deployed prod host, or that `--data
|
|
|
65
65
|
localhost. Likewise, do not assume localhost is required because the current checkout is a back-stage
|
|
66
66
|
repo or because a previous command used localhost. If host matters, read `~/.remits-cli/sessions.json`,
|
|
67
67
|
`remits-cli whoami`, or the recent `remits-cli evidence` world blocks, then pass `--base-url` explicitly.
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
For existing capabilities, try the intended reachable host before declaring a connection blocker.
|
|
69
|
+
For unreleased platform capabilities, also verify that the host has the required implementation.
|
|
70
|
+
Staged component source cannot add missing platform APIs to an older deployed binary; use the updated
|
|
71
|
+
local/test runtime and follow that platform repository's startup/restart instructions.
|
|
70
72
|
|
|
71
73
|
### Tool Execution Lifecycle
|
|
72
74
|
|
|
@@ -257,15 +259,18 @@ remits-cli components branch <name> --copy-to <newBranch> [--dry-run] [--force]
|
|
|
257
259
|
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge] # make an account resolve this branch
|
|
258
260
|
remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
|
|
259
261
|
remits-cli components branch <name> --retire [--force] # delete the branch's overlays
|
|
260
|
-
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]
|
|
261
|
-
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
|
|
262
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
|
|
263
265
|
remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case improved / regressed / changed, cost and world deltas
|
|
264
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
|
|
265
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]
|
|
266
|
-
remits-cli corpus runs --corpus <name> [--limit 20] [--json]
|
|
267
|
-
remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--json]
|
|
268
|
-
remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--json]
|
|
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
|
+
remits-cli corpus review --corpus <name> --task-id <taskId> [--sample 5] [--json]
|
|
273
|
+
remits-cli corpus review --corpus <name> --task-id <taskId> --case <key> [--repeat N] --verdict agree|disagree [--score n] [--note "..."]
|
|
269
274
|
remits-cli corpus consistency --corpus <name> --case <caseKey> [--json] # did the case produce the same metrics every run?
|
|
270
275
|
remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json] # drop a case from future runs; past measurements stay readable
|
|
271
276
|
```
|
|
@@ -318,6 +323,20 @@ of the same **shape** - same data lane, branch, workspace and case count - and s
|
|
|
318
323
|
skipped; it refuses rather than comparing across worlds. `test status --data-mode prod` on a run recorded in
|
|
319
324
|
the test lane returns the run's real lane and now says that is what happened.
|
|
320
325
|
|
|
326
|
+
For the baseline → proposed learning → human review workflow, see `ai-quality.md` and
|
|
327
|
+
`guides/test-components.md` → *Measuring a proposed learning*. `--score` is repeatable;
|
|
328
|
+
`--score retrieval --score answer` requires both metrics (`--scores retrieval,answer` is equivalent).
|
|
329
|
+
`interactions()` and `memories()` are component DSL APIs, not top-level CLI subcommands.
|
|
330
|
+
|
|
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;
|
|
337
|
+
neither operation invokes it automatically. Prefer explicit run ids and check their world.
|
|
338
|
+
`corpus review` samples graded cases or records a human spot check; it preserves the model grade.
|
|
339
|
+
|
|
321
340
|
A run labelled **`AI MOCKED`** replayed every AI turn from `aiMock`: it produces the same outcomes, scores and
|
|
322
341
|
metrics a measured run would, so read that label before treating green as evidence. `compare` warns when base and
|
|
323
342
|
head used different AI modes; `consistency` warns before calling a case unstable when its runs mixed modes.
|
|
@@ -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)
|
|
@@ -68,6 +69,10 @@ The right model is:
|
|
|
68
69
|
|
|
69
70
|
Never treat "it looks right in prod data inspection" as sufficient proof that a code change is verified.
|
|
70
71
|
|
|
72
|
+
For cited answers, retrieval changes or reviewed learnings, load `ai-quality.md`. It connects the
|
|
73
|
+
component workflow to measured baseline/candidate runs, human checks and `corpus gate`; a passing
|
|
74
|
+
mocked Test alone cannot establish improved answer quality.
|
|
75
|
+
|
|
71
76
|
## Verification Envelopes
|
|
72
77
|
|
|
73
78
|
For a concrete user workflow, browser-facing change, branch-variant release, support ticket, or any loop
|
|
@@ -426,11 +431,38 @@ about a different world.
|
|
|
426
431
|
|
|
427
432
|
The platform launch is asynchronous. By default `remits-cli test run` waits for its task to finish, and any
|
|
428
433
|
cases selected by `--names "a|b"` execute sequentially inside that suite task. If several slow cases are
|
|
429
|
-
independent,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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*.
|
|
434
466
|
|
|
435
467
|
If cases fail, read the printed summary and pivots first: each case has an `outcome`
|
|
436
468
|
(`failed`, `error`, `budget_exceeded`, `provider_unavailable`, …) with its reason, timing, trace id, AI usage
|
|
@@ -57,6 +57,8 @@ different lanes (see `platform-overview.md` → *Test and Production Data Lanes*
|
|
|
57
57
|
- **`sessionKey`** — salted user-activity session key. Pivot `mcp_user_activity` `sessions` → `story`;
|
|
58
58
|
each beat then carries a `traceId` / `threadGroupingId` for trace and log investigation.
|
|
59
59
|
- **`sessionId`** — pivots into persisted AI activity via `mcp_ai_session_search`.
|
|
60
|
+
- **`interactionId`** — one Agent answer, including feedback and citations. In component code,
|
|
61
|
+
`interactions().get(id)` / `.trace(id)` open it; use its `sessionId` / `traceId` for deeper forensics.
|
|
60
62
|
|
|
61
63
|
### Reading a record's `content` — persisted context, not a memory dump
|
|
62
64
|
|
|
@@ -109,7 +111,13 @@ embeddable patterns: `features/http-audits.md` (`mcp_get_guide`).
|
|
|
109
111
|
|
|
110
112
|
### AI activity
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
For a rated or corrected Agent answer, start with `interactions().trace(interactionId)` from an
|
|
115
|
+
Action or Test using the component DSL. `correlation: request_ids` identifies its recorded rounds;
|
|
116
|
+
legacy `time_window` matches may overlap adjacent answers. Empty rounds can mean logging was off or
|
|
117
|
+
telemetry expired. There is no top-level CLI `interactions` command: use your review Action, or pivot
|
|
118
|
+
its session/trace into the tools below. `ai-quality.md` routes the full correction/evaluation workflow.
|
|
119
|
+
|
|
120
|
+
AI request logging provides two deeper ways in:
|
|
113
121
|
|
|
114
122
|
- **`mcp_ai_session_search`** — search groupings and open their detail. A grouping spans every session
|
|
115
123
|
sharing one grouping id: the agent turns plus its guardrail and internal `ai()` calls. Use the
|