@remits/remits-cli 0.1.125 → 0.1.126

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
@@ -64,6 +64,9 @@ remits-cli install --skills --overwrite true
64
64
 
65
65
  ## How It Works
66
66
 
67
+ - `cli/index.js` opens with a line-numbered code Table of Contents. Section entries are generated from
68
+ the `##` section marker comments in the file. Run `npm run prepare:index-toc` after moving sections;
69
+ `npm test` checks that the prepared line numbers still point at the real headings.
67
70
  - `components stage` has three modes, and the difference decides what a run in the lane resolves:
68
71
  - **default (full snapshot)** — uploads the whole repository manifest and reconciles the lane to it, so stale aliases from prior stages are removed. Correct as a complete snapshot and as a "what is stale here?" reset; a poor progress signal, because the lane then holds every component in the repo.
69
72
  - **`--workset`** — uploads only the components git reports changed and reconciles the lane to exactly those. The normal iteration mode. `--changed-only --replace-lane` is the explicit spelling.
@@ -89,7 +92,7 @@ remits-cli install --skills --overwrite true
89
92
  - `remits-cli start` scans the machine for `account-info.json` files and rebuilds `~/.remits-cli/account-repos.json` before bringing up the background service.
90
93
  - Front-stage guides are **not** bundled with the CLI npm package. For account-work commands (`components`, `test`, `token`, `tools`, `tool`) and on `auth`, `remits-cli` downloads the latest guides from the authenticated `/cli/guides` endpoint and writes them into the current account repo: root `platform-overview.md` / `development-guide.md`, `guides/*.md`, `guides/features/*.md`, and the agent-guidance files `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` (from `docs/front-stage/remits-components-zip-readme.md`).
91
94
  - Guide sync requires authentication. If no valid session exists and the terminal is interactive, the CLI auto-authenticates first; in non-interactive contexts (CI, agent sandboxes) it skips silently so guides are only ever delivered to users who can authenticate to the platform.
92
- - During account-repo discovery, `remits-cli` also overwrites the installed remits-cli `SKILL.md` for `claude`, `codex`, and `gemini` so local agents stay on the latest packaged instructions.
95
+ - On every command, `remits-cli` refreshes the installed remits-cli `SKILL.md` and reference files for `claude`, `codex`, and `gemini` so local agents stay on the latest packaged instructions.
93
96
  - The platform repo's `docs/front-stage/` tree remains the single source of truth; the platform serves it from the classpath, so published CLI versions never carry guide content.
94
97
  - Authenticated users also get the **core Remits platform repo** locally. After guide sync (and on `auth` / discovery), `remits-cli` first reuses any existing local copy it already knows about, can detect directly (`REMITS_PLATFORM_DIR`, the running CLI source in dev, `~/remits`), or can discover under the configured scan roots; only if none is found does it clone `git@github.com:tmillhouse/remits.git` (default `~/remits`, override with `REMITS_PLATFORM_DIR`; cloning only runs in an interactive terminal). On each authenticated run it also fast-forwards the repo (`git pull --ff-only`, once per process) so back-stage analysis runs against current code — skipped automatically if the working tree is dirty, so local work is never clobbered. The repo is tracked in `~/.remits-cli/account-repos.json` under the reserved `platform` entry so agents can analyze back-stage seams and open a fix PR when a front-stage failure turns out to be platform brittleness.
95
98
  - Branch defaults to the current local git branch.
@@ -106,7 +109,7 @@ remits-cli install --skills --overwrite true
106
109
  - `--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.
107
110
  - 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.
108
111
 
109
- ## Service, Dashboard, WebSocket, and Tmux Lifecycle
112
+ ## Service, Dashboard, and WebSocket Lifecycle
110
113
 
111
114
  - The primary lifecycle commands are `remits-cli start`, `remits-cli stop`, and `remits-cli status`.
112
115
  - `remits-cli listen`, `remits-cli listen stop`, and `remits-cli listen status` still work as compatibility aliases.
@@ -116,16 +119,14 @@ remits-cli install --skills --overwrite true
116
119
  - `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.
117
120
  - `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.
118
121
  - 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. The test-run line also reports its source, such as `cliDefault` or `explicitFlag`.
119
- - `remits-cli stop` stops the background service, kills the shared tmux session, and clears pane tracking state.
122
+ - `remits-cli stop` stops the background service.
120
123
  - The service starts a localhost dashboard that acts as a control center for remits-cli integration state.
121
- - 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.
122
- - If websocket connections are disconnected or the tmux session is missing, the dashboard exposes actions to reconnect websockets or recreate the tmux session.
124
+ - The dashboard shows websocket connection health, topic subscriptions, registered agents, live and recent worker runs, the discovered account repo index, global state files, and per-repo remits-cli files.
125
+ - If websocket connections are disconnected, the dashboard exposes an action to reconnect them.
123
126
  - The service groups sessions by `baseUrl` so one websocket connection can service multiple authenticated accounts on the same Remits environment.
124
127
  - Websocket subscriptions are deduplicated by user topic. If multiple authenticated accounts share the same websocket topic, the listener subscribes once and maps that topic back to all related accounts.
125
- - The daemon keeps STOMP heartbeats enabled and runs a maintenance watchdog that recreates unhealthy websocket connections and recreates the shared tmux session if it disappears.
128
+ - The daemon keeps STOMP heartbeats enabled and runs a maintenance watchdog that recreates unhealthy websocket connections.
126
129
  - If the machine sleeps, the network drops, or auth sessions change while the daemon is already running, the service now attempts to reconnect and resubscribe automatically once connectivity returns.
127
- - Incoming websocket messages of type `remits-cli` are dispatched into dedicated tmux windows so the selected agent can continue working interactively with a full terminal view.
128
- - The listener creates a shared tmux session named `remits-listener`.
129
130
  - Support tickets are the primary unit of dispatched work. A serving agent launches one fresh worker process per routed ticket.
130
131
  - Agent sessions register themselves with `remits-cli agent register` or `remits-cli agent serve`; `serve` also starts the local supervisor.
131
132
  - Tickets are delivered by durable routing on the ticket record, then the agent asks for routed work with `remits-cli agent work`.
@@ -179,12 +180,14 @@ There are two separate state areas:
179
180
  PID for the background remits-cli service process.
180
181
  - `~/.remits-cli/service-state.json`
181
182
  Dashboard URL, repo scan summary, and websocket state snapshot for the running service.
182
- - `~/.remits-cli/dispatch-panes.json`
183
- Persistent map of support ticket routing key to the active tmux pane id for that ticket's dedicated window. `ticketId` is preferred, with legacy `taskId` fallback.
184
183
  - `~/.remits-cli/account-repos.json`
185
184
  Index of known account repositories on this machine, rebuilt by `remits-cli start` via account-info discovery and also refreshed when commands run inside an account repo.
186
- - `~/.remits-cli/tmux-activity.log`
187
- Human-readable global activity log for listener and tmux lifecycle events.
185
+ - `~/.remits-cli/agents.json`
186
+ Agent sessions registered from this machine, including the process each one is anchored to.
187
+ - `~/.remits-cli/workers.json`
188
+ Active and recent ticket worker state for serving agents.
189
+ - `~/.remits-cli/activity.log`
190
+ Human-readable global activity log for service lifecycle, websocket events, agent registration, and ticket routing.
188
191
 
189
192
  ## Control Center
190
193
 
@@ -192,21 +195,20 @@ There are two separate state areas:
192
195
  - Open that page in a browser to inspect the full local remits-cli integration state without manually opening JSON files.
193
196
  - The page refreshes automatically and includes the latest global state files plus per-repo `account-info.json`, local tools snapshot, and current session log tail for every indexed account repo. Large account configuration fields live in `account-configurations.json` and should be opened only when needed.
194
197
 
195
- ## Tmux Activity Log
198
+ ## Activity Log
196
199
 
197
- - `~/.remits-cli/tmux-activity.log` is the quickest way to understand what the listener is doing.
198
- - It records timestamped lifecycle events such as listener start/stop, websocket connect/disconnect, topic subscription, dispatch receipt, pane creation, follow-up delivery, pane replacement, and dispatch failures.
199
- - Prompt bodies are not written verbatim to this log. The log stores summarized metadata such as `accountId`, `ticketId`, any legacy `taskId`, available keys, and prompt length.
200
+ - `~/.remits-cli/activity.log` is the quickest way to understand what the service, websocket clients, and local agents are doing.
201
+ - It records timestamped lifecycle events such as service start/stop, websocket connect/disconnect, topic subscription, agent registration/heartbeat, ticket routing, worker launch, and worker completion.
202
+ - Prompt bodies are not written verbatim to this log. The log stores summarized metadata such as `accountId`, `ticketId`, available keys, and prompt length.
200
203
  - Typical inspection commands:
201
204
 
202
205
  ```bash
203
- tail -f ~/.remits-cli/tmux-activity.log
204
- tail -n 200 ~/.remits-cli/tmux-activity.log
206
+ tail -f ~/.remits-cli/activity.log
207
+ tail -n 200 ~/.remits-cli/activity.log
205
208
  ```
206
209
 
207
210
  ## Operational Notes
208
211
 
209
- - `tmux` must be installed for agent dispatch to work. Without it, websocket dispatch is received but agent panes cannot be created.
210
212
  - In sandboxed local agent environments, `remits-cli` network calls may fail with `ENOTFOUND`, `EAI_AGAIN`, `ECONNREFUSED`, `EPERM`, or similar errors even when dispatch worked correctly. That means the command needs escalated permissions or must be run outside the sandbox.
211
213
  - If the platform returns a 500 or other unexpected server-side failure, stop normal task execution and escalate to a Remits system admin. Agents should not invent workarounds for platform faults.
212
214
  - Recommended durable update flow for agents: `components stage --workset` for testing, then `git add/commit/push`, then `remits-cli components sync --safe`, then `git pull --ff-only` to confirm platform-generated files like `account-info.json`. To verify the COMMITTED variant rather than your staging, run `components clear --all` first — staged entries still win for CLI-scoped runs.
package/index.js CHANGED
@@ -1,5 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /*
4
+ ## Table of Contents
5
+
6
+ - L22 Runtime Bootstrap And Shared State
7
+ - L116 Sessions, Account Resolution, And Production Guards
8
+ - L595 Local State, Workspaces, And Verification Evidence
9
+ - L1234 Account Repos, Guide Sync, And Platform Repo
10
+ - L1892 Component Discovery And HTTP Logging
11
+ - L2382 Skill Delivery And TOC Resolution
12
+ - L2692 Auth And Component Staging
13
+ - L3280 Component Summaries, Status, And Sync Gates
14
+ - L4991 Branches, Promotion, Commit, And Test Runs
15
+ - L5988 Tokens, Tools, Verification, And Config
16
+ - L6889 Service Dashboard And WebSocket Listener
17
+ - L8984 Agent And Ticket Workflows
18
+ - L12042 Help, Auto Update, And Command Dispatch
19
+ */
20
+
21
+ /*
22
+ ## Runtime Bootstrap And Shared State
23
+ */
24
+
3
25
  const fs = require('fs');
4
26
  const path = require('path');
5
27
  const os = require('os');
@@ -90,6 +112,10 @@ const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names'
90
112
  // a flag, never a value, and these aliases map it to its long name. Values like `-5` or `-abc` are untouched.
91
113
  const SHORT_FLAG_ALIASES = { m: 'message', h: 'help' };
92
114
 
115
+ /*
116
+ ## Sessions, Account Resolution, And Production Guards
117
+ */
118
+
93
119
  function isFlagToken(token) {
94
120
  return typeof token === 'string' && (token.startsWith('--') || /^-[a-zA-Z]$/.test(token));
95
121
  }
@@ -565,6 +591,10 @@ function resolveDataMode(flags, session) {
565
591
  return DEFAULT_DATA_MODE;
566
592
  }
567
593
 
594
+ /*
595
+ ## Local State, Workspaces, And Verification Evidence
596
+ */
597
+
568
598
  function ensureDir(dirPath) {
569
599
  if (!fs.existsSync(dirPath)) {
570
600
  fs.mkdirSync(dirPath, { recursive: true });
@@ -1083,7 +1113,11 @@ function buildCommandWorld(response, fallback = {}) {
1083
1113
  branchName: response && response.branchName || fallback.branchName,
1084
1114
  workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
1085
1115
  stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
1086
- componentBranch: resolution.componentBranch || response && response.variantBranch || fallback.componentBranch,
1116
+ // `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
1117
+ // subscription); a bare `variantBranch` is null whenever the subscription decided.
1118
+ componentBranch: resolution.componentBranch ||
1119
+ response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
1120
+ fallback.componentBranch,
1087
1121
  variantId: resolution.variantId || fallback.variantId,
1088
1122
  sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
1089
1123
  };
@@ -1196,6 +1230,10 @@ function loadAccountInfo(cwd) {
1196
1230
  }
1197
1231
  }
1198
1232
 
1233
+ /*
1234
+ ## Account Repos, Guide Sync, And Platform Repo
1235
+ */
1236
+
1199
1237
  function readAccountRepoIndex() {
1200
1238
  if (!fs.existsSync(ACCOUNT_REPO_INDEX_FILE)) {
1201
1239
  return {};
@@ -1850,6 +1888,10 @@ function resolveSessionContext(cwd, flags) {
1850
1888
  };
1851
1889
  }
1852
1890
 
1891
+ /*
1892
+ ## Component Discovery And HTTP Logging
1893
+ */
1894
+
1853
1895
  /**
1854
1896
  * Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
1855
1897
  * sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
@@ -2336,6 +2378,10 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
2336
2378
  }
2337
2379
  }
2338
2380
 
2381
+ /*
2382
+ ## Skill Delivery And TOC Resolution
2383
+ */
2384
+
2339
2385
  // ---------------------------------------------------------------------------
2340
2386
  // Skill delivery
2341
2387
  //
@@ -2465,6 +2511,14 @@ function resolveTocEntry(line, byAnchor, byTitle) {
2465
2511
  return formatTocEntry(legacyEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
2466
2512
  }
2467
2513
 
2514
+ // Prepared form. This makes generated TOCs idempotent: a later package/build pass can re-derive the
2515
+ // current line numbers from the emitted heading text instead of preserving stale numbers.
2516
+ const preparedEntry = /^(\s*)[-*]\s+L\d+\s\s(.+?)\s*$/.exec(line);
2517
+ if (preparedEntry) {
2518
+ const title = preparedEntry[2];
2519
+ return formatTocEntry(preparedEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
2520
+ }
2521
+
2468
2522
  return null;
2469
2523
  }
2470
2524
 
@@ -2634,6 +2688,10 @@ async function installSkillsCommand(flags) {
2634
2688
  }
2635
2689
  }
2636
2690
 
2691
+ /*
2692
+ ## Auth And Component Staging
2693
+ */
2694
+
2637
2695
  async function authCommand(flags) {
2638
2696
  const cwd = process.cwd();
2639
2697
  ensureLocalState(cwd);
@@ -3218,6 +3276,10 @@ async function stageOrRefuse(api, cwd, payload) {
3218
3276
  }
3219
3277
  }
3220
3278
 
3279
+ /*
3280
+ ## Component Summaries, Status, And Sync Gates
3281
+ */
3282
+
3221
3283
  /** What the account's rules said about this stage or commit — including "they could not be read". */
3222
3284
  function printComponentPolicy(response) {
3223
3285
  const policy = response && response.policy;
@@ -3499,9 +3561,7 @@ function printLanesSummary(response, flags) {
3499
3561
  }
3500
3562
 
3501
3563
  function printStagingLaneRow(lane) {
3502
- const world = lane.onTrunk === true ? 'trunk'
3503
- : lane.onTrunk === false ? 'variant'
3504
- : 'unknown-world';
3564
+ const world = laneWorldLabel(lane);
3505
3565
  const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : ' [shared]';
3506
3566
  const workset = lane.worksetKnown && lane.worksetCountAsOf != null
3507
3567
  ? ' workset ' + lane.worksetCountAsOf
@@ -3598,10 +3658,7 @@ function printAccountLanes(response) {
3598
3658
  console.log('');
3599
3659
  console.log('Other staging lanes on this ACCOUNT (as of the last stage/clear in each):');
3600
3660
  lanes.forEach((lane) => {
3601
- // null onTrunk means the owner could not be resolved. Say "unknown" rather than guessing a world.
3602
- const world = lane.onTrunk === true ? 'trunk'
3603
- : lane.onTrunk === false ? 'variant branch'
3604
- : 'branch world unknown';
3661
+ const world = laneWorldLabel(lane);
3605
3662
  const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : '';
3606
3663
  const who = lane.userEmail ? ' ' + lane.userEmail : '';
3607
3664
  const ttl = lane.expiresInSeconds != null
@@ -3617,7 +3674,21 @@ function printAccountLanes(response) {
3617
3674
  });
3618
3675
  console.log(' (* = this command\'s lane)');
3619
3676
  console.log(' A variant-branch lane layers over that branch\'s ComponentVariant overlays, and a commit');
3620
- console.log(' from it writes overlays — never the trunk rows a trunk lane commits to.');
3677
+ console.log(' from it writes overlays — never the trunk rows a trunk lane commits to. A feature-branch lane');
3678
+ console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
3679
+ }
3680
+
3681
+ // The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
3682
+ // resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
3683
+ // non-trunk — never guessed to be a variant branch. Pure.
3684
+ function laneWorldLabel(lane) {
3685
+ if (!lane || lane.onTrunk == null) return 'branch world unknown';
3686
+ if (lane.onTrunk === true) return 'trunk';
3687
+ if (lane.variantBranchSource === 'subscription-fallback') return 'feature branch';
3688
+ if (lane.variantBranchSource === 'branch-has-variants' || lane.variantBranchSource === 'branch-has-subscribers') {
3689
+ return 'variant branch';
3690
+ }
3691
+ return 'non-trunk branch';
3621
3692
  }
3622
3693
 
3623
3694
  function isCurrentStagingLane(lane) {
@@ -3638,10 +3709,15 @@ function printBranchContext(response) {
3638
3709
  console.log('');
3639
3710
  if (ctx.onTrunk) {
3640
3711
  console.log('Working tree: TRUNK (' + response.branchName + ')');
3712
+ } else if (ctx.variantBranchSource === 'subscription-fallback') {
3713
+ console.log('Working tree: FEATURE BRANCH "' + response.branchName + '" (not a variant branch; trunk is "' + ctx.trunkBranch + '")');
3641
3714
  } else {
3642
3715
  console.log('Working tree: VARIANT BRANCH "' + response.branchName + '" (trunk is "' + ctx.trunkBranch + '")');
3643
3716
  }
3644
3717
  console.log(' runs resolve: ' + ctx.resolves);
3718
+ if (ctx.variantBranchSource) {
3719
+ console.log(' variant world: ' + (ctx.variantBranch || 'none (subscription)') + ' [' + ctx.variantBranchSource + ']');
3720
+ }
3645
3721
  console.log(' commit writes: ' + ctx.commitWrites);
3646
3722
  if (Object.prototype.hasOwnProperty.call(ctx, 'lastSyncedSha')) {
3647
3723
  // null is "unknown" (never synced, expired, or the last sync had errors) — never "nothing landed".
@@ -3755,6 +3831,9 @@ async function statusComponentsCommand(flags) {
3755
3831
  workspace,
3756
3832
  dataMode,
3757
3833
  mode: 'status',
3834
+ // So "runs resolve" describes the run these flags would launch, by the same server-side rule.
3835
+ variantBranch: flags['variant-branch'],
3836
+ asAccountId: flags['as-account'] || flags['as-account-id'],
3758
3837
  componentType: flags['component-type'] || flags.type,
3759
3838
  componentId: flags['component-id'] || flags.id
3760
3839
  }).then((r) => r.data);
@@ -3983,6 +4062,9 @@ async function syncComponentsCommand(rawFlags) {
3983
4062
  ...runContextPayload(flags),
3984
4063
  agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
3985
4064
  acknowledge: flags.acknowledge || undefined,
4065
+ // The platform refuses to sync a branch that is not a variant branch (see featureBranchLandingRefusal);
4066
+ // this is the explicit statement that a NEW variant branch is being created on purpose.
4067
+ createVariantBranch: flagEnabled(flags['create-variant-branch']) || undefined,
3986
4068
  components: []
3987
4069
  };
3988
4070
 
@@ -4005,7 +4087,7 @@ async function syncComponentsCommand(rawFlags) {
4005
4087
  // --safe is fail-closed: a sync that would reconcile a repository other than this checkout's is refused.
4006
4088
  const preflightRepoCheck = repositoryCheck(cwd, branchContext);
4007
4089
  if (preflightRepoCheck.matches === false) {
4008
- const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync');
4090
+ const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync', branchContext);
4009
4091
  if (flagEnabled(flags.json)) {
4010
4092
  const refusal = {
4011
4093
  success: false,
@@ -4132,7 +4214,7 @@ async function syncComponentsCommand(rawFlags) {
4132
4214
  response.repositoryCheck = repositoryCheck(cwd, { repository: response.sync && response.sync.repository });
4133
4215
  if (response.repositoryCheck.matches === false) {
4134
4216
  (flagEnabled(flags.json) ? console.error : console.log)('WARNING: ' +
4135
- repositoryMismatchMessage(response.repositoryCheck, 'components sync'));
4217
+ repositoryMismatchMessage(response.repositoryCheck, 'components sync', response.branchContext));
4136
4218
  }
4137
4219
  const summary = buildSyncSummary(response);
4138
4220
  summary.gates = gate.checks;
@@ -4550,11 +4632,66 @@ function repositoryCheck(cwd, branchContext) {
4550
4632
  return { platformRepository, checkoutOrigin, matches: repositoryNamesMatch(platformRepository, checkoutOrigin) };
4551
4633
  }
4552
4634
 
4553
- function repositoryMismatchMessage(check, commandName) {
4635
+ function repositoryMismatchMessage(check, commandName, branchContext) {
4554
4636
  return commandName + ': this checkout\'s origin is ' + check.checkoutOrigin + ', but the platform syncs repository "' +
4555
4637
  check.platformRepository + '" for this account. A push from here would never be synced, and the sync would ' +
4556
- 'reconcile a repository you are not editing. Run it from that repository\'s checkout, or check which account ' +
4557
- 'this command resolved (account-info.json, --account-id).';
4638
+ 'reconcile a repository you are not editing. ' + (featureBranchLandingHint(branchContext) ||
4639
+ ('Run it from that repository\'s checkout, or check which account this command resolved ' +
4640
+ '(account-info.json, --account-id).'));
4641
+ }
4642
+
4643
+ // A git branch that is not a variant world (no committed variants, no subscribers) resolves the account's
4644
+ // SUBSCRIBED branch beneath its staged entries — the platform reports that as
4645
+ // `variantBranchSource: 'subscription-fallback'`. Syncing such a branch would write overlays nobody resolves,
4646
+ // so the way to land it is through the branch it resolves. Pure; null when the context says nothing of the kind.
4647
+ function featureBranchLandingHint(branchContext) {
4648
+ if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
4649
+ const subscribed = branchContext.subscribedComponentBranch;
4650
+ const trunk = branchContext.trunkBranch || 'trunk';
4651
+ // Without a subscription the platform cannot know which branch this was cut from; naming trunk would send a
4652
+ // branch cut from a variant branch into trunk.
4653
+ return 'This git branch is not a variant branch: runs from it resolve ' +
4654
+ (subscribed ? 'the \'' + subscribed + '\' variant branch this account subscribes to' : 'trunk') +
4655
+ ' plus what you staged. To land the work, ' +
4656
+ (subscribed
4657
+ ? 'merge this branch into \'' + subscribed + '\' and commit/sync \'' + subscribed + '\' from that checkout.'
4658
+ : 'merge this branch into the branch you cut it from (trunk \'' + trunk + '\', or the variant branch it came ' +
4659
+ 'from) and commit/sync that branch from its checkout.');
4660
+ }
4661
+
4662
+ // Refuse a commit from a branch the platform reports as NOT a variant branch, unless the caller says it is
4663
+ // creating one. Mirrors the server-side refusal so nothing is pushed that cannot be synced. Pure.
4664
+ function featureBranchCommitRefusal(branchContext, branchName, flags) {
4665
+ if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
4666
+ if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
4667
+ return 'components commit refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
4668
+ 'subscribers). ' + featureBranchLandingHint(branchContext) + ' Committing here would write overlays for \'' +
4669
+ branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
4670
+ 'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
4671
+ }
4672
+
4673
+ // What a verification envelope records as the world it verified. Taken from the platform's answer
4674
+ // (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
4675
+ // variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
4676
+ // a world no run executed in. Pure.
4677
+ function verificationWorldFromStatus(statusResponse, branchName) {
4678
+ if (!statusResponse) return {};
4679
+ const ctx = statusResponse.branchContext || {};
4680
+ const world = statusResponse.variantWorld || null;
4681
+ const staged = !!(statusResponse.laneSummary && statusResponse.laneSummary.stagedCount > 0);
4682
+ let componentBranch;
4683
+ if (world) {
4684
+ componentBranch = world.componentBranch || 'trunk';
4685
+ } else {
4686
+ // An older platform that does not report the world: the previous inference, kept for compatibility.
4687
+ componentBranch = ctx.onTrunk ? 'trunk' : branchName;
4688
+ }
4689
+ const sourceLayer = staged ? 'staged' : (componentBranch && componentBranch !== 'trunk' ? 'variant' : 'trunk');
4690
+ return {
4691
+ componentBranch,
4692
+ sourceLayer,
4693
+ variantBranchSource: world ? world.variantBranchSource : undefined
4694
+ };
4558
4695
  }
4559
4696
 
4560
4697
  function printRepositoryCheck(check) {
@@ -4850,6 +4987,10 @@ function buildSyncSummary(response) {
4850
4987
  };
4851
4988
  }
4852
4989
 
4990
+ /*
4991
+ ## Branches, Promotion, Commit, And Test Runs
4992
+ */
4993
+
4853
4994
  // Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
4854
4995
  // Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
4855
4996
  // subscribing accounts actually resolve in production.
@@ -5457,7 +5598,7 @@ async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, sk
5457
5598
  // `--skip-git` pushes nothing, so there is nothing to mis-deliver.
5458
5599
  const repoCheck = skipGit ? null : repositoryCheck(cwd, branchContext);
5459
5600
  if (repoCheck && repoCheck.matches === false) {
5460
- const mismatch = repositoryMismatchMessage(repoCheck, 'components commit');
5601
+ const mismatch = repositoryMismatchMessage(repoCheck, 'components commit', branchContext);
5461
5602
  if (flagEnabled(flags.json)) {
5462
5603
  console.log(JSON.stringify({
5463
5604
  success: false,
@@ -5478,6 +5619,30 @@ async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, sk
5478
5619
  }
5479
5620
  throw new Error(mismatch + ' No git commit or push was attempted.');
5480
5621
  }
5622
+ // Before ANY git write: is this a feature branch (not a variant branch)? The platform refuses its sync, and
5623
+ // a commit pushed first would leave a pushed-but-unsyncable branch. Same decision, taken before the push.
5624
+ const featureRefusal = featureBranchCommitRefusal(branchContext, branchName, flags);
5625
+ if (featureRefusal) {
5626
+ if (flagEnabled(flags.json)) {
5627
+ console.log(JSON.stringify({
5628
+ success: false,
5629
+ mode: 'commit',
5630
+ dataMode,
5631
+ accountId,
5632
+ branchName,
5633
+ workspace,
5634
+ branchContext,
5635
+ refusal: 'feature_branch_landing',
5636
+ phase: 'pre-git',
5637
+ gitWritten: false,
5638
+ gateViolations: [featureRefusal],
5639
+ message: featureRefusal
5640
+ }, null, 2));
5641
+ process.exitCode = 1;
5642
+ return true;
5643
+ }
5644
+ throw new Error(featureRefusal + ' No git commit or push was attempted.');
5645
+ }
5481
5646
  if (!flagEnabled(flags.yes) && branchContextIsTrunk(branchContext)) {
5482
5647
  printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
5483
5648
  stderr: flagEnabled(flags.json)
@@ -5819,6 +5984,10 @@ async function testCommand(flags) {
5819
5984
  }, { quiet: jsonOutput });
5820
5985
  }
5821
5986
 
5987
+ /*
5988
+ ## Tokens, Tools, Verification, And Config
5989
+ */
5990
+
5822
5991
  async function tokenCommand(flags) {
5823
5992
  const subcommand = flags._ && flags._[1];
5824
5993
  if (subcommand === 'inspect' || subcommand === 'details' || subcommand === 'decode') {
@@ -5876,6 +6045,9 @@ async function tokenCommand(flags) {
5876
6045
  accountId: data.accountId,
5877
6046
  branchName: data.branchName,
5878
6047
  variantBranch: data.variantBranch,
6048
+ // Why the variant branch is (or is not) set — the platform's one rule, so a null is explainable.
6049
+ variantBranchSource: data.variantBranchSource,
6050
+ variantWorld: data.variantWorld,
5879
6051
  workspace: data.workspace !== undefined ? data.workspace : workspace,
5880
6052
  stagingLane: data.stagingLane,
5881
6053
  accountLanes: data.accountLanes,
@@ -6346,9 +6518,10 @@ async function verifyCommand(flags, subcommand) {
6346
6518
 
6347
6519
  const source = collectVerificationSource(cwd, flags);
6348
6520
  if (statusResponse) {
6349
- source.componentBranch = statusResponse.branchContext && statusResponse.branchContext.onTrunk ? 'trunk' : branchName;
6350
- source.sourceLayer = statusResponse.laneSummary && statusResponse.laneSummary.stagedCount > 0
6351
- ? 'staged' : (statusResponse.branchContext && statusResponse.branchContext.onTrunk ? 'trunk' : 'variant');
6521
+ const verifiedWorld = verificationWorldFromStatus(statusResponse, branchName);
6522
+ source.componentBranch = verifiedWorld.componentBranch;
6523
+ source.sourceLayer = verifiedWorld.sourceLayer;
6524
+ if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
6352
6525
  source.stagedOverlayHash = statusResponse.laneSummary ? sha256(stableStringify(statusResponse.laneSummary)) : null;
6353
6526
  }
6354
6527
 
@@ -6712,6 +6885,10 @@ async function sessionsCommand(flags, subcommand) {
6712
6885
  throw new Error('Unknown sessions subcommand: ' + subcommand + '. Use: list, remove');
6713
6886
  }
6714
6887
 
6888
+ /*
6889
+ ## Service Dashboard And WebSocket Listener
6890
+ */
6891
+
6715
6892
  // --- Listener PID management ---
6716
6893
  function isListenerRunning() {
6717
6894
  if (!fs.existsSync(LISTENER_PID_FILE)) {
@@ -8803,6 +8980,10 @@ async function runManagedWebsocketWatchdog() {
8803
8980
 
8804
8981
  // --- Persistent WebSocket listener ---
8805
8982
  // ---------------------------------------------------------------------------------------------
8983
+ /*
8984
+ ## Agent And Ticket Workflows
8985
+ */
8986
+
8806
8987
  // Local agent sessions
8807
8988
  //
8808
8989
  // A "CLI agent" is ONE terminal session — one tab running Claude, Codex, or a person — that has
@@ -11857,6 +12038,10 @@ async function listenStatusCommand(flags = {}) {
11857
12038
  printSessionIdentity(identity);
11858
12039
  }
11859
12040
 
12041
+ /*
12042
+ ## Help, Auto Update, And Command Dispatch
12043
+ */
12044
+
11860
12045
  async function whoamiCommand(flags = {}) {
11861
12046
  const identity = resolveSessionIdentity(process.cwd(), flags);
11862
12047
  printSessionResolutionWarning(identity);
@@ -12024,7 +12209,7 @@ function printComponentsHelp(subcommand) {
12024
12209
  return;
12025
12210
  }
12026
12211
  if (subcommand === 'sync') {
12027
- 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]');
12212
+ console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--json]');
12028
12213
  console.log('');
12029
12214
  console.log('On trunk this reconciles the pushed repository into live component rows.');
12030
12215
  console.log('On a variant branch this writes ComponentVariant overlays only.');
@@ -12110,7 +12295,7 @@ function printComponentsHelp(subcommand) {
12110
12295
  return;
12111
12296
  }
12112
12297
  if (subcommand === 'commit') {
12113
- console.log('Usage: remits-cli components commit [--yes] [--safe] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
12298
+ console.log('Usage: remits-cli components commit [--yes] [--safe] [--create-variant-branch] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
12114
12299
  console.log('');
12115
12300
  console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
12116
12301
  console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.125",
3
+ "version": "0.1.126",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "index.js",
14
14
  "README.md",
15
+ "scripts/prepare-index-toc.js",
15
16
  "skills/remits-cli/SKILL.md",
16
17
  "skills/remits-cli/references"
17
18
  ],
@@ -29,8 +30,10 @@
29
30
  "automation"
30
31
  ],
31
32
  "scripts": {
33
+ "prepare:index-toc": "node scripts/prepare-index-toc.js",
34
+ "prepack": "node scripts/prepare-index-toc.js",
32
35
  "start": "node index.js",
33
- "test": "node --test test/*.test.js"
36
+ "test": "node scripts/prepare-index-toc.js --check && node --test test/*.test.js"
34
37
  },
35
38
  "dependencies": {
36
39
  "@stomp/stompjs": "^7.2.0",
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const vm = require('node:vm');
7
+
8
+ const indexPath = path.join(__dirname, '..', 'index.js');
9
+ const cliSource = fs.readFileSync(indexPath, 'utf8');
10
+
11
+ function functionSource(name) {
12
+ const declaration = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
13
+ const match = declaration.exec(cliSource);
14
+ assert.notEqual(match, null, name + ' should exist');
15
+
16
+ const start = match.index;
17
+ let depth = 0;
18
+ for (let i = start + match[0].length - 1; i < cliSource.length; i += 1) {
19
+ if (cliSource[i] === '{') depth += 1;
20
+ if (cliSource[i] === '}') {
21
+ depth -= 1;
22
+ if (depth === 0) return cliSource.slice(start, i + 1);
23
+ }
24
+ }
25
+ assert.fail(name + ' body should be parseable');
26
+ }
27
+
28
+ const sandbox = vm.createContext({});
29
+ vm.runInContext(
30
+ ['headingSlug', 'formatTocEntry', 'resolveTocEntry', 'resolveTableOfContentsLineNumbers']
31
+ .map(functionSource)
32
+ .join('\n'),
33
+ sandbox
34
+ );
35
+
36
+ const resolveTableOfContentsLineNumbers = vm.runInContext('resolveTableOfContentsLineNumbers', sandbox);
37
+ const prepared = resolveTableOfContentsLineNumbers(cliSource);
38
+ const check = process.argv.includes('--check');
39
+
40
+ if (check) {
41
+ if (prepared !== cliSource) {
42
+ console.error('cli/index.js Table of Contents is stale. Run: npm run prepare:index-toc');
43
+ process.exit(1);
44
+ }
45
+ process.exit(0);
46
+ }
47
+
48
+ if (prepared !== cliSource) {
49
+ fs.writeFileSync(indexPath, prepared);
50
+ console.log('Updated cli/index.js Table of Contents line numbers.');
51
+ }
@@ -77,6 +77,11 @@ reference named after it.
77
77
  it. Each agent works in its own **clone**, never a same-branch worktree: worktrees share the branch
78
78
  ref, so one agent's pull moves `HEAD` under the others, and `components commit` refuses there.
79
79
  (`component-resolution.md`)
80
+ - **Land only from trunk or a real variant branch.** A per-agent/feature branch (`components status` says
81
+ `FEATURE BRANCH … [subscription-fallback]`) is safe to stage and run from — it resolves the same world
82
+ as its target — but `components commit`/`sync` refuse it. Merge into the branch it resolves and land
83
+ there; `--create-variant-branch` is only for deliberately creating a new variant branch.
84
+ (`branch-variants.md`)
80
85
  - **On a variant branch, sync with `remits-cli components sync --safe`.** It dry-runs first and refuses
81
86
  a plan that would write components this checkout did not change — which is what a branch that is
82
87
  behind trunk produces, because it still physically carries old copies of files nobody touched.
@@ -44,13 +44,21 @@ Three facts that everything else follows from:
44
44
 
45
45
  ### Which world does your working tree resolve? (read this before you run anything)
46
46
 
47
- You will work from **two different checkouts of the same repo**, and they behave differently on both ends
48
- of the loop. The rule turns entirely on **trunk vs non-trunk**:
47
+ You will work from **different checkouts of the same repo**, and they behave differently on both ends of
48
+ the loop. **You only ever stage the edits you intend to test** — the world beneath them comes from the
49
+ account, not from what you named your git branch:
49
50
 
50
51
  | Working tree | Staging scope | A run resolves | `components sync`/`commit` writes |
51
52
  |---|---|---|---|
52
53
  | **trunk** (`main`, or whatever `account-info.json` says) | that branch | trunk + **each account's subscribed** variant branch (production semantics) | the **live component rows** — full reconcile, creates/updates/**deletes** |
53
- | **any other branch** (`feature_branch`) | that branch | trunk + **`feature_branch`** overlays | **`ComponentVariant` overlays on that branch only** — never touches trunk rows |
54
+ | **a variant branch** (it has committed variants, or an account subscribes to it — e.g. `forked`) | that branch | trunk + **that branch's** overlays | **`ComponentVariant` overlays on that branch only** — never touches trunk rows |
55
+ | **any other branch** (`codex/x`, `feature/y`) | that branch | the **same world as trunk**: trunk + the account's subscribed branch (`forked` for its subscriber) | **refused** (`feature_branch_landing`) — merge into the branch it resolves and land from there |
56
+
57
+ A branch like that is a **feature branch**, and it is safe to *run* from: stage only the components you edited,
58
+ and runs still resolve every `forked` overlay for the subscriber. Do **not** stage untouched components to
59
+ "restore" something reported missing — check `components status` first. It is never a place to *land* from.
60
+ For parallel agents the preferred shape is still the real branch name plus a workspace; see
61
+ `features/multi-agent-development.md` ("Per-agent git branches: safe to run, never to land").
54
62
 
55
63
  Do not infer this from the branch name. Ask:
56
64
 
@@ -61,12 +69,17 @@ remits-cli components status
61
69
  ```
62
70
  Working tree: VARIANT BRANCH "feature_branch" (trunk is "main")
63
71
  runs resolve: trunk + the 'feature_branch' variant overlays
72
+ variant world: feature_branch [branch-has-variants]
64
73
  commit writes: ComponentVariant overlays on 'feature_branch' (never touches trunk rows)
65
74
  variants stored on this branch: 3
66
75
  subscribing accounts: 101 (Acme Child)
67
76
  ```
68
77
 
69
- **The precedence trap that costs the most time:** `variantBranch` **outranks every account's
78
+ From a branch that is not a variant branch the header reads `FEATURE BRANCH` and `runs resolve` names the
79
+ subscribed branch, tagged `[subscription-fallback]`. `test run`, `token` and `tool` responses carry the
80
+ same `variantBranch` / `variantBranchSource` fields, and a verification envelope records that world.
81
+
82
+ **The precedence trap that costs the most time:** a variant branch's world **outranks every account's
70
83
  subscription**. So running a Test suite that asserts *production* semantics from a **variant checkout**
71
84
  pins every account in that suite — including fixture accounts subscribed to their own generated branches —
72
85
  to your branch, where they have no variants, and they all read trunk. The suite fails in a way that looks
@@ -383,7 +396,9 @@ branch that overlays it.
383
396
 
384
397
  **`components branches` only lists branches that already have overlays.** A branch you just pushed is
385
398
  invisible here until its first sync — that is not an error. Preview it by name (`components sync --dry-run`
386
- from that checkout).
399
+ from that checkout). Its first real sync (or commit) needs `--create-variant-branch`: until a branch has
400
+ variants or a subscriber the platform treats it as a feature branch and refuses to land it, so creating a
401
+ new variant branch is always a stated decision, never a side effect of a feature branch's name.
387
402
 
388
403
  **Trunk moving also invalidates a branch.** Variant sparseness compares branch content against *current*
389
404
  trunk, so a trunk change can make an overlay obsolete without the branch changing at all. A trunk sync
@@ -197,8 +197,8 @@ remits-cli components status [--branch <name>] [--component-type <type>] [--comp
197
197
  remits-cli components lanes [--json] # every indexed staging lane on the account
198
198
  remits-cli components entries --lane-id <id> [--json|--verbose] # authoritative staged files for one lane
199
199
  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
200
- remits-cli components sync [--safe [--yes]] [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--changed-only [--changed-since <ref>]] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
201
- remits-cli components commit [--yes] [--safe] [--allow-shared-branch] [--message|-m "msg"] [--data-mode test|prod] [--force-tombstones] [--json [--summary]] # phase 1 merge-stages + compile-validates changed source (never reconciles the lane); on TRUNK refuses before any git write unless --yes; --safe gates variant sync writes
200
+ remits-cli components sync [--safe [--yes]] [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--changed-only [--changed-since <ref>]] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
201
+ remits-cli components commit [--yes] [--safe] [--allow-shared-branch] [--create-variant-branch] [--message|-m "msg"] [--data-mode test|prod] [--force-tombstones] [--json [--summary]] # phase 1 merge-stages + compile-validates changed source (never reconciles the lane); on TRUNK refuses before any git write unless --yes; --safe gates variant sync writes
202
202
  remits-cli components branches [--json] # branches carrying committed variants, with counts + drift
203
203
  remits-cli components branch <name> [--json] # one branch: owner account, overridden / added / removed, drift flags, subscribers
204
204
  remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]
@@ -341,6 +341,12 @@ For tests specifically:
341
341
  (`sharedBranchWorktrees` in `--json`). Worktrees of one branch share its ref, so a stale one would commit
342
342
  over landed work. Use one clone per agent; `--allow-shared-branch` overrides after `git status` is clean
343
343
  apart from your own changes.
344
+ - `components commit` refuses before any git write, and `components sync` refuses, on a **feature branch** — one
345
+ `components status` reports as `[subscription-fallback]` (no committed variants, no subscribers). Runs from
346
+ it resolve the subscribed branch; landing it would write overlays nobody subscribes to and flip every sibling
347
+ lane on that branch to them (`refusal: "feature_branch_landing"` in `--json`). Merge it into the branch it
348
+ resolves and land from there. `--create-variant-branch` overrides, for deliberately creating a NEW variant
349
+ branch (its first sync looks identical). `--dry-run` previews are never refused.
344
350
  - `components commit` refuses before any git write, and `components sync --safe` refuses, when this checkout's
345
351
  `origin` is not the repository the platform syncs for the resolved account (`branchContext.repository` in
346
352
  `components status`). A plain sync warns and reports `repositoryCheck`, including under `--summary`.
@@ -354,7 +354,7 @@ Inspect individual lifecycle records with line-range or grep.
354
354
  | `recordType` | yes | `object`, `object_log`, `event`, or `alert` |
355
355
  | `recordId` | yes | Record primary key |
356
356
  | `field` | no | `content` (default) or `body` (objects only) |
357
- | `revisionId` | no | Envers revision ID (not for object_log) |
357
+ | `revisionId` | no | Envers revision ID (not for object_log). Revision history does not retain `content`/`body`; omit it to read the current payload |
358
358
  | `lineRange` | no | `{start, end}` (1-based inclusive) |
359
359
  | `grep` | no | `{pattern, caseSensitive, contextBefore, contextAfter}` |
360
360
 
@@ -39,7 +39,8 @@
39
39
  | 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). |
40
40
  | Need users of an account, accounts of a user, or account-scoped user fields | Use `mcp_account_user_admin` (`action:'users'`, `action:'user'`, or `action:'user_update'`). Use `mcp_sql_query` on `user` / `user_account` only for raw join-table investigation. Remember user custom fields are stored **per bound account**, so the same person can differ per account. |
41
41
  | One account behaves differently from its siblings on the same component | It probably subscribes to a **branch variant**. Check `remits-cli components branches` and `remits-cli components branch <name> --subscribers`, and reproduce with `remits-cli test run --as-account <ID>`. Do NOT "fix" this by adding per-account logic to the origin component. |
42
- | Edits on a feature branch seem to run against trunk code | You are likely on the **trunk** branch, or passed `--variant-branch none`. Run `remits-cli components status` — it states which world the working tree resolves. |
42
+ | Edits on a feature branch seem to run against trunk code | A branch with no committed variants and no subscribers resolves the account's **subscription** (trunk, if it subscribes to nothing) beneath what you staged — `components status` shows `FEATURE BRANCH … [subscription-fallback]` and names the world. To see a specific branch's overlays, run from that branch's checkout or pass `--variant-branch <name>`. |
43
+ | `components commit`/`sync` refused with `feature_branch_landing` | The branch is a feature branch, not a variant branch. Merge it into the branch the message names and land from that checkout. Do **not** pass `--create-variant-branch` to get past it — that flag deliberately creates a new variant branch nobody subscribes to. |
43
44
  | A component vanished for one account after a variant-branch sync | Its file is missing from that branch, so the sync created a **tombstone** that hides it from subscribers. Restore the file on the branch and re-sync. Trunk is unaffected. |
44
45
  | 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. |
45
46
  | `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. |