@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 +21 -19
- package/index.js +205 -20
- package/package.json +5 -2
- package/scripts/prepare-index-toc.js +51 -0
- package/skills/remits-cli/SKILL.md +5 -0
- package/skills/remits-cli/references/branch-variants.md +20 -5
- package/skills/remits-cli/references/command-reference.md +8 -2
- package/skills/remits-cli/references/tool-reference.md +1 -1
- package/skills/remits-cli/references/troubleshooting.md +2 -1
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
|
-
-
|
|
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,
|
|
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
|
|
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,
|
|
122
|
-
- If websocket connections are disconnected
|
|
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
|
|
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/
|
|
187
|
-
|
|
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
|
-
##
|
|
198
|
+
## Activity Log
|
|
196
199
|
|
|
197
|
-
- `~/.remits-cli/
|
|
198
|
-
- It records timestamped lifecycle events such as
|
|
199
|
-
- Prompt bodies are not written verbatim to this log. The log stores summarized metadata such as `accountId`, `ticketId`,
|
|
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/
|
|
204
|
-
tail -n 200 ~/.remits-cli/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4557
|
-
'this command resolved
|
|
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
|
-
|
|
6350
|
-
source.
|
|
6351
|
-
|
|
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.
|
|
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 **
|
|
48
|
-
|
|
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
|
-
| **
|
|
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
|
-
|
|
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 |
|
|
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. |
|