@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 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
- - L126 Sessions, Account Resolution, And Production Guards
8
- - L632 Local State, Workspaces, And Verification Evidence
9
- - L2670 Account Repos, Guide Sync, And Platform Repo
10
- - L3569 Component Discovery And HTTP Logging
11
- - L4339 Skill Delivery And TOC Resolution
12
- - L4654 Auth And Component Staging
13
- - L5286 Component Summaries, Status, And Sync Gates
14
- - L7610 Branches, Promotion, Commit, And Test Runs
15
- - L9873 Tokens, Tools, Verification, And Config
16
- - L11390 Service Dashboard And WebSocket Listener
17
- - L13900 Agent And Ticket Workflows
18
- - L18202 Help, Auto Update, And Command Dispatch
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 buildAxios(baseUrl, token, timeoutMs = 60000) {
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
- if (process.env.REMITS_AGENT_ID) headers['X-Remits-Agent-Id'] = process.env.REMITS_AGENT_ID;
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 axios.create({ baseURL: baseUrl, timeout: parsePositiveInt(timeoutMs, 60000), headers });
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
- /** Every account repo checked out on this machine, as registration claims. The platform verifies them. */
14617
- function agentAccountClaims(cwd) {
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(' ask --question "..." [--context "..."] [--to WHO]');
16693
- console.log(' Put a decision to a human and park. Use it when the CHOICE is not yours.');
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 [--template T] [--subject "..."] [--idempotency-key K]');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.166",
3
+ "version": "0.1.168",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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
- Order: `--agent-id`, then `REMITS_AGENT_ID`, then the single agent registered for this working
109
- directory. With several registered and no way to tell them apart, the command **fails and lists the
110
- candidates** rather than guessing — routing work to the wrong tab is silent, and a message is not.
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
  |-----------|----------|-------------|