@remits/remits-cli 0.1.167 → 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/README.md +1 -0
- package/index.js +628 -28
- package/lib/local-ai.js +2 -2
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +3 -0
- package/skills/remits-cli/references/agent-sessions.md +32 -6
- 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/investigation.md +21 -0
- package/skills/remits-cli/references/support-tickets.md +42 -9
- package/skills/remits-cli/references/tool-reference.md +31 -1
package/index.js
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
## Table of Contents
|
|
5
5
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
7
|
+
- L128 Sessions, Account Resolution, And Production Guards
|
|
8
|
+
- L634 Local State, Workspaces, And Verification Evidence
|
|
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
|
/*
|
|
@@ -34,6 +34,8 @@ const { Client } = require('@stomp/stompjs');
|
|
|
34
34
|
const WebSocket = require('ws');
|
|
35
35
|
const { installWebsocketRetry } = require('./lib/websocket-retry');
|
|
36
36
|
const YAML = require('yaml');
|
|
37
|
+
// Invocation identity is separate from the local log-folder actor and staging workspace.
|
|
38
|
+
let requestIdentityFlags = {};
|
|
37
39
|
|
|
38
40
|
Object.assign(global, { WebSocket });
|
|
39
41
|
const openBrowser = (openModule && typeof openModule === 'function')
|
|
@@ -2095,6 +2097,7 @@ function recordEvidenceEntry(cwd, packet, context = {}) {
|
|
|
2095
2097
|
};
|
|
2096
2098
|
fs.appendFileSync(paths.evidenceFile, JSON.stringify(entry) + '\n');
|
|
2097
2099
|
trimEvidenceTrail(paths.evidenceFile);
|
|
2100
|
+
queueEvidenceReceipt(cwd, entry, context);
|
|
2098
2101
|
return entry;
|
|
2099
2102
|
} catch (_) {
|
|
2100
2103
|
// A trail write must never fail the command it is describing.
|
|
@@ -2102,6 +2105,135 @@ function recordEvidenceEntry(cwd, packet, context = {}) {
|
|
|
2102
2105
|
}
|
|
2103
2106
|
}
|
|
2104
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
|
+
|
|
2105
2237
|
function evidenceOutcome(row) {
|
|
2106
2238
|
if (row && (row.pending === true || row.success === null)) return 'pending';
|
|
2107
2239
|
return row && row.success === false ? 'failed' : 'ok';
|
|
@@ -4044,8 +4176,12 @@ function stageTimeoutMs(components, flags = {}) {
|
|
|
4044
4176
|
return Math.min(600000, Math.max(60000, Math.round(60000 + (megabytes * 60000))));
|
|
4045
4177
|
}
|
|
4046
4178
|
|
|
4047
|
-
function
|
|
4048
|
-
|
|
4179
|
+
function cliRequestHeaders(token) {
|
|
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 } : {};
|
|
4049
4185
|
// The CLI version on EVERY request, from the one place every request is built. It used to travel only on
|
|
4050
4186
|
// `agent register`, so the platform knew the version of the sessions that registered - and the agents that
|
|
4051
4187
|
// loop hardest are exactly the ones that never do. Without it the platform cannot tell an agent that the
|
|
@@ -4055,10 +4191,19 @@ function buildAxios(baseUrl, token, timeoutMs = 60000) {
|
|
|
4055
4191
|
// A worker's run identity rides on every call, so a verb whose payload does not repeat it (stage, commit,
|
|
4056
4192
|
// a tool) is still attributed to the run. The ROLE is deliberately not sent: the platform reads it from
|
|
4057
4193
|
// the session's registration, where the user's permission to declare it was checked.
|
|
4058
|
-
|
|
4194
|
+
// ticket route historically accepts --agent-id as the TARGET; it is not the caller.
|
|
4195
|
+
const targetAgent = requestIdentityFlags._?.[0] === 'ticket' && ['route', 'unroute'].includes(requestIdentityFlags._?.[1]);
|
|
4196
|
+
const rawAgentFlag = requestIdentityFlags['agent-id'] || requestIdentityFlags.agentId;
|
|
4197
|
+
const flagAgent = typeof rawAgentFlag === 'string' ? rawAgentFlag.trim() : '';
|
|
4198
|
+
const agentId = (!targetAgent && flagAgent) || process.env.REMITS_AGENT_ID;
|
|
4199
|
+
if (typeof agentId === 'string' && agentId.trim()) headers['X-Remits-Agent-Id'] = agentId.trim();
|
|
4059
4200
|
if (process.env.REMITS_WORKER_RUN_ID) headers['X-Remits-Run-Id'] = process.env.REMITS_WORKER_RUN_ID;
|
|
4060
4201
|
if (process.env.REMITS_SUPPORT_TICKET_ID) headers['X-Remits-Ticket-Id'] = process.env.REMITS_SUPPORT_TICKET_ID;
|
|
4061
|
-
return
|
|
4202
|
+
return headers;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
function buildAxios(baseUrl, token, timeoutMs = 60000) {
|
|
4206
|
+
return axios.create({ baseURL: baseUrl, timeout: parsePositiveInt(timeoutMs, 60000), headers: cliRequestHeaders(token) });
|
|
4062
4207
|
}
|
|
4063
4208
|
|
|
4064
4209
|
function describeError(err) {
|
|
@@ -4988,6 +5133,10 @@ async function pushComponentsCommand(flags) {
|
|
|
4988
5133
|
...runContextPayload(flags),
|
|
4989
5134
|
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
|
|
4990
5135
|
acknowledge: flags.acknowledge || undefined,
|
|
5136
|
+
// `commit` when this stage is the pre-push check of `components commit`: the platform then judges it as a
|
|
5137
|
+
// LANDING (the account's committed `commit`-point rules, component rules included), so a refusal
|
|
5138
|
+
// arrives before anything is pushed instead of after.
|
|
5139
|
+
intent: flags.intent || undefined,
|
|
4991
5140
|
// What this stage IS, stated rather than implied. The server derives the lane behavior from the mode
|
|
4992
5141
|
// (CliComponentStaging.stageRequest) instead of from `replace`, which was overloaded: a partial
|
|
4993
5142
|
// manifest with `replace: true` would make the reconcile pass delete nearly the whole lane, so
|
|
@@ -8073,7 +8222,7 @@ async function commitComponentsCommand(flags) {
|
|
|
8073
8222
|
};
|
|
8074
8223
|
try {
|
|
8075
8224
|
if (!skipGit) {
|
|
8076
|
-
emit('Phase 1/4: staged compile validation');
|
|
8225
|
+
emit('Phase 1/4: staged compile validation and landing policy check');
|
|
8077
8226
|
// MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
|
|
8078
8227
|
// lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
|
|
8079
8228
|
// validation pass must not be able to delete another agent's staged work as a side effect of somebody
|
|
@@ -8085,7 +8234,8 @@ async function commitComponentsCommand(flags) {
|
|
|
8085
8234
|
workset: false,
|
|
8086
8235
|
'replace-lane': false,
|
|
8087
8236
|
replaceLane: false,
|
|
8088
|
-
'changed-only': true
|
|
8237
|
+
'changed-only': true,
|
|
8238
|
+
intent: 'commit'
|
|
8089
8239
|
})));
|
|
8090
8240
|
|
|
8091
8241
|
outcome.phase = 'git';
|
|
@@ -9115,6 +9265,27 @@ function testRunSummaryLines(status = {}) {
|
|
|
9115
9265
|
return lines;
|
|
9116
9266
|
}
|
|
9117
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
|
+
|
|
9118
9289
|
// Per-case comparison of two runs of the same suite: which cases changed outcome, and how cost and duration
|
|
9119
9290
|
// moved. The answer to "did this revision do better than the last one" without reconstructing either run.
|
|
9120
9291
|
function compareTestRunResults(baseResult = {}, headResult = {}) {
|
|
@@ -9333,6 +9504,13 @@ function testRunLaunchLines(start = {}, runOptions = {}) {
|
|
|
9333
9504
|
if (runOptions.label) lines.push('Label: ' + runOptions.label);
|
|
9334
9505
|
if (runOptions.params) lines.push('Params: ' + Object.keys(runOptions.params).map((k) => k + '=' + JSON.stringify(runOptions.params[k])).join(' '));
|
|
9335
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
|
+
}
|
|
9336
9514
|
if (start.runGroupId) {
|
|
9337
9515
|
lines.push('Parallel: ' + (start.shardTaskIds || []).length + ' shards (' + (start.shardTaskIds || []).join(', ') + '), read as ONE run by ' + start.runGroupId);
|
|
9338
9516
|
}
|
|
@@ -9367,6 +9545,10 @@ async function testCommand(flags) {
|
|
|
9367
9545
|
? []
|
|
9368
9546
|
: (Array.isArray(flags.names) ? flags.names : [flags.names]);
|
|
9369
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
|
+
}
|
|
9370
9552
|
// --label / --param / --repeat / --parallel: what this run varies, and how it is executed.
|
|
9371
9553
|
const runOptions = testRunOptionsFromFlags(flags);
|
|
9372
9554
|
const runGroupId = runOptions.parallel > 1 ? 'grp-' + crypto.randomUUID() : undefined;
|
|
@@ -9416,6 +9598,11 @@ async function testCommand(flags) {
|
|
|
9416
9598
|
dataMode,
|
|
9417
9599
|
dataModeSource,
|
|
9418
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,
|
|
9419
9606
|
testId: /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
|
|
9420
9607
|
testName: /^\d+$/.test(String(testRef)) ? undefined : String(testRef),
|
|
9421
9608
|
tests: names
|
|
@@ -9434,6 +9621,7 @@ async function testCommand(flags) {
|
|
|
9434
9621
|
break;
|
|
9435
9622
|
} catch (err) {
|
|
9436
9623
|
const data = err && err.response && err.response.data;
|
|
9624
|
+
if (data && data.code === 'no_recorded_run') throw new Error(data.message);
|
|
9437
9625
|
if (shardStarts.length && shardStartBusy(err) && Date.now() < shardWaitDeadline) {
|
|
9438
9626
|
console.error('Shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel + ' is waiting for a free suite slot ' +
|
|
9439
9627
|
'(the platform runs a limited number of suites at once; the shards already running free them).');
|
|
@@ -9446,8 +9634,18 @@ async function testCommand(flags) {
|
|
|
9446
9634
|
throw err;
|
|
9447
9635
|
}
|
|
9448
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
|
+
}
|
|
9449
9647
|
if (!shardStart.success) {
|
|
9450
|
-
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 +
|
|
9451
9649
|
'; the shards already started keep running: ' + shardStarts.map((st) => st.taskId).join(', ') + ')' : ''));
|
|
9452
9650
|
}
|
|
9453
9651
|
shardStarts.push(shardStart);
|
|
@@ -9548,6 +9746,7 @@ async function testCommand(flags) {
|
|
|
9548
9746
|
.forEach((line) => console.log(line));
|
|
9549
9747
|
testRunSummaryLines(status).forEach((line) => console.log(line));
|
|
9550
9748
|
testFailureGroupLines(status, { testRef }).forEach((line) => console.log(line));
|
|
9749
|
+
testRunPacingLines(status, { testRef, selected: names.length > 0 || rerunFailed }).forEach((line) => console.log(line));
|
|
9551
9750
|
printTestRunPivots(status);
|
|
9552
9751
|
// The whole run object used to be printed here as pretty JSON. One 92-case suite measured 220 KB,
|
|
9553
9752
|
// most of it identifiers repeated once per case, spent on the turn with the least room left. It is
|
|
@@ -13959,7 +14158,7 @@ const AGENT_WORKER_MAX_LOG_BYTES = 8 * 1024 * 1024;
|
|
|
13959
14158
|
*
|
|
13960
14159
|
* `mode` is the autonomy bound, not a preference. `investigate` maps to each CLI's genuine
|
|
13961
14160
|
* read-only mode so an operator can put a real boundary around a production support agent; `edit`
|
|
13962
|
-
* 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).
|
|
13963
14162
|
*/
|
|
13964
14163
|
const AGENT_LAUNCHERS = {
|
|
13965
14164
|
codex: {
|
|
@@ -15234,6 +15433,84 @@ async function postWorkerSummary(context, agentId, ticket, summaryFile, options
|
|
|
15234
15433
|
}
|
|
15235
15434
|
}
|
|
15236
15435
|
|
|
15436
|
+
/**
|
|
15437
|
+
* What a worker run cost, read from its own JSONL transcript — the only place a local model's spend is
|
|
15438
|
+
* visible. A Claude run's `result` event carries `total_cost_usd` and token usage; a Codex run's
|
|
15439
|
+
* `turn.completed` events carry token usage and no price. Returns null when the transcript says nothing,
|
|
15440
|
+
* so an unknown cost is reported as unknown rather than as zero.
|
|
15441
|
+
*/
|
|
15442
|
+
function workerRunUsage(text) {
|
|
15443
|
+
let usd = null;
|
|
15444
|
+
let inputTokens = 0;
|
|
15445
|
+
let outputTokens = 0;
|
|
15446
|
+
let sawTokens = false;
|
|
15447
|
+
String(text || '').split('\n').forEach((line) => {
|
|
15448
|
+
let event;
|
|
15449
|
+
try { event = JSON.parse(line.trim()); } catch (_) { return; }
|
|
15450
|
+
if (!event || typeof event !== 'object') return;
|
|
15451
|
+
if (event.type === 'result') {
|
|
15452
|
+
if (typeof event.total_cost_usd === 'number') usd = event.total_cost_usd;
|
|
15453
|
+
if (event.usage) {
|
|
15454
|
+
inputTokens = Number(event.usage.input_tokens || 0) + Number(event.usage.cache_read_input_tokens || 0) +
|
|
15455
|
+
Number(event.usage.cache_creation_input_tokens || 0);
|
|
15456
|
+
outputTokens = Number(event.usage.output_tokens || 0);
|
|
15457
|
+
sawTokens = true;
|
|
15458
|
+
}
|
|
15459
|
+
} else if (event.type === 'turn.completed' && event.usage) {
|
|
15460
|
+
inputTokens += Number(event.usage.input_tokens || 0);
|
|
15461
|
+
outputTokens += Number(event.usage.output_tokens || 0);
|
|
15462
|
+
sawTokens = true;
|
|
15463
|
+
}
|
|
15464
|
+
});
|
|
15465
|
+
if (usd === null && !sawTokens) return null;
|
|
15466
|
+
return { usd, inputTokens: sawTokens ? inputTokens : null, outputTokens: sawTokens ? outputTokens : null };
|
|
15467
|
+
}
|
|
15468
|
+
|
|
15469
|
+
/**
|
|
15470
|
+
* The identity a worker attempt's cost is reported under. The platform keeps ONE report per identity (a
|
|
15471
|
+
* re-sent report replaces itself), so it must be unique per ATTEMPT, not per supervisor: the supervisor's
|
|
15472
|
+
* run id is constant for its whole life, and reusing it made each retry overwrite the previous attempt's
|
|
15473
|
+
* spend. The attempt's transcript file is unique per spawn and stable for that attempt, which is exactly
|
|
15474
|
+
* the identity a retried report must keep.
|
|
15475
|
+
*/
|
|
15476
|
+
function workerCostReportId(runId, logFile) {
|
|
15477
|
+
return String(runId || 'run') + ':' + path.basename(String(logFile || 'attempt'), '.log');
|
|
15478
|
+
}
|
|
15479
|
+
|
|
15480
|
+
/** Report a finished run's cost onto its ticket. Best-effort: a lost report under-counts, never fails a run. */
|
|
15481
|
+
async function reportWorkerCost(context, agentId, ticket, logFile, runId, agentType) {
|
|
15482
|
+
let usage = null;
|
|
15483
|
+
try {
|
|
15484
|
+
usage = workerRunUsage(fs.readFileSync(logFile, 'utf8'));
|
|
15485
|
+
} catch (_) {
|
|
15486
|
+
return false;
|
|
15487
|
+
}
|
|
15488
|
+
if (!usage || !runId) return false;
|
|
15489
|
+
try {
|
|
15490
|
+
await buildAxios(context.baseUrl, context.session.token, 20000)
|
|
15491
|
+
.post('/cli/ticket', {
|
|
15492
|
+
action: 'cost',
|
|
15493
|
+
ticketId: ticket.ticketId || ticket.id,
|
|
15494
|
+
accountId: Number(ticket.accountId) || undefined,
|
|
15495
|
+
dataMode: context.dataMode,
|
|
15496
|
+
agentId,
|
|
15497
|
+
runId,
|
|
15498
|
+
costRunId: workerCostReportId(runId, logFile),
|
|
15499
|
+
usd: usage.usd,
|
|
15500
|
+
inputTokens: usage.inputTokens,
|
|
15501
|
+
outputTokens: usage.outputTokens,
|
|
15502
|
+
provider: agentType || undefined,
|
|
15503
|
+
costSource: 'remits-cli-worker'
|
|
15504
|
+
});
|
|
15505
|
+
return true;
|
|
15506
|
+
} catch (err) {
|
|
15507
|
+
appendGlobalActivityLog('agent', 'worker.cost_post_failed', {
|
|
15508
|
+
agentId, ticketId: ticket.ticketId || ticket.id, error: describeError(err)
|
|
15509
|
+
}, 'error');
|
|
15510
|
+
return false;
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
|
|
15237
15514
|
/** Drop a claim on the platform. Best-effort: the claim expires on its own if this never lands. */
|
|
15238
15515
|
async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
15239
15516
|
try {
|
|
@@ -15260,6 +15537,22 @@ async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
|
15260
15537
|
}
|
|
15261
15538
|
}
|
|
15262
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
|
+
|
|
15263
15556
|
async function postAgentStatus(context, agentId, update) {
|
|
15264
15557
|
try {
|
|
15265
15558
|
await buildAxios(context.baseUrl, context.session.token, 10000)
|
|
@@ -15276,6 +15569,39 @@ async function postAgentStatus(context, agentId, update) {
|
|
|
15276
15569
|
* the same thing. Only the local supervisor can add machine-local repository hints; the worker owns the
|
|
15277
15570
|
* final grounding step, including creating or choosing a git worktree before it edits.
|
|
15278
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
|
+
|
|
15279
15605
|
async function spawnTicketWorker(context, ctx, ticket) {
|
|
15280
15606
|
const { spawn } = require('child_process');
|
|
15281
15607
|
const agentId = ctx.agentId;
|
|
@@ -15401,6 +15727,15 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15401
15727
|
});
|
|
15402
15728
|
|
|
15403
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
|
+
});
|
|
15404
15739
|
const child = spawn(launcher.binary, built.args, {
|
|
15405
15740
|
cwd: workingDirectory || ctx.cwd,
|
|
15406
15741
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -15411,6 +15746,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15411
15746
|
REMITS_AGENT_ID: agentId,
|
|
15412
15747
|
REMITS_SUPPORT_TICKET_ID: ticketId,
|
|
15413
15748
|
REMITS_WORKER_RUN_ID: runId,
|
|
15749
|
+
...(workerCredential ? { REMITS_AGENT_TOKEN: workerCredential.credential } : {}),
|
|
15414
15750
|
// Give the worker its OWN staging lane.
|
|
15415
15751
|
//
|
|
15416
15752
|
// The edit LEASE already serializes who may edit a repo, and this does not replace it — a lease
|
|
@@ -15472,8 +15808,10 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15472
15808
|
let lastStatusAtMs = 0;
|
|
15473
15809
|
let buffered = '';
|
|
15474
15810
|
|
|
15811
|
+
noteWorkerLiveness(ticketId, { runId, pid: child.pid, startedAtMs: Date.now() });
|
|
15475
15812
|
const consume = (chunk) => {
|
|
15476
15813
|
const text = chunk.toString();
|
|
15814
|
+
noteWorkerLiveness(ticketId, { lastOutputAtMs: Date.now() });
|
|
15477
15815
|
if (logBytes < AGENT_WORKER_MAX_LOG_BYTES) {
|
|
15478
15816
|
logBytes += text.length;
|
|
15479
15817
|
logStream.write(text);
|
|
@@ -15489,6 +15827,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15489
15827
|
if (nowMs - lastStatusAtMs < AGENT_WORKER_STATUS_THROTTLE_MS) continue;
|
|
15490
15828
|
lastStatusAtMs = nowMs;
|
|
15491
15829
|
recordWorker(agentId, ticketId, { activity: summary, activityAt: new Date().toISOString() });
|
|
15830
|
+
noteWorkerLiveness(ticketId, { lastProgress: String(summary).slice(0, 160), lastProgressAtMs: nowMs });
|
|
15492
15831
|
postAgentStatus(context, agentId, {
|
|
15493
15832
|
state: 'working', ticketId, activity: summary, step: 'worker:' + agentType
|
|
15494
15833
|
});
|
|
@@ -15503,12 +15842,23 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15503
15842
|
|
|
15504
15843
|
const done = new Promise((resolve) => {
|
|
15505
15844
|
child.on('error', (err) => {
|
|
15845
|
+
WORKER_LIVENESS.delete(String(ticketId));
|
|
15506
15846
|
appendGlobalActivityLog('agent', 'worker.spawn_failed',
|
|
15507
15847
|
{ agentId, ticketId, agentType, error: describeError(err) }, 'error');
|
|
15508
15848
|
resolve({ ticketId, ok: false, reason: 'spawn_failed', message: describeError(err) });
|
|
15509
15849
|
});
|
|
15510
15850
|
child.on('close', async (code, signal) => {
|
|
15511
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);
|
|
15512
15862
|
const ok = code === 0 && !signal;
|
|
15513
15863
|
appendGlobalActivityLog('agent', 'worker.finished',
|
|
15514
15864
|
{ agentId, ticketId, agentType, code, signal, attempt, logFile }, ok ? 'info' : 'error');
|
|
@@ -15517,6 +15867,8 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15517
15867
|
// zero having done nothing. So the TICKET decides — unclaim always, and only give up (release
|
|
15518
15868
|
// it back to the queue) once it has genuinely failed enough times and is still not terminal.
|
|
15519
15869
|
const giveUp = !ok && attempt >= AGENT_WORKER_MAX_ATTEMPTS;
|
|
15870
|
+
// Spend first, while this run still holds the claim it is reported against.
|
|
15871
|
+
await reportWorkerCost(context, agentId, { ticketId, accountId: ticket.accountId }, logFile, ctx.runId, agentType);
|
|
15520
15872
|
const result = await releaseTicketClaim(context, agentId, { ticketId, accountId: ticket.accountId }, {
|
|
15521
15873
|
runId: ctx.runId,
|
|
15522
15874
|
outcome: ok ? 'completed' : (signal ? 'killed' : 'failed'),
|
|
@@ -15662,6 +16014,7 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15662
16014
|
claim: inFlight.size < ctx.maxConcurrent,
|
|
15663
16015
|
runId: ctx.runId,
|
|
15664
16016
|
activeTickets,
|
|
16017
|
+
attempts: workerLivenessFor(activeTickets),
|
|
15665
16018
|
maxConcurrent: ctx.maxConcurrent,
|
|
15666
16019
|
// Declared so the phase the platform derives already accounts for an operator who asked for a
|
|
15667
16020
|
// read-only agent. Without it the server would hand out an edit lease this session will never
|
|
@@ -15972,6 +16325,8 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15972
16325
|
|
|
15973
16326
|
const local = ownLocalAgent(flags);
|
|
15974
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 */ }
|
|
15975
16330
|
|
|
15976
16331
|
// Routing lives on its own endpoint (the operator surface the dashboard and any Embeddable share),
|
|
15977
16332
|
// so it is dispatched here rather than folded into the lifecycle switch.
|
|
@@ -16015,6 +16370,13 @@ async function ticketCommand(flags, subcommand) {
|
|
|
16015
16370
|
route: flags.route === 'false' ? false : undefined,
|
|
16016
16371
|
details: flags.details,
|
|
16017
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,
|
|
16018
16380
|
category: flags.category,
|
|
16019
16381
|
// Clearing a planning slot has to be expressible, and `||` could not express it: an empty value is
|
|
16020
16382
|
// falsy, so it collapsed to undefined, JSON.stringify dropped the key, and the server was never told.
|
|
@@ -16063,7 +16425,24 @@ async function ticketCommand(flags, subcommand) {
|
|
|
16063
16425
|
approvers: flags.approvers ? String(flags.approvers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
16064
16426
|
reviewers: flags.reviewers ? String(flags.reviewers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
16065
16427
|
expiresAt: flags['expires-at'] || undefined,
|
|
16428
|
+
// N-of-M: how many distinct people must approve before the request is approved.
|
|
16429
|
+
minApprovals: flags['min-approvals'] ? Number(flags['min-approvals']) : undefined,
|
|
16066
16430
|
delivery: flags.delivery || undefined,
|
|
16431
|
+
// `wake`: a durable timer. `--at` an instant, `--in` minutes, `--cancel` to clear it.
|
|
16432
|
+
wakeAt: action === 'wake' ? (flags.at || undefined) : undefined,
|
|
16433
|
+
wakeInMinutes: action === 'wake' && flags.in ? Number(flags.in) : undefined,
|
|
16434
|
+
cancel: action === 'wake' && flagEnabled(flags.cancel) ? true : undefined,
|
|
16435
|
+
// `depends-on`: the tickets this one waits on. The flag with no value clears them.
|
|
16436
|
+
dependsOn: action === 'depends-on'
|
|
16437
|
+
? (flags.tickets === true || flags.tickets === '' ? [] : (flags.tickets ? String(flags.tickets).split(',').map((v) => v.trim()).filter(Boolean) : undefined))
|
|
16438
|
+
: undefined,
|
|
16439
|
+
// `cost`: what a run cost, reported by whoever can see it (a launcher, a person).
|
|
16440
|
+
usd: action === 'cost' && flags.usd !== undefined ? Number(flags.usd) : undefined,
|
|
16441
|
+
inputTokens: action === 'cost' && flags['input-tokens'] ? Number(flags['input-tokens']) : undefined,
|
|
16442
|
+
outputTokens: action === 'cost' && flags['output-tokens'] ? Number(flags['output-tokens']) : undefined,
|
|
16443
|
+
model: action === 'cost' ? (flags.model || undefined) : undefined,
|
|
16444
|
+
provider: action === 'cost' ? (flags.provider || undefined) : undefined,
|
|
16445
|
+
costRunId: action === 'cost' ? (flags['cost-run-id'] || undefined) : undefined,
|
|
16067
16446
|
// The revision a call acts on or a decision looked at: a patch digest, a staged snapshot, a version.
|
|
16068
16447
|
revision: flags.revision || undefined,
|
|
16069
16448
|
note: flags.note,
|
|
@@ -16780,7 +17159,7 @@ function printTicketHelp() {
|
|
|
16780
17159
|
console.log(' ask --question "..." [--context "..."] [--to WHO] [--visibility internal]');
|
|
16781
17160
|
console.log(' Put a question to a human and park. Use it when the CHOICE is not yours. Recorded as');
|
|
16782
17161
|
console.log(' request R<n>; answering it resumes the work even if no worker is online right now.');
|
|
16783
|
-
console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--summary "..."]');
|
|
17162
|
+
console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--min-approvals N] [--summary "..."]');
|
|
16784
17163
|
console.log(' Ask a PERSON to authorize one action on one revision (promote a patch, send a message).');
|
|
16785
17164
|
console.log(' An answer is not an approval: only an authenticated person can decide it, never an');
|
|
16786
17165
|
console.log(' agent run, and a changed revision needs a new approval.');
|
|
@@ -16791,10 +17170,25 @@ function printTicketHelp() {
|
|
|
16791
17170
|
console.log(' cancel-request --request R2 [--reason "..."]');
|
|
16792
17171
|
console.log(' handoff --summary "what the next worker should do" [--workstream review] [--exclude]');
|
|
16793
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]');
|
|
16794
17174
|
console.log(' Hand the next step to another session (queued if nobody is online). --exclude keeps');
|
|
16795
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.');
|
|
16796
17179
|
console.log(' continue [--reason requested] Queue the ticket for the automation to work again.');
|
|
16797
|
-
console.log('
|
|
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).');
|
|
17183
|
+
console.log(' reset-budget OPERATOR: restore a ticket\'s run and spend budget after looking at it.');
|
|
17184
|
+
console.log(' wake --in MINUTES | --at ISO-TIME [--reason "..."] | wake --cancel');
|
|
17185
|
+
console.log(' A durable timer: when it is due the ticket is runnable work again (reason `timer`) and');
|
|
17186
|
+
console.log(' the run that takes it is told why. Use it for "look again after X", never sleep-and-poll.');
|
|
17187
|
+
console.log(' depends-on --tickets 101,102 This ticket waits on those; the sweep will not start it until');
|
|
17188
|
+
console.log(' every one is resolved or closed. `--tickets` with no value clears it.');
|
|
17189
|
+
console.log(' cost --usd 1.25 [--input-tokens N] [--output-tokens N] [--model M] [--cost-run-id R]');
|
|
17190
|
+
console.log(' Record what a run cost. One report per run (a second replaces the first); the account\'s');
|
|
17191
|
+
console.log(' supportRouting.maxTicketSpendUsd then stops automatic starts.');
|
|
16798
17192
|
console.log(' answer --message "..." [--route false] (alias: reply)');
|
|
16799
17193
|
console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
|
|
16800
17194
|
console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
|
|
@@ -16962,6 +17356,113 @@ function usageListFlag(value) {
|
|
|
16962
17356
|
return items.length ? items : undefined;
|
|
16963
17357
|
}
|
|
16964
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
|
+
|
|
16965
17466
|
async function usageCommand(flags) {
|
|
16966
17467
|
const cwd = process.cwd();
|
|
16967
17468
|
ensureLocalState(cwd);
|
|
@@ -17419,9 +17920,29 @@ function listWorkstreamIndexes(cwd) {
|
|
|
17419
17920
|
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
|
|
17420
17921
|
}
|
|
17421
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
|
+
|
|
17422
17932
|
async function evidenceCommand(flags) {
|
|
17423
17933
|
const cwd = process.cwd();
|
|
17424
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
|
+
}
|
|
17425
17946
|
const limit = Math.max(1, Math.min(parseInt(flags.limit || flags.max || '40', 10) || 40, 500));
|
|
17426
17947
|
const entries = readEvidenceTrail(cwd, limit);
|
|
17427
17948
|
if (flagEnabled(flags.json)) {
|
|
@@ -17430,6 +17951,10 @@ async function evidenceCommand(flags) {
|
|
|
17430
17951
|
}
|
|
17431
17952
|
const paths = localStatePaths(cwd);
|
|
17432
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
|
+
}
|
|
17433
17958
|
if (!entries.length) {
|
|
17434
17959
|
console.log('Nothing recorded yet. Every stage, test, token, tool and sync appends one world-stamped line here,');
|
|
17435
17960
|
console.log('with no envelope and nothing to start.');
|
|
@@ -17531,6 +18056,35 @@ async function workstreamCommand(flags, subcommand) {
|
|
|
17531
18056
|
* and the one an operator asks when a queue is not moving. Agents, leases and tickets were each readable
|
|
17532
18057
|
* on their own and never together, so every caller invented its own join and they disagreed.
|
|
17533
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
|
+
|
|
17534
18088
|
async function agentWorkMapCommand(flags) {
|
|
17535
18089
|
const agentId = resolveAgentId(flags, { optional: true });
|
|
17536
18090
|
const local = agentId ? (listLocalAgents().find((entry) => entry.agentId === agentId) || {}) : {};
|
|
@@ -17557,12 +18111,19 @@ async function agentWorkMapCommand(flags) {
|
|
|
17557
18111
|
return;
|
|
17558
18112
|
}
|
|
17559
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);
|
|
17560
18118
|
console.log('');
|
|
17561
18119
|
for (const repo of repositories) {
|
|
17562
18120
|
const lease = repo.editLease;
|
|
17563
18121
|
console.log(' ' + repo.accountName + ' (' + repo.accountId + ')' +
|
|
17564
18122
|
(repo.accountType ? ' ' + repo.accountType : ''));
|
|
17565
|
-
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) {
|
|
17566
18127
|
const who = lease.agentLabel || lease.agentId;
|
|
17567
18128
|
console.log(' EDIT LEASE ' + who +
|
|
17568
18129
|
(lease.ticketId ? ' ticket ' + lease.ticketId : '') +
|
|
@@ -18638,7 +19199,7 @@ function printComponentsHelp(subcommand) {
|
|
|
18638
19199
|
}
|
|
18639
19200
|
|
|
18640
19201
|
function printTestHelp() {
|
|
18641
|
-
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]');
|
|
18642
19203
|
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
18643
19204
|
console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
|
|
18644
19205
|
console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
|
|
@@ -18647,11 +19208,15 @@ function printTestHelp() {
|
|
|
18647
19208
|
console.log('Examples:');
|
|
18648
19209
|
console.log(' remits-cli test run --test 11');
|
|
18649
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');
|
|
18650
19212
|
console.log(' remits-cli test status --task-id 7a1b...');
|
|
18651
19213
|
console.log('');
|
|
18652
19214
|
console.log('Notes:');
|
|
18653
19215
|
console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
|
|
18654
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.');
|
|
18655
19220
|
console.log(' --as-account changes the execution account so subscriber branch edges apply.');
|
|
18656
19221
|
console.log(' --variant-branch <name> probes a committed variant branch; use "none" or "trunk"');
|
|
18657
19222
|
console.log(' to force production/subscription semantics from a variant checkout.');
|
|
@@ -18836,6 +19401,7 @@ async function main() {
|
|
|
18836
19401
|
migrateSessionIfNeeded();
|
|
18837
19402
|
const originalArgv = process.argv.slice(2);
|
|
18838
19403
|
const args = parseArgs(originalArgv);
|
|
19404
|
+
requestIdentityFlags = args;
|
|
18839
19405
|
configureLocalActor(args);
|
|
18840
19406
|
const [command, subcommand] = args._;
|
|
18841
19407
|
const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
|
|
@@ -18851,7 +19417,7 @@ async function main() {
|
|
|
18851
19417
|
requireAccountRepoContext(process.cwd(), flags, 'ai ' + subcommand, context);
|
|
18852
19418
|
await localAi.command({
|
|
18853
19419
|
subcommand, flags, context, root: SESSION_DIR, axios, Client, WebSocket,
|
|
18854
|
-
defaultAgent: getPreferredAgent()
|
|
19420
|
+
defaultAgent: getPreferredAgent(), requestHeaders: cliRequestHeaders
|
|
18855
19421
|
});
|
|
18856
19422
|
return;
|
|
18857
19423
|
}
|
|
@@ -18910,7 +19476,9 @@ async function main() {
|
|
|
18910
19476
|
|
|
18911
19477
|
if (!command || command === 'help' || command === '--help') {
|
|
18912
19478
|
console.log('Usage: remits-cli <command>');
|
|
18913
|
-
console.log('Global options: --no-auto-update, --local-agent NAME');
|
|
19479
|
+
console.log('Global options: --no-auto-update, --local-agent NAME, --agent-id ID');
|
|
19480
|
+
console.log(' --agent-id / REMITS_AGENT_ID declares the agent acting for the authenticated user on CLI requests.');
|
|
19481
|
+
console.log(' --local-agent names only the local log folder; it does not declare an agent to the platform.');
|
|
18914
19482
|
console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
|
|
18915
19483
|
console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
|
|
18916
19484
|
console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
|
|
@@ -18924,6 +19492,8 @@ async function main() {
|
|
|
18924
19492
|
console.log(' remits-cli activity inspect [--account-id ID] [--scope self|children|related] [--workspace NAME] [--workstream ID] [--user-email E] [--summary|--compact] [--json]');
|
|
18925
19493
|
console.log(' remits-cli workstream status [--workstream ID|--workspace NAME] [--json]');
|
|
18926
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)');
|
|
18927
19497
|
console.log(' remits-cli agent release # stop receiving tickets now');
|
|
18928
19498
|
console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
|
|
18929
19499
|
console.log(' remits-cli stop');
|
|
@@ -18955,7 +19525,7 @@ async function main() {
|
|
|
18955
19525
|
console.log(' remits-cli components branch <name> --subscribe <accountId> [--dry-run] [--confirm-primary-edge]');
|
|
18956
19526
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
18957
19527
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
18958
|
-
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]');
|
|
18959
19529
|
console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
|
|
18960
19530
|
console.log(' remits-cli test compare --base <taskId> --head <taskId>');
|
|
18961
19531
|
console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
|
|
@@ -19186,6 +19756,33 @@ async function main() {
|
|
|
19186
19756
|
throw new Error('Unknown activity subcommand: ' + subcommand);
|
|
19187
19757
|
}
|
|
19188
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
|
+
|
|
19189
19786
|
if (command === 'usage') {
|
|
19190
19787
|
if (wantsHelp) {
|
|
19191
19788
|
console.log('Usage: remits-cli usage --from DATE [--to DATE] [--timezone ZONE] [--by hour|day|month]');
|
|
@@ -19205,7 +19802,7 @@ async function main() {
|
|
|
19205
19802
|
|
|
19206
19803
|
if (command === 'evidence') {
|
|
19207
19804
|
if (wantsHelp) {
|
|
19208
|
-
console.log('Usage: remits-cli evidence [--limit N] [--json]');
|
|
19805
|
+
console.log('Usage: remits-cli evidence [--limit N] [--json] | remits-cli evidence flush');
|
|
19209
19806
|
console.log('');
|
|
19210
19807
|
console.log('What you have actually run, and in which world. Every stage, test, token, tool and sync');
|
|
19211
19808
|
console.log('appends one world-stamped line automatically - there is nothing to start and nothing to');
|
|
@@ -19213,6 +19810,9 @@ async function main() {
|
|
|
19213
19810
|
console.log('');
|
|
19214
19811
|
console.log('This is per-actor, so agents sharing a checkout never read each other\'s trail.');
|
|
19215
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.');
|
|
19216
19816
|
return;
|
|
19217
19817
|
}
|
|
19218
19818
|
await evidenceCommand(args);
|