@remits/remits-cli 0.1.90 → 0.1.91

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
@@ -30,6 +30,7 @@ git commit -m "sync passing changes"
30
30
  git push
31
31
  remits-cli components sync
32
32
  remits-cli components sync --branch feature_branch --dry-run
33
+ remits-cli components sync --branch feature_branch --dry-run --summary
33
34
  remits-cli components sync --branch feature_branch --force-tombstones
34
35
  remits-cli token --path page/my-embeddable
35
36
  remits-cli token --path page/my-embeddable --variant-branch feature_branch
@@ -60,9 +61,9 @@ remits-cli install --skills --overwrite true
60
61
  - Schema `.meta.yml` sidecars can stage/sync the schema flags: `enableTrigger`, `enableFullText`, `enableRAG`, `enableRevisions`, `enableBigQuerySync`, `enableRules`, `anchor`, and `auxiliary`.
61
62
  - `components status` shows the branch/user staging entries that can shadow DB components during CLI-scoped test-mode execution.
62
63
  - `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.
63
- - `components stage`, `components status`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response.
64
+ - `components stage`, `components status`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response. `components stage` separates local working-tree component deltas from the full materialized staging cache count.
64
65
  - `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.
65
- - 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` 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. `--force-tombstones` is accepted only for non-trunk variant syncs and should be used only when missing trunk component files are intentional tombstone overrides.
66
+ - 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` 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.
66
67
  - `components commit` is a convenience wrapper that performs local git commit/push, then `components sync`, then local `git fetch`/`git pull --ff-only`.
67
68
  - `components sync` returns the post-sync branch SHA produced by the platform. `components commit` verifies that `origin/<branch>` and local `HEAD` both match that exact SHA after the final pull.
68
69
  - `components push` is deprecated and currently behaves the same as `components stage`.
@@ -79,6 +80,10 @@ remits-cli install --skills --overwrite true
79
80
  - Data mode defaults to `test`. For `remits-cli test run`, that default is now enforced even if the most-recent authenticated session for the account is `prod`; a production test run therefore requires an explicit `--data-mode prod` on the command line. Use `remits-cli data-mode set prod` only for production investigation.
80
81
  - If the same account is authenticated against more than one host and you omit `--base-url`, the CLI auto-resolves the best matching session and now prints the resolved host. Pass `--base-url` explicitly whenever the target host matters.
81
82
  - Avoid commas in individual test names. The `--names` filter is comma-delimited, so a single test case whose name contains commas cannot be targeted cleanly through `remits-cli test run --names ...`.
83
+ - Nested help is available before required-argument validation, including `remits-cli test run --help`, `remits-cli components sync --help`, and `remits-cli tool --help`.
84
+ - `components sync` has fail-closed safety gates for unattended/agent use. Each exits non-zero instead of printing a wall of JSON: `--changed-only` (fail unless every planned write is a component this checkout edited), `--names-only` (print only `BUCKET type:id name` lines), `--fail-on-removed`, `--fail-on-errors`, and `--expected-removed <type:id>` (repeatable or comma-delimited; implies `--fail-on-removed`, so any removal you did not name fails). `--changed-only` also fails closed when the checkout is not a git working tree, because "git could not answer" must never be read as "nothing changed".
85
+ - Any command that can touch production prints a `PROD DATA` banner naming the operation, the resolved account, and the host, and distinguishes a live **write** from a live **read** (and from a dry run). This covers `tool`, `test run`, and `components sync`.
86
+ - When a tool call fails inside the platform runtime rather than inside the tool — Groovy reflective dispatch of a runtime-compiled component, an empty connection pool, a Redis reconnect, a lock-wait timeout — the server returns `503` with `failureClass: "transient_infrastructure"` and `retryable: true`, and the CLI prints `TRANSIENT INFRASTRUCTURE FAILURE (retryable)`. A genuine tool error stays a `500` with `failureClass: "tool_error"`. Retry the first; do not retry the second.
82
87
  - Test activity is streamed from websocket `TestSuite` events while final status is also polled from `/cli/test`.
83
88
  - Tool execution writes the full response to a separate file so large payloads do not bloat the session log.
84
89
  - `--variant-branch <name|none>` is available on `test run`, `token`, `tools`, and `tool`. Use it to probe a committed branch variant from any checkout; omit it to resolve the execution account's normal subscription, or pass `none`/`trunk` to force subscription semantics from a variant checkout.
package/index.js CHANGED
@@ -72,6 +72,10 @@ const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
72
72
  'tmp'
73
73
  ]);
74
74
 
75
+ // Flags that are legitimately repeatable accumulate into an array instead of last-wins. Every other
76
+ // flag keeps last-wins so existing callers are unaffected.
77
+ const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved']);
78
+
75
79
  function parseArgs(argv) {
76
80
  const out = { _: [] };
77
81
  for (let i = 0; i < argv.length; i++) {
@@ -82,12 +86,18 @@ function parseArgs(argv) {
82
86
  }
83
87
  const key = arg.slice(2);
84
88
  const next = argv[i + 1];
89
+ let value;
85
90
  if (!next || next.startsWith('--')) {
86
- out[key] = true;
91
+ value = true;
87
92
  } else {
88
- out[key] = next;
93
+ value = next;
89
94
  i += 1;
90
95
  }
96
+ if (REPEATABLE_FLAGS.has(key) && Object.prototype.hasOwnProperty.call(out, key)) {
97
+ out[key] = (Array.isArray(out[key]) ? out[key] : [out[key]]).concat(value);
98
+ } else {
99
+ out[key] = value;
100
+ }
91
101
  }
92
102
  return out;
93
103
  }
@@ -268,6 +278,35 @@ function printResolvedBaseUrl(baseUrl) {
268
278
  console.log('Base URL:', normalizeBaseUrl(baseUrl || DEFAULT_BASE_URL));
269
279
  }
270
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.
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
+
285
+ function looksLikeDryRun(input) {
286
+ if (!input || typeof input !== 'object') return false;
287
+ const value = input.dryRun !== undefined ? input.dryRun : input.dry_run;
288
+ return value === true || value === 'true';
289
+ }
290
+
291
+ /**
292
+ * One consistent banner across every surface that can touch production. Agents have repeatedly run a
293
+ * prod write believing they were in test mode, so the banner is loud, states the account it resolved,
294
+ * and distinguishes a live WRITE from a live read.
295
+ */
296
+ function printProdDataBanner({ dataMode, accountId, baseUrl, operation, mutating = false, dryRun = false }) {
297
+ if (String(dataMode || '').toLowerCase() !== 'prod') return;
298
+ const bar = '='.repeat(72);
299
+ const headline = dryRun
300
+ ? 'PROD DATA — DRY RUN (no write will be attempted)'
301
+ : (mutating ? 'PROD DATA WRITE — this runs against LIVE production data' : 'PROD DATA READ — this reads LIVE production data');
302
+ console.log(bar);
303
+ console.log(' ' + headline);
304
+ console.log(' operation: ' + (operation || 'unknown'));
305
+ if (accountId != null) console.log(' account: ' + accountId);
306
+ if (baseUrl) console.log(' host: ' + normalizeBaseUrl(baseUrl));
307
+ console.log(bar);
308
+ }
309
+
271
310
  // A tool's OWN verdict, which is separate from whether the call was dispatched. Only an explicit
272
311
  // failure signal counts: tools legitimately return strings, arrays, and maps with no `success` key.
273
312
  // Used as a fallback when the platform build predates the envelope's `toolSuccess`.
@@ -1242,6 +1281,124 @@ function collectComponents(cwd) {
1242
1281
  return components;
1243
1282
  }
1244
1283
 
1284
+ function componentPathInfo(cwd, filePath) {
1285
+ const mapping = {
1286
+ schemas: 'schema',
1287
+ readers: 'reader',
1288
+ actions: 'action',
1289
+ embeddables: 'embeddable',
1290
+ i18n: 'i18n',
1291
+ rules: 'rule',
1292
+ templates: 'htmltemplate',
1293
+ agents: 'utility',
1294
+ tools: 'tool',
1295
+ prompts: 'prompt',
1296
+ tests: 'test'
1297
+ };
1298
+ const relative = String(filePath || '').replace(/\\/g, '/').replace(/^"|"$/g, '');
1299
+ const parts = relative.split('/');
1300
+ if (parts.length < 3 || parts[0] !== 'components') {
1301
+ return null;
1302
+ }
1303
+ const type = mapping[parts[1]];
1304
+ if (!type) {
1305
+ return null;
1306
+ }
1307
+ const fileName = parts.slice(2).join('/');
1308
+ if (fileName.includes('/')) {
1309
+ return null;
1310
+ }
1311
+ const metaMatch = fileName.match(/^(.+?)_(.+)\.meta\.ya?ml$/i);
1312
+ const match = metaMatch || fileName.match(/^(.+?)_(.+)\.([^.]+)$/);
1313
+ if (!match) {
1314
+ return null;
1315
+ }
1316
+ const prefix = match[1];
1317
+ const rawName = match[2];
1318
+ const ext = metaMatch ? 'meta' : match[3].toLowerCase();
1319
+ const id = /^\d+$/.test(prefix) ? Number(prefix) : null;
1320
+ const name = normalizeName(rawName);
1321
+ const key = type + ':' + (id ? 'id:' + id : 'name:' + name.toLowerCase());
1322
+ let field = ext;
1323
+ if (ext === 'meta') field = 'metadata';
1324
+ else if ((type === 'utility' || type === 'prompt') && ext === 'md') field = 'prompt';
1325
+ else if (ext === 'groovy' || ext === 'md') field = 'source';
1326
+ else if (type === 'embeddable' && ext === 'html') field = 'html';
1327
+ else if (type === 'embeddable' && ext === 'js') field = 'javascript';
1328
+ else if (type === 'htmltemplate' && ext === 'html') field = 'html';
1329
+ else if (type === 'htmltemplate' && ext === 'json') field = 'previewData';
1330
+ else if (type === 'tool' && ext === 'json') field = 'inputSchema';
1331
+ else if (type === 'i18n' && ext === 'json') field = 'messages';
1332
+ else if (type === 'schema' && ext === 'json') field = 'schema';
1333
+
1334
+ return { key, type, id, name, field, path: relative };
1335
+ }
1336
+
1337
+ // Returns null (not []) when git cannot answer — e.g. this checkout is not a git repo. The caller
1338
+ // must not render "0 components edited" in that case, because it is a missing answer, not a zero.
1339
+ function changedComponentsFromWorkingTree(cwd) {
1340
+ let porcelain = '';
1341
+ try {
1342
+ // NOT runGit(): it trims the whole output, which eats the leading space of porcelain's 2-char
1343
+ // status field on the FIRST line (` M path` -> `M path`). Any fixed-offset slice then reads the
1344
+ // path one character short and silently drops that entry — so a single modified component
1345
+ // reported as zero, while an untracked one ("?? path", no leading space) reported fine.
1346
+ porcelain = execSync('git status --porcelain -- components', {
1347
+ cwd,
1348
+ stdio: ['ignore', 'pipe', 'pipe']
1349
+ }).toString();
1350
+ } catch (_) {
1351
+ return null;
1352
+ }
1353
+ if (!porcelain.trim()) {
1354
+ return [];
1355
+ }
1356
+ const byKey = new Map();
1357
+ for (const rawLine of porcelain.split(/\r?\n/)) {
1358
+ // Status is exactly 2 columns, then a space, then the path. Match it rather than slicing, so a
1359
+ // status whose first column is a space (unstaged change) parses identically to a staged one.
1360
+ const parsed = /^(..) (.*)$/.exec(rawLine.replace(/\s+$/, ''));
1361
+ if (!parsed) continue;
1362
+ const status = parsed[1].trim() || parsed[1];
1363
+ let filePath = parsed[2].trim();
1364
+ // Renames render as "old -> new"; the new path is the one that exists on disk.
1365
+ if (filePath.includes(' -> ')) {
1366
+ filePath = filePath.split(' -> ').pop().trim();
1367
+ }
1368
+ // Quoted paths (non-ASCII / spaces) come back wrapped in double quotes.
1369
+ if (filePath.startsWith('"') && filePath.endsWith('"')) {
1370
+ filePath = filePath.slice(1, -1);
1371
+ }
1372
+ const info = componentPathInfo(cwd, filePath);
1373
+ if (!info) {
1374
+ continue;
1375
+ }
1376
+ if (!byKey.has(info.key)) {
1377
+ byKey.set(info.key, {
1378
+ type: info.type,
1379
+ id: info.id,
1380
+ name: info.name,
1381
+ fields: [],
1382
+ statuses: [],
1383
+ paths: []
1384
+ });
1385
+ }
1386
+ const entry = byKey.get(info.key);
1387
+ if (!entry.fields.includes(info.field)) entry.fields.push(info.field);
1388
+ if (!entry.statuses.includes(status)) entry.statuses.push(status);
1389
+ if (!entry.paths.includes(info.path)) entry.paths.push(info.path);
1390
+ }
1391
+ return Array.from(byKey.values()).map((entry) => ({
1392
+ ...entry,
1393
+ fields: entry.fields.sort(),
1394
+ paths: entry.paths.sort()
1395
+ })).sort((a, b) => {
1396
+ const typeCmp = String(a.type).localeCompare(String(b.type));
1397
+ if (typeCmp !== 0) return typeCmp;
1398
+ return String(a.id || a.name || '').localeCompare(String(b.id || b.name || ''));
1399
+ });
1400
+ }
1401
+
1245
1402
  function parsePositiveInt(value, fallback) {
1246
1403
  const parsed = Number(value);
1247
1404
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
@@ -1259,6 +1416,14 @@ function describeError(err) {
1259
1416
  const parts = [];
1260
1417
  if (err.response && err.response.data) {
1261
1418
  const responseData = err.response.data;
1419
+ // The server distinguishes a failure in the RUNTIME (Groovy reflective dispatch, empty connection
1420
+ // pool, Redis reconnect, lock-wait timeout) from a tool that ran and genuinely failed. Surface that
1421
+ // verdict, or the caller cannot tell "retry this" from "this is broken".
1422
+ if (responseData.failureClass === 'transient_infrastructure') {
1423
+ parts.push('TRANSIENT INFRASTRUCTURE FAILURE (retryable)');
1424
+ if (responseData.failureReason) parts.push('signal: ' + responseData.failureReason);
1425
+ if (responseData.hint) parts.push(responseData.hint);
1426
+ }
1262
1427
  if (responseData.message) {
1263
1428
  parts.push(String(responseData.message));
1264
1429
  } else if (typeof responseData === 'string') {
@@ -1679,6 +1844,7 @@ async function pushComponentsCommand(flags) {
1679
1844
  const requestedMode = String(flags.mode || 'stage').toLowerCase();
1680
1845
  const mode = requestedMode === 'push' ? 'stage' : requestedMode;
1681
1846
  const components = collectComponents(cwd);
1847
+ const changedFromWorkingTree = changedComponentsFromWorkingTree(cwd);
1682
1848
 
1683
1849
  if (requestedMode === 'push') {
1684
1850
  console.log('Warning: `components push` is deprecated and currently behaves the same as `components stage`.');
@@ -1694,6 +1860,8 @@ async function pushComponentsCommand(flags) {
1694
1860
  replace: true,
1695
1861
  components
1696
1862
  }).then((r) => r.data);
1863
+ response.changedFromWorkingTree = changedFromWorkingTree || [];
1864
+ response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
1697
1865
 
1698
1866
  if (flagEnabled(flags.json)) {
1699
1867
  console.log(JSON.stringify(response, null, 2));
@@ -1744,14 +1912,35 @@ function printComponentCommandResponse(label, response, flags) {
1744
1912
  }
1745
1913
 
1746
1914
  function printStageSummary(response, flags) {
1747
- console.log('Updated:', response.updated || 0);
1748
- console.log('Unchanged:', response.unchanged || 0);
1915
+ // `stage` always uploads EVERY component in the repo, so the server's Updated/Unchanged counts
1916
+ // describe the whole staging overlay, not what this checkout edited. That is what made "Updated: 75"
1917
+ // read as "I changed 75 components". Lead with the working-tree delta, which is the number an agent
1918
+ // is actually asking about, and label the server counts as the overlay they describe.
1919
+ const changed = Array.isArray(response.changedFromWorkingTree) ? response.changedFromWorkingTree : [];
1920
+ const tracked = response.changedFromWorkingTreeAvailable !== false;
1921
+ if (tracked) {
1922
+ console.log('Components edited in this working tree:', changed.length);
1923
+ changed.slice(0, 20).forEach((entry) => {
1924
+ const label = (entry.type || 'component') + ' ' + (entry.id || entry.name || '(unknown)');
1925
+ const name = entry.name && entry.id ? ' ' + entry.name : '';
1926
+ const fields = Array.isArray(entry.fields) && entry.fields.length ? ': ' + entry.fields.join(', ') : '';
1927
+ console.log(' ' + label + name + fields);
1928
+ });
1929
+ if (changed.length > 20) {
1930
+ console.log(' ...' + (changed.length - 20) + ' more');
1931
+ }
1932
+ if (!changed.length) {
1933
+ console.log(' (no uncommitted component edits — already-committed edits are staged but not listed here)');
1934
+ }
1935
+ }
1936
+ console.log('Staging overlay — newly written:', response.updated || 0);
1937
+ console.log('Staging overlay — already current:', response.unchanged || 0);
1749
1938
  console.log('Skipped:', Array.isArray(response.skipped) ? response.skipped.length : 0);
1750
1939
  if (response.reconcile) {
1751
1940
  console.log('Reconciled stale keys:', response.reconcile.removedCount || 0);
1752
1941
  }
1753
1942
  if (response.staging) {
1754
- console.log('Remaining staged count:', response.staging.remainingCount || 0);
1943
+ console.log('Total staged entries for this branch/user:', response.staging.remainingCount || 0);
1755
1944
  }
1756
1945
  printComponentCommandResponse('Components stage', response, flags);
1757
1946
  }
@@ -1901,6 +2090,15 @@ async function syncComponentsCommand(flags) {
1901
2090
  const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
1902
2091
  const api = buildAxios(baseUrl, session.token);
1903
2092
 
2093
+ printProdDataBanner({
2094
+ dataMode,
2095
+ accountId,
2096
+ baseUrl,
2097
+ operation: 'components sync (branch ' + branchName + ')',
2098
+ mutating: true,
2099
+ dryRun
2100
+ });
2101
+
1904
2102
  const response = await loggedPost(api, cwd, '/cli/components', {
1905
2103
  token: session.token,
1906
2104
  accountId,
@@ -1916,8 +2114,13 @@ async function syncComponentsCommand(flags) {
1916
2114
  throw new Error(response.message || 'Server sync failed');
1917
2115
  }
1918
2116
 
2117
+ const summary = buildSyncSummary(response);
2118
+ const gate = evaluateSyncGates(response, flags, changedComponentsFromWorkingTree(cwd));
2119
+ summary.gates = gate.checks;
2120
+
1919
2121
  if (flagEnabled(flags.json)) {
1920
- console.log(JSON.stringify(response, null, 2));
2122
+ console.log(JSON.stringify(flagEnabled(flags.summary) ? summary : response, null, 2));
2123
+ failOnSyncGate(gate);
1921
2124
  return response;
1922
2125
  }
1923
2126
 
@@ -1928,10 +2131,169 @@ async function syncComponentsCommand(flags) {
1928
2131
  if (response.sync && response.sync.dryRun) {
1929
2132
  console.log('Dry run: no variants were written and staging was not cleared.');
1930
2133
  }
2134
+ if (flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly)) {
2135
+ printSyncNames(response);
2136
+ failOnSyncGate(gate);
2137
+ return response;
2138
+ }
2139
+ if (flagEnabled(flags.summary)) {
2140
+ console.log('Components sync summary:', JSON.stringify(summary, null, 2));
2141
+ failOnSyncGate(gate);
2142
+ return response;
2143
+ }
1931
2144
  console.log('Components sync:', JSON.stringify(response, null, 2));
2145
+ failOnSyncGate(gate);
1932
2146
  return response;
1933
2147
  }
1934
2148
 
2149
+ // One "type:id" / "type:name" token per planned write, so an agent can eyeball or diff the plan
2150
+ // without paging through the full server response.
2151
+ function syncPlanEntries(response) {
2152
+ const results = ((response && response.sync) || {}).syncResults || {};
2153
+ const entries = [];
2154
+ ['overridden', 'added', 'removed', 'errors'].forEach((bucket) => {
2155
+ (Array.isArray(results[bucket]) ? results[bucket] : []).forEach((entry) => {
2156
+ entries.push({
2157
+ bucket,
2158
+ type: String(entry.type || entry.kind || 'component').toLowerCase(),
2159
+ id: entry.id == null ? null : String(entry.id),
2160
+ name: entry.name || null
2161
+ });
2162
+ });
2163
+ });
2164
+ return entries;
2165
+ }
2166
+
2167
+ function syncEntryToken(entry) {
2168
+ return entry.type + ':' + (entry.id != null ? entry.id : (entry.name || '?'));
2169
+ }
2170
+
2171
+ function printSyncNames(response) {
2172
+ const entries = syncPlanEntries(response);
2173
+ if (!entries.length) {
2174
+ console.log('No planned component writes.');
2175
+ return;
2176
+ }
2177
+ entries.forEach((entry) => {
2178
+ console.log(entry.bucket.toUpperCase().padEnd(10), syncEntryToken(entry), entry.name || '');
2179
+ });
2180
+ }
2181
+
2182
+ function parseExpectedRemoved(flags) {
2183
+ const raw = flags['expected-removed'] != null ? flags['expected-removed'] : flags.expectedRemoved;
2184
+ if (raw == null) return null;
2185
+ // A bare `--expected-removed` with no value means "I expect none" — fail closed on any removal.
2186
+ const values = raw === true ? [] : (Array.isArray(raw) ? raw : [raw]);
2187
+ const tokens = new Set();
2188
+ values.forEach((value) => {
2189
+ String(value).split(',').forEach((token) => {
2190
+ const trimmed = token.trim().toLowerCase();
2191
+ if (trimmed) tokens.add(trimmed);
2192
+ });
2193
+ });
2194
+ return tokens;
2195
+ }
2196
+
2197
+ // Fail-closed gates. Each returns a violation string or null; the command exits non-zero if any fire.
2198
+ function evaluateSyncGates(response, flags, changedFromWorkingTree) {
2199
+ const entries = syncPlanEntries(response);
2200
+ const results = ((response && response.sync) || {}).syncResults || {};
2201
+ const removed = entries.filter((e) => e.bucket === 'removed');
2202
+ const errors = Array.isArray(results.errors) ? results.errors : [];
2203
+ const expectedRemoved = parseExpectedRemoved(flags);
2204
+ const violations = [];
2205
+ const checks = {};
2206
+
2207
+ if (flagEnabled(flags['fail-on-errors']) || flagEnabled(flags.failOnErrors)) {
2208
+ checks.failOnErrors = errors.length === 0;
2209
+ if (errors.length) {
2210
+ violations.push('--fail-on-errors: server reported ' + errors.length + ' component sync error(s): ' +
2211
+ errors.map((e) => (e.type || 'component') + ':' + (e.id != null ? e.id : e.name)).join(', '));
2212
+ }
2213
+ }
2214
+
2215
+ // --expected-removed implies --fail-on-removed: you named the removals you accept, so any other
2216
+ // removal is by definition unexpected.
2217
+ const failOnRemoved = flagEnabled(flags['fail-on-removed']) || flagEnabled(flags.failOnRemoved) || expectedRemoved !== null;
2218
+ if (failOnRemoved) {
2219
+ const unexpected = removed.filter((entry) => !expectedRemoved || !expectedRemoved.has(syncEntryToken(entry)));
2220
+ checks.failOnRemoved = unexpected.length === 0;
2221
+ if (unexpected.length) {
2222
+ violations.push((expectedRemoved ? '--expected-removed' : '--fail-on-removed') +
2223
+ ': plan removes/tombstones ' + unexpected.length + ' unlisted component(s): ' +
2224
+ unexpected.map(syncEntryToken).join(', '));
2225
+ }
2226
+ }
2227
+
2228
+ if (flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) {
2229
+ if (changedFromWorkingTree === null) {
2230
+ violations.push('--changed-only: this checkout is not a git working tree, so the changed set cannot be established.');
2231
+ checks.changedOnly = false;
2232
+ } else {
2233
+ // A component the checkout edited is identified by type + id, or type + name for `new_` files.
2234
+ const allowedIds = new Set(changedFromWorkingTree.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
2235
+ const allowedNames = new Set(changedFromWorkingTree.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
2236
+ const unexpected = entries.filter((entry) => {
2237
+ if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
2238
+ if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
2239
+ return true;
2240
+ });
2241
+ checks.changedOnly = unexpected.length === 0;
2242
+ if (unexpected.length) {
2243
+ violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this working tree did not edit: ' +
2244
+ unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : ''));
2245
+ }
2246
+ }
2247
+ }
2248
+
2249
+ return { violations, checks };
2250
+ }
2251
+
2252
+ function failOnSyncGate(gate) {
2253
+ if (gate && gate.violations && gate.violations.length) {
2254
+ throw new Error('components sync safety gate failed:\n ' + gate.violations.join('\n '));
2255
+ }
2256
+ }
2257
+
2258
+ function buildSyncSummary(response) {
2259
+ const sync = (response && response.sync) || {};
2260
+ const results = sync.syncResults || {};
2261
+ const removed = Array.isArray(results.removed) ? results.removed : [];
2262
+ const errors = Array.isArray(results.errors) ? results.errors : [];
2263
+ const skipped = Array.isArray(results.skipped) ? results.skipped : [];
2264
+ const added = Array.isArray(results.added) ? results.added : [];
2265
+ const overridden = Array.isArray(results.overridden) ? results.overridden : [];
2266
+ const unchanged = Array.isArray(results.unchanged) ? results.unchanged : [];
2267
+ const warnings = [];
2268
+ if (removed.length) warnings.push(String(removed.length) + ' removed/tombstone entr' + (removed.length === 1 ? 'y' : 'ies') + ' present');
2269
+ if (errors.length) warnings.push(String(errors.length) + ' sync error' + (errors.length === 1 ? '' : 's') + ' present');
2270
+ return {
2271
+ success: response && response.success === true,
2272
+ accountId: response && response.accountId,
2273
+ branchName: sync.branchName || (response && response.branchName),
2274
+ mode: sync.mode || 'trunk',
2275
+ dryRun: Boolean(sync.dryRun),
2276
+ overridden: overridden.length,
2277
+ added: added.length,
2278
+ removed: removed.map((entry) => ({
2279
+ type: entry.type,
2280
+ kind: entry.kind,
2281
+ name: entry.name,
2282
+ id: entry.id,
2283
+ variantId: entry.variantId
2284
+ })),
2285
+ unchanged: unchanged.length,
2286
+ skipped: skipped.length,
2287
+ errors: errors.length,
2288
+ errorDetails: errors.map((entry) => ({
2289
+ type: entry.type,
2290
+ id: entry.id,
2291
+ error: entry.error
2292
+ })),
2293
+ warnings
2294
+ };
2295
+ }
2296
+
1935
2297
  // Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
1936
2298
  // Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
1937
2299
  // subscribing accounts actually resolve in production.
@@ -2285,6 +2647,15 @@ async function testCommand(flags) {
2285
2647
  // --variant-branch explicitly probes a committed variant branch. Normally omitted: variants resolve
2286
2648
  // from the account's subscription edge, which is what production does.
2287
2649
  const variantBranch = flags['variant-branch'];
2650
+
2651
+ printProdDataBanner({
2652
+ dataMode,
2653
+ accountId: asAccountId || accountId,
2654
+ baseUrl,
2655
+ operation: 'test run ' + String(testRef),
2656
+ mutating: true
2657
+ });
2658
+
2288
2659
  const start = await loggedPost(api, cwd, '/cli/test', {
2289
2660
  token: session.token,
2290
2661
  accountId,
@@ -2507,6 +2878,15 @@ async function toolCommand(flags) {
2507
2878
  return;
2508
2879
  }
2509
2880
 
2881
+ printProdDataBanner({
2882
+ dataMode,
2883
+ accountId,
2884
+ baseUrl,
2885
+ operation: 'tool ' + String(toolName),
2886
+ mutating: MUTATING_TOOL_PATTERN.test(String(toolName)),
2887
+ dryRun: looksLikeDryRun(input)
2888
+ });
2889
+
2510
2890
  const response = await loggedPost(api, cwd, '/cli/tool', {
2511
2891
  token: session.token,
2512
2892
  accountId,
@@ -4965,6 +5345,113 @@ function autoUpdateIfNeeded(originalArgv, options = {}) {
4965
5345
  }
4966
5346
  }
4967
5347
 
5348
+ function printComponentsHelp(subcommand) {
5349
+ if (subcommand === 'sync') {
5350
+ console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--json]');
5351
+ console.log('');
5352
+ console.log('On trunk this reconciles the pushed repository into live component rows.');
5353
+ console.log('On a variant branch this writes ComponentVariant overlays only.');
5354
+ console.log('--dry-run is only supported on variant branches and writes nothing.');
5355
+ console.log('--summary prints compact counts, removals/tombstones, errors, skipped items, and warnings.');
5356
+ console.log('');
5357
+ console.log('Agent safety gates (each exits non-zero instead of printing a wall of JSON):');
5358
+ console.log(' --changed-only fail unless every planned write is a component this checkout changed');
5359
+ console.log(' --names-only print only "type id name" lines for the planned writes');
5360
+ console.log(' --fail-on-removed fail if the plan removes/tombstones anything');
5361
+ console.log(' --fail-on-errors fail if the server reported any per-component sync error');
5362
+ console.log(' --expected-removed t:id whitelist an intended removal; repeatable/comma-delimited.');
5363
+ console.log(' Implies --fail-on-removed, so any OTHER removal fails.');
5364
+ return;
5365
+ }
5366
+ if (subcommand === 'stage' || subcommand === 'push') {
5367
+ console.log('Usage: remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
5368
+ console.log('');
5369
+ console.log('Stages local component files into the Redis staging cache. It never writes the database or git.');
5370
+ console.log('Terminal output separates local working-tree component deltas from the full materialized staging cache count.');
5371
+ return;
5372
+ }
5373
+ if (subcommand === 'status') {
5374
+ console.log('Usage: remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
5375
+ console.log('');
5376
+ console.log('Shows staged entries and whether this checkout resolves/writes trunk or a variant branch.');
5377
+ return;
5378
+ }
5379
+ if (subcommand === 'clear') {
5380
+ console.log('Usage: remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
5381
+ console.log('');
5382
+ console.log('Clears staged Redis entries without touching the database or git.');
5383
+ console.log('Use --component-type with --component-id when the id could exist in multiple component families.');
5384
+ console.log('Use --all for the temporary-experiment cleanup flow.');
5385
+ return;
5386
+ }
5387
+ if (subcommand === 'commit') {
5388
+ console.log('Usage: remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
5389
+ console.log('');
5390
+ console.log('Runs local git add/commit/push, then server sync. Prefer explicit git + components sync when you need inspectable phases.');
5391
+ console.log('components commit does not support --dry-run.');
5392
+ return;
5393
+ }
5394
+ console.log('Usage: remits-cli components <stage|status|clear|sync|commit|branches|branch>');
5395
+ console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
5396
+ console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
5397
+ console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
5398
+ console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
5399
+ console.log(' remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
5400
+ console.log('');
5401
+ console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
5402
+ console.log(' component file is intentionally being recorded as a tombstone override.');
5403
+ console.log(' --dry-run reports the variant override/add/remove plan without writing variants,');
5404
+ console.log(' updating sync SHA cache, or clearing staged CLI entries.');
5405
+ console.log(' --summary, --changed-only, --names-only, --fail-on-removed, --fail-on-errors, and');
5406
+ console.log(' --expected-removed keep sync output small and fail closed. See `components sync --help`.');
5407
+ console.log('');
5408
+ console.log('Committed branch variants (durable overlays subscribing accounts resolve in production):');
5409
+ console.log(' remits-cli components branches [--json]');
5410
+ console.log(' remits-cli components branch <name> [--json] # overridden/added/removed + drift');
5411
+ console.log(' remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]');
5412
+ console.log(' remits-cli components branch <name> --subscribers [--json]');
5413
+ console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]');
5414
+ console.log(' remits-cli components branch <name> --unsubscribe <accountId>');
5415
+ console.log(' remits-cli components branch <name> --retire [--force]');
5416
+ }
5417
+
5418
+ function printTestHelp() {
5419
+ console.log('Usage: 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|none]');
5420
+ console.log('');
5421
+ console.log('Runs a Test component against the staged/variant world for this checkout.');
5422
+ console.log('Examples:');
5423
+ console.log(' remits-cli test run --test 11');
5424
+ console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
5425
+ console.log('');
5426
+ console.log('Notes:');
5427
+ console.log(' --names is comma-delimited, so avoid commas in individual test case names.');
5428
+ console.log(' --as-account changes the execution account so subscriber branch edges apply.');
5429
+ console.log(' --data-mode prod intentionally targets live production data.');
5430
+ }
5431
+
5432
+ function printToolHelp() {
5433
+ console.log('Usage: remits-cli tool --name <toolName> [--base-url URL] [--account-id ID] [--branch BRANCH] [--input "{...}"|--input-file file.json] [--data-mode test|prod] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
5434
+ console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
5435
+ console.log('');
5436
+ console.log('Examples:');
5437
+ console.log(' remits-cli tool --name mcp_firestore_search --input-file query.json --data-mode prod');
5438
+ console.log(' remits-cli tool --name mcp_run_action --input \'{"accountId":49,"actionId":200,"executionMode":"async"}\' --data-mode prod');
5439
+ console.log('');
5440
+ console.log('For mcp_run_action and mcp_run_agent, prefer the tool\'s own executionMode:"async" and poll with that tool\'s controlAction:"status".');
5441
+ }
5442
+
5443
+ function printToolsHelp() {
5444
+ console.log('Usage: remits-cli tools [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--variant-branch NAME|none]');
5445
+ console.log('');
5446
+ console.log('Refreshes .remits-cli/tools/tools.json for the current account/branch/data-mode context.');
5447
+ }
5448
+
5449
+ function printTokenHelp() {
5450
+ 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]');
5451
+ console.log('');
5452
+ console.log('Mints a branch-aware browser URL for embeddable verification.');
5453
+ }
5454
+
4968
5455
  async function main() {
4969
5456
  migrateSessionIfNeeded();
4970
5457
  const originalArgv = process.argv.slice(2);
@@ -5022,7 +5509,7 @@ async function main() {
5022
5509
  console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
5023
5510
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
5024
5511
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
5025
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run]');
5512
+ console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
5026
5513
  console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
5027
5514
  console.log(' remits-cli components branches [--json] # committed branch variants for this account');
5028
5515
  console.log(' remits-cli components branch <name> [--diff <componentId> --component-type <kind>] [--subscribers] [--json]');
@@ -5039,27 +5526,28 @@ async function main() {
5039
5526
  process.exit(0);
5040
5527
  }
5041
5528
 
5042
- if (command === 'components' && wantsHelp) {
5043
- console.log('Usage: remits-cli components <stage|status|clear|sync|commit|branches|branch>');
5044
- console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
5045
- console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
5046
- console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
5047
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run]');
5048
- console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
5049
- console.log('');
5050
- console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
5051
- console.log(' component file is intentionally being recorded as a tombstone override.');
5052
- console.log(' --dry-run reports the variant override/add/remove plan without writing variants,');
5053
- console.log(' updating sync SHA cache, or clearing staged CLI entries.');
5054
- console.log('');
5055
- console.log('Committed branch variants (durable overlays subscribing accounts resolve in production):');
5056
- console.log(' remits-cli components branches [--json]');
5057
- console.log(' remits-cli components branch <name> [--json] # overridden/added/removed + drift');
5058
- console.log(' remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]');
5059
- console.log(' remits-cli components branch <name> --subscribers [--json]');
5060
- console.log(' remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>]');
5061
- console.log(' remits-cli components branch <name> --unsubscribe <accountId>');
5062
- console.log(' remits-cli components branch <name> --retire [--force]');
5529
+ if (wantsHelp && command === 'test') {
5530
+ printTestHelp();
5531
+ process.exit(0);
5532
+ }
5533
+
5534
+ if (wantsHelp && command === 'tool') {
5535
+ printToolHelp();
5536
+ process.exit(0);
5537
+ }
5538
+
5539
+ if (wantsHelp && command === 'tools') {
5540
+ printToolsHelp();
5541
+ process.exit(0);
5542
+ }
5543
+
5544
+ if (wantsHelp && command === 'token') {
5545
+ printTokenHelp();
5546
+ process.exit(0);
5547
+ }
5548
+
5549
+ if (wantsHelp && command === 'components') {
5550
+ printComponentsHelp(subcommand);
5063
5551
  process.exit(0);
5064
5552
  }
5065
5553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -7,121 +7,124 @@ description: Use remits-cli for fast branch-scoped component staging, test execu
7
7
 
8
8
  ## Table of Contents
9
9
 
10
- - Line 127: Account Targeting Model
11
- - Line 134: Account types
12
- - Line 149: How accounts connect (and why it changes what runs)
13
- - Line 179: Users are a separate model — don't reason about them as a tree
14
- - Line 192: Reading the shape
15
- - Line 218: Repo selection rules
16
- - Line 230: Component Integrity Rules: Repo ↔ Database Reconciliation (read before any sync/commit)
17
- - Line 236: How the platform reconciles the repo into the database (the mechanism you must understand)
18
- - Line 298: The surfaces and their intended behavior
19
- - Line 322: Intended workflows
20
- - Line 343: Pre-sync safety check (confirm ALL before `components sync` or `components commit`)
21
- - Line 356: If something looks wrong — stop, don't paper over
22
- - Line 380: Required Local Index Reads
23
- - Line 414: Big Picture: How remits-cli State Is Organized
24
- - Line 444: Support Ticket Mental Model
25
- - Line 480: Efficiency Rules
26
- - Line 492: Account Repository Index
27
- - Line 517: Two Workflows
28
- - Line 524: Test Mode vs Prod Mode
29
- - Line 556: Platform Mental Model: Front Stage vs Back Stage
30
- - Line 563: Documents vs Records
31
- - Line 573: HTTP Audits
32
- - Line 627: Record `content` / Context Mental Model
33
- - Line 647: Provenance Fields
34
- - Line 665: Why Persisted Context Looks Different From Runtime Context
35
- - Line 686: Investigation Rule for Record Context
36
- - Line 701: AI Request Response Records
37
- - Line 708: AI Session Groupings
38
- - Line 727: Correlation Keys
39
- - Line 733: Node Reference Table
40
- - Line 743: Repository vs External Context
41
- - Line 750: Getting Started
42
- - Line 752: Authentication
43
- - Line 765: Host vs Data Mode
44
- - Line 784: Tool Execution Lifecycle
45
- - Line 838: Data Mode
46
- - Line 848: Development Workflow
47
- - Line 850: The Golden Rule: Writing Code Is Not Finishing the Job
48
- - Line 862: Avoid Brittle Front-Stage Intelligence
49
- - Line 875: The Development Fast Loop
50
- - Line 879: Step 1: Understand the Request
51
- - Line 894: Step 2: Make the Change
52
- - Line 932: Step 3: Stage to Platform
53
- - Line 947: Step 4: Verify the Change
54
- - Line 1012: Step 5: Iterate If Needed
55
- - Line 1022: Step 6: Update Documentation
56
- - Line 1034: Step 7: Commit and Durable Sync
57
- - Line 1072: Step 8: Close the Ticket
58
- - Line 1087: User Confirmation Preferences
59
- - Line 1097: Component Resolution: Staging Cache vs DB (which "version" actually runs)
60
- - Line 1103: The three source layers + the compile cache
61
- - Line 1126: Staging cache key format
62
- - Line 1141: How the platform picks staged vs DB (the compile signature)
63
- - Line 1163: When staged overrides apply
64
- - Line 1187: Diagnosing which version is in play
65
- - Line 1208: Stage / commit / clear with the MCP tools
66
- - Line 1232: Stale after sync / commit (the in-memory compile cache)
67
- - Line 1242: Account Resolution: how a request travels the account graph
68
- - Line 1335: Branched Component Variants (per-account component overrides)
69
- - Line 1343: The model (three moving parts)
70
- - Line 1410: Which world does your working tree resolve? (read this before you run anything)
71
- - Line 1476: Two levers, two different questions
72
- - Line 1490: The SDLC is identical on a variant branch
73
- - Line 1515: Promotion: getting the branch back into trunk
74
- - Line 1557: Verifying as the subscriber
75
- - Line 1576: Agents (Utility) on a variant branch
76
- - Line 1588: Danger profile on a variant branch (different, not absent)
77
- - Line 1616: Inspecting branches and drift
78
- - Line 1648: Diagnosing a variant
79
- - Line 1655: Production Support Workflow
80
- - Line 1663: Investigation Strategy
81
- - Line 1693: Presenting Findings
82
- - Line 1701: Verifying a Production Issue Fix
83
- - Line 1714: Tool Reference
84
- - Line 1716: Execute a Tool
85
- - Line 1745: `mcp_account_view`
86
- - Line 1777: `mcp_account_user_admin`
87
- - Line 1812: `mcp_firestore_search`
88
- - Line 1850: `mcp_object_activity`
89
- - Line 1859: `mcp_record_listing`
90
- - Line 1884: `mcp_record_view`
91
- - Line 1897: `mcp_ai_session_search`
92
- - Line 1933: `mcp_run_action`
93
- - Line 1977: `mcp_run_agent`
94
- - Line 2013: `mcp_system_logs`
95
- - Line 2036: `mcp_component_view`
96
- - Line 2059: `mcp_component_grep`
97
- - Line 2072: `mcp_support_ticket`
98
- - Line 2121: `mcp_run_test`
99
- - Line 2130: `mcp_component_edit`
100
- - Line 2145: `mcp_component_create`
101
- - Line 2159: `mcp_component_commit`
102
- - Line 2167: `mcp_component_branches`
103
- - Line 2186: `mcp_cache`
104
- - Line 2198: `mcp_sql_query`
105
- - Line 2233: `mcp_index_search`
106
- - Line 2253: `mcp_get_guide`
107
- - Line 2265: `mcp_test_fixture`
108
- - Line 2278: `mcp_embeddable_test_url`
109
- - Line 2290: `mcp_playwright_replay`
110
- - Line 2305: `mcp_jvm_spike_triage`
111
- - Line 2318: `mcp_support_ticket_queue`
112
- - Line 2335: Multi-Session Support
113
- - Line 2355: Persistent Service, Control Center, and Agent Dispatch
114
- - Line 2364: Starting the Service
115
- - Line 2390: Control Center
116
- - Line 2405: Agent Dispatch
117
- - Line 2426: Configuring the Preferred Agent
118
- - Line 2437: Local State Files
119
- - Line 2485: Command Reference
120
- - Line 2527: Troubleshooting
121
- - Line 2569: When Something Doesn't Work as Expected
122
- - Line 2580: Back-Stage Escalation Workflow (platform defect/limitation)
123
- - Line 2589: Escalation Bundle (tooling/operational issue)
124
-
10
+ - Line 130: Account Targeting Model
11
+ - Line 137: Account types
12
+ - Line 152: How accounts connect (and why it changes what runs)
13
+ - Line 184: Users are a separate model — don't reason about them as a tree
14
+ - Line 197: Reading the shape
15
+ - Line 223: Repo selection rules
16
+ - Line 235: Component Integrity Rules: Repo ↔ Database Reconciliation (read before any sync/commit)
17
+ - Line 241: How the platform reconciles the repo into the database (the mechanism you must understand)
18
+ - Line 303: The surfaces and their intended behavior
19
+ - Line 327: Intended workflows
20
+ - Line 348: Pre-sync safety check (confirm ALL before `components sync` or `components commit`)
21
+ - Line 361: If something looks wrong — stop, don't paper over
22
+ - Line 385: Required Local Index Reads
23
+ - Line 419: Big Picture: How remits-cli State Is Organized
24
+ - Line 449: Support Ticket Mental Model
25
+ - Line 485: Efficiency Rules
26
+ - Line 497: Account Repository Index
27
+ - Line 522: Two Workflows
28
+ - Line 529: Test Mode vs Prod Mode
29
+ - Line 561: Platform Mental Model: Front Stage vs Back Stage
30
+ - Line 568: Documents vs Records
31
+ - Line 578: HTTP Audits
32
+ - Line 632: Record `content` / Context Mental Model
33
+ - Line 652: Provenance Fields
34
+ - Line 670: Why Persisted Context Looks Different From Runtime Context
35
+ - Line 691: Investigation Rule for Record Context
36
+ - Line 706: AI Request Response Records
37
+ - Line 713: AI Session Groupings
38
+ - Line 732: Correlation Keys
39
+ - Line 738: Node Reference Table
40
+ - Line 748: Repository vs External Context
41
+ - Line 755: Getting Started
42
+ - Line 757: Authentication
43
+ - Line 770: Host vs Data Mode
44
+ - Line 789: Tool Execution Lifecycle
45
+ - Line 843: Data Mode
46
+ - Line 853: Development Workflow
47
+ - Line 855: The Golden Rule: Writing Code Is Not Finishing the Job
48
+ - Line 867: Avoid Brittle Front-Stage Intelligence
49
+ - Line 880: The Development Fast Loop
50
+ - Line 884: Step 1: Understand the Request
51
+ - Line 899: Step 2: Make the Change
52
+ - Line 938: Step 3: Stage to Platform
53
+ - Line 953: Step 4: Verify the Change
54
+ - Line 1018: Step 5: Iterate If Needed
55
+ - Line 1028: Step 6: Update Documentation
56
+ - Line 1040: Temporary Experiment Workflow
57
+ - Line 1060: Step 7: Commit and Durable Sync
58
+ - Line 1098: Step 8: Close the Ticket
59
+ - Line 1113: User Confirmation Preferences
60
+ - Line 1123: Component Resolution: Staging Cache vs DB (which "version" actually runs)
61
+ - Line 1129: The three source layers + the compile cache
62
+ - Line 1152: Staging cache key format
63
+ - Line 1167: How the platform picks staged vs DB (the compile signature)
64
+ - Line 1189: When staged overrides apply
65
+ - Line 1213: Diagnosing which version is in play
66
+ - Line 1234: Stage / commit / clear with the MCP tools
67
+ - Line 1258: Stale after sync / commit (the in-memory compile cache)
68
+ - Line 1268: Account Resolution: how a request travels the account graph
69
+ - Line 1373: Branched Component Variants (per-account component overrides)
70
+ - Line 1381: The model (three moving parts)
71
+ - Line 1454: Which world does your working tree resolve? (read this before you run anything)
72
+ - Line 1521: Two levers, two different questions
73
+ - Line 1535: The SDLC is identical on a variant branch
74
+ - Line 1560: Promotion: getting the branch back into trunk
75
+ - Line 1602: Verifying as the subscriber
76
+ - Line 1621: Agents (Utility) on a variant branch
77
+ - Line 1633: Danger profile on a variant branch (different, not absent)
78
+ - Line 1661: Inspecting branches and drift
79
+ - Line 1693: Diagnosing a variant
80
+ - Line 1700: Production Support Workflow
81
+ - Line 1708: Investigation Strategy
82
+ - Line 1770: Presenting Findings
83
+ - Line 1778: Verifying a Production Issue Fix
84
+ - Line 1791: Tool Reference
85
+ - Line 1793: Execute a Tool
86
+ - Line 1839: `mcp_account_view`
87
+ - Line 1871: `mcp_account_user_admin`
88
+ - Line 1965: `mcp_firestore_search`
89
+ - Line 2004: `mcp_firestore_patch`
90
+ - Line 2033: `mcp_object_activity`
91
+ - Line 2042: `mcp_record_listing`
92
+ - Line 2067: `mcp_record_view`
93
+ - Line 2080: `mcp_ai_session_search`
94
+ - Line 2116: `mcp_run_action`
95
+ - Line 2169: `mcp_run_agent`
96
+ - Line 2205: `mcp_system_logs`
97
+ - Line 2228: `mcp_performance_trace`
98
+ - Line 2263: `mcp_component_view`
99
+ - Line 2286: `mcp_component_grep`
100
+ - Line 2299: `mcp_support_ticket`
101
+ - Line 2359: `mcp_run_test`
102
+ - Line 2368: `mcp_component_edit`
103
+ - Line 2383: `mcp_component_create`
104
+ - Line 2397: `mcp_component_commit`
105
+ - Line 2405: `mcp_component_branches`
106
+ - Line 2424: `mcp_cache`
107
+ - Line 2436: `mcp_sql_query`
108
+ - Line 2471: `mcp_index_search`
109
+ - Line 2491: `mcp_get_guide`
110
+ - Line 2503: `mcp_test_fixture`
111
+ - Line 2516: `mcp_embeddable_test_url`
112
+ - Line 2528: `mcp_playwright_replay`
113
+ - Line 2543: `mcp_jvm_spike_triage`
114
+ - Line 2556: `mcp_support_ticket_queue`
115
+ - Line 2573: Multi-Session Support
116
+ - Line 2593: Persistent Service, Control Center, and Agent Dispatch
117
+ - Line 2602: Starting the Service
118
+ - Line 2628: Control Center
119
+ - Line 2643: Agent Dispatch
120
+ - Line 2664: Configuring the Preferred Agent
121
+ - Line 2675: Local State Files
122
+ - Line 2723: Command Reference
123
+ - Line 2783: Prod banners and retryable failures
124
+ - Line 2795: Troubleshooting
125
+ - Line 2837: When Something Doesn't Work as Expected
126
+ - Line 2848: Back-Stage Escalation Workflow (platform defect/limitation)
127
+ - Line 2857: Escalation Bundle (tooling/operational issue)
125
128
  `remits-cli` is the brainstem for both **development** and **production support**. You might run it inside an account repository (where `account-info.json` and `/components` already exist) or from outside any repo when a user needs help on another account. Your users are business professionals — they think in terms of what they can see in a browser.
126
129
 
127
130
  ## Account Targeting Model
@@ -173,8 +176,10 @@ Two consequences that generate most "this account is weird" tickets:
173
176
  different component version, a different namespace, a different host. Establish which path a failing
174
177
  request travelled before comparing behavior. (Full detail in "Account Resolution: how a request travels
175
178
  the account graph".)
176
- 2. **An account with no primary parent and several membership edges inherits nothing and runs trunk** until
177
- a path is named — the platform refuses to guess between equally valid parents. That is by design.
179
+ 2. **An account with no primary parent and SEVERAL membership edges inherits nothing and runs trunk** until
180
+ a path is named — the platform refuses to guess between equally valid parents. That is by design. With
181
+ exactly ONE membership edge there is nothing to guess, so it (and everything below it) inherits normally
182
+ — a missing `parentId` is not itself a problem.
178
183
 
179
184
  ### Users are a separate model — don't reason about them as a tree
180
185
 
@@ -1032,6 +1037,26 @@ Before committing, update metadata so the next session understands what changed:
1032
1037
  describes the owning repo account. On a subscriber-initiated variant sync it describes the subscribing
1033
1038
  account reached through the branch edge, even though the component files still belong to the owner's repo.
1034
1039
 
1040
+ #### Temporary Experiment Workflow
1041
+
1042
+ Use this when you need to prove a guard or assertion by temporarily making a local component fail. The staged
1043
+ Redis cache can affect later test/tool runs, so always clear it after restoring the file:
1044
+
1045
+ ```bash
1046
+ # make temporary local edit
1047
+ remits-cli components stage
1048
+ remits-cli test run --test <id-or-name> --names "<case name>"
1049
+ git restore <file>
1050
+ remits-cli components clear --all
1051
+ remits-cli components status
1052
+ ```
1053
+
1054
+ For narrower cleanup when only one staged component should be cleared:
1055
+
1056
+ ```bash
1057
+ remits-cli components clear --component-type Action --component-id 25
1058
+ ```
1059
+
1035
1060
  #### Step 7: Commit and Durable Sync
1036
1061
 
1037
1062
  Once verified and documented, create a normal git commit first:
@@ -1281,19 +1306,31 @@ with a branch.
1281
1306
 
1282
1307
  **The anchor is the branch of the graph you travelled.** At runtime it is `scope_account_id` (stamped
1283
1308
  onto the `Object`/`Event`/`Alert`/`ObjectLog` records a run creates, so async workers re-resolve on the
1284
- same branch). A **null** anchor means "walk the primary `parentId` chain" — today's legacy behavior.
1309
+ same branch). A **null** anchor means "walk the structural chain" — see below.
1285
1310
 
1286
- **Default-anchor derivation is deliberately conservative, and this is the #1 gotcha:**
1311
+ **How the chain is walked, and the #1 gotcha.** The walk follows `Account.parentId`, and at an account with
1312
+ **no** `parentId` it continues through that account's *single* active membership edge. Applied at every hop:
1287
1313
 
1288
- | Account shape | Derived anchor |
1314
+ | Account shape at a hop | What the walk does |
1289
1315
  |---|---|
1290
- | has a `parentId` | `null` → primary chain (legacy, unchanged) |
1291
- | no `parentId`, **exactly one** membership edge | that edge's parent |
1292
- | no `parentId`, **multiple** membership edges | `null` — **ambiguous, refuses to guess** |
1316
+ | has a `parentId` | follow it (legacy, unchanged) |
1317
+ | no `parentId`, **exactly one** membership edge | **continue through that edge** |
1318
+ | no `parentId`, **multiple** membership edges | **stop** — ambiguous, refuses to guess |
1319
+ | no `parentId`, no edges | stop — a true root |
1293
1320
 
1294
- So a `parentId`-less account with two membership edges resolves **trunk and inherits nothing** until an
1295
- anchor is supplied. That is correct-by-design, not a bug. Entry points that already know the branch
1296
- disambiguate it by branch name; from the CLI you supply it explicitly.
1321
+ **Primary-vs-membership does not gate inheritance; only ambiguity does.** A `parentId`-less subscriber shell
1322
+ linked to its owner by one membership edge inherits that owner's components and branch — **and so does
1323
+ everything beneath it**. You do not need to give it a real `parentId`.
1324
+
1325
+ But a `parentId`-less account with **two** membership edges resolves **trunk and inherits nothing** until an
1326
+ anchor is supplied. That is correct-by-design (the inheritance path must stay linear, or component
1327
+ name-dedupe would pick an arbitrary winner between two sibling products), not a bug. Entry points that
1328
+ already know the branch disambiguate it by branch name; from the CLI you supply it explicitly.
1329
+
1330
+ > Before 2026-08 the continuation was applied **only to the account a lookup started from**, so a
1331
+ > membership-only account truncated the chain for all of its descendants — they inherited nothing and
1332
+ > resolved trunk while the subscriber itself looked fine. If you are reading an older investigation that
1333
+ > "fixed" this by reparenting a subscriber under its owner, that workaround is no longer needed.
1297
1334
 
1298
1335
  **Same account, two parents, two answers.** An account reached through Product A versus Product B can
1299
1336
  legitimately resolve a different component variant *and* a different data segment. When you investigate
@@ -1466,8 +1503,9 @@ Read the `resolution` block before acting from any checkout — it is the ONE pl
1466
1503
  - `resolution.componentBranch` / `resolution.scopeAccountId` — the branch and the path anchor that made this
1467
1504
  subscriber resolution possible.
1468
1505
  - `resolution.parents` / `resolution.children` / `resolution.relationships` — the account graph, stated once
1469
- and compactly. The full descendant tree is a lookup (`mcp_account_user_admin`, `mcp_account_view`), not a
1470
- committed artifact.
1506
+ and compactly. For quick live hierarchy checks, prefer `mcp_account_user_admin` with `action:"account"` or
1507
+ `action:"hierarchy"`; use `mcp_account_view` only when component inventory or detailed configuration is also
1508
+ needed. The full descendant tree is a lookup, not a committed artifact.
1471
1509
 
1472
1510
  **One branch, one `account-info.json`.** There is exactly one git branch, so when a branch has MORE THAN ONE
1473
1511
  subscriber the refresh is skipped (the sync result says so under `accountInfo.skipped`/`reason`) and the file
@@ -1684,8 +1722,9 @@ Before starting an investigation outside the confirmed current repo:
1684
1722
  5. `mcp_record_listing` — search or filter alerts, events, object logs, or objects when you need to find the suspicious record first.
1685
1723
  6. `mcp_record_view` — drill into suspicious entries for full content.
1686
1724
  7. `mcp_ai_session_search` — if the workflow involves AI, inspect session groupings, prompts, tool definitions, and responses in human-readable form.
1687
- 8. `mcp_system_logs` — correlate via `threadGroupingId` for execution traces.
1688
- 9. `mcp_component_view`/`mcp_component_grep` — explain how the responsible component works.
1725
+ 8. `mcp_performance_trace` — for slow/sluggish reports, start with `action:"slowest"` when you only have a window, or `action:"trace"` when you have a `threadGroupingId`; this is the MCP wrapper for the Diagnostics page request-trace data.
1726
+ 9. `mcp_system_logs` — correlate via `threadGroupingId` for raw log context when the trace needs supporting log lines.
1727
+ 10. `mcp_component_view`/`mcp_component_grep` — explain how the responsible component works.
1689
1728
 
1690
1729
  **Error or Alert Investigation:**
1691
1730
  1. `mcp_record_listing` — search by alert type, content, error text, action, status, `threadGroupingId`, or other exact-match record properties when you do not yet know the record ID.
@@ -1697,6 +1736,40 @@ Before starting an investigation outside the confirmed current repo:
1697
1736
  7. If the context looks missing, redacted, or truncated, consider sanitization rules before concluding data was never present.
1698
1737
  8. If AI behavior is part of the symptom, use `mcp_ai_session_search` and compare the persisted session content against the Agent component implementation and `docs/guides/features/ai-support.md`.
1699
1738
 
1739
+ **Stuck / failed / recovered Event:**
1740
+
1741
+ Do **not** open the Action source first. The platform records each attempt's delivery envelope (which
1742
+ queue delivered it, which delivery attempt this was, which instance owned it, how long it was ever
1743
+ allowed to run) and will classify the failure for you. From a Test or any component:
1744
+
1745
+ ```groovy
1746
+ eventDiagnostics(18838)
1747
+ ```
1748
+
1749
+ Read `classification` before anything else:
1750
+
1751
+ - `APPLICATION_FAILURE` — **the only one that means the bug is in the component.** Read `errorMessage`
1752
+ and the correlated `alerts`, then investigate the component normally.
1753
+ - `ORPHANED_*` / `RECOVERED_*` — the attempt was abandoned. Read `abandonmentCause`:
1754
+ - `REQUEST_TIMEOUT_LIKELY` — used its whole deadline (`timing.deadlineUsed` near `1.0`). The unit of
1755
+ work is too big for one event; the fix is resumable batches, not component logic.
1756
+ - `PROCESS_TERMINATED_LIKELY` — stopped well inside its deadline (`deadlineUsed` near `0`). The worker
1757
+ was killed (memory pressure, restart). Not a logic bug.
1758
+ - `UNKNOWN_NO_DEADLINE_EVIDENCE` — no deadline recorded, so the cause is genuinely unknown. Use
1759
+ `logQuery`; do not assume. Events predating delivery-envelope capture always look like this.
1760
+ - `AWAITING_DELIVERY` — never claimed. A queue/delivery problem.
1761
+ - `IN_FLIGHT_HEALTHY` — still running with a fresh heartbeat. A long Action is not a stuck one; wait.
1762
+
1763
+ `delivery.deliveryAttempt` above `1` means Cloud Tasks had **already** retried this event, so any
1764
+ non-idempotent side effect may have run more than once — check for duplicate records before concluding
1765
+ the component "ran twice for no reason".
1766
+
1767
+ `delivery.threadGroupingId` is the same id everything else uses, so you can pivot straight into
1768
+ `mcp_performance_trace` (`action:"trace"`) or `mcp_system_logs` with it. `logQuery` in the response
1769
+ carries ready-made Cloud Logging filters, including the container-lifecycle and 504 queries.
1770
+
1771
+ Full detail: `docs/guides/features/observability.md` §5b and `events-builder-guide.md`.
1772
+
1700
1773
  ### Presenting Findings
1701
1774
 
1702
1775
  Users are not engineers. When reporting investigation results:
@@ -1867,11 +1940,19 @@ client accounts beneath it:
1867
1940
  type:'CLIENT' # inherits the branch automatically
1868
1941
  ```
1869
1942
 
1870
- > **Put the branch on the account's PRIMARY edge.** Component inheritance follows the *anchored* path, which
1871
- > for an account with a `parentId` is its primary chain. An account that has both a primary edge and a
1872
- > *membership* edge carrying the branch resolves **trunk** by default — the membership edge is never
1873
- > anchored unless a request names it (`--as-account`, `--variant-branch`, or the edge's own host). Descendants
1874
- > then inherit the branch down that primary chain automatically, which is what makes step 4 free.
1943
+ > **Put the branch on the edge that is UNAMBIGUOUSLY on the account's path up — primary or membership.**
1944
+ > Inheritance walks `parentId` and, at an account that has no `parentId`, continues through its *single*
1945
+ > active membership edge. So a subscriber shell with **no `parentId` and one membership edge** to its owner
1946
+ > is a fully supported shape: it resolves the branch, **and so do all of its descendants**. You do not need
1947
+ > to make the subscriber a structural child of its owner.
1948
+ >
1949
+ > What is NOT resolved by default is genuine **ambiguity** — an account with *several* upward links, where
1950
+ > the platform refuses to guess which product it was reached through. That account (and its descendants)
1951
+ > resolve trunk until a request names the path: `--as-account`, `--variant-branch`, or the edge's own host.
1952
+ > An account that has a `parentId` **and** a separate membership edge carrying the branch is this case: the
1953
+ > `parentId` wins, so put the branch on the link the account actually inherits through.
1954
+ >
1955
+ > Either way, descendants inherit the branch down the chain automatically, which is what makes step 4 free.
1875
1956
 
1876
1957
  | Parameter | Required | Description |
1877
1958
  |-----------|----------|-------------|
@@ -1899,6 +1980,7 @@ Query Firestore documents.
1899
1980
  | `aggregation` | no | `{sum: [...], avg: [...], min: [...], max: [...], count: true}` |
1900
1981
  | `dateRanges` | no | `[{field, startDate, endDate}]` (yyyy-MM-dd) |
1901
1982
  | `textSearch` | no | `[{field, prefix}]` for prefix matching |
1983
+ | `fallbackOnMissingIndex` | no | When `true`, a sorted read that fails on a missing composite index retries without sort and returns `warning`, `missingIndexUrl`, and `sortApplied:false`. Use when an unsorted first page is still useful. |
1902
1984
 
1903
1985
  HTTP audit usage notes:
1904
1986
 
@@ -1922,6 +2004,35 @@ remits-cli tool --name "mcp_firestore_search" --input '{"accountId": 37, "collec
1922
2004
  - `response.statusCode`
1923
2005
  - `success`
1924
2006
 
2007
+ ### `mcp_firestore_patch`
2008
+ Guarded exact-document Firestore patch tool for bounded repairs. It defaults to `dryRun:true` and refuses broad
2009
+ updates, wildcard collections, delete/remove operations, protected identity fields, and `_lastModified*` audit
2010
+ fields.
2011
+
2012
+ Use it when the desired repair is mechanical and smaller than rerunning an expensive Action, for example copying
2013
+ canonical fields into stale UI mirror fields. Always dry-run first and include preconditions:
2014
+
2015
+ ```bash
2016
+ remits-cli tool --name mcp_firestore_patch --data-mode prod --input '{
2017
+ "accountId":743,
2018
+ "collection":"statements",
2019
+ "documentId":"20958",
2020
+ "dryRun":true,
2021
+ "preconditions":[
2022
+ {"field":"interchangeOptimization.status","equals":"Calculated"},
2023
+ {"field":"interchangeOptimizationChecked","equals":false}
2024
+ ],
2025
+ "patch":{
2026
+ "interchangeOptimizationChecked":true,
2027
+ "feeBreakdown.interchangeOptimization":{"$copyFrom":"interchangeOptimization"},
2028
+ "feeBreakdown.interchange.optimization":{"$copyFrom":"interchangeOptimization"}
2029
+ }
2030
+ }'
2031
+ ```
2032
+
2033
+ Response fields include `dataMode`, `accountId`, `collection`, `documentId`, `dryRun`, `patchedFields`, and
2034
+ `diff`. Switch to `"dryRun":false` only after the diff and preconditions are exactly what you intended.
2035
+
1925
2036
  ### `mcp_object_activity`
1926
2037
  Object lifecycle timeline — metadata + recent activity.
1927
2038
 
@@ -2009,6 +2120,15 @@ Audit flow: `action:"search"` to find the grouping → `action:"detail"` + `summ
2009
2120
  Run an Action on a target account, with explicit prod/test data mode, optional staged branch resolution, and
2010
2121
  staged-vs-DB provenance in the result.
2011
2122
 
2123
+ Describe the Action first when the input shape is not obvious. This does not execute the Action:
2124
+
2125
+ ```bash
2126
+ remits-cli tool --name mcp_run_action --input '{"controlAction":"describe","accountId":743,"actionId":25,"includeInputSchema":true}' --data-mode prod
2127
+ ```
2128
+
2129
+ The describe response reports `hasInputSchema`, optional `inputSchema`, `inferredInputKeys`, and component
2130
+ provenance. If `hasInputSchema:false`, treat `inferredInputKeys` as a best-effort static scan, not a contract.
2131
+
2012
2132
  Use direct mode only for quick Actions:
2013
2133
 
2014
2134
  ```bash
@@ -2108,6 +2228,41 @@ Query Cloud Run service logs.
2108
2228
  {"node": "remitsAdmin-east5", "timeRange": "4h", "severity": "ERROR"}
2109
2229
  ```
2110
2230
 
2231
+ ### `mcp_performance_trace`
2232
+ Read Remits request traces through the same `traces(...)` DSL that powers the admin Diagnostics "Request
2233
+ traces" panel. Use this before raw log spelunking for slow or sluggish requests because it returns profiled
2234
+ span rollups, component annotations, and retained slow-request summaries directly.
2235
+
2236
+ Common flows:
2237
+
2238
+ ```bash
2239
+ # User only knows it was slow this afternoon
2240
+ remits-cli tool --name mcp_performance_trace --input '{"action":"slowest","lookbackHours":4,"accountId":52,"minMs":2000,"limit":10}' --data-mode prod
2241
+
2242
+ # You have the Diagnostics/request/Object/Event/Alert threadGroupingId
2243
+ remits-cli tool --name mcp_performance_trace --input '{"action":"trace","traceId":"msf1y65n-001","lookbackHours":6,"format":"markdown"}' --data-mode prod
2244
+
2245
+ # Same local-node data as the Diagnostics table
2246
+ remits-cli tool --name mcp_performance_trace --input '{"action":"snapshot","limit":100}' --data-mode prod
2247
+ ```
2248
+
2249
+ | Parameter | Required | Description |
2250
+ |-----------|----------|-------------|
2251
+ | `action` | no | `snapshot`, `slowest`, or `trace`. Default: `snapshot`. |
2252
+ | `traceId` / `threadGroupingId` | for `trace` | Request/grouping id to open across local ring and Cloud Logging. |
2253
+ | `lookbackHours` | no | Cloud Logging window for `trace`/`slowest`. Defaults are 6 and 4 hours. |
2254
+ | `accountId` | no | Account filter for `slowest`. |
2255
+ | `kind` | no | Operation kind filter for `slowest`, e.g. `embeddable`, `action`, `reader`. |
2256
+ | `minMs` | no | Minimum duration for `slowest`. Default: 1500. |
2257
+ | `limit` | no | Result limit. |
2258
+ | `format` / `markdown` | no | Set `format:"markdown"` or `markdown:true` for a rendered trace report. |
2259
+
2260
+ Reading rule: first use `slowest` to get candidate trace ids, then call `trace` on the suspicious id, then use
2261
+ `mcp_system_logs` only if you need surrounding log lines. A trace dominated by `firestore.*`, `http.*`,
2262
+ `gorm.save.*`, or component spans points you at the relevant platform seam or front-stage component. Large
2263
+ unaccounted wall time is itself a finding: check cold compile, queueing, blocking I/O, or missing
2264
+ `measure(...)` instrumentation.
2265
+
2111
2266
  ### `mcp_component_view`
2112
2267
  Read component field content with line numbers.
2113
2268
 
@@ -2582,7 +2737,7 @@ remits-cli data-mode [set test|prod]
2582
2737
  remits-cli components stage [--branch <name>] [--data-mode test|prod] [--json|--verbose]
2583
2738
  remits-cli components status [--branch <name>] [--component-type <type>] [--component-id <id>] [--json|--verbose]
2584
2739
  remits-cli components clear [--branch <name>] [--component-type <type>] [--component-id <id>] [--all] [--json|--verbose] # id alone scopes to one component when unambiguous (ids are type-local; add --component-type if the same id is staged in multiple families); no filter clears the whole branch scope; --all forces the full wipe
2585
- remits-cli components sync [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
2740
+ remits-cli components sync [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
2586
2741
  remits-cli components commit [--message "msg"] [--data-mode test|prod] [--force-tombstones]
2587
2742
  remits-cli components branches [--json] # branches carrying committed variants, with counts + drift
2588
2743
  remits-cli components branch <name> [--json] # one branch: overridden / added / removed, drift flags, subscribers
@@ -2609,6 +2764,36 @@ For tests specifically:
2609
2764
  intentional tombstone overrides. It is rejected on trunk.
2610
2765
  - `--dry-run` is only for `components sync` on non-trunk variant branches. It reports the variant write
2611
2766
  plan without writing rows, caching the sync SHA, or clearing staging.
2767
+ - `--summary` on `components sync --dry-run` prints compact counts, removals/tombstones, skipped items, errors,
2768
+ and warnings instead of the full override/add/remove payload.
2769
+ - **Fail-closed sync gates.** The row below ("Sync response reports unexpected deletes...") tells you to
2770
+ stop *after* an unexpected sync. These flags make sync refuse it up front instead, each exiting
2771
+ non-zero rather than printing a wall of JSON you have to read carefully:
2772
+ - `--changed-only` — fail unless every planned write is a component **this checkout actually edited**.
2773
+ This is the strongest guard against a sync that quietly rewrites components you never touched. It
2774
+ also fails when the checkout is not a git working tree, because "git could not answer" must never
2775
+ be read as "nothing changed".
2776
+ - `--fail-on-removed` — fail if the plan removes or tombstones anything.
2777
+ - `--expected-removed <type:id>` — whitelist the removals you intend (repeatable, or comma-delimited,
2778
+ e.g. `--expected-removed action:5,reader:9`). It **implies** `--fail-on-removed`, so any removal you
2779
+ did not name fails the sync.
2780
+ - `--fail-on-errors` — fail if the server reported any per-component sync error.
2781
+ - `--names-only` — print only `BUCKET type:id name` lines for the planned writes.
2782
+
2783
+ A good default for an unattended promotion is:
2784
+ `remits-cli components sync --dry-run --summary --changed-only --fail-on-errors`
2785
+
2786
+ ### Prod banners and retryable failures
2787
+
2788
+ - Every command that can touch production (`tool`, `test run`, `components sync`) prints a `PROD DATA`
2789
+ banner naming the operation, the resolved account, and the host — and distinguishes a live **WRITE**
2790
+ from a live **READ** and from a **DRY RUN**. If you see a WRITE banner you did not intend, stop.
2791
+ - A tool call that fails **in the platform runtime** rather than in the tool (Groovy reflective dispatch
2792
+ of a runtime-compiled component, an empty connection pool, a Redis reconnect, a lock-wait timeout)
2793
+ now comes back as HTTP `503` with `failureClass: "transient_infrastructure"` and `retryable: true`,
2794
+ and the CLI prints `TRANSIENT INFRASTRUCTURE FAILURE (retryable)`. Retry that **once**; prefer an
2795
+ idempotent input if the tool has side effects. A genuine tool error stays HTTP `500` with
2796
+ `failureClass: "tool_error"` — do **not** retry it, fix the input or the component.
2612
2797
 
2613
2798
  ## Troubleshooting
2614
2799
 
@@ -2630,7 +2815,7 @@ For tests specifically:
2630
2815
  | 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". |
2631
2816
  | Need to know an account's shape (role, type, parents, namespace, branch) | 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`, `relationships`, `componentBranch`, plus top-level `componentBranches`. Never infer structure from the account's name. |
2632
2817
  | Need the account tree below an account, or its users | `mcp_account_user_admin` (`action:'hierarchy'` with a `depth`, or `action:'users'`). `account-info.json` deliberately carries only direct children plus counts. |
2633
- | 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. |
2818
+ | 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. |
2634
2819
  | 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. |
2635
2820
  | A custom hostname resolves to an unexpected account | Compare `resolution.domainName` with `resolvedDomainName` and the edge `domainName`s. An **edge** host wins over the account's own host and additionally supplies the path travelled (which is what makes that edge's branch variants apply). |
2636
2821
  | Need users of an account, or accounts of a user | There is no user MCP tool. Use `mcp_sql_query` on `user` / `user_account` (or `account.users([scope:'children'])` inside a component). Remember user custom fields are stored **per bound account**, so the same person can differ per account. |