@remits/remits-cli 0.1.166 → 0.1.168
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 +257 -27
- package/lib/local-ai.js +2 -2
- package/package.json +1 -1
- package/skills/remits-cli/references/agent-sessions.md +21 -3
- package/skills/remits-cli/references/investigation.md +21 -0
- package/skills/remits-cli/references/support-tickets.md +70 -0
- package/skills/remits-cli/references/tool-reference.md +30 -1
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ remits-cli install --skills --overwrite true
|
|
|
90
90
|
- `components clear` clears staged entries. Scope it with `--component-type` and/or `--component-id`. Component ids are type-local, so an id alone clears that one component when the id is staged in only one family; if the same id is staged across multiple families it returns an ambiguity error asking you to add `--component-type`. With no filter it clears every staged entry for the current branch; pass `--all` to force the full-branch wipe explicitly.
|
|
91
91
|
- `components stage`, `components status`, `components lanes`, `components entries`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response.
|
|
92
92
|
- `activity inspect` is a read-only workstream view for operator/agent review. It joins staging lanes, verification envelopes, Test runs, corpus measurements, live agent presence, and heuristic smell signals. Use `--scope children` from a platform/root account and `--user-email` or `--user-id` to focus on one CLI user.
|
|
93
|
+
- System → Activity captures CLI requests under the authenticated user. Declare an interactive agent with `REMITS_AGENT_ID` or `--agent-id`; supervised workers already carry agent/run/ticket identity. The page distinguishes declared agents, browser activity, and CLI calls without declared agent identity. `--local-agent` only chooses a local log folder. Request labels describe intent, not a background job's completion, and local edits between platform calls are not observed.
|
|
93
94
|
- `components stage` and `components status` report three separate numbers, and they answer three different questions: the **workset** (components git reports this working tree changed), what was **submitted**, and the materialized **overlay** the lane now holds — which is what a run resolves. A number that could not be established prints as `unknown`, never as `0`.
|
|
94
95
|
- `components sync` performs a server-side sync from the git remote into the Remits platform for the selected branch. It does not run local git commands. After a successful non-dry-run sync, the platform clears the branch/user staging scope so staged aliases cannot keep shadowing the newly synced DB rows.
|
|
95
96
|
- On trunk, `components sync` performs the full repo-to-DB reconcile. On a non-trunk branch, it writes `ComponentVariant` overlays only and may refresh branch-local `account-info.json`, `account-hierarchy.json`, and `account-configurations.json` for the subscribing account that initiated the sync. `--dry-run` is accepted only for non-trunk variant syncs and reports overrides/additions/tombstones without writing variants, caching the sync SHA, updating metadata, or clearing staging. Add `--summary` to dry-run output when you only need counts, removals/tombstones, errors, skipped items, and warnings. `--force-tombstones` is accepted only for non-trunk variant syncs and should be used only when missing trunk component files are intentional tombstone overrides.
|
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
|
+
- L2672 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L3571 Component Discovery And HTTP Logging
|
|
11
|
+
- L4350 Skill Delivery And TOC Resolution
|
|
12
|
+
- L4665 Auth And Component Staging
|
|
13
|
+
- L5301 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L7625 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L9889 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11406 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L13916 Agent And Ticket Workflows
|
|
18
|
+
- L18429 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')
|
|
@@ -4044,7 +4046,7 @@ function stageTimeoutMs(components, flags = {}) {
|
|
|
4044
4046
|
return Math.min(600000, Math.max(60000, Math.round(60000 + (megabytes * 60000))));
|
|
4045
4047
|
}
|
|
4046
4048
|
|
|
4047
|
-
function
|
|
4049
|
+
function cliRequestHeaders(token) {
|
|
4048
4050
|
const headers = token ? { Authorization: 'Bearer ' + token } : {};
|
|
4049
4051
|
// The CLI version on EVERY request, from the one place every request is built. It used to travel only on
|
|
4050
4052
|
// `agent register`, so the platform knew the version of the sessions that registered - and the agents that
|
|
@@ -4055,10 +4057,19 @@ function buildAxios(baseUrl, token, timeoutMs = 60000) {
|
|
|
4055
4057
|
// A worker's run identity rides on every call, so a verb whose payload does not repeat it (stage, commit,
|
|
4056
4058
|
// a tool) is still attributed to the run. The ROLE is deliberately not sent: the platform reads it from
|
|
4057
4059
|
// the session's registration, where the user's permission to declare it was checked.
|
|
4058
|
-
|
|
4060
|
+
// ticket route historically accepts --agent-id as the TARGET; it is not the caller.
|
|
4061
|
+
const targetAgent = requestIdentityFlags._?.[0] === 'ticket' && ['route', 'unroute'].includes(requestIdentityFlags._?.[1]);
|
|
4062
|
+
const rawAgentFlag = requestIdentityFlags['agent-id'] || requestIdentityFlags.agentId;
|
|
4063
|
+
const flagAgent = typeof rawAgentFlag === 'string' ? rawAgentFlag.trim() : '';
|
|
4064
|
+
const agentId = (!targetAgent && flagAgent) || process.env.REMITS_AGENT_ID;
|
|
4065
|
+
if (typeof agentId === 'string' && agentId.trim()) headers['X-Remits-Agent-Id'] = agentId.trim();
|
|
4059
4066
|
if (process.env.REMITS_WORKER_RUN_ID) headers['X-Remits-Run-Id'] = process.env.REMITS_WORKER_RUN_ID;
|
|
4060
4067
|
if (process.env.REMITS_SUPPORT_TICKET_ID) headers['X-Remits-Ticket-Id'] = process.env.REMITS_SUPPORT_TICKET_ID;
|
|
4061
|
-
return
|
|
4068
|
+
return headers;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function buildAxios(baseUrl, token, timeoutMs = 60000) {
|
|
4072
|
+
return axios.create({ baseURL: baseUrl, timeout: parsePositiveInt(timeoutMs, 60000), headers: cliRequestHeaders(token) });
|
|
4062
4073
|
}
|
|
4063
4074
|
|
|
4064
4075
|
function describeError(err) {
|
|
@@ -4988,6 +4999,10 @@ async function pushComponentsCommand(flags) {
|
|
|
4988
4999
|
...runContextPayload(flags),
|
|
4989
5000
|
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
|
|
4990
5001
|
acknowledge: flags.acknowledge || undefined,
|
|
5002
|
+
// `commit` when this stage is the pre-push check of `components commit`: the platform then judges it as a
|
|
5003
|
+
// LANDING (the account's committed `commit`-point rules, component rules included), so a refusal
|
|
5004
|
+
// arrives before anything is pushed instead of after.
|
|
5005
|
+
intent: flags.intent || undefined,
|
|
4991
5006
|
// What this stage IS, stated rather than implied. The server derives the lane behavior from the mode
|
|
4992
5007
|
// (CliComponentStaging.stageRequest) instead of from `replace`, which was overloaded: a partial
|
|
4993
5008
|
// manifest with `replace: true` would make the reconcile pass delete nearly the whole lane, so
|
|
@@ -8073,7 +8088,7 @@ async function commitComponentsCommand(flags) {
|
|
|
8073
8088
|
};
|
|
8074
8089
|
try {
|
|
8075
8090
|
if (!skipGit) {
|
|
8076
|
-
emit('Phase 1/4: staged compile validation');
|
|
8091
|
+
emit('Phase 1/4: staged compile validation and landing policy check');
|
|
8077
8092
|
// MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
|
|
8078
8093
|
// lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
|
|
8079
8094
|
// validation pass must not be able to delete another agent's staged work as a side effect of somebody
|
|
@@ -8085,7 +8100,8 @@ async function commitComponentsCommand(flags) {
|
|
|
8085
8100
|
workset: false,
|
|
8086
8101
|
'replace-lane': false,
|
|
8087
8102
|
replaceLane: false,
|
|
8088
|
-
'changed-only': true
|
|
8103
|
+
'changed-only': true,
|
|
8104
|
+
intent: 'commit'
|
|
8089
8105
|
})));
|
|
8090
8106
|
|
|
8091
8107
|
outcome.phase = 'git';
|
|
@@ -14613,8 +14629,50 @@ function agentSessionFor(flags, entry) {
|
|
|
14613
14629
|
};
|
|
14614
14630
|
}
|
|
14615
14631
|
|
|
14616
|
-
/**
|
|
14617
|
-
|
|
14632
|
+
/**
|
|
14633
|
+
* `--accounts 33,52` -> { roots: [33, 52], descendants: true }; `--no-descendants` narrows each root to
|
|
14634
|
+
* itself. Returns null when coverage was not declared, which keeps today's behaviour (every indexed repo).
|
|
14635
|
+
*/
|
|
14636
|
+
function parseAgentCoverage(flags) {
|
|
14637
|
+
const raw = flags && (flags.accounts || flags['account-ids']);
|
|
14638
|
+
if (raw === undefined || raw === null || raw === true || raw === false) {
|
|
14639
|
+
return null;
|
|
14640
|
+
}
|
|
14641
|
+
const roots = String(raw).split(',')
|
|
14642
|
+
.map((entry) => Number(String(entry).trim()))
|
|
14643
|
+
.filter((id, index, all) => Number.isFinite(id) && id > 0 && all.indexOf(id) === index);
|
|
14644
|
+
if (!roots.length) {
|
|
14645
|
+
throw new Error('--accounts needs one or more numeric account ids, e.g. --accounts 33 or --accounts 33,52.');
|
|
14646
|
+
}
|
|
14647
|
+
const descendants = !(flagEnabled(flags['no-descendants']) || flags.descendants === false ||
|
|
14648
|
+
String(flags.descendants || '').toLowerCase() === 'false');
|
|
14649
|
+
return { roots, descendants };
|
|
14650
|
+
}
|
|
14651
|
+
|
|
14652
|
+
/**
|
|
14653
|
+
* Every account repo checked out on this machine, as registration claims. The platform verifies them.
|
|
14654
|
+
*
|
|
14655
|
+
* With declared coverage (`--accounts`), ONLY those roots are claimed — each with its descendants unless
|
|
14656
|
+
* `--no-descendants` — whether or not a checkout of it is indexed. Without it, every indexed repository is
|
|
14657
|
+
* claimed, which is the historical behaviour and is too wide for a fleet member meant to serve one tree.
|
|
14658
|
+
* The session account (`--account-id`) is context, never coverage.
|
|
14659
|
+
*/
|
|
14660
|
+
function agentAccountClaims(cwd, coverage) {
|
|
14661
|
+
if (coverage) {
|
|
14662
|
+
const indexed = listAccountRepoEntries(readAccountRepoIndex());
|
|
14663
|
+
return coverage.roots.map((accountId) => {
|
|
14664
|
+
const entry = indexed.find((candidate) => Number(candidate.accountId) === accountId) || {};
|
|
14665
|
+
return {
|
|
14666
|
+
accountId,
|
|
14667
|
+
name: entry.name || null,
|
|
14668
|
+
type: entry.type || null,
|
|
14669
|
+
platformAccountId: entry.platformAccountId || null,
|
|
14670
|
+
directory: entry.directory || null,
|
|
14671
|
+
routingScope: coverage.descendants ? 'self_and_descendants' : 'self',
|
|
14672
|
+
source: 'coverage'
|
|
14673
|
+
};
|
|
14674
|
+
});
|
|
14675
|
+
}
|
|
14618
14676
|
const claims = listAccountRepoEntries(readAccountRepoIndex()).map((entry) => ({
|
|
14619
14677
|
accountId: Number(entry.accountId),
|
|
14620
14678
|
name: entry.name || null,
|
|
@@ -14662,6 +14720,7 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14662
14720
|
try { discoverAccountRepos(); } catch (_) { /* best-effort */ }
|
|
14663
14721
|
|
|
14664
14722
|
const { session, baseUrl, dataMode } = agentSessionFor(flags, null);
|
|
14723
|
+
const coverage = parseAgentCoverage(Object.assign({}, options.serve && options.serve.coverageFlags, flags));
|
|
14665
14724
|
const anchor = resolveAgentAnchorPid();
|
|
14666
14725
|
if (!anchor.pid) {
|
|
14667
14726
|
throw new Error(
|
|
@@ -14698,7 +14757,8 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14698
14757
|
workspace: resolveWorkspace(cwd, flags) || undefined,
|
|
14699
14758
|
dataMode,
|
|
14700
14759
|
accountId: Number(session.accountId) || undefined,
|
|
14701
|
-
accounts: agentAccountClaims(cwd),
|
|
14760
|
+
accounts: agentAccountClaims(cwd, coverage),
|
|
14761
|
+
coverageExplicit: coverage ? true : undefined,
|
|
14702
14762
|
// Capacity is how many workers this session can actually RUN, which only this side knows. The
|
|
14703
14763
|
// platform stops routing past it, so a supervisor at capacity is not handed work it cannot start.
|
|
14704
14764
|
maxConcurrent: options.serve ? options.serve.maxConcurrent : (flags['max-concurrent'] ? parsePositiveInt(flags['max-concurrent'], 1) : undefined),
|
|
@@ -14733,6 +14793,7 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14733
14793
|
// The accounts the platform ACCEPTED, so `agent list` can scope itself to the work this session
|
|
14734
14794
|
// can actually be routed rather than to whichever session happened to be most recent.
|
|
14735
14795
|
accountIds: response.accountIds || [],
|
|
14796
|
+
coverage: coverage || null,
|
|
14736
14797
|
accountId: Number(session.accountId) || null,
|
|
14737
14798
|
anchorPid: anchor.pid,
|
|
14738
14799
|
anchorSource: anchor.source,
|
|
@@ -14774,6 +14835,13 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14774
14835
|
console.log(' data lane ' + dataMode + (dataMode === 'prod' ? ' (live support tickets)' : ' (fixtures only)'));
|
|
14775
14836
|
console.log(' accounts ' + accepted.length + ' account(s) this session can be routed work for');
|
|
14776
14837
|
console.log(' ' + accepted.join(', '));
|
|
14838
|
+
// The scope the platform ACCEPTED, per claim — "33 + descendants" vs "every repo on this machine" is the
|
|
14839
|
+
// difference between a bounded fleet member and one that takes any account it can reach.
|
|
14840
|
+
const scopes = (response.accounts || []).map((entry) => entry.accountId +
|
|
14841
|
+
(entry.routingScope === 'self_and_descendants' ? ' + descendants' : ''));
|
|
14842
|
+
console.log(' coverage ' + (coverage
|
|
14843
|
+
? 'declared (--accounts): ' + scopes.join(', ')
|
|
14844
|
+
: 'every indexed account repo (bound it with --accounts <id>[,<id>] [--no-descendants]): ' + scopes.join(', ')));
|
|
14777
14845
|
console.log(' heartbeat every ' + Math.round(AGENT_HEARTBEAT_INTERVAL_MS / 1000) + 's, anchored to pid ' +
|
|
14778
14846
|
anchor.pid + ' (' + anchor.source + '), ends when this session ends');
|
|
14779
14847
|
if ((response.skippedAccounts || []).length) {
|
|
@@ -15010,6 +15078,14 @@ function startAgentHeartbeatDaemon(entry, serve = null) {
|
|
|
15010
15078
|
if (entry.persona) {
|
|
15011
15079
|
args.push('--persona', entry.persona);
|
|
15012
15080
|
}
|
|
15081
|
+
// Declared coverage travels with the daemon, so a re-registration after a TTL lapse cannot silently
|
|
15082
|
+
// widen a fleet member back to every indexed repository.
|
|
15083
|
+
if (entry.coverage && Array.isArray(entry.coverage.roots) && entry.coverage.roots.length) {
|
|
15084
|
+
args.push('--accounts', entry.coverage.roots.join(','));
|
|
15085
|
+
if (entry.coverage.descendants === false) {
|
|
15086
|
+
args.push('--no-descendants', 'true');
|
|
15087
|
+
}
|
|
15088
|
+
}
|
|
15013
15089
|
if (serve) {
|
|
15014
15090
|
args.push(
|
|
15015
15091
|
'--serve', 'true',
|
|
@@ -15174,6 +15250,84 @@ async function postWorkerSummary(context, agentId, ticket, summaryFile, options
|
|
|
15174
15250
|
}
|
|
15175
15251
|
}
|
|
15176
15252
|
|
|
15253
|
+
/**
|
|
15254
|
+
* What a worker run cost, read from its own JSONL transcript — the only place a local model's spend is
|
|
15255
|
+
* visible. A Claude run's `result` event carries `total_cost_usd` and token usage; a Codex run's
|
|
15256
|
+
* `turn.completed` events carry token usage and no price. Returns null when the transcript says nothing,
|
|
15257
|
+
* so an unknown cost is reported as unknown rather than as zero.
|
|
15258
|
+
*/
|
|
15259
|
+
function workerRunUsage(text) {
|
|
15260
|
+
let usd = null;
|
|
15261
|
+
let inputTokens = 0;
|
|
15262
|
+
let outputTokens = 0;
|
|
15263
|
+
let sawTokens = false;
|
|
15264
|
+
String(text || '').split('\n').forEach((line) => {
|
|
15265
|
+
let event;
|
|
15266
|
+
try { event = JSON.parse(line.trim()); } catch (_) { return; }
|
|
15267
|
+
if (!event || typeof event !== 'object') return;
|
|
15268
|
+
if (event.type === 'result') {
|
|
15269
|
+
if (typeof event.total_cost_usd === 'number') usd = event.total_cost_usd;
|
|
15270
|
+
if (event.usage) {
|
|
15271
|
+
inputTokens = Number(event.usage.input_tokens || 0) + Number(event.usage.cache_read_input_tokens || 0) +
|
|
15272
|
+
Number(event.usage.cache_creation_input_tokens || 0);
|
|
15273
|
+
outputTokens = Number(event.usage.output_tokens || 0);
|
|
15274
|
+
sawTokens = true;
|
|
15275
|
+
}
|
|
15276
|
+
} else if (event.type === 'turn.completed' && event.usage) {
|
|
15277
|
+
inputTokens += Number(event.usage.input_tokens || 0);
|
|
15278
|
+
outputTokens += Number(event.usage.output_tokens || 0);
|
|
15279
|
+
sawTokens = true;
|
|
15280
|
+
}
|
|
15281
|
+
});
|
|
15282
|
+
if (usd === null && !sawTokens) return null;
|
|
15283
|
+
return { usd, inputTokens: sawTokens ? inputTokens : null, outputTokens: sawTokens ? outputTokens : null };
|
|
15284
|
+
}
|
|
15285
|
+
|
|
15286
|
+
/**
|
|
15287
|
+
* The identity a worker attempt's cost is reported under. The platform keeps ONE report per identity (a
|
|
15288
|
+
* re-sent report replaces itself), so it must be unique per ATTEMPT, not per supervisor: the supervisor's
|
|
15289
|
+
* run id is constant for its whole life, and reusing it made each retry overwrite the previous attempt's
|
|
15290
|
+
* spend. The attempt's transcript file is unique per spawn and stable for that attempt, which is exactly
|
|
15291
|
+
* the identity a retried report must keep.
|
|
15292
|
+
*/
|
|
15293
|
+
function workerCostReportId(runId, logFile) {
|
|
15294
|
+
return String(runId || 'run') + ':' + path.basename(String(logFile || 'attempt'), '.log');
|
|
15295
|
+
}
|
|
15296
|
+
|
|
15297
|
+
/** Report a finished run's cost onto its ticket. Best-effort: a lost report under-counts, never fails a run. */
|
|
15298
|
+
async function reportWorkerCost(context, agentId, ticket, logFile, runId, agentType) {
|
|
15299
|
+
let usage = null;
|
|
15300
|
+
try {
|
|
15301
|
+
usage = workerRunUsage(fs.readFileSync(logFile, 'utf8'));
|
|
15302
|
+
} catch (_) {
|
|
15303
|
+
return false;
|
|
15304
|
+
}
|
|
15305
|
+
if (!usage || !runId) return false;
|
|
15306
|
+
try {
|
|
15307
|
+
await buildAxios(context.baseUrl, context.session.token, 20000)
|
|
15308
|
+
.post('/cli/ticket', {
|
|
15309
|
+
action: 'cost',
|
|
15310
|
+
ticketId: ticket.ticketId || ticket.id,
|
|
15311
|
+
accountId: Number(ticket.accountId) || undefined,
|
|
15312
|
+
dataMode: context.dataMode,
|
|
15313
|
+
agentId,
|
|
15314
|
+
runId,
|
|
15315
|
+
costRunId: workerCostReportId(runId, logFile),
|
|
15316
|
+
usd: usage.usd,
|
|
15317
|
+
inputTokens: usage.inputTokens,
|
|
15318
|
+
outputTokens: usage.outputTokens,
|
|
15319
|
+
provider: agentType || undefined,
|
|
15320
|
+
costSource: 'remits-cli-worker'
|
|
15321
|
+
});
|
|
15322
|
+
return true;
|
|
15323
|
+
} catch (err) {
|
|
15324
|
+
appendGlobalActivityLog('agent', 'worker.cost_post_failed', {
|
|
15325
|
+
agentId, ticketId: ticket.ticketId || ticket.id, error: describeError(err)
|
|
15326
|
+
}, 'error');
|
|
15327
|
+
return false;
|
|
15328
|
+
}
|
|
15329
|
+
}
|
|
15330
|
+
|
|
15177
15331
|
/** Drop a claim on the platform. Best-effort: the claim expires on its own if this never lands. */
|
|
15178
15332
|
async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
15179
15333
|
try {
|
|
@@ -15185,6 +15339,9 @@ async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
|
15185
15339
|
accountId: Number(ticket.accountId),
|
|
15186
15340
|
outcome: options.outcome || null,
|
|
15187
15341
|
notes: options.notes || null,
|
|
15342
|
+
// The run whose claim this is. The platform ignores the call when a NEWER run holds the claim, so
|
|
15343
|
+
// a late exit callback cannot drop a restarted supervisor's claim or erase a handoff it routed.
|
|
15344
|
+
runId: options.runId || null,
|
|
15188
15345
|
release: options.release === true,
|
|
15189
15346
|
unroute: options.unroute === true
|
|
15190
15347
|
}).then((r) => r.data);
|
|
@@ -15454,7 +15611,10 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15454
15611
|
// zero having done nothing. So the TICKET decides — unclaim always, and only give up (release
|
|
15455
15612
|
// it back to the queue) once it has genuinely failed enough times and is still not terminal.
|
|
15456
15613
|
const giveUp = !ok && attempt >= AGENT_WORKER_MAX_ATTEMPTS;
|
|
15614
|
+
// Spend first, while this run still holds the claim it is reported against.
|
|
15615
|
+
await reportWorkerCost(context, agentId, { ticketId, accountId: ticket.accountId }, logFile, ctx.runId, agentType);
|
|
15457
15616
|
const result = await releaseTicketClaim(context, agentId, { ticketId, accountId: ticket.accountId }, {
|
|
15617
|
+
runId: ctx.runId,
|
|
15458
15618
|
outcome: ok ? 'completed' : (signal ? 'killed' : 'failed'),
|
|
15459
15619
|
release: giveUp,
|
|
15460
15620
|
// A clean exit that left the ticket short of terminal is a DELIVERED judgement, not a failure —
|
|
@@ -15551,7 +15711,7 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15551
15711
|
const context = agentSessionFor(flags, { cwd, baseUrl: flags['base-url'] });
|
|
15552
15712
|
for (const [ticketId, entry] of inFlight.entries()) {
|
|
15553
15713
|
await releaseTicketClaim(context, agentId,
|
|
15554
|
-
{ ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped' });
|
|
15714
|
+
{ ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped', runId: ctx.runId });
|
|
15555
15715
|
}
|
|
15556
15716
|
await buildAxios(context.baseUrl, context.session.token, 5000)
|
|
15557
15717
|
.post('/cli/agentDeregister', { agentId });
|
|
@@ -15691,6 +15851,8 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15691
15851
|
'max-concurrent': String(ctx.maxConcurrent),
|
|
15692
15852
|
role: ctx.role || undefined,
|
|
15693
15853
|
persona: ctx.persona || undefined,
|
|
15854
|
+
accounts: flags.accounts || undefined,
|
|
15855
|
+
'no-descendants': flags['no-descendants'] || undefined,
|
|
15694
15856
|
json: 'true'
|
|
15695
15857
|
},
|
|
15696
15858
|
// Re-register with THIS supervisor's declared bound and queues. The platform treats an absent
|
|
@@ -15990,6 +16152,43 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15990
16152
|
idempotencyKey: flags['idempotency-key'] || flags.idempotencyKey || undefined,
|
|
15991
16153
|
providerMessageId: flags['provider-message-id'] || undefined,
|
|
15992
16154
|
reason: flags.reason,
|
|
16155
|
+
// Explicit requests and decisions. A request id is short (R1, R2); `2`, `r2` and `#2` all work.
|
|
16156
|
+
request: flags.request || undefined,
|
|
16157
|
+
decision: flags.decision || flags._?.[3] || undefined,
|
|
16158
|
+
requestAction: flags.for || flags['request-action'] || undefined,
|
|
16159
|
+
approvers: flags.approvers ? String(flags.approvers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
16160
|
+
reviewers: flags.reviewers ? String(flags.reviewers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
16161
|
+
expiresAt: flags['expires-at'] || undefined,
|
|
16162
|
+
// N-of-M: how many distinct people must approve before the request is approved.
|
|
16163
|
+
minApprovals: flags['min-approvals'] ? Number(flags['min-approvals']) : undefined,
|
|
16164
|
+
delivery: flags.delivery || undefined,
|
|
16165
|
+
// `wake`: a durable timer. `--at` an instant, `--in` minutes, `--cancel` to clear it.
|
|
16166
|
+
wakeAt: action === 'wake' ? (flags.at || undefined) : undefined,
|
|
16167
|
+
wakeInMinutes: action === 'wake' && flags.in ? Number(flags.in) : undefined,
|
|
16168
|
+
cancel: action === 'wake' && flagEnabled(flags.cancel) ? true : undefined,
|
|
16169
|
+
// `depends-on`: the tickets this one waits on. The flag with no value clears them.
|
|
16170
|
+
dependsOn: action === 'depends-on'
|
|
16171
|
+
? (flags.tickets === true || flags.tickets === '' ? [] : (flags.tickets ? String(flags.tickets).split(',').map((v) => v.trim()).filter(Boolean) : undefined))
|
|
16172
|
+
: undefined,
|
|
16173
|
+
// `cost`: what a run cost, reported by whoever can see it (a launcher, a person).
|
|
16174
|
+
usd: action === 'cost' && flags.usd !== undefined ? Number(flags.usd) : undefined,
|
|
16175
|
+
inputTokens: action === 'cost' && flags['input-tokens'] ? Number(flags['input-tokens']) : undefined,
|
|
16176
|
+
outputTokens: action === 'cost' && flags['output-tokens'] ? Number(flags['output-tokens']) : undefined,
|
|
16177
|
+
model: action === 'cost' ? (flags.model || undefined) : undefined,
|
|
16178
|
+
provider: action === 'cost' ? (flags.provider || undefined) : undefined,
|
|
16179
|
+
costRunId: action === 'cost' ? (flags['cost-run-id'] || undefined) : undefined,
|
|
16180
|
+
// The revision a call acts on or a decision looked at: a patch digest, a staged snapshot, a version.
|
|
16181
|
+
revision: flags.revision || undefined,
|
|
16182
|
+
note: flags.note,
|
|
16183
|
+
// Handoff: who should take the next step, and whether this agent must be kept off it (independence).
|
|
16184
|
+
exclude: flagEnabled(flags.exclude) ? true : undefined,
|
|
16185
|
+
reviewAction: flags['review-action'] || flags.review || undefined,
|
|
16186
|
+
findings: flags.findings ? String(flags.findings).split('|').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
16187
|
+
// `complete` after new input arrived during this run: say you read it.
|
|
16188
|
+
acknowledgeInput: flagEnabled(flags['acknowledge-input']) ? true : undefined,
|
|
16189
|
+
// `deliver`: bind the request to one conversation entry, and hold it for a person's approval.
|
|
16190
|
+
messageId: flags['message-id'] || undefined,
|
|
16191
|
+
requireApproval: flagEnabled(flags['require-approval']) ? true : undefined,
|
|
15993
16192
|
// Lease operations act as the AGENT, not the user: the lease is held by a worker session, and a
|
|
15994
16193
|
// human's email is not a thing that can hold one. Inside a worker these come from the environment
|
|
15995
16194
|
// the supervisor set, so the commands need no flags.
|
|
@@ -16688,9 +16887,35 @@ function printTicketHelp() {
|
|
|
16688
16887
|
console.log(' accept Claim it as yours before you start work.');
|
|
16689
16888
|
console.log(' status --status in_progress|pending_review');
|
|
16690
16889
|
console.log(' progress --summary "..." [--category investigation|fix|verification|...] [--next-step "..."]');
|
|
16691
|
-
console.log(' complete --resolution "what you found and did"');
|
|
16692
|
-
console.log('
|
|
16693
|
-
console.log('
|
|
16890
|
+
console.log(' complete --resolution "what you found and did" [--revision R] [--acknowledge-input]');
|
|
16891
|
+
console.log(' Refused when new input arrived after your run started and you have not read it;');
|
|
16892
|
+
console.log(' read the ticket, then pass --acknowledge-input if your resolution still stands.');
|
|
16893
|
+
console.log(' ask --question "..." [--context "..."] [--to WHO] [--visibility internal]');
|
|
16894
|
+
console.log(' Put a question to a human and park. Use it when the CHOICE is not yours. Recorded as');
|
|
16895
|
+
console.log(' request R<n>; answering it resumes the work even if no worker is online right now.');
|
|
16896
|
+
console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--min-approvals N] [--summary "..."]');
|
|
16897
|
+
console.log(' Ask a PERSON to authorize one action on one revision (promote a patch, send a message).');
|
|
16898
|
+
console.log(' An answer is not an approval: only an authenticated person can decide it, never an');
|
|
16899
|
+
console.log(' agent run, and a changed revision needs a new approval.');
|
|
16900
|
+
console.log(' request-review --for ACTION --revision R [--reviewers ...]');
|
|
16901
|
+
console.log(' Ask another worker or a person to review exactly this revision. You cannot review your own.');
|
|
16902
|
+
console.log(' decide --request R2 --decision approve|reject|changes [--revision R] [--note "..."]');
|
|
16903
|
+
console.log(' Decide an approval or review. Name the revision you looked at; a stale one is refused.');
|
|
16904
|
+
console.log(' cancel-request --request R2 [--reason "..."]');
|
|
16905
|
+
console.log(' handoff --summary "what the next worker should do" [--workstream review] [--exclude]');
|
|
16906
|
+
console.log(' [--review-action promote --revision R] [--next-step "..."] [--findings "a|b"]');
|
|
16907
|
+
console.log(' Hand the next step to another session (queued if nobody is online). --exclude keeps');
|
|
16908
|
+
console.log(' YOU off it — use it for independent review. Your exit will not erase this handoff.');
|
|
16909
|
+
console.log(' continue [--reason requested] Queue the ticket for the automation to work again.');
|
|
16910
|
+
console.log(' reset-budget OPERATOR: restore a ticket\'s run and spend budget after looking at it.');
|
|
16911
|
+
console.log(' wake --in MINUTES | --at ISO-TIME [--reason "..."] | wake --cancel');
|
|
16912
|
+
console.log(' A durable timer: when it is due the ticket is runnable work again (reason `timer`) and');
|
|
16913
|
+
console.log(' the run that takes it is told why. Use it for "look again after X", never sleep-and-poll.');
|
|
16914
|
+
console.log(' depends-on --tickets 101,102 This ticket waits on those; the sweep will not start it until');
|
|
16915
|
+
console.log(' every one is resolved or closed. `--tickets` with no value clears it.');
|
|
16916
|
+
console.log(' cost --usd 1.25 [--input-tokens N] [--output-tokens N] [--model M] [--cost-run-id R]');
|
|
16917
|
+
console.log(' Record what a run cost. One report per run (a second replaces the first); the account\'s');
|
|
16918
|
+
console.log(' supportRouting.maxTicketSpendUsd then stops automatic starts.');
|
|
16694
16919
|
console.log(' answer --message "..." [--route false] (alias: reply)');
|
|
16695
16920
|
console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
|
|
16696
16921
|
console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
|
|
@@ -16699,11 +16924,13 @@ function printTicketHelp() {
|
|
|
16699
16924
|
console.log(' message --message "..." [--to a@x,b@y] [--subject "..."] [--channel email]');
|
|
16700
16925
|
console.log(' A PUBLIC conversation entry the requester reads. Writing it does not send it:');
|
|
16701
16926
|
console.log(' the record is vendor-agnostic and the account\'s own rule chooses the transport.');
|
|
16702
|
-
console.log(' deliver --channel email --to a@x [--
|
|
16927
|
+
console.log(' deliver --channel email --to a@x [--message-id M] [--require-approval] [--subject "..."] [--idempotency-key K]');
|
|
16703
16928
|
console.log(' Ask for that entry to actually be SENT. It records an outbox request; an account-owned');
|
|
16704
16929
|
console.log(' rule or scheduled Action delivers it and marks the result. Use it when a ticket is');
|
|
16705
16930
|
console.log(' waiting on somebody who is not watching the queue.');
|
|
16706
16931
|
console.log(' deliveries [--channel email] What is queued to go out, and what already went.');
|
|
16932
|
+
console.log(' Senders take a delivery exclusively (claimDelivery) before sending; a failure retries');
|
|
16933
|
+
console.log(' with backoff; an "uncertain" or "blocked" one waits for a person to reconcile it.');
|
|
16707
16934
|
console.log(' delivered --delivery-id ID | delivery-failed --delivery-id ID --reason "..."');
|
|
16708
16935
|
console.log(' release [--notes "..."] Hand it back to the queue.');
|
|
16709
16936
|
console.log(' reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply]');
|
|
@@ -18730,6 +18957,7 @@ async function main() {
|
|
|
18730
18957
|
migrateSessionIfNeeded();
|
|
18731
18958
|
const originalArgv = process.argv.slice(2);
|
|
18732
18959
|
const args = parseArgs(originalArgv);
|
|
18960
|
+
requestIdentityFlags = args;
|
|
18733
18961
|
configureLocalActor(args);
|
|
18734
18962
|
const [command, subcommand] = args._;
|
|
18735
18963
|
const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
|
|
@@ -18745,7 +18973,7 @@ async function main() {
|
|
|
18745
18973
|
requireAccountRepoContext(process.cwd(), flags, 'ai ' + subcommand, context);
|
|
18746
18974
|
await localAi.command({
|
|
18747
18975
|
subcommand, flags, context, root: SESSION_DIR, axios, Client, WebSocket,
|
|
18748
|
-
defaultAgent: getPreferredAgent()
|
|
18976
|
+
defaultAgent: getPreferredAgent(), requestHeaders: cliRequestHeaders
|
|
18749
18977
|
});
|
|
18750
18978
|
return;
|
|
18751
18979
|
}
|
|
@@ -18804,7 +19032,9 @@ async function main() {
|
|
|
18804
19032
|
|
|
18805
19033
|
if (!command || command === 'help' || command === '--help') {
|
|
18806
19034
|
console.log('Usage: remits-cli <command>');
|
|
18807
|
-
console.log('Global options: --no-auto-update, --local-agent NAME');
|
|
19035
|
+
console.log('Global options: --no-auto-update, --local-agent NAME, --agent-id ID');
|
|
19036
|
+
console.log(' --agent-id / REMITS_AGENT_ID declares the agent acting for the authenticated user on CLI requests.');
|
|
19037
|
+
console.log(' --local-agent names only the local log folder; it does not declare an agent to the platform.');
|
|
18808
19038
|
console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
|
|
18809
19039
|
console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
|
|
18810
19040
|
console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
|
package/lib/local-ai.js
CHANGED
|
@@ -525,7 +525,7 @@ function help() {
|
|
|
525
525
|
console.log('Decisions: {"content":"reply","toolCalls":[]} or {"content":null,"toolCalls":[{"name":"tool","arguments":{}}]}');
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
async function command({ subcommand, flags, context, root, axios, Client, WebSocket, defaultAgent = 'claude' }) {
|
|
528
|
+
async function command({ subcommand, flags, context, root, axios, Client, WebSocket, defaultAgent = 'claude', requestHeaders }) {
|
|
529
529
|
if (!subcommand || flags.help || subcommand === 'help') return help();
|
|
530
530
|
if (!['serve', 'workers', 'work', 'request', 'respond', 'status', 'sessions', 'release'].includes(subcommand)) throw new Error('Unknown ai subcommand');
|
|
531
531
|
const agent = flags.agent || defaultAgent;
|
|
@@ -545,7 +545,7 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
545
545
|
try {
|
|
546
546
|
const result = await axios.post(baseUrl.replace(/\/+$/, '') + '/cli/ai',
|
|
547
547
|
{ ...payload, command: action, workerId: worker.workerId, accountId, dataMode },
|
|
548
|
-
{ headers: { Authorization: 'Bearer ' + session.token }, timeout: 15000, maxContentLength: MAX_BYTES, maxBodyLength: MAX_BYTES });
|
|
548
|
+
{ headers: requestHeaders ? requestHeaders(session.token) : { Authorization: 'Bearer ' + session.token }, timeout: 15000, maxContentLength: MAX_BYTES, maxBodyLength: MAX_BYTES });
|
|
549
549
|
if (!result.data?.success) throw new Error(result.data?.message || 'Local AI command failed');
|
|
550
550
|
return result.data;
|
|
551
551
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -26,6 +26,7 @@ independent of everything else — no background service is required, and every
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
remits-cli agent serve # routable AND autonomously working tickets — the usual choice
|
|
29
|
+
remits-cli agent serve --accounts 33 # ...bounded to account 33 and its descendants
|
|
29
30
|
remits-cli agent workers # what this session is running right now
|
|
30
31
|
remits-cli agent list # who else is available across the accounts you cover
|
|
31
32
|
remits-cli agent release # stop serving and stop receiving tickets right now
|
|
@@ -105,9 +106,26 @@ review.
|
|
|
105
106
|
|
|
106
107
|
### Resolving which agent a command means
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
For agent lifecycle commands, explicit `--agent-id` or `REMITS_AGENT_ID` wins; otherwise the CLI looks
|
|
110
|
+
for the registration anchored to this terminal's owning process. It never borrows another terminal's
|
|
111
|
+
registration just because it shares the checkout.
|
|
112
|
+
|
|
113
|
+
For platform request attribution, set `REMITS_AGENT_ID` for the agent's shell or pass `--agent-id` on
|
|
114
|
+
each command. Both reach the shared HTTP client, including status, tools and local-AI requests.
|
|
115
|
+
Supervised ticket workers receive `REMITS_AGENT_ID`, `REMITS_WORKER_RUN_ID` and
|
|
116
|
+
`REMITS_SUPPORT_TICKET_ID` automatically. A manually named interactive agent does not need to register
|
|
117
|
+
for ticket routing just to declare its identity. Registered roles and labels are accepted only from a
|
|
118
|
+
registration owned by the authenticated user. `--local-agent` / `REMITS_LOCAL_AGENT_ID` remains a local
|
|
119
|
+
folder choice and does not declare platform agent identity.
|
|
120
|
+
|
|
121
|
+
System → Activity groups these requests under the authenticated user, with separate agent/run sessions
|
|
122
|
+
and source filters. Undeclared CLI requests are labeled **CLI · no declared agent**, never assumed to be
|
|
123
|
+
human. Browser sessions can also be automated. Only platform requests are observed, not local editing
|
|
124
|
+
or reasoning between commands. Creating a preview token does not mark every subsequent browser user
|
|
125
|
+
of that token as an agent.
|
|
126
|
+
|
|
127
|
+
`ticket route --agent <target>` names the destination. Its legacy `--agent-id` alias also names the
|
|
128
|
+
destination, not the caller; use `REMITS_AGENT_ID` to identify the agent making a routing request.
|
|
111
129
|
|
|
112
130
|
### Which agent gets a ticket
|
|
113
131
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [Investigation Strategy](#investigation-strategy)
|
|
21
21
|
- [Presenting Findings](#presenting-findings)
|
|
22
22
|
- [Verifying a Production Issue Fix](#verifying-a-production-issue-fix)
|
|
23
|
+
- [Missing or deleted business documents](#missing-or-deleted-business-documents)
|
|
23
24
|
|
|
24
25
|
## The Investigation Model
|
|
25
26
|
|
|
@@ -281,3 +282,23 @@ When a bug is reported from production, use this pattern:
|
|
|
281
282
|
6. If useful, create or update a dedicated embeddable "playground" in test mode to reproduce the scenario in a controlled way.
|
|
282
283
|
|
|
283
284
|
Do not move production customer data into another account's test collection as a routine verification strategy. If you cannot verify with a Test component, Playwright flow, or controlled test-mode embeddable, explain the gap clearly instead of improvising with live production validation.
|
|
285
|
+
|
|
286
|
+
## Missing or deleted business documents
|
|
287
|
+
|
|
288
|
+
`PROCESSED` is the MySQL object's processing outcome, not a guarantee that its Firestore document
|
|
289
|
+
still exists. Read `documentEvidence` from `mcp_object_activity` or a current object `mcp_record_view`:
|
|
290
|
+
it includes the resolved path, observed data lane, presence, recent revision metadata, and the latest
|
|
291
|
+
Object `Document Delete` receipt when available. `state` is `present`, `missing`, `deleted`, `unknown`
|
|
292
|
+
(read failed), or `out_of_scope`. `deleted` requires retained deletion evidence; absence alone is not proof.
|
|
293
|
+
A `laneMismatch` means the requested read lane differs from the object's persisted lane.
|
|
294
|
+
|
|
295
|
+
`Document Delete` ObjectLog entries expose structured `documentOperation` (outcome, document path,
|
|
296
|
+
user/agent, component/action, node, data lane, scope, run and trace ids). The operation's
|
|
297
|
+
`threadGroupingId` can differ from `request.traceId`; both are retained for correlation. Failed deletes
|
|
298
|
+
never generate success revisions or downstream deletion messages. The processing status remains intact.
|
|
299
|
+
|
|
300
|
+
To read retained history without a live parent document, call `mcp_firestore_search` with one
|
|
301
|
+
`targetAccountId`, `collection`, `documentId`, and `includeHistory: true`. Use `historyLimit` (default 10,
|
|
302
|
+
maximum 100), `includeRevisionSnapshots: true` for the pre-delete business snapshot, or `revisionId`
|
|
303
|
+
for one exact revision. History is explicitly truncated when more rows exist. Legacy revisions may
|
|
304
|
+
identify the user and time without the newer component/request provenance; do not invent missing facts.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- [You are an agent, and you register yourself](#you-are-an-agent-and-you-register-yourself)
|
|
13
13
|
- [Autonomous: one ticket, one process](#autonomous-one-ticket-one-process)
|
|
14
14
|
- [Serving only some workstreams](#serving-only-some-workstreams)
|
|
15
|
+
- [Serving only some accounts](#serving-only-some-accounts)
|
|
15
16
|
- [The manual loop](#the-manual-loop)
|
|
16
17
|
- [If you are the worker](#if-you-are-the-worker)
|
|
17
18
|
- [Before you edit anything: where you are, and whether you may](#before-you-edit-anything-where-you-are-and-whether-you-may)
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
- [Agent components are workers too](#agent-components-are-workers-too)
|
|
25
26
|
- [Moving a ticket through its lifecycle](#moving-a-ticket-through-its-lifecycle)
|
|
26
27
|
- [When a worker needs a decision from a human](#when-a-worker-needs-a-decision-from-a-human)
|
|
28
|
+
- [Approvals, reviews and handing work on](#approvals-reviews-and-handing-work-on)
|
|
29
|
+
- [Coming back later, waiting on other tickets, and cost](#coming-back-later-waiting-on-other-tickets-and-cost)
|
|
27
30
|
|
|
28
31
|
## Support Ticket Mental Model
|
|
29
32
|
|
|
@@ -155,6 +158,22 @@ than quietly overriding the declaration —
|
|
|
155
158
|
**So after specialising a fleet, run `remits-cli agent map`** and confirm every workstream the
|
|
156
159
|
account ingests is covered by something.
|
|
157
160
|
|
|
161
|
+
### Serving only some accounts
|
|
162
|
+
|
|
163
|
+
Without a bound, a session claims **every account repository indexed on this machine**. `--accounts`
|
|
164
|
+
bounds it to named roots and their descendants:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
remits-cli agent serve --accounts 33 # account 33 and everything beneath it
|
|
168
|
+
remits-cli agent serve --accounts 33,52 --no-descendants
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`--account-id` only chooses which session context a command uses; it is not coverage. The registration
|
|
172
|
+
output prints the coverage the platform **accepted** — check it before leaving a production supervisor
|
|
173
|
+
running. The bound survives heartbeat re-registration. Only use it when the user asked for a bounded
|
|
174
|
+
fleet member, and never register a production worker merely to look at configuration: registering makes
|
|
175
|
+
the session routable.
|
|
176
|
+
|
|
158
177
|
### The manual loop
|
|
159
178
|
|
|
160
179
|
Only when the session registered with `agent register` rather than `agent serve`.
|
|
@@ -542,6 +561,57 @@ Inside an autonomous worker `--ticket` defaults to the ticket that worker was la
|
|
|
542
561
|
A refusal is an **answer**: "accept it first", "already assigned to someone else", "reopen it
|
|
543
562
|
first". Act on the sentence — do not go looking for another tool that lets you skip the step.
|
|
544
563
|
|
|
564
|
+
An answer resumes the work **even if no worker is online when it arrives**: the ticket records queued work
|
|
565
|
+
(`pendingWork`) and the next eligible worker takes it, whatever its status and owner. Each question is a
|
|
566
|
+
request with a short id (`R1`); answer a specific one with `ticket answer --request R1 --message "..."`.
|
|
567
|
+
If new input arrives while you are working, `ticket complete` is refused and quotes it — read it, then
|
|
568
|
+
complete with `--acknowledge-input` if your resolution still stands.
|
|
569
|
+
|
|
570
|
+
### Approvals, reviews and handing work on
|
|
571
|
+
|
|
572
|
+
An answer is not an authorization. When a person must **authorize** something — promoting a patch, a
|
|
573
|
+
production data change, sending a sensitive message — request an approval bound to the exact revision:
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
remits-cli ticket request-approval --for promote --revision <patch-digest> --summary "what and why"
|
|
577
|
+
remits-cli ticket request-approval --for deliver --delivery <delivery-id> # approve exactly that message
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
You cannot decide it — a person does, with `remits-cli ticket decide --request R2 --decision approve
|
|
581
|
+
--revision <rev>`. A changed patch needs a new approval. Record verification against the revision with
|
|
582
|
+
`ticket progress --category verification --revision <rev>`; an account rule may require exactly that.
|
|
583
|
+
|
|
584
|
+
When somebody else should do the next step — independent review, a different workstream:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
remits-cli ticket handoff --summary "fix staged in ticket-19463; needs independent review" \
|
|
588
|
+
--workstream review --exclude --review-action promote --revision <patch-digest> \
|
|
589
|
+
--next-step "review the patch against the reported case"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
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. A reviewer decides with `ticket decide --request R3 --decision
|
|
594
|
+
approve|changes|reject --revision <rev>`; "changes" sends the work back with the reviewer's note.
|
|
595
|
+
|
|
596
|
+
An approval or review can need several people: `request-approval ... --min-approvals 2` stays open until two
|
|
597
|
+
different eligible deciders approve; one rejection settles it.
|
|
598
|
+
|
|
599
|
+
### Coming back later, waiting on other tickets, and cost
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
remits-cli ticket wake --in 1440 --reason "requester promised logs tomorrow" # or --at <ISO time>; --cancel
|
|
603
|
+
remits-cli ticket depends-on --tickets 19463,19470 # no value clears
|
|
604
|
+
remits-cli ticket cost --usd 1.25 # usually reported for you
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- **`wake` instead of waiting.** Never sleep or poll inside a run. Record what you are waiting for, set a
|
|
608
|
+
wake-up, and finish the run; when the time comes a fresh worker gets the ticket with `resumed for: timer`
|
|
609
|
+
and your reason in its brief.
|
|
610
|
+
- **`depends-on`** when this ticket genuinely cannot start until others are resolved. The sweep holds it
|
|
611
|
+
until they are; nothing else has to remember to release it.
|
|
612
|
+
- **Cost** is reported automatically by `agent serve` from each worker's transcript. An account can cap a
|
|
613
|
+
ticket's spend; when it is reached, starts are refused until a person resets the budget.
|
|
614
|
+
|
|
545
615
|
**`mcp_support_ticket` is an account convention, not a platform guarantee.** It belongs to one
|
|
546
616
|
particular System Account: it may not exist on the platform you are on, it scopes to its own
|
|
547
617
|
account, and it will refuse a ticket owned by a different one. Use it only when you are working in
|
|
@@ -141,6 +141,28 @@ The account-graph and user surface: the middle ground between `account-info.json
|
|
|
141
141
|
compactly, because it is read into your context every session) and `mcp_account_view` (everything about one
|
|
142
142
|
account).
|
|
143
143
|
|
|
144
|
+
**Recurring schedules:** `action_schedules` lists the target account's Action schedules;
|
|
145
|
+
`action_schedule_update` upserts by `targetAccountId` + `actionId` (or an account-scoped `scheduleId`);
|
|
146
|
+
`action_schedule_delete` removes that schedule and its cloud job. Updates accept `cron`, `timeZone`,
|
|
147
|
+
`enabled` and `dryRun`. `enabled:false` pauses; `enabled:true` resumes while preserving omitted settings.
|
|
148
|
+
`regression_schedule` reads the per-account regression settings and latest report;
|
|
149
|
+
`regression_schedule_update` accepts the same scheduling fields plus `resultAction`, `suiteConcurrency`
|
|
150
|
+
(1–16) and explicit `ownerUserId`. Existing owners are preserved. Only admins may assign another owner;
|
|
151
|
+
an eligible caller may take ownership themselves.
|
|
152
|
+
|
|
153
|
+
Both use five-field cron and IANA time zones, default UTC. Existing Action schedules retain UTC.
|
|
154
|
+
All writes require production mode; `dryRun:true` validates from either lane without writing or creating
|
|
155
|
+
a cloud job. Action jobs resolve committed enabled `job:true` Actions on the target account; regression
|
|
156
|
+
suites run committed source in test data. Optional `includeSchedulerStatus:true` on reads reports cloud
|
|
157
|
+
registration/next delivery/last transport status; this does not prove Action or regression success.
|
|
158
|
+
An empty regression update does not repair registration: explicitly pass cron/timeZone/enabled.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
remits-cli tool --name mcp_account_user_admin --data-mode test --input '{"action":"action_schedule_update","targetAccountId":4655,"actionId":21,"cron":"0 1 * * *","timeZone":"America/New_York","enabled":true,"dryRun":true}'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Inspect the preview, then omit `dryRun` and use `--data-mode prod` when authorized to save the schedule.
|
|
165
|
+
|
|
144
166
|
- `action: 'hierarchy'` (default) — the descendant tree trimmed to `depth` (1-10, default 2; nodes cut off
|
|
145
167
|
are marked `truncated` and still report their child count) and/or the anchored ancestor chain plus every
|
|
146
168
|
edge (`direction: 'down'|'up'|'both'`). Nodes include `testAccount`. **This is how you get the deep tree
|
|
@@ -299,6 +321,11 @@ evidence at all, is in `investigation.md` → *HTTP audits*.
|
|
|
299
321
|
remits-cli tool --name "mcp_firestore_search" --input '{"accountId": 37, "collection": "http-audits/http-audits-2026-05/entries", "filters": [{"field": "direction", "operation": "EQUALS", "value": "OUTBOUND"}, {"field": "request.path", "operation": "EQUALS", "value": "/api/orders"}, {"field": "success", "operation": "EQUALS", "value": false}], "sort": [{"field": "timestamp", "direction": "DESC"}], "pagination": {"limit": 10}}' --data-mode prod
|
|
300
322
|
```
|
|
301
323
|
|
|
324
|
+
Exact-account history reads also accept `includeHistory` (boolean), `historyLimit` (1–100, default 10),
|
|
325
|
+
`includeRevisionSnapshots` (boolean, default false), and `revisionId` (exact Firestore revision id).
|
|
326
|
+
These require one `targetAccountId`, a collection name and `documentId`. They return `documentEvidence`
|
|
327
|
+
even when the live document is deleted. A normal exact-id miss also returns bounded evidence.
|
|
328
|
+
|
|
302
329
|
### `mcp_firestore_patch`
|
|
303
330
|
Guarded exact-document Firestore patch tool for bounded repairs. It defaults to `dryRun:true` and refuses broad
|
|
304
331
|
updates, wildcard collections, delete/remove operations, protected identity fields, and `_lastModified*` audit
|
|
@@ -329,7 +356,9 @@ Response fields include `dataMode`, `accountId`, `collection`, `documentId`, `dr
|
|
|
329
356
|
`diff`. Switch to `"dryRun":false` only after the diff and preconditions are exactly what you intended.
|
|
330
357
|
|
|
331
358
|
### `mcp_object_activity`
|
|
332
|
-
Object lifecycle timeline — metadata + recent activity.
|
|
359
|
+
Object lifecycle timeline — metadata + recent activity. Object metadata includes `documentEvidence`
|
|
360
|
+
with path, current presence/data lane, recent revisions and latest deletion receipt. Document Delete
|
|
361
|
+
activity entries include structured `documentOperation` evidence; PROCESSED is independent of document presence.
|
|
333
362
|
|
|
334
363
|
| Parameter | Required | Description |
|
|
335
364
|
|-----------|----------|-------------|
|