@remits/remits-cli 0.1.168 → 0.1.169
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 +493 -17
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +3 -0
- package/skills/remits-cli/references/agent-sessions.md +12 -3
- package/skills/remits-cli/references/command-reference.md +20 -4
- package/skills/remits-cli/references/development-loop.md +8 -0
- package/skills/remits-cli/references/support-tickets.md +22 -9
- package/skills/remits-cli/references/tool-reference.md +2 -1
package/index.js
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
7
|
- L128 Sessions, Account Resolution, And Production Guards
|
|
8
8
|
- L634 Local State, Workspaces, And Verification Evidence
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
9
|
+
- L2802 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L3701 Component Discovery And HTTP Logging
|
|
11
|
+
- L4484 Skill Delivery And TOC Resolution
|
|
12
|
+
- L4799 Auth And Component Staging
|
|
13
|
+
- L5435 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L7759 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L10072 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11589 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L14099 Agent And Ticket Workflows
|
|
18
|
+
- L18869 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -2097,6 +2097,7 @@ function recordEvidenceEntry(cwd, packet, context = {}) {
|
|
|
2097
2097
|
};
|
|
2098
2098
|
fs.appendFileSync(paths.evidenceFile, JSON.stringify(entry) + '\n');
|
|
2099
2099
|
trimEvidenceTrail(paths.evidenceFile);
|
|
2100
|
+
queueEvidenceReceipt(cwd, entry, context);
|
|
2100
2101
|
return entry;
|
|
2101
2102
|
} catch (_) {
|
|
2102
2103
|
// A trail write must never fail the command it is describing.
|
|
@@ -2104,6 +2105,135 @@ function recordEvidenceEntry(cwd, packet, context = {}) {
|
|
|
2104
2105
|
}
|
|
2105
2106
|
}
|
|
2106
2107
|
|
|
2108
|
+
/**
|
|
2109
|
+
* Shared passive evidence: inside a work attempt (a supervised worker, or a command given a ticket), each
|
|
2110
|
+
* trail entry is ALSO written to a durable local outbox and published to the work item as a client-reported
|
|
2111
|
+
* receipt, so a successor on another machine can retrieve it. Write before upload; a stable receiptId makes
|
|
2112
|
+
* a replay after a lost response record nothing twice. Never a verdict and never something to satisfy.
|
|
2113
|
+
*/
|
|
2114
|
+
const EVIDENCE_OUTBOX_MAX_LINES = 1000;
|
|
2115
|
+
|
|
2116
|
+
function evidenceOutboxFile(cwd, actorId) {
|
|
2117
|
+
// A supervisor drains its CHILD's outbox: the child's state lives under its own actor (REMITS_AGENT_ID),
|
|
2118
|
+
// not under the supervisor's, so the actor can be named explicitly.
|
|
2119
|
+
if (actorId) {
|
|
2120
|
+
return path.join(cwd, '.remits-cli', 'actors', normalizeLocalActor(actorId) || String(actorId), 'evidence-outbox.jsonl');
|
|
2121
|
+
}
|
|
2122
|
+
return path.join(path.dirname(localStatePaths(cwd).evidenceFile), 'evidence-outbox.jsonl');
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function evidenceReceiptId(entry, ticketId) {
|
|
2126
|
+
const crypto = require('crypto');
|
|
2127
|
+
return 'r-' + crypto.createHash('sha256').update(String(ticketId) + '|' + JSON.stringify(entry)).digest('hex').slice(0, 20);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function queueEvidenceReceipt(cwd, entry, context = {}) {
|
|
2131
|
+
try {
|
|
2132
|
+
const ticketId = String((context && context.ticketId) || process.env.REMITS_SUPPORT_TICKET_ID || '').trim();
|
|
2133
|
+
if (!ticketId || !entry) return null;
|
|
2134
|
+
const receipt = {
|
|
2135
|
+
receiptId: evidenceReceiptId(entry, ticketId),
|
|
2136
|
+
ticketId,
|
|
2137
|
+
// Partitioned by platform: a receipt queued against one host is never uploaded to another.
|
|
2138
|
+
host: entry.world && entry.world.host || (context && context.baseUrl) || null,
|
|
2139
|
+
attemptId: process.env.REMITS_WORKER_RUN_ID || null,
|
|
2140
|
+
type: entry.type,
|
|
2141
|
+
claim: entry.claim,
|
|
2142
|
+
outcome: evidenceOutcome(entry),
|
|
2143
|
+
observedAt: entry.ts,
|
|
2144
|
+
world: entry.world,
|
|
2145
|
+
ref: { packetId: entry.ref && entry.ref.packetId, taskId: entry.ref && entry.ref.taskId, callId: entry.ref && entry.ref.callId },
|
|
2146
|
+
queuedAt: new Date().toISOString()
|
|
2147
|
+
};
|
|
2148
|
+
fs.appendFileSync(evidenceOutboxFile(cwd), JSON.stringify(receipt) + '\n');
|
|
2149
|
+
return receipt;
|
|
2150
|
+
} catch (_) {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Publish queued receipts. The outbox is RENAMED before it is read, so a concurrent writer appends to a
|
|
2157
|
+
* fresh file and nothing it writes can be lost by this rewrite; whatever is not accepted is appended back.
|
|
2158
|
+
* Returns { sent, duplicate, kept, rejected }.
|
|
2159
|
+
*/
|
|
2160
|
+
async function drainEvidenceOutbox(cwd, context, actorId) {
|
|
2161
|
+
const file = evidenceOutboxFile(cwd, actorId);
|
|
2162
|
+
const summary = { sent: 0, duplicate: 0, kept: 0, rejected: 0 };
|
|
2163
|
+
// A drain that crashed mid-flight left its renamed file behind; its receipts go back in the queue.
|
|
2164
|
+
try {
|
|
2165
|
+
const dir = path.dirname(file);
|
|
2166
|
+
for (const name of fs.readdirSync(dir)) {
|
|
2167
|
+
const m = /^evidence-outbox\.jsonl\.(\d+)\.draining$/.exec(name);
|
|
2168
|
+
if (m && Number(m[1]) !== process.pid && !processIsAlive(Number(m[1]))) {
|
|
2169
|
+
const orphan = path.join(dir, name);
|
|
2170
|
+
fs.appendFileSync(file, fs.readFileSync(orphan, 'utf8'));
|
|
2171
|
+
fs.unlinkSync(orphan);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
} catch (_) { /* best effort */ }
|
|
2175
|
+
if (!fs.existsSync(file) || !context || !context.session) return summary;
|
|
2176
|
+
const draining = file + '.' + process.pid + '.draining';
|
|
2177
|
+
try { fs.renameSync(file, draining); } catch (_) { return summary; }
|
|
2178
|
+
let receipts = [];
|
|
2179
|
+
try {
|
|
2180
|
+
receipts = fs.readFileSync(draining, 'utf8').split('\n').filter(Boolean)
|
|
2181
|
+
.map((line) => { try { return JSON.parse(line); } catch (_) { return null; } }).filter(Boolean);
|
|
2182
|
+
} catch (_) { /* unreadable: keep nothing */ }
|
|
2183
|
+
const keep = [];
|
|
2184
|
+
const host = String(context.baseUrl || '').replace(/\/$/, '');
|
|
2185
|
+
// Grouped by ticket AND the data lane the receipt was observed in: a ticket lives in one lane, and
|
|
2186
|
+
// addressing it from another (e.g. the draining supervisor's session lane) finds no document there.
|
|
2187
|
+
const byTicket = new Map();
|
|
2188
|
+
for (const r of receipts) {
|
|
2189
|
+
// Another platform's receipts stay queued for that platform.
|
|
2190
|
+
if (r.host && host && String(r.host).replace(/\/$/, '') !== host) { keep.push(r); continue; }
|
|
2191
|
+
const lane = (r.world && r.world.dataMode) || context.dataMode || null;
|
|
2192
|
+
const groupKey = r.ticketId + '|' + (lane || '');
|
|
2193
|
+
if (!byTicket.has(groupKey)) byTicket.set(groupKey, { ticketId: r.ticketId, lane, list: [] });
|
|
2194
|
+
byTicket.get(groupKey).list.push(r);
|
|
2195
|
+
}
|
|
2196
|
+
const api = buildAxios(context.baseUrl, context.session.token, 20000);
|
|
2197
|
+
for (const { ticketId, lane, list } of byTicket.values()) {
|
|
2198
|
+
for (let i = 0; i < list.length; i += 50) {
|
|
2199
|
+
const batch = list.slice(i, i + 50);
|
|
2200
|
+
try {
|
|
2201
|
+
const response = await api.post('/cli/ticket', { action: 'receipt', ticketId, dataMode: lane || undefined, receipts: batch })
|
|
2202
|
+
.then((res) => res.data);
|
|
2203
|
+
if (response && response.success) {
|
|
2204
|
+
summary.sent += response.accepted || 0;
|
|
2205
|
+
summary.duplicate += response.duplicate || 0;
|
|
2206
|
+
summary.rejected += (response.rejected || []).length;
|
|
2207
|
+
} else {
|
|
2208
|
+
keep.push(...batch);
|
|
2209
|
+
}
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
// Only a request the platform says is malformed or not permitted is dropped (loudly). Everything else —
|
|
2212
|
+
// unreachable, busy (409 also covers a ticket mid-write or a stale revision), throttled, not found yet —
|
|
2213
|
+
// stays queued for the next drain: losing evidence is worse than retrying it.
|
|
2214
|
+
const status = err && err.response && err.response.status;
|
|
2215
|
+
const message = err && err.response && err.response.data && err.response.data.message;
|
|
2216
|
+
if (status === 400 || status === 401 || status === 403 || status === 413 || status === 422) {
|
|
2217
|
+
summary.rejected += batch.length;
|
|
2218
|
+
appendGlobalActivityLog('evidence', 'outbox.rejected', { ticketId, count: batch.length, status, message }, 'error');
|
|
2219
|
+
} else {
|
|
2220
|
+
keep.push(...batch);
|
|
2221
|
+
appendGlobalActivityLog('evidence', 'outbox.kept', { ticketId, count: batch.length, status: status || null,
|
|
2222
|
+
message: message || describeError(err) }, 'warn');
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
if (keep.length) {
|
|
2229
|
+
fs.appendFileSync(file, keep.slice(-EVIDENCE_OUTBOX_MAX_LINES).map((r) => JSON.stringify(r)).join('\n') + '\n');
|
|
2230
|
+
}
|
|
2231
|
+
fs.unlinkSync(draining);
|
|
2232
|
+
} catch (_) { /* the draining file remains and is retried by name below */ }
|
|
2233
|
+
summary.kept = keep.length;
|
|
2234
|
+
return summary;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2107
2237
|
function evidenceOutcome(row) {
|
|
2108
2238
|
if (row && (row.pending === true || row.success === null)) return 'pending';
|
|
2109
2239
|
return row && row.success === false ? 'failed' : 'ok';
|
|
@@ -4047,7 +4177,11 @@ function stageTimeoutMs(components, flags = {}) {
|
|
|
4047
4177
|
}
|
|
4048
4178
|
|
|
4049
4179
|
function cliRequestHeaders(token) {
|
|
4050
|
-
|
|
4180
|
+
// A supervised worker authenticates with the credential its supervisor minted for THIS attempt
|
|
4181
|
+
// (/cli/agentCredential). The platform then knows which participant is calling from the credential itself,
|
|
4182
|
+
// so omitting or mistyping an identity header cannot turn the worker into a human or another agent.
|
|
4183
|
+
const effectiveToken = process.env.REMITS_AGENT_TOKEN || token;
|
|
4184
|
+
const headers = effectiveToken ? { Authorization: 'Bearer ' + effectiveToken } : {};
|
|
4051
4185
|
// The CLI version on EVERY request, from the one place every request is built. It used to travel only on
|
|
4052
4186
|
// `agent register`, so the platform knew the version of the sessions that registered - and the agents that
|
|
4053
4187
|
// loop hardest are exactly the ones that never do. Without it the platform cannot tell an agent that the
|
|
@@ -9131,6 +9265,27 @@ function testRunSummaryLines(status = {}) {
|
|
|
9131
9265
|
return lines;
|
|
9132
9266
|
}
|
|
9133
9267
|
|
|
9268
|
+
// A full-suite run that took minutes is the most expensive thing an agent's build loop does, and the run itself
|
|
9269
|
+
// is the one place to say so. Production 2026-11: agents re-ran whole multi-minute suites after every edit,
|
|
9270
|
+
// every case passing. Printed only for an unselected run long enough (90 s of case time) to matter.
|
|
9271
|
+
function testRunPacingLines(status = {}, options = {}) {
|
|
9272
|
+
const minMs = 90 * 1000;
|
|
9273
|
+
const result = status.result || {};
|
|
9274
|
+
const tests = Array.isArray(result.tests) ? result.tests.filter((t) => t && t.name) : [];
|
|
9275
|
+
if (options.selected || tests.length < 4) return [];
|
|
9276
|
+
const durationOf = (t) => Number(t.duration) || 0;
|
|
9277
|
+
const totalMs = tests.reduce((sum, t) => sum + durationOf(t), 0);
|
|
9278
|
+
if (totalMs < minMs) return [];
|
|
9279
|
+
const slowest = tests.slice().sort((a, b) => durationOf(b) - durationOf(a)).slice(0, 3)
|
|
9280
|
+
.filter((t) => durationOf(t) > 0);
|
|
9281
|
+
const lines = ['Pacing: ' + tests.length + ' cases, ' + formatDurationMs(totalMs) + ' of case time' +
|
|
9282
|
+
(slowest.length ? '; slowest ' + slowest.map((t) => JSON.stringify(String(t.name)) + ' ' + formatDurationMs(durationOf(t))).join(', ') : '') + '.'];
|
|
9283
|
+
const ref = options.testRef !== undefined ? JSON.stringify(String(options.testRef)) : '<suite>';
|
|
9284
|
+
lines.push(' While iterating, re-run only what you are fixing: remits-cli test run --test ' + ref + ' --failed (or --names "a|b").');
|
|
9285
|
+
lines.push(' Run the whole suite once before landing.');
|
|
9286
|
+
return lines;
|
|
9287
|
+
}
|
|
9288
|
+
|
|
9134
9289
|
// Per-case comparison of two runs of the same suite: which cases changed outcome, and how cost and duration
|
|
9135
9290
|
// moved. The answer to "did this revision do better than the last one" without reconstructing either run.
|
|
9136
9291
|
function compareTestRunResults(baseResult = {}, headResult = {}) {
|
|
@@ -9349,6 +9504,13 @@ function testRunLaunchLines(start = {}, runOptions = {}) {
|
|
|
9349
9504
|
if (runOptions.label) lines.push('Label: ' + runOptions.label);
|
|
9350
9505
|
if (runOptions.params) lines.push('Params: ' + Object.keys(runOptions.params).map((k) => k + '=' + JSON.stringify(runOptions.params[k])).join(' '));
|
|
9351
9506
|
if (runOptions.repeat) lines.push('Repeat: every selected case runs ' + runOptions.repeat + ' times');
|
|
9507
|
+
if (start.rerunOf && Array.isArray(start.rerunOf.failedCases)) {
|
|
9508
|
+
const r = start.rerunOf;
|
|
9509
|
+
lines.push('Re-running ' + r.failedCases.length + ' case(s) that did not pass in run ' + r.taskId +
|
|
9510
|
+
(r.dataMode ? ' (' + r.dataMode + ' lane)' : '') + ':');
|
|
9511
|
+
r.failedCases.slice(0, 20).forEach((name) => lines.push(' - ' + name));
|
|
9512
|
+
if (r.failedCases.length > 20) lines.push(' ... and ' + (r.failedCases.length - 20) + ' more');
|
|
9513
|
+
}
|
|
9352
9514
|
if (start.runGroupId) {
|
|
9353
9515
|
lines.push('Parallel: ' + (start.shardTaskIds || []).length + ' shards (' + (start.shardTaskIds || []).join(', ') + '), read as ONE run by ' + start.runGroupId);
|
|
9354
9516
|
}
|
|
@@ -9383,6 +9545,10 @@ async function testCommand(flags) {
|
|
|
9383
9545
|
? []
|
|
9384
9546
|
: (Array.isArray(flags.names) ? flags.names : [flags.names]);
|
|
9385
9547
|
const names = parseTestCaseSelectors(rawNames);
|
|
9548
|
+
const rerunFailed = flagEnabled(flags.failed);
|
|
9549
|
+
if (rerunFailed && names.length) {
|
|
9550
|
+
throw new Error('--failed picks the cases itself; drop --names (or drop --failed).');
|
|
9551
|
+
}
|
|
9386
9552
|
// --label / --param / --repeat / --parallel: what this run varies, and how it is executed.
|
|
9387
9553
|
const runOptions = testRunOptionsFromFlags(flags);
|
|
9388
9554
|
const runGroupId = runOptions.parallel > 1 ? 'grp-' + crypto.randomUUID() : undefined;
|
|
@@ -9432,6 +9598,11 @@ async function testCommand(flags) {
|
|
|
9432
9598
|
dataMode,
|
|
9433
9599
|
dataModeSource,
|
|
9434
9600
|
caseConcurrency: flags.parallel !== undefined && runOptions.parallel === 1 ? 1 : undefined,
|
|
9601
|
+
// --candidate <laneContentHash>: refuse (stale_revision) if the lane no longer holds that staged content.
|
|
9602
|
+
expectedLaneContentHash: flags.candidate || undefined,
|
|
9603
|
+
// --failed: the platform re-runs only the cases that did not pass in your last finished run of this suite
|
|
9604
|
+
// on this git branch and staging workspace (and says which run it read).
|
|
9605
|
+
rerunFailed: rerunFailed || undefined,
|
|
9435
9606
|
testId: /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
|
|
9436
9607
|
testName: /^\d+$/.test(String(testRef)) ? undefined : String(testRef),
|
|
9437
9608
|
tests: names
|
|
@@ -9450,6 +9621,7 @@ async function testCommand(flags) {
|
|
|
9450
9621
|
break;
|
|
9451
9622
|
} catch (err) {
|
|
9452
9623
|
const data = err && err.response && err.response.data;
|
|
9624
|
+
if (data && data.code === 'no_recorded_run') throw new Error(data.message);
|
|
9453
9625
|
if (shardStarts.length && shardStartBusy(err) && Date.now() < shardWaitDeadline) {
|
|
9454
9626
|
console.error('Shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel + ' is waiting for a free suite slot ' +
|
|
9455
9627
|
'(the platform runs a limited number of suites at once; the shards already running free them).');
|
|
@@ -9462,8 +9634,18 @@ async function testCommand(flags) {
|
|
|
9462
9634
|
throw err;
|
|
9463
9635
|
}
|
|
9464
9636
|
}
|
|
9637
|
+
if (!shardStart.success && shardStart.code === 'nothing_failed' && !shardStarts.length) {
|
|
9638
|
+
// Not an error: the thing --failed exists for is already true. Exit 0 and say what to run next.
|
|
9639
|
+
if (jsonOutput) {
|
|
9640
|
+
console.log(JSON.stringify(shardStart, null, 2));
|
|
9641
|
+
} else {
|
|
9642
|
+
console.log(shardStart.message);
|
|
9643
|
+
console.log('Before landing, run the whole suite once: remits-cli test run --test ' + JSON.stringify(String(testRef)));
|
|
9644
|
+
}
|
|
9645
|
+
return;
|
|
9646
|
+
}
|
|
9465
9647
|
if (!shardStart.success) {
|
|
9466
|
-
throw new Error('Failed to start test run' + (shardStarts.length ? ' (shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel +
|
|
9648
|
+
throw new Error('Failed to start test run' + (shardStart.message ? ': ' + shardStart.message : '') + (shardStarts.length ? ' (shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel +
|
|
9467
9649
|
'; the shards already started keep running: ' + shardStarts.map((st) => st.taskId).join(', ') + ')' : ''));
|
|
9468
9650
|
}
|
|
9469
9651
|
shardStarts.push(shardStart);
|
|
@@ -9564,6 +9746,7 @@ async function testCommand(flags) {
|
|
|
9564
9746
|
.forEach((line) => console.log(line));
|
|
9565
9747
|
testRunSummaryLines(status).forEach((line) => console.log(line));
|
|
9566
9748
|
testFailureGroupLines(status, { testRef }).forEach((line) => console.log(line));
|
|
9749
|
+
testRunPacingLines(status, { testRef, selected: names.length > 0 || rerunFailed }).forEach((line) => console.log(line));
|
|
9567
9750
|
printTestRunPivots(status);
|
|
9568
9751
|
// The whole run object used to be printed here as pretty JSON. One 92-case suite measured 220 KB,
|
|
9569
9752
|
// most of it identifiers repeated once per case, spent on the turn with the least room left. It is
|
|
@@ -13975,7 +14158,7 @@ const AGENT_WORKER_MAX_LOG_BYTES = 8 * 1024 * 1024;
|
|
|
13975
14158
|
*
|
|
13976
14159
|
* `mode` is the autonomy bound, not a preference. `investigate` maps to each CLI's genuine
|
|
13977
14160
|
* read-only mode so an operator can put a real boundary around a production support agent; `edit`
|
|
13978
|
-
* lets it change files
|
|
14161
|
+
* lets it change files; whether it may land is the account's landing profile, stated in the brief (nothing here runs git).
|
|
13979
14162
|
*/
|
|
13980
14163
|
const AGENT_LAUNCHERS = {
|
|
13981
14164
|
codex: {
|
|
@@ -15354,6 +15537,22 @@ async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
|
15354
15537
|
}
|
|
15355
15538
|
}
|
|
15356
15539
|
|
|
15540
|
+
/**
|
|
15541
|
+
* Per-attempt liveness, in memory in the supervisor: when each child last wrote output and its last
|
|
15542
|
+
* summarized progress line. Reported on the supervisor's poll as `attempts`, so the platform can tell a
|
|
15543
|
+
* connected supervisor from a child that is actually producing work. Never persisted; never a heuristic.
|
|
15544
|
+
*/
|
|
15545
|
+
const WORKER_LIVENESS = new Map();
|
|
15546
|
+
|
|
15547
|
+
function noteWorkerLiveness(ticketId, patch) {
|
|
15548
|
+
const key = String(ticketId);
|
|
15549
|
+
WORKER_LIVENESS.set(key, { ...(WORKER_LIVENESS.get(key) || { ticketId: key }), ...patch });
|
|
15550
|
+
}
|
|
15551
|
+
|
|
15552
|
+
function workerLivenessFor(ticketIds) {
|
|
15553
|
+
return (ticketIds || []).map((id) => WORKER_LIVENESS.get(String(id))).filter(Boolean);
|
|
15554
|
+
}
|
|
15555
|
+
|
|
15357
15556
|
async function postAgentStatus(context, agentId, update) {
|
|
15358
15557
|
try {
|
|
15359
15558
|
await buildAxios(context.baseUrl, context.session.token, 10000)
|
|
@@ -15370,6 +15569,39 @@ async function postAgentStatus(context, agentId, update) {
|
|
|
15370
15569
|
* the same thing. Only the local supervisor can add machine-local repository hints; the worker owns the
|
|
15371
15570
|
* final grounding step, including creating or choosing a git worktree before it edits.
|
|
15372
15571
|
*/
|
|
15572
|
+
/** Mint a credential bound to one worker attempt. Null (legacy identity) when the server cannot. */
|
|
15573
|
+
async function mintWorkerCredential(context, binding) {
|
|
15574
|
+
try {
|
|
15575
|
+
const response = await buildAxios(context.baseUrl, context.session.token, 15000)
|
|
15576
|
+
.post('/cli/agentCredential', {
|
|
15577
|
+
mode: 'mint', agentId: binding.agentId, ticketId: binding.ticketId, runId: binding.runId,
|
|
15578
|
+
capabilities: binding.capabilities
|
|
15579
|
+
})
|
|
15580
|
+
.then((r) => r.data);
|
|
15581
|
+
if (response && response.success && response.credential) {
|
|
15582
|
+
appendGlobalActivityLog('agent', 'worker.credential_minted', {
|
|
15583
|
+
agentId: binding.agentId, ticketId: binding.ticketId, expiresAt: response.expiresAt
|
|
15584
|
+
});
|
|
15585
|
+
return { credential: response.credential, expiresAt: response.expiresAt };
|
|
15586
|
+
}
|
|
15587
|
+
} catch (err) {
|
|
15588
|
+
appendGlobalActivityLog('agent', 'worker.credential_unavailable', {
|
|
15589
|
+
agentId: binding.agentId, ticketId: binding.ticketId, error: describeError(err)
|
|
15590
|
+
}, 'warn');
|
|
15591
|
+
}
|
|
15592
|
+
return null;
|
|
15593
|
+
}
|
|
15594
|
+
|
|
15595
|
+
async function revokeWorkerCredential(context, workerCredential) {
|
|
15596
|
+
if (!workerCredential || !workerCredential.credential) return;
|
|
15597
|
+
try {
|
|
15598
|
+
await buildAxios(context.baseUrl, context.session.token, 15000)
|
|
15599
|
+
.post('/cli/agentCredential', { mode: 'revoke', credential: workerCredential.credential });
|
|
15600
|
+
} catch (_) {
|
|
15601
|
+
// It expires on its own; a failed revoke only shortens nothing.
|
|
15602
|
+
}
|
|
15603
|
+
}
|
|
15604
|
+
|
|
15373
15605
|
async function spawnTicketWorker(context, ctx, ticket) {
|
|
15374
15606
|
const { spawn } = require('child_process');
|
|
15375
15607
|
const agentId = ctx.agentId;
|
|
@@ -15495,6 +15727,15 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15495
15727
|
});
|
|
15496
15728
|
|
|
15497
15729
|
const attempt = recordWorkerAttempt(agentId, ticketId);
|
|
15730
|
+
// A credential bound to this attempt (agent + ticket + run). Best effort: an older server without the
|
|
15731
|
+
// endpoint leaves the worker on the legacy header-declared identity, exactly as before.
|
|
15732
|
+
// A session the OPERATOR launched read-only (--mode investigate) gets a credential WITHOUT stage/land, so
|
|
15733
|
+
// the platform refuses source changes from it rather than relying on the brief alone. A worker that is
|
|
15734
|
+
// investigating only because another run holds the lease keeps full reach: it may take the lease later.
|
|
15735
|
+
const workerCredential = await mintWorkerCredential(context, {
|
|
15736
|
+
agentId, ticketId, runId,
|
|
15737
|
+
capabilities: ctx.mode === 'investigate' ? ['read', 'ticket', 'test', 'tool', 'token'] : undefined
|
|
15738
|
+
});
|
|
15498
15739
|
const child = spawn(launcher.binary, built.args, {
|
|
15499
15740
|
cwd: workingDirectory || ctx.cwd,
|
|
15500
15741
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -15505,6 +15746,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15505
15746
|
REMITS_AGENT_ID: agentId,
|
|
15506
15747
|
REMITS_SUPPORT_TICKET_ID: ticketId,
|
|
15507
15748
|
REMITS_WORKER_RUN_ID: runId,
|
|
15749
|
+
...(workerCredential ? { REMITS_AGENT_TOKEN: workerCredential.credential } : {}),
|
|
15508
15750
|
// Give the worker its OWN staging lane.
|
|
15509
15751
|
//
|
|
15510
15752
|
// The edit LEASE already serializes who may edit a repo, and this does not replace it — a lease
|
|
@@ -15566,8 +15808,10 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15566
15808
|
let lastStatusAtMs = 0;
|
|
15567
15809
|
let buffered = '';
|
|
15568
15810
|
|
|
15811
|
+
noteWorkerLiveness(ticketId, { runId, pid: child.pid, startedAtMs: Date.now() });
|
|
15569
15812
|
const consume = (chunk) => {
|
|
15570
15813
|
const text = chunk.toString();
|
|
15814
|
+
noteWorkerLiveness(ticketId, { lastOutputAtMs: Date.now() });
|
|
15571
15815
|
if (logBytes < AGENT_WORKER_MAX_LOG_BYTES) {
|
|
15572
15816
|
logBytes += text.length;
|
|
15573
15817
|
logStream.write(text);
|
|
@@ -15583,6 +15827,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15583
15827
|
if (nowMs - lastStatusAtMs < AGENT_WORKER_STATUS_THROTTLE_MS) continue;
|
|
15584
15828
|
lastStatusAtMs = nowMs;
|
|
15585
15829
|
recordWorker(agentId, ticketId, { activity: summary, activityAt: new Date().toISOString() });
|
|
15830
|
+
noteWorkerLiveness(ticketId, { lastProgress: String(summary).slice(0, 160), lastProgressAtMs: nowMs });
|
|
15586
15831
|
postAgentStatus(context, agentId, {
|
|
15587
15832
|
state: 'working', ticketId, activity: summary, step: 'worker:' + agentType
|
|
15588
15833
|
});
|
|
@@ -15597,12 +15842,23 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15597
15842
|
|
|
15598
15843
|
const done = new Promise((resolve) => {
|
|
15599
15844
|
child.on('error', (err) => {
|
|
15845
|
+
WORKER_LIVENESS.delete(String(ticketId));
|
|
15600
15846
|
appendGlobalActivityLog('agent', 'worker.spawn_failed',
|
|
15601
15847
|
{ agentId, ticketId, agentType, error: describeError(err) }, 'error');
|
|
15602
15848
|
resolve({ ticketId, ok: false, reason: 'spawn_failed', message: describeError(err) });
|
|
15603
15849
|
});
|
|
15604
15850
|
child.on('close', async (code, signal) => {
|
|
15605
15851
|
try { logStream.end(); } catch (_) { /* nothing to do */ }
|
|
15852
|
+
// The attempt is over: its credential must not outlive it (a late or orphaned child cannot keep acting).
|
|
15853
|
+
WORKER_LIVENESS.delete(String(ticketId));
|
|
15854
|
+
// The worker may have exited with receipts still queued in its checkout: a worker exit must not erase
|
|
15855
|
+
// unsent evidence. Drained with the SUPERVISOR's session (the worker credential is about to be revoked).
|
|
15856
|
+
if (workingDirectory) {
|
|
15857
|
+
try {
|
|
15858
|
+
await drainEvidenceOutbox(workingDirectory, { ...context, dataMode: context.dataMode || ctx.dataMode }, agentId);
|
|
15859
|
+
} catch (_) { /* stays queued for the next drain */ }
|
|
15860
|
+
}
|
|
15861
|
+
await revokeWorkerCredential(context, workerCredential);
|
|
15606
15862
|
const ok = code === 0 && !signal;
|
|
15607
15863
|
appendGlobalActivityLog('agent', 'worker.finished',
|
|
15608
15864
|
{ agentId, ticketId, agentType, code, signal, attempt, logFile }, ok ? 'info' : 'error');
|
|
@@ -15758,6 +16014,7 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15758
16014
|
claim: inFlight.size < ctx.maxConcurrent,
|
|
15759
16015
|
runId: ctx.runId,
|
|
15760
16016
|
activeTickets,
|
|
16017
|
+
attempts: workerLivenessFor(activeTickets),
|
|
15761
16018
|
maxConcurrent: ctx.maxConcurrent,
|
|
15762
16019
|
// Declared so the phase the platform derives already accounts for an operator who asked for a
|
|
15763
16020
|
// read-only agent. Without it the server would hand out an edit lease this session will never
|
|
@@ -16068,6 +16325,8 @@ async function ticketCommand(flags, subcommand) {
|
|
|
16068
16325
|
|
|
16069
16326
|
const local = ownLocalAgent(flags);
|
|
16070
16327
|
const context = agentSessionFor(flags, local);
|
|
16328
|
+
// Publish any passive receipts this checkout queued, before the verb that may hand the work to someone else.
|
|
16329
|
+
try { await drainEvidenceOutbox(process.cwd(), context); } catch (_) { /* never blocks the verb */ }
|
|
16071
16330
|
|
|
16072
16331
|
// Routing lives on its own endpoint (the operator surface the dashboard and any Embeddable share),
|
|
16073
16332
|
// so it is dispatched here rather than folded into the lifecycle switch.
|
|
@@ -16111,6 +16370,13 @@ async function ticketCommand(flags, subcommand) {
|
|
|
16111
16370
|
route: flags.route === 'false' ? false : undefined,
|
|
16112
16371
|
details: flags.details,
|
|
16113
16372
|
nextStep: flags['next-step'],
|
|
16373
|
+
// The structured handoff checkpoint (durable references, never local paths) and the revision a
|
|
16374
|
+
// decision was made from: a stale one is refused with code stale_revision instead of being rebased.
|
|
16375
|
+
artifactRefs: csvFlag(flags['artifact-refs'] || flags.artifactRefs),
|
|
16376
|
+
evidenceRefs: csvFlag(flags['evidence-refs'] || flags.evidenceRefs),
|
|
16377
|
+
waitingOn: csvFlag(flags['waiting-on'] || flags.waitingOn),
|
|
16378
|
+
requiredCapabilities: csvFlag(flags.needs || flags['required-capabilities']),
|
|
16379
|
+
expectedRevision: flags['expected-revision'] !== undefined ? Number(flags['expected-revision']) : undefined,
|
|
16114
16380
|
category: flags.category,
|
|
16115
16381
|
// Clearing a planning slot has to be expressible, and `||` could not express it: an empty value is
|
|
16116
16382
|
// falsy, so it collapsed to undefined, JSON.stringify dropped the key, and the server was never told.
|
|
@@ -16904,9 +17170,16 @@ function printTicketHelp() {
|
|
|
16904
17170
|
console.log(' cancel-request --request R2 [--reason "..."]');
|
|
16905
17171
|
console.log(' handoff --summary "what the next worker should do" [--workstream review] [--exclude]');
|
|
16906
17172
|
console.log(' [--review-action promote --revision R] [--next-step "..."] [--findings "a|b"]');
|
|
17173
|
+
console.log(' [--artifact-refs a,b] [--evidence-refs e1,e2] [--waiting-on X] [--needs capability]');
|
|
16907
17174
|
console.log(' Hand the next step to another session (queued if nobody is online). --exclude keeps');
|
|
16908
17175
|
console.log(' YOU off it — use it for independent review. Your exit will not erase this handoff.');
|
|
17176
|
+
console.log(' The checkpoint (findings, next step, refs, waiting-on, needs) is in the next worker\'s brief.');
|
|
17177
|
+
console.log(' --expected-revision N (handoff, decide, complete): refuse with stale_revision if the ticket moved');
|
|
17178
|
+
console.log(' past revision N since you read it, instead of merging your decision over newer changes.');
|
|
16909
17179
|
console.log(' continue [--reason requested] Queue the ticket for the automation to work again.');
|
|
17180
|
+
console.log(' cancel --reason "..." [--expected-revision N] The outcome is no longer wanted: closed with disposition');
|
|
17181
|
+
console.log(' cancelled; history kept; open requests cancelled; anything already landed or sent is NOT undone.');
|
|
17182
|
+
console.log(' redeliver-events OPERATOR: re-dispatch undelivered lifecycle events (deduplicated by eventId).');
|
|
16910
17183
|
console.log(' reset-budget OPERATOR: restore a ticket\'s run and spend budget after looking at it.');
|
|
16911
17184
|
console.log(' wake --in MINUTES | --at ISO-TIME [--reason "..."] | wake --cancel');
|
|
16912
17185
|
console.log(' A durable timer: when it is due the ticket is runnable work again (reason `timer`) and');
|
|
@@ -17083,6 +17356,113 @@ function usageListFlag(value) {
|
|
|
17083
17356
|
return items.length ? items : undefined;
|
|
17084
17357
|
}
|
|
17085
17358
|
|
|
17359
|
+
/** `remits-cli capabilities` — the platform's collaboration catalog (features, verbs, refusal codes). */
|
|
17360
|
+
async function capabilitiesCommand(flags) {
|
|
17361
|
+
const cwd = process.cwd();
|
|
17362
|
+
let baseUrl = flags['base-url'];
|
|
17363
|
+
if (!baseUrl) {
|
|
17364
|
+
try { baseUrl = resolveSessionContext(cwd, flags).session.baseUrl; } catch (_) { /* no session is fine */ }
|
|
17365
|
+
}
|
|
17366
|
+
baseUrl = baseUrl || DEFAULT_BASE_URL;
|
|
17367
|
+
let response;
|
|
17368
|
+
try {
|
|
17369
|
+
response = await axios.get(baseUrl.replace(/\/$/, '') + '/cli/capabilities', { timeout: 15000, headers: cliRequestHeaders(null) })
|
|
17370
|
+
.then((r) => r.data);
|
|
17371
|
+
} catch (err) {
|
|
17372
|
+
if (err && err.response && err.response.status === 404) {
|
|
17373
|
+
response = { success: false, code: 'unsupported',
|
|
17374
|
+
message: 'This platform predates the capability catalog; it supports none of the negotiated collaboration features.' };
|
|
17375
|
+
} else {
|
|
17376
|
+
throw err;
|
|
17377
|
+
}
|
|
17378
|
+
}
|
|
17379
|
+
if (flagEnabled(flags.json)) {
|
|
17380
|
+
console.log(JSON.stringify(response, null, 2));
|
|
17381
|
+
return response;
|
|
17382
|
+
}
|
|
17383
|
+
if (!response || response.success !== true) {
|
|
17384
|
+
console.log(response && response.message ? response.message : 'No capability catalog available.');
|
|
17385
|
+
return response;
|
|
17386
|
+
}
|
|
17387
|
+
console.log('Collaboration capabilities — ' + baseUrl + ' (protocol ' + response.protocolVersion + ')');
|
|
17388
|
+
console.log('');
|
|
17389
|
+
console.log('Features:');
|
|
17390
|
+
Object.entries(response.featureDetails || {}).forEach(([name, detail]) => console.log(' ' + name.padEnd(24) + ' ' + detail));
|
|
17391
|
+
console.log('');
|
|
17392
|
+
console.log('Verbs:');
|
|
17393
|
+
(response.verbs || []).forEach((v) => console.log(' ' + String(v.verb).padEnd(30) + ' ' + (v.sideEffect || '') +
|
|
17394
|
+
(v.refusals && v.refusals.length ? ' [refusals: ' + v.refusals.join(', ') + ']' : '')));
|
|
17395
|
+
console.log('');
|
|
17396
|
+
console.log('Refusal codes:');
|
|
17397
|
+
Object.entries(response.codes || {}).forEach(([code, meaning]) => console.log(' ' + code.padEnd(20) + ' ' + meaning));
|
|
17398
|
+
console.log('');
|
|
17399
|
+
console.log(response.note);
|
|
17400
|
+
return response;
|
|
17401
|
+
}
|
|
17402
|
+
|
|
17403
|
+
/**
|
|
17404
|
+
* `remits-cli policy explain` — what the account's deterministic rules would decide for a proposed action,
|
|
17405
|
+
* and why, WITHOUT applying it. The verb re-evaluates when it actually runs; this is the preview.
|
|
17406
|
+
*/
|
|
17407
|
+
async function policyExplainCommand(flags) {
|
|
17408
|
+
const cwd = process.cwd();
|
|
17409
|
+
ensureLocalState(cwd);
|
|
17410
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
17411
|
+
const { session, accountId } = sessionContext;
|
|
17412
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
17413
|
+
const api = buildAxios(baseUrl, session.token, 60000);
|
|
17414
|
+
const components = csvFlag(flags.components || flags.component);
|
|
17415
|
+
const response = await loggedPost(api, cwd, '/cli/policyExplain', {
|
|
17416
|
+
// The checkout's branch and staging workspace: a person explaining their STAGED process draft is answered
|
|
17417
|
+
// from it (the authoring lane shows staged prompts only when the call names its branch).
|
|
17418
|
+
...runContextPayload(flags),
|
|
17419
|
+
token: session.token,
|
|
17420
|
+
accountId: flags['target-account-id'] ? Number(flags['target-account-id']) : accountId,
|
|
17421
|
+
dataMode: resolveDataMode(flags, session),
|
|
17422
|
+
point: flags.point,
|
|
17423
|
+
workstream: flags.workstream,
|
|
17424
|
+
capabilities: csvFlag(flags.capabilities || flags.capability) || undefined,
|
|
17425
|
+
actorKind: flags.as || flags['actor-kind'],
|
|
17426
|
+
acknowledge: flags.acknowledge,
|
|
17427
|
+
components: components ? components.map((entry) => {
|
|
17428
|
+
const [type, name, category] = String(entry).split(':');
|
|
17429
|
+
return { type, name, category };
|
|
17430
|
+
}) : undefined
|
|
17431
|
+
}).then((r) => r.data).catch((err) => {
|
|
17432
|
+
if (err && err.response && err.response.data) return err.response.data;
|
|
17433
|
+
throw err;
|
|
17434
|
+
});
|
|
17435
|
+
if (flagEnabled(flags.json)) {
|
|
17436
|
+
console.log(JSON.stringify(response, null, 2));
|
|
17437
|
+
return response;
|
|
17438
|
+
}
|
|
17439
|
+
if (!response || response.success !== true) {
|
|
17440
|
+
throw new Error((response && response.message) || 'Policy explanation failed');
|
|
17441
|
+
}
|
|
17442
|
+
const decision = response.decision || {};
|
|
17443
|
+
console.log('Policy at "' + response.point + '" on account ' + response.accountId +
|
|
17444
|
+
(response.workstream ? ' (workstream ' + response.workstream + ')' : '') + ' — ' + response.dataMode + ' lane');
|
|
17445
|
+
console.log(' as: ' + ((response.actor && response.actor.kind) || '?') +
|
|
17446
|
+
((response.actor && response.actor.evidence && response.actor.evidence.length) ? ' (' + response.actor.evidence.join(', ') + ')' : ''));
|
|
17447
|
+
console.log(' decision: ' + (decision.allowed ? 'ALLOWED' : 'REFUSED (' + (decision.code || 'policy_denied') + ')'));
|
|
17448
|
+
if (decision.message) console.log(' ' + decision.message);
|
|
17449
|
+
(decision.warnings || []).forEach((w) => console.log(' warn ' + w.ruleId + ': ' + (w.message || '')));
|
|
17450
|
+
const relevant = (response.rules || []).filter((r) => r.appliesAtPoint || r.fires);
|
|
17451
|
+
if (relevant.length) {
|
|
17452
|
+
console.log(' rules that apply at this point:');
|
|
17453
|
+
relevant.forEach((r) => console.log(' ' + (r.fires ? '*' : ' ') + ' [' + r.grade + '] ' + r.id +
|
|
17454
|
+
(r.final ? ' (final)' : '') + ' from ' + (r.source || '?') +
|
|
17455
|
+
(r.distance !== undefined ? ' (distance ' + r.distance + ')' : '')));
|
|
17456
|
+
} else {
|
|
17457
|
+
console.log(' no rule applies at this point.');
|
|
17458
|
+
}
|
|
17459
|
+
const eff = response.effectivePolicy || {};
|
|
17460
|
+
if (eff.unavailable) console.log(' POLICY UNAVAILABLE: mandatory decisions are refused until it can be read.');
|
|
17461
|
+
if (eff.error) console.log(' policy problem: ' + eff.error);
|
|
17462
|
+
console.log(' ' + response.note);
|
|
17463
|
+
return response;
|
|
17464
|
+
}
|
|
17465
|
+
|
|
17086
17466
|
async function usageCommand(flags) {
|
|
17087
17467
|
const cwd = process.cwd();
|
|
17088
17468
|
ensureLocalState(cwd);
|
|
@@ -17540,9 +17920,29 @@ function listWorkstreamIndexes(cwd) {
|
|
|
17540
17920
|
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
|
|
17541
17921
|
}
|
|
17542
17922
|
|
|
17923
|
+
function evidenceOutboxPending(cwd) {
|
|
17924
|
+
try {
|
|
17925
|
+
const file = evidenceOutboxFile(cwd);
|
|
17926
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).length : 0;
|
|
17927
|
+
} catch (_) {
|
|
17928
|
+
return 0;
|
|
17929
|
+
}
|
|
17930
|
+
}
|
|
17931
|
+
|
|
17543
17932
|
async function evidenceCommand(flags) {
|
|
17544
17933
|
const cwd = process.cwd();
|
|
17545
17934
|
ensureLocalState(cwd);
|
|
17935
|
+
if (flags._ && flags._[1] === 'flush') {
|
|
17936
|
+
const context = agentSessionFor(flags, ownLocalAgent(flags));
|
|
17937
|
+
const result = await drainEvidenceOutbox(cwd, context);
|
|
17938
|
+
if (flagEnabled(flags.json)) {
|
|
17939
|
+
console.log(JSON.stringify({ success: true, ...result }, null, 2));
|
|
17940
|
+
} else {
|
|
17941
|
+
console.log('Published ' + result.sent + ' receipt(s)' + (result.duplicate ? ', ' + result.duplicate + ' already recorded' : '') +
|
|
17942
|
+
(result.rejected ? ', ' + result.rejected + ' rejected' : '') + (result.kept ? '; ' + result.kept + ' still queued' : '') + '.');
|
|
17943
|
+
}
|
|
17944
|
+
return result;
|
|
17945
|
+
}
|
|
17546
17946
|
const limit = Math.max(1, Math.min(parseInt(flags.limit || flags.max || '40', 10) || 40, 500));
|
|
17547
17947
|
const entries = readEvidenceTrail(cwd, limit);
|
|
17548
17948
|
if (flagEnabled(flags.json)) {
|
|
@@ -17551,6 +17951,10 @@ async function evidenceCommand(flags) {
|
|
|
17551
17951
|
}
|
|
17552
17952
|
const paths = localStatePaths(cwd);
|
|
17553
17953
|
console.log('Evidence trail for actor ' + paths.actor.id + ' (' + paths.evidenceFile + ')');
|
|
17954
|
+
const pending = evidenceOutboxPending(cwd);
|
|
17955
|
+
if (pending) {
|
|
17956
|
+
console.log(pending + ' receipt(s) queued for the work item and not yet published — remits-cli evidence flush');
|
|
17957
|
+
}
|
|
17554
17958
|
if (!entries.length) {
|
|
17555
17959
|
console.log('Nothing recorded yet. Every stage, test, token, tool and sync appends one world-stamped line here,');
|
|
17556
17960
|
console.log('with no envelope and nothing to start.');
|
|
@@ -17652,6 +18056,35 @@ async function workstreamCommand(flags, subcommand) {
|
|
|
17652
18056
|
* and the one an operator asks when a queue is not moving. Agents, leases and tickets were each readable
|
|
17653
18057
|
* on their own and never together, so every caller invented its own join and they disagreed.
|
|
17654
18058
|
*/
|
|
18059
|
+
/**
|
|
18060
|
+
* One line from the platform's CollaborationActivity projection. Null when an older server sent none.
|
|
18061
|
+
* Live work and stored facts are named separately, and unknown coverage is said, never shown as zero.
|
|
18062
|
+
*/
|
|
18063
|
+
function describeActivityHeadline(activity) {
|
|
18064
|
+
if (!activity || typeof activity !== 'object') return null;
|
|
18065
|
+
const parts = [];
|
|
18066
|
+
const coverage = activity.coverage || {};
|
|
18067
|
+
if (coverage.registry === 'unavailable') {
|
|
18068
|
+
parts.push('live participants unknown');
|
|
18069
|
+
} else {
|
|
18070
|
+
const running = (activity.attempts && activity.attempts.running) || 0;
|
|
18071
|
+
const online = (activity.participants && activity.participants.online) || 0;
|
|
18072
|
+
parts.push('live: ' + running + ' attempt(s) running, ' + online + ' participant(s) online');
|
|
18073
|
+
}
|
|
18074
|
+
if (coverage.staging === 'unavailable') {
|
|
18075
|
+
parts.push('staging unknown');
|
|
18076
|
+
} else if (activity.staged && activity.staged.laneCount) {
|
|
18077
|
+
const changes = activity.changes || {};
|
|
18078
|
+
parts.push('stored: ' + (changes.componentCount != null ? changes.componentCount + ' last-reported change(s), ' : '') +
|
|
18079
|
+
activity.staged.entryCount + ' staged entr(ies) in ' + activity.staged.laneCount + ' lane(s)' +
|
|
18080
|
+
(changes.asOf ? ' (as of ' + changes.asOf + ')' : ''));
|
|
18081
|
+
}
|
|
18082
|
+
if (Array.isArray(activity.offlineAuthors) && activity.offlineAuthors.length) {
|
|
18083
|
+
parts.push('staged by offline author(s): ' + activity.offlineAuthors.join(', '));
|
|
18084
|
+
}
|
|
18085
|
+
return ' ' + parts.join(' · ');
|
|
18086
|
+
}
|
|
18087
|
+
|
|
17655
18088
|
async function agentWorkMapCommand(flags) {
|
|
17656
18089
|
const agentId = resolveAgentId(flags, { optional: true });
|
|
17657
18090
|
const local = agentId ? (listLocalAgents().find((entry) => entry.agentId === agentId) || {}) : {};
|
|
@@ -17678,12 +18111,19 @@ async function agentWorkMapCommand(flags) {
|
|
|
17678
18111
|
return;
|
|
17679
18112
|
}
|
|
17680
18113
|
console.log('Work map — ' + context.baseUrl + ' · ' + response.dataMode + ' lane');
|
|
18114
|
+
// The platform's activity projection (the same one the account console renders): live work is the
|
|
18115
|
+
// running/online counts; last-reported changes and the staged overlay are stored facts with an as-of.
|
|
18116
|
+
const activityLine = describeActivityHeadline(response && response.activity);
|
|
18117
|
+
if (activityLine) console.log(activityLine);
|
|
17681
18118
|
console.log('');
|
|
17682
18119
|
for (const repo of repositories) {
|
|
17683
18120
|
const lease = repo.editLease;
|
|
17684
18121
|
console.log(' ' + repo.accountName + ' (' + repo.accountId + ')' +
|
|
17685
18122
|
(repo.accountType ? ' ' + repo.accountType : ''));
|
|
17686
|
-
if (lease) {
|
|
18123
|
+
if (lease && lease.unavailable) {
|
|
18124
|
+
// UNKNOWN is its own state: nobody may edit, and nobody is named as the holder.
|
|
18125
|
+
console.log(' EDIT LEASE unknown (coordination store unavailable) — investigate read-only');
|
|
18126
|
+
} else if (lease) {
|
|
17687
18127
|
const who = lease.agentLabel || lease.agentId;
|
|
17688
18128
|
console.log(' EDIT LEASE ' + who +
|
|
17689
18129
|
(lease.ticketId ? ' ticket ' + lease.ticketId : '') +
|
|
@@ -18759,7 +19199,7 @@ function printComponentsHelp(subcommand) {
|
|
|
18759
19199
|
}
|
|
18760
19200
|
|
|
18761
19201
|
function printTestHelp() {
|
|
18762
|
-
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]');
|
|
19202
|
+
console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b" | --failed] [--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]');
|
|
18763
19203
|
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
18764
19204
|
console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
|
|
18765
19205
|
console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
|
|
@@ -18768,11 +19208,15 @@ function printTestHelp() {
|
|
|
18768
19208
|
console.log('Examples:');
|
|
18769
19209
|
console.log(' remits-cli test run --test 11');
|
|
18770
19210
|
console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
|
|
19211
|
+
console.log(' remits-cli test run --test "Merchant Statements" --failed');
|
|
18771
19212
|
console.log(' remits-cli test status --task-id 7a1b...');
|
|
18772
19213
|
console.log('');
|
|
18773
19214
|
console.log('Notes:');
|
|
18774
19215
|
console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
|
|
18775
19216
|
console.log(' A selector that matches no case fails the run rather than reporting 0 passed / 0 failed.');
|
|
19217
|
+
console.log(' --failed re-runs only the cases that did not pass in your last finished run of this suite on');
|
|
19218
|
+
console.log(' this git branch and staging workspace. Iterate with --failed / --names; run the whole');
|
|
19219
|
+
console.log(' suite once before landing.');
|
|
18776
19220
|
console.log(' --as-account changes the execution account so subscriber branch edges apply.');
|
|
18777
19221
|
console.log(' --variant-branch <name> probes a committed variant branch; use "none" or "trunk"');
|
|
18778
19222
|
console.log(' to force production/subscription semantics from a variant checkout.');
|
|
@@ -19048,6 +19492,8 @@ async function main() {
|
|
|
19048
19492
|
console.log(' remits-cli activity inspect [--account-id ID] [--scope self|children|related] [--workspace NAME] [--workstream ID] [--user-email E] [--summary|--compact] [--json]');
|
|
19049
19493
|
console.log(' remits-cli workstream status [--workstream ID|--workspace NAME] [--json]');
|
|
19050
19494
|
console.log(' remits-cli usage --from DATE [--to DATE] [--by hour|day|month] [--group-by account,sku] [--capabilities ai,document_store] [--json]');
|
|
19495
|
+
console.log(' remits-cli capabilities [--json] Collaboration features, verbs and refusal codes this platform supports');
|
|
19496
|
+
console.log(' remits-cli policy explain --point commit [--as agent] What the account rules would decide, and why (nothing applied)');
|
|
19051
19497
|
console.log(' remits-cli agent release # stop receiving tickets now');
|
|
19052
19498
|
console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
|
|
19053
19499
|
console.log(' remits-cli stop');
|
|
@@ -19079,7 +19525,7 @@ async function main() {
|
|
|
19079
19525
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
19080
19526
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
19081
19527
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
19082
|
-
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]');
|
|
19528
|
+
console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b" | --failed] [--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]');
|
|
19083
19529
|
console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
|
|
19084
19530
|
console.log(' remits-cli test compare --base <taskId> --head <taskId>');
|
|
19085
19531
|
console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
|
|
@@ -19310,6 +19756,33 @@ async function main() {
|
|
|
19310
19756
|
throw new Error('Unknown activity subcommand: ' + subcommand);
|
|
19311
19757
|
}
|
|
19312
19758
|
|
|
19759
|
+
if (command === 'capabilities') {
|
|
19760
|
+
if (wantsHelp) {
|
|
19761
|
+
console.log('Usage: remits-cli capabilities [--json] [--base-url URL]');
|
|
19762
|
+
console.log('');
|
|
19763
|
+
console.log('What this platform build supports for collaboration: negotiated features, the stable verbs and');
|
|
19764
|
+
console.log('how each settles, and what every refusal code means. Read-only; needs no special access.');
|
|
19765
|
+
return;
|
|
19766
|
+
}
|
|
19767
|
+
await capabilitiesCommand(args);
|
|
19768
|
+
return;
|
|
19769
|
+
}
|
|
19770
|
+
|
|
19771
|
+
if (command === 'policy') {
|
|
19772
|
+
if (wantsHelp || subcommand !== 'explain') {
|
|
19773
|
+
console.log('Usage: remits-cli policy explain --point <commit|land|stage|complete|deliver|...> [--workstream W]');
|
|
19774
|
+
console.log(' [--capabilities a,b] [--as human|agent] [--components type:name[:category],...]');
|
|
19775
|
+
console.log(' [--target-account-id ID] [--json]');
|
|
19776
|
+
console.log('');
|
|
19777
|
+
console.log("What this account's remits-policy rules would decide for that action, which rules apply, where");
|
|
19778
|
+
console.log('they come from (document, account distance, final) — without applying anything. The real verb');
|
|
19779
|
+
console.log('re-checks the policy in force when it runs.');
|
|
19780
|
+
return;
|
|
19781
|
+
}
|
|
19782
|
+
await policyExplainCommand(args);
|
|
19783
|
+
return;
|
|
19784
|
+
}
|
|
19785
|
+
|
|
19313
19786
|
if (command === 'usage') {
|
|
19314
19787
|
if (wantsHelp) {
|
|
19315
19788
|
console.log('Usage: remits-cli usage --from DATE [--to DATE] [--timezone ZONE] [--by hour|day|month]');
|
|
@@ -19329,7 +19802,7 @@ async function main() {
|
|
|
19329
19802
|
|
|
19330
19803
|
if (command === 'evidence') {
|
|
19331
19804
|
if (wantsHelp) {
|
|
19332
|
-
console.log('Usage: remits-cli evidence [--limit N] [--json]');
|
|
19805
|
+
console.log('Usage: remits-cli evidence [--limit N] [--json] | remits-cli evidence flush');
|
|
19333
19806
|
console.log('');
|
|
19334
19807
|
console.log('What you have actually run, and in which world. Every stage, test, token, tool and sync');
|
|
19335
19808
|
console.log('appends one world-stamped line automatically - there is nothing to start and nothing to');
|
|
@@ -19337,6 +19810,9 @@ async function main() {
|
|
|
19337
19810
|
console.log('');
|
|
19338
19811
|
console.log('This is per-actor, so agents sharing a checkout never read each other\'s trail.');
|
|
19339
19812
|
console.log('Use `remits-cli verify` only when someone needs a checkable VERDICT on top of this.');
|
|
19813
|
+
console.log('');
|
|
19814
|
+
console.log('Inside a work attempt (REMITS_SUPPORT_TICKET_ID), each line is also queued and published to the');
|
|
19815
|
+
console.log('work item as a passive receipt, so a successor elsewhere can read it. `flush` publishes now.');
|
|
19340
19816
|
return;
|
|
19341
19817
|
}
|
|
19342
19818
|
await evidenceCommand(args);
|
package/package.json
CHANGED
|
@@ -64,6 +64,9 @@ reference named after it.
|
|
|
64
64
|
- **edit → stage → run, every time.** The platform executes whatever is in the staging cache at the
|
|
65
65
|
moment a run starts. Edit a file, run a test without `remits-cli components stage`, and the test runs
|
|
66
66
|
the OLD code. This is the single most common mistake. (`development-loop.md`)
|
|
67
|
+
- **Iterate on the failing cases; run the whole suite once before landing.** Waiting on test runs is most
|
|
68
|
+
of a slow build loop, not staging. After each edit run `test run --test X --failed` (only what did not
|
|
69
|
+
pass last time) or `--names "a|b"`, not the full suite. (`development-loop.md`)
|
|
67
70
|
- **Staging and sync fail early on broken runtime-compiled Groovy.** Stage validates changed source in the
|
|
68
71
|
active lane; sync validates changed/new compiled components before writing DB rows or branch variants.
|
|
69
72
|
Agent files are Utility components internally. A stage refused BY COMPILE VALIDATION still wrote the
|
|
@@ -100,9 +100,18 @@ whole point is that it needs no supervision. Following the transcript is the way
|
|
|
100
100
|
strictly better than a terminal would be: the output is structured, so it can be read as events
|
|
101
101
|
rather than scraped back out of ANSI text.
|
|
102
102
|
|
|
103
|
-
**
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
**Whether a worker lands is the account's choice, not the supervisor's.** Nothing in the supervisor runs
|
|
104
|
+
git. The brief states the account's landing profile: by default a person lands (the worker leaves changes in
|
|
105
|
+
the working tree and says so on the ticket); an account with `supportPolicy.landing: agent` lets the worker
|
|
106
|
+
land with `remits-cli components commit`, which re-checks the account's rules when it runs.
|
|
107
|
+
|
|
108
|
+
**Each worker gets its own credential.** For every child the supervisor mints a credential bound to that
|
|
109
|
+
attempt (agent + ticket + run) and revokes it when the child exits; the child authenticates with it
|
|
110
|
+
(`REMITS_AGENT_TOKEN`). The platform therefore knows which participant is calling from the credential, so a
|
|
111
|
+
missing or wrong identity header cannot make a worker a human or another agent. Against an older platform
|
|
112
|
+
the worker simply uses the supervisor's session, as before. Each poll also reports, per running child, when
|
|
113
|
+
it last produced output and its last progress line — separate from the supervisor's own heartbeat, so
|
|
114
|
+
`agent map` can tell a connected supervisor from a child that is actually producing work.
|
|
106
115
|
|
|
107
116
|
### Resolving which agent a command means
|
|
108
117
|
|
|
@@ -218,13 +218,21 @@ remits-cli agent register [--label NAME] [--data-mode test|prod] [--account-id I
|
|
|
218
218
|
remits-cli agent work [--wait SECONDS] [--json]
|
|
219
219
|
remits-cli agent status [--state idle|working|paused] [--ticket ID] [--activity "..."] [--step "..."]
|
|
220
220
|
remits-cli agent list [--account-id ID] [--json]
|
|
221
|
-
remits-cli agent map [--account-ids 1,4] [--json] # who is EDITING which repository, from which checkout/branch/staging lane
|
|
221
|
+
remits-cli agent map [--account-ids 1,4] [--json] # who is EDITING which repository, from which checkout/branch/staging lane; first line = live work vs stored (staged/last-reported) facts
|
|
222
|
+
remits-cli capabilities [--json] # collaboration features this platform supports, verbs + how they settle, refusal codes
|
|
223
|
+
remits-cli policy explain --point commit|land|stage|complete|deliver|... [--as human|agent] [--workstream W] [--components type:name[:category],...] [--json] # what the account rules would decide, and why; nothing applied
|
|
224
|
+
remits-cli evidence [--limit N] [--json] | evidence flush # your world-stamped trail; inside a ticket attempt it is also published to the ticket as receipts
|
|
222
225
|
remits-cli agent inspect [--account-id ID] [--scope self|children] [--user-id ID|--user-email EMAIL] [--limit N] [--json]
|
|
223
226
|
remits-cli activity inspect [--account-id ID] [--scope self|children|related] [--workspace NAME] [--branch NAME] [--workstream ID] [--user-id ID|--user-email EMAIL] [--limit N] [--json]
|
|
224
227
|
remits-cli workstream status [--workstream ID|--workspace NAME] [--json]
|
|
225
228
|
remits-cli agent release
|
|
226
|
-
remits-cli ticket read|accept|status|progress|complete|release|reopen|assign|planning --ticket ID [--status S] [--resolution "..."] [--summary "..."] [--category C] [--assignee EMAIL] [--workstream VALUE] [--planned-in VALUE] [--board-stage VALUE] [--rank N] [--size VALUE] [--blocked-by VALUE] [--notes "..."]
|
|
227
|
-
remits-cli ticket
|
|
229
|
+
remits-cli ticket read|accept|status|progress|complete|cancel|release|reopen|assign|planning --ticket ID [--status S] [--resolution "..."] [--reason "..."] [--summary "..."] [--category C] [--assignee EMAIL] [--workstream VALUE] [--planned-in VALUE] [--board-stage VALUE] [--rank N] [--size VALUE] [--blocked-by VALUE] [--notes "..."]
|
|
230
|
+
remits-cli ticket cancel --ticket ID --reason "..." [--expected-revision N] # close with disposition cancelled; history kept; stops worker
|
|
231
|
+
remits-cli ticket reset-budget --ticket ID # OPERATOR: restore run and spend budget after review
|
|
232
|
+
remits-cli ticket redeliver-events --ticket ID # OPERATOR: re-dispatch undelivered lifecycle events
|
|
233
|
+
remits-cli ticket lease|unlease|force-unlease --ticket ID [--reason "..."] # force-unlease breaks SOMEBODY ELSE'S lease; operator only. 503 code=unavailable = holder unknown: investigate, retry
|
|
234
|
+
remits-cli ticket handoff --ticket ID --summary "..." [--next-step "..."] [--findings "a|b"] [--artifact-refs a,b] [--evidence-refs r1,r2] [--waiting-on X] [--needs cap] [--workstream W] [--exclude] [--expected-revision N]
|
|
235
|
+
remits-cli ticket decide|complete ... [--expected-revision N] # refused with code=stale_revision (nothing written) if the ticket moved past N
|
|
228
236
|
remits-cli ticket where --ticket ID # WHERE this work is: repo account, checkout, branch, staging lane, live lease + holder's location, claim, your phase
|
|
229
237
|
remits-cli ticket ask --ticket ID --question "..." [--context "..."] [--to WHO] # park on a human decision
|
|
230
238
|
remits-cli ticket answer --ticket ID --message "..." [--route false] # answer it (alias: reply); re-routes by default
|
|
@@ -265,7 +273,7 @@ remits-cli components branch <name> --copy-to <newBranch> [--dry-run] [--force]
|
|
|
265
273
|
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge] # make an account resolve this branch
|
|
266
274
|
remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
|
|
267
275
|
remits-cli components branch <name> --retire [--force] # delete the branch's overlays
|
|
268
|
-
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]
|
|
276
|
+
remits-cli test run --test <id|name> [--candidate <laneContentHash>] [--branch <stagingScope>] [--names "a|b" | --failed] [--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]
|
|
269
277
|
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
|
|
270
278
|
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
|
|
271
279
|
remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case improved / regressed / changed, cost and world deltas
|
|
@@ -281,6 +289,10 @@ remits-cli corpus consistency --corpus <name> --case <caseKey> [--json]
|
|
|
281
289
|
remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json] # drop a case from future runs; past measurements stay readable
|
|
282
290
|
```
|
|
283
291
|
|
|
292
|
+
`--candidate <hash>` names the staged content you mean to test (the lane content hash `components status`
|
|
293
|
+
reports); if another process restaged or the lane expired, the run is refused with `stale_revision` instead of
|
|
294
|
+
testing different source. It binds the start of the run, not every later lookup.
|
|
295
|
+
|
|
284
296
|
`test run` starts a server-side task immediately. By default the CLI process polls until the task is
|
|
285
297
|
terminal; `--watch false` disables websocket progress streaming but still waits. Pass `--wait false` to
|
|
286
298
|
return after launch with the task id. A single run with `--names "a|b|c"` selects cases into one suite task,
|
|
@@ -589,6 +601,10 @@ For tests specifically:
|
|
|
589
601
|
- `--names` is `|`-delimited and may be repeated. A selector containing commas matches a case with that
|
|
590
602
|
exact name first, else its comma-separated parts; an unmatched selector fails the run instead of
|
|
591
603
|
reporting zero cases as success.
|
|
604
|
+
- `--failed` re-runs only the cases that did not pass in YOUR last finished run of the suite on this git
|
|
605
|
+
branch and staging workspace (a `--parallel` run is read as its whole group; the launch lane is preferred,
|
|
606
|
+
else the lane the suite recorded in). The launch prints the run it read and the cases. "Nothing failed"
|
|
607
|
+
starts nothing and exits 0; no earlier run is refused. Not combinable with `--names`.
|
|
592
608
|
- `--as-account <ID>` runs AS a descendant subscriber so its edge selects the component branch
|
|
593
609
|
(*"what does customer X get?"*); `--variant-branch <name>` probes a branch from any checkout
|
|
594
610
|
(*"what does branch Y look like?"*), and `--variant-branch none` forces production/subscription semantics.
|
|
@@ -421,9 +421,17 @@ This applies to:
|
|
|
421
421
|
```bash
|
|
422
422
|
remits-cli test run --test <TEST_ID_OR_NAME>
|
|
423
423
|
remits-cli test run --test "Invoice Tests" --names "specific test case"
|
|
424
|
+
remits-cli test run --test "Invoice Tests" --failed
|
|
424
425
|
remits-cli test status --task-id <TASK_ID>
|
|
425
426
|
```
|
|
426
427
|
|
|
428
|
+
**Iterate narrowly, and run the whole suite once before landing.** Waiting on test runs is most of
|
|
429
|
+
an hour-long build loop, not staging. A suite whose cases run one after another re-pays every case's
|
|
430
|
+
fixture work on every rerun. Run the suite once to see what fails. After each edit, run `--failed` (the
|
|
431
|
+
cases that did not pass in your last finished run of this suite, on this git branch and staging
|
|
432
|
+
workspace) or `--names` for the cases your change touches. Then run the full suite once before landing.
|
|
433
|
+
A full rerun after every edit, with every case passing, is the most common way an agent spends an hour.
|
|
434
|
+
|
|
427
435
|
Tests run on the platform against your staged snapshot. They stream results in real-time. **Read the
|
|
428
436
|
`World:` block printed before the run** — host, execution account, data lane, component world, staging lane
|
|
429
437
|
id + content hash, platform sync vs local HEAD. If it is not the world you meant, stop: the result will be
|
|
@@ -347,19 +347,26 @@ discouraged.
|
|
|
347
347
|
- **A refusal is an ANSWER.** The message names the rule and says what to do instead. Act on that
|
|
348
348
|
sentence. Do not retry the identical call, and do not go looking for another door — the rules govern the
|
|
349
349
|
DSL, the MCP tools and this CLI identically.
|
|
350
|
-
- **`[require]` rules mean a human.** You cannot acknowledge one
|
|
351
|
-
|
|
352
|
-
`ticket progress` and hand off.
|
|
350
|
+
- **`[require]` rules mean a human.** You cannot acknowledge one. A supervised worker authenticates with a
|
|
351
|
+
credential bound to its own attempt, so the platform knows it is an agent from the credential itself —
|
|
352
|
+
omitting identity headers does not change that. Record what you found with `ticket progress` and hand off.
|
|
353
353
|
- **`[warn]` rules let the call through and write a `policy` worklog entry.** Mention it in your handoff.
|
|
354
354
|
- **Rules can name your role** (`"when":{"role":"triage"}`, or `"role":{"not":["release_manager"]}` for
|
|
355
355
|
"only release managers"). Your role is the one your session registered with; you cannot change it from a
|
|
356
356
|
worker, and passing one on a call does nothing. A refusal naming your role means hand off to the role that
|
|
357
357
|
may.
|
|
358
|
-
- **If you see
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
358
|
+
- **If you see a policy problem banner**, part of the account's rules could not be parsed. The rules listed
|
|
359
|
+
under it are still enforced (a broken replacement keeps its last valid version); do not read the absence
|
|
360
|
+
of a refusal as permission for the rest — say so on the ticket. **`policy_unavailable`** means a published
|
|
361
|
+
policy could not be read at all: landing, committing, completing and sending are refused until it can be.
|
|
362
|
+
Retry later; do not look for another door.
|
|
363
|
+
- **Check before you act**: `remits-cli policy explain --point commit --as agent` says what the rules would
|
|
364
|
+
decide and which rules apply, without applying anything.
|
|
365
|
+
|
|
366
|
+
The same rules govern `components stage` and `components commit`. Whether you may land at all is the
|
|
367
|
+
account's **landing profile**, stated in your brief: by default a person lands (leave changes in the working
|
|
368
|
+
tree and describe them); an account that sets `supportPolicy.landing: agent` lets you land with
|
|
369
|
+
`remits-cli components commit`, which re-checks the rules when it runs. A `no-commit` rule always wins.
|
|
363
370
|
|
|
364
371
|
### Roles and who may launch them
|
|
365
372
|
|
|
@@ -433,6 +440,7 @@ remits-cli ticket accept --ticket 22454 # claim it before changing anyt
|
|
|
433
440
|
remits-cli ticket progress --ticket 22454 --summary "Traced it to the posting Action" --category investigation
|
|
434
441
|
remits-cli ticket status --ticket 22454 --status in_progress
|
|
435
442
|
remits-cli ticket complete --ticket 22454 --resolution "What you found and did"
|
|
443
|
+
remits-cli ticket cancel --ticket 22454 --reason "Customer withdrew request" # outcome no longer wanted; stops running worker
|
|
436
444
|
remits-cli ticket ask --ticket 22454 --question "Re-issue or skip?" --context "412 affected"
|
|
437
445
|
remits-cli ticket answer --ticket 22454 --message "Skip them." # alias: reply
|
|
438
446
|
remits-cli ticket message --ticket 22454 --message "We reproduced it; a fix is staged."
|
|
@@ -590,7 +598,12 @@ remits-cli ticket handoff --summary "fix staged in ticket-19463; needs independe
|
|
|
590
598
|
```
|
|
591
599
|
|
|
592
600
|
The handoff is queued even if nobody is online, `--exclude` keeps you off it (independent review needs
|
|
593
|
-
that), and your own exit will not undo it.
|
|
601
|
+
that), and your own exit will not undo it. It is a checkpoint: `--findings`, `--next-step`,
|
|
602
|
+
`--artifact-refs`, `--evidence-refs`, `--waiting-on` and `--needs` land in the next worker's brief, so give
|
|
603
|
+
durable references (artifact ids, receipt ids), never local paths. Add `--expected-revision N` (the revision
|
|
604
|
+
you read) to `handoff`, `decide` or `complete` and a ticket that moved on is refused with `stale_revision`
|
|
605
|
+
instead of being overwritten. Your evidence trail is published to the ticket as receipts automatically;
|
|
606
|
+
`remits-cli evidence flush` sends anything still queued. A reviewer decides with `ticket decide --request R3 --decision
|
|
594
607
|
approve|changes|reject --revision <rev>`; "changes" sends the work back with the reviewer's note.
|
|
595
608
|
|
|
596
609
|
An approval or review can need several people: `request-approval ... --min-approvals 2` stays open until two
|
|
@@ -144,7 +144,8 @@ account).
|
|
|
144
144
|
**Recurring schedules:** `action_schedules` lists the target account's Action schedules;
|
|
145
145
|
`action_schedule_update` upserts by `targetAccountId` + `actionId` (or an account-scoped `scheduleId`);
|
|
146
146
|
`action_schedule_delete` removes that schedule and its cloud job. Updates accept `cron`, `timeZone`,
|
|
147
|
-
`enabled` and `dryRun`.
|
|
147
|
+
`enabled` and `dryRun`. Each run executes as the schedule's owner (`ownerUserId` in reads), who defaults to
|
|
148
|
+
whoever first saves it and is rechecked at every run. `enabled:false` pauses; `enabled:true` resumes while preserving omitted settings.
|
|
148
149
|
`regression_schedule` reads the per-account regression settings and latest report;
|
|
149
150
|
`regression_schedule_update` accepts the same scheduling fields plus `resultAction`, `suiteConcurrency`
|
|
150
151
|
(1–16) and explicit `ownerUserId`. Existing owners are preserved. Only admins may assign another owner;
|