@remits/remits-cli 0.1.100 → 0.1.103

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
@@ -16,6 +16,7 @@ On each command run, `remits-cli` checks the npm `latest` version for `@remits/r
16
16
  remits-cli auth --base-url https://your-remits-host --account-id 123
17
17
  remits-cli start
18
18
  remits-cli status
19
+ remits-cli whoami
19
20
  remits-cli stop
20
21
  remits-cli install --skills
21
22
  remits-cli tools
@@ -89,6 +90,7 @@ remits-cli install --skills --overwrite true
89
90
  - Test activity is streamed from websocket `TestSuite` events while final status is also polled from `/cli/test`.
90
91
  - Tool execution writes the full response to a separate file so large payloads do not bloat the session log.
91
92
  - `--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.
93
+ - On `test run`, `--branch <name>` is only the Redis staging namespace. Pair an unused value with `--variant-branch none` when existing staged entries on the real git branch would shadow committed trunk/variant rows. Do not apply that shortcut to `components sync` or `components commit`, where `--branch` names the GitHub branch to reconcile.
92
94
 
93
95
  ## Service, Dashboard, WebSocket, and Tmux Lifecycle
94
96
 
@@ -97,7 +99,9 @@ remits-cli install --skills --overwrite true
97
99
  - Most non-lifecycle commands auto-start the background service if authenticated sessions already exist and no service is running.
98
100
  - Successful `remits-cli auth` also attempts to auto-start the background service.
99
101
  - `remits-cli start` starts a detached background process by default. Use `remits-cli start --foreground true` only when you want to run the daemon in the current terminal.
100
- - `remits-cli status` reports whether the background service is alive and prints the dashboard URL when available.
102
+ - `remits-cli status` reports whether the background service is alive, prints the dashboard URL when available, and prints the resolved session tuple: Account ID, User ID, current git branch, and active data mode.
103
+ - `remits-cli whoami` prints only the resolved session tuple. Use `--base-url`, `--account-id`, and `--data-mode` to prove the exact host/account/lane before running a tool or test.
104
+ - Both print **two** data modes. "Data mode" governs `tool` / `tools` / `token` and falls back to the stored session lane; "Data mode (test run)" governs `test run`, which ignores the session and defaults to `test` unless `--data-mode prod` is passed.
101
105
  - `remits-cli stop` stops the background service, kills the shared tmux session, and clears pane tracking state.
102
106
  - The service starts a localhost dashboard that acts as a control center for remits-cli integration state.
103
107
  - The dashboard shows websocket connection health, topic subscriptions, tmux session/panes, the discovered account repo index, global state files, and per-repo remits-cli files.
package/index.js CHANGED
@@ -278,6 +278,120 @@ function printResolvedBaseUrl(baseUrl) {
278
278
  console.log('Base URL:', normalizeBaseUrl(baseUrl || DEFAULT_BASE_URL));
279
279
  }
280
280
 
281
+ function accountNameFromAccountInfo(info) {
282
+ return (info && (
283
+ (info.resolution && info.resolution.accountName) ||
284
+ (info.repoContext && info.repoContext.accountInfoAccountName) ||
285
+ info.name ||
286
+ info.accountName ||
287
+ (info.account && info.account.name)
288
+ )) || null;
289
+ }
290
+
291
+ function resolveSessionIdentity(cwd, flags = {}) {
292
+ let accountId = null;
293
+ const fromFlag = Number(flags['account-id']);
294
+ if (Number.isFinite(fromFlag) && fromFlag > 0) {
295
+ accountId = fromFlag;
296
+ }
297
+
298
+ const accountInfo = loadAccountInfo(cwd);
299
+ if (!accountId && accountInfo) {
300
+ accountId = Number(accountIdFromAccountInfo(accountInfo));
301
+ try { updateAccountRepoIndex(cwd); } catch (_) { /* best-effort */ }
302
+ }
303
+
304
+ const requestedBaseUrl = flags['base-url'] ? normalizeBaseUrl(flags['base-url']) : null;
305
+ const requestedDataMode = hasExplicitDataModeFlag(flags) ? resolveDataMode(flags, null) : null;
306
+ const session = readSession({
307
+ accountId,
308
+ baseUrl: requestedBaseUrl,
309
+ dataMode: requestedDataMode
310
+ }, {
311
+ strictBaseUrl: Boolean(requestedBaseUrl),
312
+ strictDataMode: Boolean(requestedDataMode)
313
+ });
314
+
315
+ if (!accountId && session && session.accountId) {
316
+ accountId = Number(session.accountId);
317
+ }
318
+
319
+ const dataMode = requestedDataMode || resolveDataMode(flags, session);
320
+ const branchName = flags.branch || flags.branchName || currentBranch(cwd);
321
+ const user = session && session.user ? session.user : {};
322
+ const repoName = accountNameFromAccountInfo(accountInfo);
323
+ const localAccountInfoAccountId = accountInfo ? Number(accountIdFromAccountInfo(accountInfo)) : null;
324
+ const sessionResolutionWarning = session
325
+ ? buildSessionResolutionWarning(accountId, requestedBaseUrl, session)
326
+ : null;
327
+
328
+ return {
329
+ authenticated: Boolean(session && session.token),
330
+ accountId: accountId || (session && session.accountId) || null,
331
+ accountName: (session && session.account && session.account.name) || null,
332
+ localAccountName: repoName,
333
+ localAccountInfoAccountId: Number.isFinite(localAccountInfoAccountId) ? localAccountInfoAccountId : null,
334
+ userId: user.id || session?.userId || null,
335
+ userName: user.name || user.username || user.email || null,
336
+ branchName,
337
+ dataMode,
338
+ // `test run` resolves its lane from the FLAG ONLY (testRunCommand), never the stored session, so that
339
+ // an account whose session is parked on prod cannot have a test run silently follow it. Report both
340
+ // rather than a single number that governs only some commands.
341
+ testRunDataMode: requestedDataMode || DEFAULT_DATA_MODE,
342
+ baseUrl: normalizeBaseUrl((session && session.baseUrl) || requestedBaseUrl || DEFAULT_BASE_URL),
343
+ updatedAt: session ? session.updatedAt : null,
344
+ sessionResolutionWarning
345
+ };
346
+ }
347
+
348
+ function printSessionIdentity(identity, options = {}) {
349
+ if (options.json) {
350
+ console.log(JSON.stringify(identity, null, 2));
351
+ return;
352
+ }
353
+
354
+ if (!identity.authenticated) {
355
+ console.log('Active session: not authenticated');
356
+ console.log('Account ID:', identity.accountId || 'unresolved');
357
+ console.log('User ID:', 'unresolved');
358
+ console.log('Branch:', identity.branchName || 'unresolved');
359
+ console.log('Data mode:', (identity.dataMode || DEFAULT_DATA_MODE) + ' (applies to tool/tools/token)');
360
+ console.log('Data mode (test run):', identity.testRunDataMode || DEFAULT_DATA_MODE);
361
+ console.log('Base URL:', identity.baseUrl);
362
+ return;
363
+ }
364
+
365
+ console.log('Active session:');
366
+ console.log(' Account ID:', identity.accountId);
367
+ if (identity.accountName) {
368
+ console.log(' Account:', identity.accountName);
369
+ }
370
+ if (identity.localAccountName) {
371
+ let localDetails = identity.localAccountInfoAccountId ? 'id ' + identity.localAccountInfoAccountId : '';
372
+ if (identity.localAccountInfoAccountId && identity.accountId &&
373
+ Number(identity.localAccountInfoAccountId) !== Number(identity.accountId)) {
374
+ localDetails += '; command targets account ' + identity.accountId;
375
+ }
376
+ console.log(' Local repo account:', identity.localAccountName + (localDetails ? ' (' + localDetails + ')' : ''));
377
+ }
378
+ console.log(' User ID:', identity.userId || 'unknown');
379
+ if (identity.userName) {
380
+ console.log(' User:', identity.userName);
381
+ }
382
+ console.log(' Branch:', identity.branchName);
383
+ console.log(' Data mode:', identity.dataMode, '(applies to tool/tools/token)');
384
+ // `test run` deliberately ignores the stored session lane and defaults to test, so a session sitting on
385
+ // prod would otherwise read as "prod" here while the next test run went to the test lane. Say so rather
386
+ // than letting the tuple imply a lane it does not govern.
387
+ console.log(' Data mode (test run):', identity.testRunDataMode,
388
+ identity.testRunDataMode === identity.dataMode ? '' : '— test run ignores the session lane; pass --data-mode prod to override');
389
+ console.log(' Base URL:', identity.baseUrl);
390
+ if (identity.updatedAt) {
391
+ console.log(' Session updated:', identity.updatedAt);
392
+ }
393
+ }
394
+
281
395
  // Tools/actions whose names signal they mutate state rather than only reading it. Used solely to make
282
396
  // data-lane banners precise — never to block or permit anything.
283
397
  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;
@@ -5333,8 +5447,28 @@ async function waitForListenerStopped(timeoutMs = 5000) {
5333
5447
  return !isListenerRunning();
5334
5448
  }
5335
5449
 
5336
- async function listenStatusCommand() {
5450
+ async function listenStatusCommand(flags = {}) {
5451
+ const wantsJson = flagEnabled(flags.json);
5337
5452
  const state = readServiceState();
5453
+ const identity = resolveSessionIdentity(process.cwd(), flags);
5454
+
5455
+ if (wantsJson) {
5456
+ console.log(JSON.stringify({
5457
+ service: {
5458
+ running: isListenerRunning(),
5459
+ pid: isListenerRunning() && fs.existsSync(LISTENER_PID_FILE)
5460
+ ? fs.readFileSync(LISTENER_PID_FILE, 'utf8').trim()
5461
+ : null,
5462
+ dashboardUrl: state.dashboardUrl || null,
5463
+ indexedRepos: state.discovery && Number.isFinite(state.discovery.repoCount)
5464
+ ? state.discovery.repoCount
5465
+ : null
5466
+ },
5467
+ session: identity
5468
+ }, null, 2));
5469
+ return;
5470
+ }
5471
+
5338
5472
  if (isListenerRunning()) {
5339
5473
  const pid = fs.readFileSync(LISTENER_PID_FILE, 'utf8').trim();
5340
5474
  console.log('remits-cli service is running (pid: ' + pid + ')');
@@ -5347,6 +5481,14 @@ async function listenStatusCommand() {
5347
5481
  } else {
5348
5482
  console.log('remits-cli service is not running.');
5349
5483
  }
5484
+ printSessionResolutionWarning(identity);
5485
+ printSessionIdentity(identity);
5486
+ }
5487
+
5488
+ async function whoamiCommand(flags = {}) {
5489
+ const identity = resolveSessionIdentity(process.cwd(), flags);
5490
+ printSessionResolutionWarning(identity);
5491
+ printSessionIdentity(identity, { json: flagEnabled(flags.json) });
5350
5492
  }
5351
5493
 
5352
5494
  function npmCommand() {
@@ -5562,7 +5704,7 @@ function printComponentsHelp(subcommand) {
5562
5704
  }
5563
5705
 
5564
5706
  function printTestHelp() {
5565
- 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]');
5707
+ console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
5566
5708
  console.log('');
5567
5709
  console.log('Runs a Test component against the staged/variant world for this checkout.');
5568
5710
  console.log('Examples:');
@@ -5572,6 +5714,10 @@ function printTestHelp() {
5572
5714
  console.log('Notes:');
5573
5715
  console.log(' --names is comma-delimited, so avoid commas in individual test case names.');
5574
5716
  console.log(' --as-account changes the execution account so subscriber branch edges apply.');
5717
+ console.log(' --variant-branch <name> probes a committed variant branch; use "none" or "trunk"');
5718
+ console.log(' to force production/subscription semantics from a variant checkout.');
5719
+ console.log(' --branch changes only the CLI staging namespace for test execution. Pair an unused');
5720
+ console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
5575
5721
  console.log(' --data-mode prod intentionally targets live production data.');
5576
5722
  }
5577
5723
 
@@ -5618,7 +5764,7 @@ async function main() {
5618
5764
 
5619
5765
  // Auto-start the background service if not already running.
5620
5766
  // Skip for lifecycle subcommands and help.
5621
- const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status';
5767
+ const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status' || command === 'whoami';
5622
5768
  const isHelpCmd = !command || command === 'help' || command === '--help' || wantsHelp;
5623
5769
  if (!wantsJson && !isHelpCmd && !isLifecycleCmd && !isListenerRunning()) {
5624
5770
  try {
@@ -5647,7 +5793,8 @@ async function main() {
5647
5793
  console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
5648
5794
  console.log(' remits-cli start [--foreground true] [--port 8787]');
5649
5795
  console.log(' remits-cli stop');
5650
- console.log(' remits-cli status');
5796
+ console.log(' remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
5797
+ console.log(' remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
5651
5798
  console.log(' remits-cli listen [stop|status] [--foreground true] # compatibility alias');
5652
5799
  console.log(' remits-cli data-mode [set test|prod]');
5653
5800
  console.log(' remits-cli install --skills [--target codex|claude|gemini|all] [--overwrite true]');
@@ -5664,7 +5811,7 @@ async function main() {
5664
5811
  console.log(' remits-cli components branch <name> --subscribe <accountId> # make an account resolve this branch');
5665
5812
  console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
5666
5813
  console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
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]');
5814
+ console.log(' remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names name1,name2] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME]');
5668
5815
  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
5816
  console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
5670
5817
  console.log('');
@@ -5759,7 +5906,7 @@ async function main() {
5759
5906
  return;
5760
5907
  }
5761
5908
  if (subcommand === 'status') {
5762
- await listenStatusCommand();
5909
+ await listenStatusCommand(args);
5763
5910
  return;
5764
5911
  }
5765
5912
  await listenCommand(args);
@@ -5777,7 +5924,12 @@ async function main() {
5777
5924
  }
5778
5925
 
5779
5926
  if (command === 'status') {
5780
- await listenStatusCommand();
5927
+ await listenStatusCommand(args);
5928
+ return;
5929
+ }
5930
+
5931
+ if (command === 'whoami') {
5932
+ await whoamiCommand(args);
5781
5933
  return;
5782
5934
  }
5783
5935
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.100",
3
+ "version": "0.1.103",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -166,31 +166,52 @@ 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:
169
+ **Test-data flags are first-class safety evidence.** `Account.testAccount` and `User.testUser` are set by
170
+ the `account(...)` / `user(...)` factories when the execution data lane is `test` (which is how
171
+ `mcp_account_user_admin` creates); prod-data creates leave them false. Two limits worth knowing: a record
172
+ built some other way (`new Account(...).save()` inside a component) is only flagged during an **automated
173
+ Test** in the test lane, so `testAccount:false` on something you created in test mode means it is a real
174
+ account; and updating an existing real account/user in test mode does not convert it into test data,
175
+ though its Firestore extension-field writes still go to the test lane. `Object.testMode` /
176
+ `Event.testMode` / `Alert.testMode` (and `testMode` inside test-lane Audit documents) identify lifecycle
177
+ rows in the test data lane. Agent-facing surfaces expose these fields:
175
178
 
176
179
  - `account-info.json`, `account-hierarchy.json`, and `mcp_account_view`: `resolution.testAccount` for the
177
180
  described account, and `testAccount` on returned hierarchy nodes.
178
181
  - `mcp_account_user_admin`: `testAccount` on `hierarchy`, `account`, and `account_create` results;
179
182
  `testUser` on `users` / `user` results.
180
- - `mcp_record_listing` / `mcp_record_view`: `testMode` on `object`, `event`, and `alert` records.
183
+ - `mcp_record_listing`: top-level `dataMode` (the lane it searched — listings are **filtered** by lane,
184
+ so `totalItems:0` in the wrong lane reads exactly like "no such record"), plus `testMode` per record.
185
+ - `mcp_record_view`: `record.dataMode` (the lane read in) next to `record.testMode` (the lane the row
186
+ belongs to). This one loads **by id and does not filter**, so those two can legitimately disagree —
187
+ and when they do, that is the finding.
181
188
  - `mcp_object_activity`: top-level `dataMode`, `object.testMode`, and `testMode` on Event/Alert
182
189
  timeline entries.
183
190
  - `mcp_event_diagnostics`: top-level `dataMode` and `result.event.testMode`.
184
- - `remits-cli token inspect`: owner `account.testAccount` and `user.testUser`, plus token `dataMode`.
191
+ - `remits-cli token inspect`: owner `account.testAccount` and `user.testUser`, plus a `safety` block
192
+ carrying `dataMode`, `dataModeDeclared`, and an explicit warning when the token does not declare one.
193
+ - `remits-cli whoami`: the resolved account / user / branch / lane / host tuple for the **next tool
194
+ call**. It does not describe `remits-cli test run`, which ignores the stored session lane — see below.
185
195
 
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.
196
+ Two surfaces deliberately have **no** lane flags, and both mislead if you forget it:
197
+
198
+ - **`mcp_sql_query` reads MySQL directly and is lane-blind.** `object` / `event` / `alert` come back with
199
+ **both lanes mixed**, and `user` / `account` with test fixtures mixed into real records. Filter
200
+ explicitly — `test_mode = 0`, `test_user = 0`, `test_account = 0` — or use the purpose-built tool.
201
+ - **Persisted AI session rows** store no durable per-grouping `testMode` / `dataMode`. Use
202
+ `mcp_ai_session_search.dataMode` as the current execution lane only, never as proof of the historical
203
+ grouping's lane.
189
204
 
190
205
  If any of those fields contradict the lane you intended, stop and rerun the command with an explicit
191
206
  `--data-mode test` or `--data-mode prod`. Never infer prod/test from an account name, URL, branch name, or
192
207
  the mere existence of a created account.
193
208
 
209
+ **A tool's `dataMode` input never widens the lane.** Tools that accept a `dataMode` argument clamp it
210
+ against the lane the *command* was launched with: it may narrow `prod` -> `test`, never escalate
211
+ `test` -> `prod`. So `--data-mode test --input '{"dataMode":"prod"}'` stays in **test**, and the response's
212
+ `dataMode` — not your input — is the truth. To reach prod data, pass `--data-mode prod` on the command
213
+ line. (Under MCP, the launch lane is the caller's own `dataMode` argument, which defaults to `prod`.)
214
+
194
215
  Use `remits-cli` for everything else: staging changes, running tests, generating embeddable tokens,
195
216
  committing work, and diagnosing production issues.
196
217
 
@@ -297,8 +318,11 @@ Key implications:
297
318
  `git add -A && git commit && git push`, `remits-cli components sync`, `git pull --ff-only`.
298
319
 
299
320
  **Create a new component:** add `new_Name.groovy` (+ `.json` / `.meta.yml` as applicable). Sync assigns the
300
- durable id and renames the files. Do not create a direct database row to work around an id/name mismatch, and
301
- never create a replacement for a component that was unexpectedly deleted or renumbered.
321
+ durable id and renames the files. Standalone Prompts live in `components/prompts/new_Name.md`, and their
322
+ sidecar must include `name`, `summary`, `description`, and `purpose` (usually `CUSTOM`). AGENT prompts do
323
+ not live there; they are the `.md` sidecar beside the Utility in `components/agents/`. Do not create a
324
+ direct database row to work around an id/name mismatch, and never create a replacement for a component
325
+ that was unexpectedly deleted or renumbered.
302
326
 
303
327
  **Delete a component (deliberate only):** remove **all** of that component's files from the repo, confirm via
304
328
  `git status` that only those files are gone, then sync — the delete phase removes exactly that DB row. Deletion
@@ -811,6 +835,8 @@ components/embeddables/new_MerchantPortal.groovy # source
811
835
  components/embeddables/new_MerchantPortal.html # markup
812
836
  components/embeddables/new_MerchantPortal.js # client script
813
837
  components/embeddables/new_MerchantPortal.meta.yml # metadata sidecar
838
+ components/prompts/new_PricingReviewPrompt.md # standalone Prompt body
839
+ components/prompts/new_PricingReviewPrompt.meta.yml # standalone Prompt metadata
814
840
  ```
815
841
 
816
842
  **Do NOT put an `id:` in a new component's sidecar.** `id` is what links a sidecar to an *existing*
@@ -833,6 +859,10 @@ mermaid: |
833
859
  A[Request] --> B[Load documents]
834
860
  ```
835
861
 
862
+ For `components/prompts/new_*.meta.yml`, include `description` and `purpose: CUSTOM`; missing
863
+ `description` fails trunk validation, and missing/mismatched `purpose` leaves a post-promotion Prompt
864
+ overlay instead of pruning cleanly.
865
+
836
866
  > **Staging creates nothing in the database, so a `new_` component has no id yet — address it BY NAME.**
837
867
  > `remits-cli test run --test "My Suite"`, not `--test <id>`. Component-to-component resolution and
838
868
  > request-level addressing are name-based too; the component guides cover those. After a trunk sync the
@@ -1322,6 +1352,18 @@ remits-cli token --path embeddable/index/50 --as-account 101 # subscriber -> v
1322
1352
  remits-cli token --path embeddable/index/50 # owner -> trunk
1323
1353
  ```
1324
1354
 
1355
+ If `components status` shows staged entries that you cannot safely clear, isolate verification in an
1356
+ unused staging namespace instead of deleting someone else's cache:
1357
+
1358
+ ```bash
1359
+ remits-cli test run --test "Invoice Tests" --branch promotion-check-empty --variant-branch none --as-account 101
1360
+ remits-cli token --path embeddable/index/50 --branch promotion-check-empty --variant-branch none --as-account 101
1361
+ ```
1362
+
1363
+ Here `--branch` is only the CLI staging-cache namespace, and `--variant-branch none` keeps runtime
1364
+ resolution on production/subscription semantics. Do **not** use this pattern with `components sync` or
1365
+ `components commit`: for those commands `--branch` is the GitHub branch to reconcile.
1366
+
1325
1367
  ### The SDLC is identical on a variant branch
1326
1368
 
1327
1369
  ```bash
@@ -1337,7 +1379,9 @@ remits-cli components sync # writes ComponentVariant overlays ONLY
1337
1379
 
1338
1380
  Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
1339
1381
  for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
1340
- `features/subscriber-branch-promotions.md`.
1382
+ `features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
1383
+ `remits-cli components sync --summary --timeout-ms 300000`; the platform may need longer than the default
1384
+ 60 seconds to create rows, push rename/meta commits, and regenerate account metadata.
1341
1385
 
1342
1386
  ### Subscribing, unsubscribing, retiring
1343
1387
 
@@ -1662,8 +1706,23 @@ intended and `testAccount:false` for real provisioning. `testAccount:true` means
1662
1706
  account, even if the name and structure look correct.
1663
1707
 
1664
1708
  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.
1709
+ `testAccount:true` for **newly created** accounts (or `testUser:true` for created users).
1710
+
1711
+ Read `reusedExisting` before reading anything into the flag. `account_create` is find-or-create, and a
1712
+ **test**-lane create can legitimately match a **real** account: real accounts are visible in both lanes,
1713
+ so a rehearsal for a name that already exists in prod returns `reusedExisting:true` /
1714
+ `testAccount:false` and changes nothing. That is correct reuse, not a lane error. Only
1715
+ `reusedExisting:false` with `testAccount:false` in a test rehearsal means the lane was not the one you
1716
+ intended. (The prod direction is not symmetrical: a prod-lane create never resolves onto a test
1717
+ CLIENT/PROVIDER account, so the same name can exist once per lane.)
1718
+
1719
+ **A test-lane account does not get the `code` the prod one will.** `code` is derived from `name` and is
1720
+ globally unique, so a test-lane create with no explicit `code` is assigned `test_<code>_<parentId>`.
1721
+ A namespace resolves as `databaseName ?: platform.code ?: code`, so a rehearsal **does not prove the
1722
+ storage namespace** the real create will land in unless you set `databaseName` explicitly. Conversely,
1723
+ passing an explicit `code` in a test rehearsal opts out of the prefix, and the later prod create then
1724
+ fails on `code unique:true` — as it also will against a test account created before this rule existed.
1725
+ Check the existing account's `code` before assuming a name is free.
1667
1726
 
1668
1727
  Account/User schema `fields` are Firestore-backed extension fields. Their physical storage follows the
1669
1728
  same data lane as the tool call: `--data-mode test` writes under `testing/<resolvedDatabaseName>/...`, while
@@ -2326,6 +2385,23 @@ SELECT parent_id, is_primary, branch_name, database_name, domain_name, active
2326
2385
  FROM account_relationship WHERE account_id = ?;
2327
2386
  ```
2328
2387
 
2388
+ **This tool is lane-blind — filter the data lane yourself.** Every other record surface segments test from
2389
+ prod data for you; raw SQL does not. `object`, `event`, and `alert` carry a `test_mode` column, `user`
2390
+ carries `test_user`, and `account` carries `test_account`, and an unfiltered query returns **both lanes
2391
+ mixed** — so "who has access to this account" silently includes throwaway test users, and a row count
2392
+ silently includes test fixtures. Add the predicate explicitly:
2393
+
2394
+ ```sql
2395
+ -- prod lane only (legacy rows predate the column, so NULL counts as prod)
2396
+ SELECT id, name, status FROM object
2397
+ WHERE account_id = ? AND (test_mode IS NULL OR test_mode = 0);
2398
+
2399
+ -- real users of an account, excluding test fixtures
2400
+ SELECT u.id, u.username, u.enabled FROM user u
2401
+ JOIN user_account ua ON ua.user_id = u.id
2402
+ WHERE ua.account_id = ? AND (u.test_user IS NULL OR u.test_user = 0);
2403
+ ```
2404
+
2329
2405
  Prefer the purpose-built tools when one fits — they apply account scoping, data-mode segmentation, and
2330
2406
  resolution awareness that raw SQL does not. Reach for SQL when nothing else models the question.
2331
2407
 
@@ -2448,6 +2524,17 @@ remits-cli sessions remove --account-id 42 --base-url http://localhost:8080
2448
2524
 
2449
2525
  You can work in multiple account repos simultaneously across different terminal windows — each uses its own session. You can also be authenticated against different base URLs (e.g., localhost for development and production) for the same account at the same time.
2450
2526
 
2527
+ Use `remits-cli whoami` when you need a compact proof of the active target before a sensitive operation.
2528
+ It prints the resolved Account ID, User ID, current git branch, data mode, and base URL. `remits-cli status`
2529
+ prints the same session tuple after the service/dashboard status. Pass `--base-url`, `--account-id`, and
2530
+ `--data-mode` when host or lane matters; do not infer those values from the repo directory or account name.
2531
+
2532
+ **The reported data mode describes the next `tool` / `tools` / `token` call, not `test run`.** It falls back
2533
+ to the stored session lane, whereas `remits-cli test run` deliberately ignores that and defaults to `test`
2534
+ unless you pass `--data-mode prod` explicitly. So `whoami` can read `prod` while a test run goes to the test
2535
+ lane — which is the safe direction, but not the one you would predict from the output alone. `whoami` says
2536
+ so in its own output; when a test run genuinely needs prod data, pass the flag.
2537
+
2451
2538
  ## Persistent Service, Control Center, and Agent Dispatch
2452
2539
 
2453
2540
  The current mental model is a **background remits-cli service**, not just a listener.
@@ -2463,6 +2550,7 @@ That service does three jobs:
2463
2550
  remits-cli start
2464
2551
  remits-cli start --foreground true
2465
2552
  remits-cli status
2553
+ remits-cli whoami
2466
2554
  remits-cli stop
2467
2555
  ```
2468
2556
 
@@ -2586,7 +2674,8 @@ remits-cli sessions [list|remove] [--account-id ID]
2586
2674
  remits-cli config [set] [--agent claude|codex|gemini]
2587
2675
  remits-cli start [--foreground true] [--port 8787]
2588
2676
  remits-cli stop
2589
- remits-cli status
2677
+ remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]
2678
+ remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]
2590
2679
  remits-cli listen [stop|status] [--foreground true] # compatibility alias
2591
2680
  remits-cli data-mode [set test|prod]
2592
2681
  remits-cli components stage [--branch <name>] [--data-mode test|prod] [--json|--verbose]
@@ -2601,7 +2690,7 @@ remits-cli components branch <name> --subscribers [--json]
2601
2690
  remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] # make an account resolve this branch
2602
2691
  remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
2603
2692
  remits-cli components branch <name> --retire [--force] # delete the branch's overlays
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>]
2693
+ remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a,b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
2605
2694
  remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
2606
2695
  remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
2607
2696
  remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
@@ -2616,6 +2705,10 @@ For tests specifically:
2616
2705
  (*"what does customer X get?"*); `--variant-branch <name>` probes a branch from any checkout
2617
2706
  (*"what does branch Y look like?"*), and `--variant-branch none` forces production/subscription semantics.
2618
2707
  Omit both and the working tree decides — see "Branched Component Variants".
2708
+ - `--branch <stagingScope>` on `test run` selects the Redis staging namespace only. It is useful with
2709
+ `--variant-branch none` when an existing staged cache on the real git branch would shadow committed trunk
2710
+ or variant rows. On `components sync` / `commit`, `--branch` is different: it names the GitHub branch to
2711
+ reconcile.
2619
2712
  - `--force-tombstones` is only for non-trunk variant syncs, when missing trunk component files are known,
2620
2713
  intentional tombstone overrides. It is rejected on trunk.
2621
2714
  - `--dry-run` is only for `components sync` on non-trunk variant branches. It reports the variant write
@@ -2687,6 +2780,7 @@ For tests specifically:
2687
2780
  | A variant Test suite fails wholesale, asserting trunk where you expect a variant | You are almost certainly running it from a **variant checkout**: `variantBranch` outranks every subscription, so the suite's own fixture accounts resolve YOUR branch. Re-run from trunk or with `--variant-branch none` before treating it as a regression. |
2688
2781
  | `components sync` on a branch says "No changes detected" but trunk has moved | Re-run it; a trunk sync now invalidates the branch's cached verdict. If it still skips, the branch genuinely matches trunk - check `components branch <name>` for what is actually stored. |
2689
2782
  | After promoting a `new_*` component to trunk, the branch still shows it as `added` | You have not re-synced the branch since the trunk sync. Merge trunk into the branch and `components sync`: the file adopts the promoted id and, if unchanged, removes its own overlay. If the branch carries BOTH `new_Foo.*` and `<id>_Foo.*`, the id file wins and the `new_` one is reported `skipped: superseded` - delete it. |
2783
+ | After promoting a standalone Prompt, the branch still shows `OVERRIDDEN prompt:<id>` with only `purpose` changed | The trunk Prompt row does not match repo metadata. Ensure the Prompt sidecar has `description` and `purpose: CUSTOM`, run a platform build that imports Prompt `purpose`, re-sync trunk, merge back, and re-sync the branch. |
2690
2784
  | A branch preview reports a `removed` component nobody deleted | Check whether that component has a file on **trunk**. A DB row with no trunk file is missing from every branch, so it reads as a removal everywhere (and the trunk sync tries to hard-delete it each run). Repair trunk, not the branch. |
2691
2785
  | `--as-account <id>` resolves trunk, or 404s | The account probably has several edges each carrying a branch, so the anchor is ambiguous and the platform refuses to guess. Name the branch with `--variant-branch <name>`, and confirm the edge with `components branch <name> --subscribers`. |
2692
2786
  | A branch variant is reported DRIFTED | The origin component changed after the variant was cut, so the branch is based on a stale version. `remits-cli components branch <name> --diff <id> --component-type <kind>` to compare, then reconcile the branch. |