@remits/remits-cli 0.1.167 → 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
- - L18308 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';
@@ -15234,6 +15250,84 @@ async function postWorkerSummary(context, agentId, ticket, summaryFile, options
15234
15250
  }
15235
15251
  }
15236
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
+
15237
15331
  /** Drop a claim on the platform. Best-effort: the claim expires on its own if this never lands. */
15238
15332
  async function releaseTicketClaim(context, agentId, ticket, options = {}) {
15239
15333
  try {
@@ -15517,6 +15611,8 @@ async function spawnTicketWorker(context, ctx, ticket) {
15517
15611
  // zero having done nothing. So the TICKET decides — unclaim always, and only give up (release
15518
15612
  // it back to the queue) once it has genuinely failed enough times and is still not terminal.
15519
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);
15520
15616
  const result = await releaseTicketClaim(context, agentId, { ticketId, accountId: ticket.accountId }, {
15521
15617
  runId: ctx.runId,
15522
15618
  outcome: ok ? 'completed' : (signal ? 'killed' : 'failed'),
@@ -16063,7 +16159,24 @@ async function ticketCommand(flags, subcommand) {
16063
16159
  approvers: flags.approvers ? String(flags.approvers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
16064
16160
  reviewers: flags.reviewers ? String(flags.reviewers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
16065
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,
16066
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,
16067
16180
  // The revision a call acts on or a decision looked at: a patch digest, a staged snapshot, a version.
16068
16181
  revision: flags.revision || undefined,
16069
16182
  note: flags.note,
@@ -16780,7 +16893,7 @@ function printTicketHelp() {
16780
16893
  console.log(' ask --question "..." [--context "..."] [--to WHO] [--visibility internal]');
16781
16894
  console.log(' Put a question to a human and park. Use it when the CHOICE is not yours. Recorded as');
16782
16895
  console.log(' request R<n>; answering it resumes the work even if no worker is online right now.');
16783
- console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--summary "..."]');
16896
+ console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--min-approvals N] [--summary "..."]');
16784
16897
  console.log(' Ask a PERSON to authorize one action on one revision (promote a patch, send a message).');
16785
16898
  console.log(' An answer is not an approval: only an authenticated person can decide it, never an');
16786
16899
  console.log(' agent run, and a changed revision needs a new approval.');
@@ -16794,7 +16907,15 @@ function printTicketHelp() {
16794
16907
  console.log(' Hand the next step to another session (queued if nobody is online). --exclude keeps');
16795
16908
  console.log(' YOU off it — use it for independent review. Your exit will not erase this handoff.');
16796
16909
  console.log(' continue [--reason requested] Queue the ticket for the automation to work again.');
16797
- console.log(' reset-budget OPERATOR: restore a ticket\'s run budget after looking at it.');
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.');
16798
16919
  console.log(' answer --message "..." [--route false] (alias: reply)');
16799
16920
  console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
16800
16921
  console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
@@ -18836,6 +18957,7 @@ async function main() {
18836
18957
  migrateSessionIfNeeded();
18837
18958
  const originalArgv = process.argv.slice(2);
18838
18959
  const args = parseArgs(originalArgv);
18960
+ requestIdentityFlags = args;
18839
18961
  configureLocalActor(args);
18840
18962
  const [command, subcommand] = args._;
18841
18963
  const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
@@ -18851,7 +18973,7 @@ async function main() {
18851
18973
  requireAccountRepoContext(process.cwd(), flags, 'ai ' + subcommand, context);
18852
18974
  await localAi.command({
18853
18975
  subcommand, flags, context, root: SESSION_DIR, axios, Client, WebSocket,
18854
- defaultAgent: getPreferredAgent()
18976
+ defaultAgent: getPreferredAgent(), requestHeaders: cliRequestHeaders
18855
18977
  });
18856
18978
  return;
18857
18979
  }
@@ -18910,7 +19032,9 @@ async function main() {
18910
19032
 
18911
19033
  if (!command || command === 'help' || command === '--help') {
18912
19034
  console.log('Usage: remits-cli <command>');
18913
- 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.');
18914
19038
  console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
18915
19039
  console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
18916
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.167",
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,
@@ -106,9 +106,26 @@ review.
106
106
 
107
107
  ### Resolving which agent a command means
108
108
 
109
- Order: `--agent-id`, then `REMITS_AGENT_ID`, then the single agent registered for this working
110
- directory. With several registered and no way to tell them apart, the command **fails and lists the
111
- 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.
112
129
 
113
130
  ### Which agent gets a ticket
114
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.
@@ -26,6 +26,7 @@
26
26
  - [Moving a ticket through its lifecycle](#moving-a-ticket-through-its-lifecycle)
27
27
  - [When a worker needs a decision from a human](#when-a-worker-needs-a-decision-from-a-human)
28
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)
29
30
 
30
31
  ## Support Ticket Mental Model
31
32
 
@@ -592,6 +593,25 @@ The handoff is queued even if nobody is online, `--exclude` keeps you off it (in
592
593
  that), and your own exit will not undo it. A reviewer decides with `ticket decide --request R3 --decision
593
594
  approve|changes|reject --revision <rev>`; "changes" sends the work back with the reviewer's note.
594
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
+
595
615
  **`mcp_support_ticket` is an account convention, not a platform guarantee.** It belongs to one
596
616
  particular System Account: it may not exist on the platform you are on, it scopes to its own
597
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
  |-----------|----------|-------------|