@remits/remits-cli 0.1.99 → 0.1.100

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
@@ -34,6 +34,8 @@ remits-cli components sync --branch feature_branch --dry-run --summary
34
34
  remits-cli components sync --branch feature_branch --force-tombstones
35
35
  remits-cli token --path page/my-embeddable
36
36
  remits-cli token --path page/my-embeddable --variant-branch feature_branch
37
+ remits-cli token inspect --token https://example.test/s/<tokenKey>/page/my-embeddable
38
+ remits-cli tool --name mcp_account_user_admin --data-mode prod --input '{"action":"account_create","parentAccountId":4,"name":"Freto","type":"PRODUCT","dryRun":true}'
37
39
  remits-cli data-mode
38
40
  remits-cli data-mode set prod
39
41
  remits-cli sessions list
package/index.js CHANGED
@@ -278,9 +278,29 @@ function printResolvedBaseUrl(baseUrl) {
278
278
  console.log('Base URL:', normalizeBaseUrl(baseUrl || DEFAULT_BASE_URL));
279
279
  }
280
280
 
281
- // Tools whose name signals they mutate state rather than only reading it. Used solely to escalate the
282
- // prod banner from "reading live data" to "WRITING live data" — never to block or permit anything.
281
+ // Tools/actions whose names signal they mutate state rather than only reading it. Used solely to make
282
+ // data-lane banners precise — never to block or permit anything.
283
283
  const MUTATING_TOOL_PATTERN = /(^|_)(patch|edit|create|commit|update|delete|remove|write|set|assign|run|execute|send|pause|unpause|interrupt|restore|repair|migrate|sync)(_|$)/i;
284
+ const MUTATING_TOOL_ACTION_PATTERN = /(^|_)(patch|edit|create|commit|update|delete|remove|write|set|assign|run|execute|send|pause|unpause|interrupt|restore|repair|migrate|sync|add|upsert|reparent|structure|accept|complete|release|record|clear|retire|subscribe|unsubscribe|stop|cancel|kill)(_|$)/i;
285
+
286
+ function toolInputAction(input) {
287
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
288
+ const value = input.action || input.controlAction || input.command || input.mode;
289
+ if (value === undefined || value === null || value === true) return null;
290
+ const normalized = String(value).trim();
291
+ return normalized || null;
292
+ }
293
+
294
+ function isMutatingToolCall(toolName, input) {
295
+ if (MUTATING_TOOL_PATTERN.test(String(toolName || ''))) return true;
296
+ const action = toolInputAction(input);
297
+ return action ? MUTATING_TOOL_ACTION_PATTERN.test(action) : false;
298
+ }
299
+
300
+ function toolOperationLabel(toolName, input) {
301
+ const action = toolInputAction(input);
302
+ return 'tool ' + String(toolName) + (action ? ' action ' + action : '');
303
+ }
284
304
 
285
305
  function looksLikeDryRun(input) {
286
306
  if (!input || typeof input !== 'object') return false;
@@ -294,25 +314,29 @@ function isLocalBaseUrl(baseUrl) {
294
314
  }
295
315
 
296
316
  /**
297
- * One consistent banner across every surface that can touch production. Agents have repeatedly run a
298
- * prod write believing they were in test mode, so the banner is loud, states the account it resolved,
299
- * and distinguishes a live WRITE from a live read.
317
+ * One consistent banner across risky data-lane surfaces. Agents have repeatedly crossed prod/test
318
+ * boundaries by assumption, so the banner is loud, states the account it resolved, and distinguishes
319
+ * live production writes from isolated test-data writes.
300
320
  */
301
321
  function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false }) {
302
322
  const normalizedDataMode = String(dataMode || '').toLowerCase();
303
323
  const prodData = normalizedDataMode === 'prod';
304
324
  const prodHost = baseUrl && !isLocalBaseUrl(baseUrl);
305
- if (!prodData && !prodHost) return;
325
+ if (!prodData && !prodHost && !mutating) return;
306
326
  const bar = '='.repeat(72);
307
327
  let headline;
308
328
  if (prodData) {
309
329
  headline = dryRun
310
330
  ? 'PROD DATA — DRY RUN (no write will be attempted)'
311
331
  : (mutating ? 'PROD DATA WRITE — this runs against LIVE production data' : 'PROD DATA READ — this reads LIVE production data');
312
- } else {
332
+ } else if (prodHost) {
313
333
  headline = dryRun
314
334
  ? 'PROD HOST / TEST DATA — DRY RUN'
315
335
  : (mutating ? 'PROD HOST / TEST DATA WRITE — deployed app, isolated test data' : 'PROD HOST / TEST DATA READ — deployed app, isolated test data');
336
+ } else {
337
+ headline = dryRun
338
+ ? 'TEST DATA — DRY RUN (no write will be attempted)'
339
+ : 'TEST DATA WRITE — isolated test data';
316
340
  }
317
341
  console.log(bar);
318
342
  console.log(' ' + headline);
@@ -421,6 +445,17 @@ function sessionJsonlFile(cwd) {
421
445
  }
422
446
 
423
447
  const CONTENT_FIELDS = new Set(['source', 'html', 'javascript', 'css', 'schema', 'inputSchema', 'previewData', 'messages']);
448
+ const SECRET_LOG_FIELDS = new Set([
449
+ 'token',
450
+ 'tokeninput',
451
+ 'tokenkey',
452
+ 'authtoken',
453
+ 'xauthtoken',
454
+ 'x-auth-token',
455
+ 'remitstoken',
456
+ 'x-remits-token',
457
+ 'authorization'
458
+ ]);
424
459
 
425
460
  function sanitizeForLog(value) {
426
461
  if (value === null || value === undefined) {
@@ -434,7 +469,7 @@ function sanitizeForLog(value) {
434
469
  }
435
470
  const out = {};
436
471
  for (const [k, v] of Object.entries(value)) {
437
- if (k.toLowerCase() === 'token') {
472
+ if (SECRET_LOG_FIELDS.has(k.toLowerCase())) {
438
473
  out[k] = '[redacted]';
439
474
  } else if (CONTENT_FIELDS.has(k) && typeof v === 'string') {
440
475
  out[k] = '[' + v.length + ' chars]';
@@ -2777,6 +2812,12 @@ async function testCommand(flags) {
2777
2812
  }
2778
2813
 
2779
2814
  async function tokenCommand(flags) {
2815
+ const subcommand = flags._ && flags._[1];
2816
+ if (subcommand === 'inspect' || subcommand === 'details' || subcommand === 'decode') {
2817
+ await tokenInspectCommand(flags);
2818
+ return;
2819
+ }
2820
+
2780
2821
  const cwd = process.cwd();
2781
2822
  ensureLocalState(cwd);
2782
2823
  const sessionContext = resolveSessionContext(cwd, flags);
@@ -2829,6 +2870,35 @@ async function tokenCommand(flags) {
2829
2870
  console.log(JSON.stringify(output, null, 2));
2830
2871
  }
2831
2872
 
2873
+ async function tokenInspectCommand(flags) {
2874
+ const cwd = process.cwd();
2875
+ ensureLocalState(cwd);
2876
+ const sessionContext = resolveSessionContext(cwd, flags);
2877
+ const { session, accountId } = sessionContext;
2878
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
2879
+ const positional = flags._ && flags._[2];
2880
+ const tokenInput = flags.token || flags['token-key'] || flags.tokenKey || flags.value || flags.url || positional;
2881
+
2882
+ if (!tokenInput || tokenInput === true) {
2883
+ throw new Error('Token inspection requires --token <token|tokenKey|URL>, --token-key <key>, --url <URL>, or a positional value.');
2884
+ }
2885
+
2886
+ const api = buildAxios(baseUrl, session.token);
2887
+ const data = await loggedPost(api, cwd, '/cli/tokenInspect', {
2888
+ token: session.token,
2889
+ accountId,
2890
+ tokenInput
2891
+ }).then((r) => r.data);
2892
+
2893
+ if (!data.success) {
2894
+ throw new Error(data.message || 'Failed to inspect token');
2895
+ }
2896
+
2897
+ printSessionResolutionWarning(sessionContext);
2898
+ printResolvedBaseUrl(baseUrl);
2899
+ console.log(JSON.stringify(data, null, 2));
2900
+ }
2901
+
2832
2902
  async function toolsCommand(flags) {
2833
2903
  const cwd = process.cwd();
2834
2904
  ensureLocalState(cwd);
@@ -2957,8 +3027,8 @@ async function toolCommand(flags) {
2957
3027
  dataMode,
2958
3028
  accountId,
2959
3029
  baseUrl,
2960
- operation: 'tool ' + String(toolName),
2961
- mutating: MUTATING_TOOL_PATTERN.test(String(toolName)),
3030
+ operation: toolOperationLabel(toolName, input),
3031
+ mutating: isMutatingToolCall(toolName, input),
2962
3032
  dryRun: looksLikeDryRun(input)
2963
3033
  });
2964
3034
 
@@ -5524,8 +5594,10 @@ function printToolsHelp() {
5524
5594
 
5525
5595
  function printTokenHelp() {
5526
5596
  console.log('Usage: remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
5597
+ console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
5527
5598
  console.log('');
5528
5599
  console.log('Mints a branch-aware browser URL for embeddable verification.');
5600
+ console.log('Inspect decodes a persisted Remits token and prints token metadata, recognized routing fields, safety/dataMode evidence, and full context.');
5529
5601
  }
5530
5602
 
5531
5603
  async function main() {
@@ -5594,6 +5666,7 @@ async function main() {
5594
5666
  console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
5595
5667
  console.log(' remits-cli test run --test <id|name> [--base-url URL] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME]');
5596
5668
  console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
5669
+ console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
5597
5670
  console.log('');
5598
5671
  console.log(' --as-account <ID> verifies AS a descendant subscriber account, so its relationship edge');
5599
5672
  console.log(' selects the component branch and the run resolves exactly what production will.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -166,6 +166,31 @@ For access questions — "who can see this client account?", "why does this user
166
166
  `mcp_sql_query` only when you need raw join-table investigation. Remember a user's custom fields are stored
167
167
  **per bound account**, so the same person can differ per account.
168
168
 
169
+ **Test-data flags are first-class safety evidence.** `Account.testAccount` and `User.testUser` are set for
170
+ accounts/users created while the execution data lane is `test` (including `mcp_account_user_admin`
171
+ creation paths); prod-data creates leave them false. Updating an existing real account/user in test mode
172
+ does not convert it into test data, but its Firestore extension-field writes still go to the test lane.
173
+ `Object.testMode` / `Event.testMode` / `Alert.testMode` identify lifecycle rows in the test data lane.
174
+ Agent-facing surfaces expose these fields:
175
+
176
+ - `account-info.json`, `account-hierarchy.json`, and `mcp_account_view`: `resolution.testAccount` for the
177
+ described account, and `testAccount` on returned hierarchy nodes.
178
+ - `mcp_account_user_admin`: `testAccount` on `hierarchy`, `account`, and `account_create` results;
179
+ `testUser` on `users` / `user` results.
180
+ - `mcp_record_listing` / `mcp_record_view`: `testMode` on `object`, `event`, and `alert` records.
181
+ - `mcp_object_activity`: top-level `dataMode`, `object.testMode`, and `testMode` on Event/Alert
182
+ timeline entries.
183
+ - `mcp_event_diagnostics`: top-level `dataMode` and `result.event.testMode`.
184
+ - `remits-cli token inspect`: owner `account.testAccount` and `user.testUser`, plus token `dataMode`.
185
+
186
+ Persisted AI session rows currently do **not** store durable per-grouping `testMode` / `dataMode`; use
187
+ `mcp_ai_session_search.dataMode` only as the current tool execution lane, not as proof of the historical
188
+ grouping's data lane.
189
+
190
+ If any of those fields contradict the lane you intended, stop and rerun the command with an explicit
191
+ `--data-mode test` or `--data-mode prod`. Never infer prod/test from an account name, URL, branch name, or
192
+ the mere existence of a created account.
193
+
169
194
  Use `remits-cli` for everything else: staging changes, running tests, generating embeddable tokens,
170
195
  committing work, and diagnosing production issues.
171
196
 
@@ -400,6 +425,7 @@ Use `mcp_support_ticket` to manage lifecycle:
400
425
  - `complete` — resolve the ticket with a summary of what was done
401
426
  - `release` — unassign if you cannot continue
402
427
  - **Pulling a ticket by number is enough — you do NOT need to know its account first.** The `ticketId` IS the ticket's globally-unique anchor id, and `mcp_support_ticket` resolves the owning account from it for every action that operates on an existing ticket (`read`, `accept`, `update_status`, `complete`, `release`, `record_progress`, `add_artifact`, `get_attachment`). So when a user says "pull ticket 19463", just call `read` with `{ "action": "read", "ticketId": "19463" }` from any authenticated **prod** session (the ticket lives in prod data) — omit `accountId` entirely. The response returns the resolved `accountId`/`accountName` (and `implementationAccountId` when set); use those for any follow-up work. **Only `create` requires an explicit `accountId`** (a brand-new ticket has no anchor to resolve from). If a bare `ticketId` returns "Support ticket not found", double-check you are in `--data-mode prod`, then fall back to passing an explicit `accountId`.
428
+ - `read` can return `documentState:'missing_or_empty'` with `ticket.mirrorOnly:true` and `ticket.canMutate:false`. That means the support-ticket anchor exists and the queue row is real, but the backing Firestore `support_tickets/{ticketId}` document is missing or metadata-only. Treat this as a degraded ticket, not as "ticket not found"; run or request the System Account action `Restore Support Ticket Documents From Anchor Mirrors` for the owning account before lifecycle mutations. The restore recovers mirrored scalar fields, but document-only arrays such as alert snapshots, email threads, attachments, worklog entries, and artifacts may be lost.
403
429
  - If a ticket is part of the request, manage the lifecycle proactively. Do not wait for the human user to remind you to read, accept, update, complete, or release it.
404
430
 
405
431
  Ticket-routing context:
@@ -1602,15 +1628,16 @@ account).
1602
1628
 
1603
1629
  - `action: 'hierarchy'` (default) — the descendant tree trimmed to `depth` (1-10, default 2; nodes cut off
1604
1630
  are marked `truncated` and still report their child count) and/or the anchored ancestor chain plus every
1605
- edge (`direction: 'down'|'up'|'both'`). **This is how you get the deep tree account-info.json omits.**
1606
- - `action: 'account'` — one account's `resolution` block plus masked configuration fields, without paying
1607
- for the component inventory.
1631
+ edge (`direction: 'down'|'up'|'both'`). Nodes include `testAccount`. **This is how you get the deep tree
1632
+ account-info.json omits.**
1633
+ - `action: 'account'` — one account's `resolution` block, including `testAccount`, plus masked
1634
+ configuration fields, without paying for the component inventory.
1608
1635
  - `action: 'users'` — an account's users at a hierarchy `scope` (`self`/`children`/`parents`/`hierarchy`),
1609
- optional `email` substring filter. Extension fields are omitted here on purpose: they are stored **per
1610
- account** and these users are bound to their own.
1636
+ optional `email` substring filter. Rows include `testUser`. Extension fields are omitted here on purpose:
1637
+ they are stored **per account** and these users are bound to their own.
1611
1638
  - `action: 'user'` — one user (`userId` or `email`) with roles, account memberships, and extension fields
1612
- **correctly scoped to the requested account**. It never grants membership as a side effect of a read, and
1613
- tells you when the fields shown belong to a different account.
1639
+ **correctly scoped to the requested account**, plus `testUser`. It never grants membership as a side
1640
+ effect of a read, and tells you when the fields shown belong to a different account.
1614
1641
  - `action: 'account_update'` — write Account-schema configuration `fields`, and/or `name`/`status`
1615
1642
  (`ACTIVE`/`ON_HOLD`/`PENDING`).
1616
1643
  - `action: 'user_update'` — write User-schema `fields` under the named account, plus `name`/`enabled` and
@@ -1621,12 +1648,35 @@ account).
1621
1648
  **Building an account hierarchy** (the structural writes — this is how a coding agent provisions accounts
1622
1649
  without a browser):
1623
1650
 
1651
+ **Data-lane rule for provisioning:** `remits-cli tool` defaults to `--data-mode test`. That is correct for
1652
+ fixtures and rehearsals, but it means `mcp_account_user_admin` `action:'account_create'` creates test-lane
1653
+ accounts unless the command explicitly passes `--data-mode prod`. For real platform/product/customer
1654
+ provisioning, always dry-run in prod first, check the response's `dataMode`, then run the write in prod:
1655
+
1656
+ ```bash
1657
+ remits-cli tool --name mcp_account_user_admin --data-mode prod --input '{"action":"account_create","parentAccountId":4,"name":"Freto","type":"PRODUCT","dryRun":true}'
1658
+ ```
1659
+
1660
+ After the real write, verify the response (or re-read `action:'account'`) shows the command `dataMode` you
1661
+ intended and `testAccount:false` for real provisioning. `testAccount:true` means you created a test-data
1662
+ account, even if the name and structure look correct.
1663
+
1664
+ For a test rehearsal, make the opposite assertion explicit: the response should show `dataMode:'test'` and
1665
+ `testAccount:true` for created accounts (or `testUser:true` for created users). A false test flag in a
1666
+ test rehearsal means the data lane or the returned entity is not the one you intended.
1667
+
1668
+ Account/User schema `fields` are Firestore-backed extension fields. Their physical storage follows the
1669
+ same data lane as the tool call: `--data-mode test` writes under `testing/<resolvedDatabaseName>/...`, while
1670
+ `--data-mode prod` writes under `accounts/<resolvedDatabaseName>/...` (for modern segmented accounts). If a
1671
+ test-lane rehearsal should become real provisioning, rerun the create/update in prod mode; do not assume the
1672
+ test-lane Firestore fields moved.
1673
+
1624
1674
  - `action: 'account_create'` — create a child under `parentAccountId`, with its **primary relationship edge**,
1625
1675
  applying `type` / `databaseName` / `domainName` / `authPath` / `targetPath` / `code` /
1626
1676
  `repositoryNameOverride` / `branchName` / `editMode` / … **at birth**. That ordering matters: the storage
1627
1677
  namespace is resolved from those properties, and the parent's cascaded schema fields are written into it
1628
1678
  during creation. Idempotent — an existing same-name account under that parent comes back with
1629
- `reusedExisting: true`, unchanged.
1679
+ `reusedExisting: true`, unchanged. The response includes `testAccount`.
1630
1680
  - `action: 'account_structure'` — change those properties on an existing account, including account-level
1631
1681
  custom host, login path, and landing path.
1632
1682
  - `action: 'edge_add'` / `'edge_update'` / `'edge_remove'` — manage a membership `AccountRelationship` edge to
@@ -1752,8 +1802,11 @@ Object lifecycle timeline — metadata + recent activity.
1752
1802
  |-----------|----------|-------------|
1753
1803
  | `accountId` | yes | Account ID |
1754
1804
  | `objectId` | yes | Object ID (from `object_id` in documents) |
1805
+ | `dataMode` | no | Explicit lane: `prod` or `test`. Response echoes `dataMode`. |
1755
1806
  | `activityOptions` | no | `{limit, offset, types, start, end, order}`. Default: limit=5, order=desc. Types: `OBJECT_LOG`, `EVENT`, `ALERT`. |
1756
1807
 
1808
+ Response fields include `dataMode`, `object.testMode`, and `testMode` on Event/Alert timeline entries.
1809
+
1757
1810
  ### `mcp_record_listing`
1758
1811
  List and search lifecycle records when you do not already know the record ID.
1759
1812
 
@@ -1807,6 +1860,7 @@ Front-stage references:
1807
1860
  | Parameter | Required | Description |
1808
1861
  |-----------|----------|-------------|
1809
1862
  | `action` | no | `search` (default), `detail`, or session control `pause`/`unpause`/`interrupt` |
1863
+ | `dataMode` | no | Explicit execution lane: `prod` or `test`. Response echoes `dataMode`, but persisted groupings do not have durable per-row lane flags. |
1810
1864
  | `search` | no | Broad text match against session IDs and grouping IDs |
1811
1865
  | `sessionId` | no | Session ID filter in search mode, or grouping/session key in detail mode |
1812
1866
  | `groupingId` | no | Grouping ID filter in search mode, or grouping key in detail mode |
@@ -2191,6 +2245,7 @@ This keeps file retrieval self-contained — no separate download endpoint is ne
2191
2245
 
2192
2246
  **Recommended flow** — `accountId` is optional throughout; `ticketId` resolves the owning account:
2193
2247
  1. `read` — check ticket state and any attachments
2248
+ - If `read` returns `ticket.mirrorOnly:true`, use the mirrored fields for triage context, restore the backing document first, then claim/update/complete it.
2194
2249
  2. **`accept`** (with `assignee`) — this is a **hard precondition for `update_status`**, not just etiquette
2195
2250
  3. `get_attachment` if attachments are present and relevant to the investigation
2196
2251
  4. `update_status` — `in_progress` while working, `pending_review` when the fix is done but not yet deployed
@@ -2356,6 +2411,10 @@ optional class-histogram sampling, **no heap dump and no JFR**.
2356
2411
  List and filter tickets **across an account and its descendants** so you can choose what to work on. Lifecycle
2357
2412
  actions stay on `mcp_support_ticket`.
2358
2413
 
2414
+ Queue rows are built from support-ticket anchor mirrors by default. A row in this list means a
2415
+ support-ticket anchor exists; it does not guarantee the full Firestore document is healthy. Open the
2416
+ ticket with `mcp_support_ticket read` before lifecycle work and honor `mirrorOnly` / `documentState` if present.
2417
+
2359
2418
  | Parameter | Required | Description |
2360
2419
  |-----------|----------|-------------|
2361
2420
  | `action` | no | `list` (default) |
@@ -2544,6 +2603,7 @@ remits-cli components branch <name> --unsubscribe <accountId>
2544
2603
  remits-cli components branch <name> --retire [--force] # delete the branch's overlays
2545
2604
  remits-cli test run --test <id|name> [--names "a,b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
2546
2605
  remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
2606
+ remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
2547
2607
  remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
2548
2608
  remits-cli tool --name <toolName> [--branch <name>] [--input "{...}"] [--data-mode test|prod] [--variant-branch <name|none>] [--timeout-ms 60000] [--async true --wait true]
2549
2609
  remits-cli tool status --call-id <callId> [--data-mode test|prod]
@@ -2584,7 +2644,10 @@ For tests specifically:
2584
2644
 
2585
2645
  - Every command that can touch production (`tool`, `test run`, `components sync`) prints a `PROD DATA`
2586
2646
  banner naming the operation, the resolved account, and the host — and distinguishes a live **WRITE**
2587
- from a live **READ** and from a **DRY RUN**. If you see a WRITE banner you did not intend, stop.
2647
+ from a live **READ** and from a **DRY RUN**. `remits-cli tool` also prints an explicit **TEST DATA WRITE**
2648
+ banner for mutating tool calls in the test lane, including multi-action tools such as
2649
+ `mcp_account_user_admin` where the write is signaled by `input.action` (`account_create`, `user_update`,
2650
+ `edge_update`, etc.). If you see a WRITE banner you did not intend, stop.
2588
2651
  - A tool call that fails **in the platform runtime** rather than in the tool (Groovy reflective dispatch
2589
2652
  of a runtime-compiled component, an empty connection pool, a Redis reconnect, a lock-wait timeout)
2590
2653
  now comes back as HTTP `503` with `failureClass: "transient_infrastructure"` and `retryable: true`,
@@ -2612,6 +2675,7 @@ For tests specifically:
2612
2675
  | Staged change has no effect in a live (non-CLI) run | Staged overrides resolve only under a CLI TestMode (`branchName`+`cliUserId`). Live webhooks and other non-CLI runtime paths still use the DB (trunk, or the account's subscribed variant). `commit` to make it durable. See "Component Resolution". |
2613
2676
  | Need to know an account's shape (role, type, parents, namespace, branch, host/login routes) | Read `resolution` — from the repo's `account-info.json`, or `mcp_account_user_admin` `action:'account'` (cheap), or `mcp_account_view` (full inventory): `role`/`summary`, `type`, `resolvedDatabaseName`, `domainName`/`resolvedDomainName`, `authPath`/`targetPath`, `relationships`, `componentBranch`, plus top-level `componentBranches`. Never infer structure from the account's name. |
2614
2677
  | Need the account tree below an account, or its users | In a local repo, read `account-hierarchy.json` for the generated tree. For live data, use `mcp_account_user_admin` (`action:'hierarchy'` with a `depth`, or `action:'users'`). `account-info.json` deliberately omits the tree. |
2678
+ | Need to prove whether an account/user/record is test data | Check explicit flags: `resolution.testAccount` / hierarchy `testAccount`, `mcp_account_user_admin` `testAccount` / `testUser`, token inspect owner flags, and `mcp_record_listing` / `mcp_record_view` `testMode` for object/event/alert rows. Do not infer from names or branch labels. |
2615
2679
  | Need account configuration values | In a local repo, read `account-configurations.json`. For live data, use `mcp_account_user_admin` (`action:'account'`) or `mcp_account_view`. `account-info.json` deliberately omits configurations. |
2616
2680
  | An account has two parents and you don't know which one a run used | `resolution.relationships` lists every link with its own `branchName`/`databaseName`/`domainName`. A membership-only account with SEVERAL edges resolves **trunk and inherits nothing** until a path is named (`--as-account`, `--variant-branch`, or an edge host) — that is by design, not a bug. With exactly ONE membership edge it inherits normally, descendants included. |
2617
2681
  | Documents missing / written to the wrong place | Compare `resolution.databaseName` (the account's own override) with `resolution.resolvedDatabaseName` (what is actually in effect), and check for a `databaseName` on one of the `relationships` edges. Data does not inherit; components do. |