@remits/remits-cli 0.1.113 → 0.1.115

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.
@@ -0,0 +1,391 @@
1
+ # Branched Component Variants
2
+
3
+ > A `remits-cli` skill reference. **Load this when** one account needs different behavior from a component another account owns, or you are working from a non-trunk checkout, or you are promoting a branch back to trunk.
4
+ >
5
+ > The table of contents below carries **real line numbers** (`- L84 Some Heading`), resolved when
6
+ > this file is installed, so they are never stale. Read the head, pick your sections, and offset-read
7
+ > only those. The entry text is the heading verbatim, so it also greps.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Branched Component Variants (per-account component overrides)](#branched-component-variants-per-account-component-overrides)
12
+ - [Which world does your working tree resolve? (read this before you run anything)](#which-world-does-your-working-tree-resolve-read-this-before-you-run-anything)
13
+ - [Two levers, two different questions](#two-levers-two-different-questions)
14
+ - [The SDLC is identical on a variant branch](#the-sdlc-is-identical-on-a-variant-branch)
15
+ - [Where am I in the promotion loop?](#where-am-i-in-the-promotion-loop)
16
+ - [Subscribing, unsubscribing, retiring](#subscribing-unsubscribing-retiring)
17
+ - [Danger profile on a variant branch (different, not absent)](#danger-profile-on-a-variant-branch-different-not-absent)
18
+ - [Inspecting branches and drift](#inspecting-branches-and-drift)
19
+ - [Diagnosing a variant](#diagnosing-a-variant)
20
+
21
+ ## Branched Component Variants (per-account component overrides)
22
+
23
+ When one account — often a customer nested several levels down — needs *slightly* different behavior from
24
+ a component owned by its platform or product account, the answer is a **branch variant**: a durable,
25
+ branch-scoped overlay of that component, resolved only by accounts subscribed to that branch. The wrong
26
+ answer is per-account `if/then` logic inside the origin component.
27
+
28
+ > **`features/subscriber-branch-promotions.md` (`mcp_get_guide`) is the guide for this.** It owns the
29
+ > mental model, the five scenarios you will actually meet, the full promotion procedure, what happens to a
30
+ > `new_` component across a promotion, merge-conflict resolution file by file, removals, and the approval
31
+ > gates for running a promotion as a coding agent. **Load it before promoting anything.** This section
32
+ > covers only the CLI surface and the traps that bite at the command line.
33
+
34
+ Three facts that everything else follows from:
35
+
36
+ - **A branch is not an account.** Subscription is many-to-many, it applies to the subscribing account
37
+ *and its descendants*, and a subscribing account still has its own trunk components which merge on top.
38
+ - **A variant keeps the origin's component id** — it is an overlay, not a copy. That is what makes drift
39
+ computable.
40
+ - **Sparseness is computed at sync time, not declared.** A git branch physically contains every file; only
41
+ the ones whose content *differs from trunk* become variants. A file that is absent becomes a
42
+ **tombstone**; a file whose id prefix is not a live trunk id (`new_Foo.groovy`) becomes a **branch-only**
43
+ component keyed by name.
44
+
45
+ ### Which world does your working tree resolve? (read this before you run anything)
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**:
49
+
50
+ | Working tree | Staging scope | A run resolves | `components sync`/`commit` writes |
51
+ |---|---|---|---|
52
+ | **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
+
55
+ Do not infer this from the branch name. Ask:
56
+
57
+ ```bash
58
+ remits-cli components status
59
+ ```
60
+
61
+ ```
62
+ Working tree: VARIANT BRANCH "feature_branch" (trunk is "main")
63
+ runs resolve: trunk + the 'feature_branch' variant overlays
64
+ commit writes: ComponentVariant overlays on 'feature_branch' (never touches trunk rows)
65
+ variants stored on this branch: 3
66
+ subscribing accounts: 101 (Acme Child)
67
+ ```
68
+
69
+ **The precedence trap that costs the most time:** `variantBranch` **outranks every account's
70
+ subscription**. So running a Test suite that asserts *production* semantics from a **variant checkout**
71
+ pins every account in that suite — including fixture accounts subscribed to their own generated branches —
72
+ to your branch, where they have no variants, and they all read trunk. The suite fails in a way that looks
73
+ exactly like a resolution regression. (Live example: a branch-variant suite scored 4/15 from a variant
74
+ checkout and 15/15 from trunk, with no code difference.) **Run branch-variant suites from trunk, or pass
75
+ `--variant-branch none`.** Before concluding "variant resolution is broken", re-run from trunk.
76
+
77
+ **Two other things differ from trunk while you work on a branch:**
78
+
79
+ - **Keep the origin id in the filename.** `50_ExtractInvoice.groovy` on the branch overlays Action 50.
80
+ That is what preserves component identity and lets drift be computed against the origin.
81
+ - **Nothing is renamed.** A variant sync never renames files and never repoints the account's trunk
82
+ branch. A `new_*` file stays `new_*`.
83
+
84
+ **Start from a current branch, not just an isolated workspace.** Before the first edit in a variant
85
+ checkout:
86
+
87
+ ```bash
88
+ git fetch origin
89
+ git status --porcelain
90
+ git log origin/<branch>..<branch> # must be empty: nothing local-only
91
+ git log <branch>..origin/<branch> # must be empty: not behind the remote
92
+ remits-cli components promotion --branch <branch>
93
+ ```
94
+
95
+ `components promotion` reads the remote and reports whether the branch is also current with trunk. If it
96
+ says `diverged` or `STALE`, merge trunk in or sync first, as instructed, before writing new work. A
97
+ workspace lane prevents another agent from overwriting your staged Redis snapshot; it does not say which
98
+ commit your files are based on.
99
+
100
+ **Branch-local `account-info.json` can describe a subscriber.** On a variant branch the repository is
101
+ still the OWNER's component repo — files overlay that owner's trunk ids and sync writes overlays owned by
102
+ that owner. But when the checkout was synced from a subscribing account reached through an
103
+ `AccountRelationship` edge, and the branch has **exactly one** subscriber, the branch's
104
+ `account-info.json` is intentionally rooted at that subscriber. With **more than one** subscriber the
105
+ refresh is skipped (the sync result says so under `accountInfo.skipped`/`reason`) and the file keeps
106
+ describing the owner, which is at least true for all of them. Overlay ownership is unaffected either way.
107
+
108
+ Read `resolution` before acting from any checkout — in particular `resolution.accountId` (the account this
109
+ checkout should be treated as; **read this, not the top-level `id`**), `resolution.role`,
110
+ `resolution.componentOwnerAccountId` (whose trunk ids the files overlay and whose overlays a sync writes),
111
+ and `resolution.componentBranch` / `resolution.scopeAccountId` (the branch and the path anchor that made
112
+ this subscriber resolution possible). If a variant checkout's `account-info.json` is stale
113
+ and still names the owner, run the first repair sync with an explicit subscriber target
114
+ (`remits-cli components sync --account-id 101`), then `git pull --ff-only`.
115
+
116
+ ### Two levers, two different questions
117
+
118
+ - **`--as-account <id>`** changes **who** the run executes as, so that account's own edge picks the branch.
119
+ Answers *"what does customer X actually get?"* Only narrows downward (the target must be reachable from
120
+ an account you already have access to). It works for a `parentId`-less, membership-only subscriber too:
121
+ the anchor is derived from the branch you name, or from the account's single branch subscription. It
122
+ **refuses to guess** when an account has several edges each carrying a different branch — the run then
123
+ resolves trunk, and you must name the branch.
124
+ - **`--variant-branch <name>`** changes **which branch**, from any checkout. Answers *"what does branch Y
125
+ look like?"* — most useful for verifying a freshly committed branch **before** any edge subscribes to
126
+ it. `--variant-branch none` (or `trunk`) forces production/subscription semantics without leaving the
127
+ branch.
128
+
129
+ Both work on `remits-cli test run` and `remits-cli token`; `--variant-branch` also works on
130
+ `remits-cli tools` and `remits-cli tool`, so tests, browser URLs, tool discovery, and tool execution can
131
+ all inspect the same committed variant world.
132
+
133
+ The strongest end-to-end proof for a UI-visible variant is a token, not a log line:
134
+
135
+ ```bash
136
+ remits-cli token --path embeddable/index/50 --as-account 101 # subscriber -> variant
137
+ remits-cli token --path embeddable/index/50 # owner -> trunk
138
+ ```
139
+
140
+ Each answer carries a **`resolution`** block for the account the token executes as — `role`,
141
+ `componentBranch` and its owner, `resolvedDatabaseName`, `resolvedDomainName`, `scopeAccountId`, and a
142
+ one-sentence `summary`. Read it before opening the URL: `accountId` alone does not say whether a branch
143
+ overlay applies or which storage namespace the page will read, and those are exactly what
144
+ `--as-account` is being used to change.
145
+
146
+ Separate branch fields, because these questions answer differently and can disagree:
147
+
148
+ | Field | Answers |
149
+ |---|---|
150
+ | `componentBranch` | what **this token** will resolve |
151
+ | `componentBranchSource` | `probe` (an explicit `--variant-branch`), `subscription` (the account's edge), or `trunk` |
152
+ | `subscribedComponentBranch` | what the **account graph** says, independent of this token |
153
+ | `componentBranchAnchored` | whether **this resolution actually reached** that subscription |
154
+
155
+ A single `componentBranch` field would have reported `trunk` under `--variant-branch X`, for a token that
156
+ resolves `X`.
157
+
158
+ **Subscribing to a branch and resolving through it are different facts.** An account subscribes on an
159
+ edge; a *request* resolves through that edge only when something named the path (`--as-account`, the
160
+ edge's own host, an explicit `--variant-branch`). An account with no `parentId` and several upward links
161
+ resolves trunk by design — the resolver refuses to guess a path. So this is a normal, explainable state:
162
+
163
+ ```
164
+ "componentBranch": null, // this token resolves TRUNK
165
+ "componentBranchSource": "trunk",
166
+ "subscribedComponentBranch": "forked", // ...but the account does subscribe
167
+ "componentBranchAnchored": false // ...and nothing anchored this request to it
168
+ ```
169
+
170
+ Reading `componentBranch` alone there tells you the account is on trunk, which is true — and leads you to
171
+ conclude it has no branch, which is false. If several edges carry branches, none is picked for you:
172
+ `subscribedComponentBranch` is `null` and `subscribedComponentBranches` lists them.
173
+
174
+ Under a probe, `componentOwnerAccountId` is the **nearest account on the token's resolution path that owns
175
+ variant rows for X** — not an edge lookup, which answers `null` for the ordinary case of a variant
176
+ committed before anything subscribes to it. `null` means no account on that path owns rows for X, and what
177
+ that implies depends on the account, so read `summary`:
178
+
179
+ - **on trunk** — the page resolves what it would without the probe; the branch is not committed yet.
180
+ - **resolving through a subscription** — the probe **suppresses** it. `variantBranch` outranks the
181
+ subscription before it is consulted, so the page resolves **trunk**, not the overlays that account
182
+ normally gets. Drop `--variant-branch` to see the subscription.
183
+ - **subscribed but not anchored** — it was already resolving trunk before you probed. Dropping
184
+ `--variant-branch` will *not* by itself show you the branch; name the path as well.
185
+
186
+ The page itself then states the same facts. Its hidden `remits-session-info` line — which appears in an
187
+ accessibility snapshot with no script — carries `Account` (what it runs as), `Addressed Account` (what
188
+ the URL/token named), `Data Lane`, `Component Branch`, `Variant Applied`, and `Scope Account`.
189
+
190
+ **`Component Branch` is the branch SELECTED; `Variant Applied` is what actually overlaid.** They are two
191
+ fields because a branch can be selected and overlay nothing: `--variant-branch missing_branch` reports
192
+ `Component Branch: missing_branch` while the page renders trunk components. So read `Variant Applied` —
193
+ the `ComponentVariant` row id this page's component resolved through, or `none` — when the question is
194
+ "did my variant apply?". That is a far sharper signal than inspecting the rendered markup for a style you
195
+ expected.
196
+
197
+ If `components status` shows staged entries that you cannot safely clear, isolate verification in an
198
+ unused staging namespace instead of deleting someone else's cache:
199
+
200
+ ```bash
201
+ remits-cli test run --test "Invoice Tests" --branch promotion-check-empty --variant-branch none --as-account 101
202
+ remits-cli token --path embeddable/index/50 --branch promotion-check-empty --variant-branch none --as-account 101
203
+ ```
204
+
205
+ Here `--branch` is only the CLI staging-cache namespace, and `--variant-branch none` keeps runtime
206
+ resolution on production/subscription semantics. Do **not** use this pattern with `components sync` or
207
+ `components commit`: for those commands `--branch` is the GitHub branch to reconcile.
208
+
209
+ ### The SDLC is identical on a variant branch
210
+
211
+ ```bash
212
+ git fetch origin
213
+ git checkout feature_branch
214
+ git pull --ff-only origin feature_branch
215
+ remits-cli components promotion --branch feature_branch
216
+ # edit components/actions/50_ExtractInvoice.groovy (KEEP the trunk id)
217
+ remits-cli components stage # Redis, scoped to this branch — same as always
218
+ remits-cli test run --test "Invoice Tests" # the feature_branch world
219
+ remits-cli test run --test "Invoice Tests" --as-account 101 # ...as the real subscriber
220
+ git add -A && git commit -m "..." && git push origin feature_branch
221
+ remits-cli components sync --dry-run # inspect overrides/additions/tombstones without writes
222
+ remits-cli components sync # writes ComponentVariant overlays ONLY
223
+ ```
224
+
225
+ Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
226
+ for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
227
+ `features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
228
+ `remits-cli components sync --summary --timeout-ms 300000`; the platform may need longer than the default
229
+ 60 seconds to create rows, push rename/meta commits, and regenerate account metadata.
230
+
231
+ ### Where am I in the promotion loop?
232
+
233
+ ```bash
234
+ remits-cli components promotion # the branch you are standing on
235
+ remits-cli components promotion --branch forked --json
236
+ ```
237
+
238
+ **Run this before every promotion step and after every one.** It reports only — no git, no writes — and it
239
+ reads the remote (which is what the platform syncs from, not your working tree). It works from either
240
+ checkout, because it resolves the branch's owner itself. It exits non-zero while blockers remain, so treat
241
+ that as "do not proceed".
242
+
243
+ | Phase | Meaning | Next |
244
+ |---|---|---|
245
+ | `converged` | branch == trunk | nothing to do; this is also what a **finished** promotion looks like |
246
+ | `ready` | ahead of trunk, current with it | promote |
247
+ | `diverged` | behind trunk | `git merge <trunk>` into the branch, re-sync, re-read the plan |
248
+ | `awaiting-merge-back` | fully contained in trunk, trunk has moved on | **steps 4–5 are owed** — merge trunk back and re-sync |
249
+
250
+ Two things it tells you that nothing else does:
251
+
252
+ - **Which commit the stored counts describe, on BOTH axes.** An overlay is stored only while a file
253
+ differs from trunk, so the stored set is a statement about a (branch, trunk) pair and either side moving
254
+ makes it stale: `[STALE — does not match the branch HEAD]` (the branch was pushed since) or
255
+ `[STALE — matches the branch HEAD, but trunk has moved since]`. The second is the easy one to miss —
256
+ measured live, a branch at its own synced HEAD reported 2 stored overlays against a real plan of 21.
257
+ - **That a promotion is unfinished.** `awaiting-merge-back` is what a promotion that *looked* successful
258
+ leaves behind: trunk is correct and its tests pass, while the subscriber silently keeps resolving its
259
+ pre-promotion overlays — including for any fix made afterwards. **A trunk sync succeeding is step 2 of 5,
260
+ not completion.**
261
+
262
+ ### Subscribing, unsubscribing, retiring
263
+
264
+ ```bash
265
+ remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge]
266
+ remits-cli components branch <name> --unsubscribe <accountId>
267
+ remits-cli components branch <name> --retire [--force]
268
+ ```
269
+
270
+ Branch administration commands act **as the account you are running from**, which is treated as the branch
271
+ **owner** — run them from the owner's trunk checkout. Authorization is downward-only.
272
+
273
+ - **`--subscribe` sets the branch on an edge that must already exist — it cannot create one.** Failure
274
+ reads *"Account N has no relationship edge to subscribe; add a membership edge first"*. Create it with
275
+ `mcp_account_user_admin` `action:'edge_add'` (`targetAccountId` = the child, `parentAccountId` = the
276
+ owner), which also accepts `branchName` so you can create and subscribe in one call.
277
+ - **Many older accounts have no relationship row at all** — the edge table was added after the fact and
278
+ never backfilled, so an account whose parent link is only `Account.parentId` resolves fine but has
279
+ nothing to attach to. `mcp_account_user_admin` `action:'reparent'` with the account's *current* parent
280
+ is the one-call repair: it creates the missing primary edge and changes nothing else.
281
+ - **When the account has several parents, say which edge you mean** with `--parent-account <ownerId>`.
282
+ Without it the CLI picks the edge to the owner whose branch you are managing, then falls back to the
283
+ account's primary edge — which may not be the one you intended. `--subscribers` prints
284
+ `via primary|membership edge -> parent N` so you can confirm.
285
+ - **Primary-edge subscriptions are structural.** If the selected edge is the account's `parentId` edge,
286
+ subscribing it makes that account and descendants resolve the branch by default even though `parentId`
287
+ still points at the same parent. The server refuses this write unless you pass
288
+ `--confirm-primary-edge`; run `--dry-run` first and prefer a membership edge for fork/pilot
289
+ subscriptions.
290
+ - **Retiring is explicit.** Deleting the *git* branch does **not** remove its overlays; subscribers would
291
+ keep resolving a branch that no longer exists. `--retire` refuses while subscribers remain unless you
292
+ pass `--force`.
293
+
294
+ **Every component kind can be varied** — `schema`, `reader`, `action`, `embeddable`, `i18n`, `htmltemplate`,
295
+ `rule`, `test`, `utility` (agent), `tool`, `prompt` — including tombstones and branch-only additions. A
296
+ **schema** variant deserves the same care as a trunk schema change: it can change the JSON schema *and*
297
+ `collectionName`, so it changes validation and where the subscriber's documents physically land. A
298
+ branch-only agent (a `new_*` file under `components/agents/`) defaults to `type: AI` so agent lookups
299
+ find it.
300
+
301
+ A variant speaks the same `.meta.yml` vocabulary trunk does; keys outside that set are ignored on purpose,
302
+ because trunk cannot express them either — allowing them would mean a branch behaves one way and silently
303
+ loses that behavior the moment it is promoted. **One documented exception: an agent's `tools:` list cannot
304
+ be overridden by a variant.** It maps to a GORM association that `agent()` populates from the trunk row, so
305
+ a `tools:` list on a branch is ignored. Change trunk, or have the agent select tools at runtime.
306
+
307
+ ### Danger profile on a variant branch (different, not absent)
308
+
309
+ The `component-integrity.md` worst case — *"a missing file hard-deletes a live component"* — **does not
310
+ apply on a variant branch.** Variant sync writes overlay rows only; it cannot create, delete, rename, or
311
+ overwrite a trunk component. That makes a variant branch a genuinely safer place to iterate.
312
+
313
+ The analogous hazard is different, and you must still respect it:
314
+
315
+ - **A trunk component with no file on the branch becomes a TOMBSTONE**, which *hides* that component from
316
+ every subscriber. It is reversible (restore the file, re-sync) and never touches trunk — but to a
317
+ subscribing account it looks exactly like the component was deleted. Confirm every omission is
318
+ deliberate before syncing.
319
+ - **Deleting a file means "remove the component", not "stop overriding it."** To withdraw an override,
320
+ make the file identical to trunk again — the sync then removes the variant row.
321
+ - **Keep the branch rebased.** A branch physically carries every file, so anything trunk added *after* the
322
+ branch was cut is absent from it. The sync now asks git which of those absences are real deletions
323
+ (comparing against the merge base) and refuses to tombstone the rest, reporting them as
324
+ `skipped: absent from the branch but never deleted on it`. Treat any such entry as "merge trunk in" —
325
+ if an older sync already stored one of those absences as a tombstone, a non-dry-run sync prunes it and a
326
+ dry run reports `wouldPrune: true`. The classification falls back to a conservative ratio guard when
327
+ GitHub's compare is unavailable or its file list comes back truncated, which the sync output says
328
+ explicitly.
329
+ - **Use `components sync --dry-run` before risky variant syncs.** It reports `overridden`, `added`,
330
+ `removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
331
+ staging. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
332
+ branch has converged back to trunk and the overlay would be removed. It is rejected on trunk, and
333
+ `components commit --dry-run` is unsupported because `commit` performs local git writes before syncing.
334
+ - **The sync refuses a wholesale removal.** Above roughly a third of a kind — or **100% of a kind at any
335
+ size** — it aborts that kind, reports why, and points at a rebase. Rebasing is almost always the real
336
+ fix. Only when the removals are genuinely deliberate, re-run with `--force-tombstones`.
337
+ - **Deleting a trunk component cascades**: its overlays on every branch are removed with it.
338
+ - **A removal you did not author usually means TRUNK is drifted**, not that the branch deleted something.
339
+ A component that exists in the DB with **no file on trunk** is missing from every branch too, so it
340
+ shows up as a phantom `removed` in *every* branch preview — while the trunk sync separately tries to
341
+ hard-delete it on every run. Check whether the file exists on trunk before "fixing" it on the branch;
342
+ the repair is to mirror the live DB source back into the trunk repo (`component-integrity.md`).
343
+
344
+ ### Inspecting branches and drift
345
+
346
+ ```bash
347
+ remits-cli components branches # branches with variants, counts, drift
348
+ remits-cli components branch feature_branch # overridden / added / removed + subscribers
349
+ remits-cli components branch feature_branch --diff 50 --component-type action
350
+ remits-cli components branch feature_branch --subscribers
351
+ remits-cli components branch feature_branch --json # the stored overlay content
352
+ ```
353
+
354
+ **Drift is the number to watch.** Each variant records the trunk content hash at the moment it was cut
355
+ (`originHash`). When the origin component later changes, the variant is reported **DRIFTED** — the branch
356
+ is now based on a stale version and someone should reconcile it. Before editing an origin component, check
357
+ whether variants of it exist: the owner account's `account-info.json` carries a `componentBranches`
358
+ summary, and `components branches` gives the live view. A change to the origin silently drifts every
359
+ branch that overlays it.
360
+
361
+ **`components branches` only lists branches that already have overlays.** A branch you just pushed is
362
+ invisible here until its first sync — that is not an error. Preview it by name (`components sync --dry-run`
363
+ from that checkout).
364
+
365
+ **Trunk moving also invalidates a branch.** Variant sparseness compares branch content against *current*
366
+ trunk, so a trunk change can make an overlay obsolete without the branch changing at all. A trunk sync
367
+ drops the affected branches' cached sync verdicts, so the next `components sync` on the branch really
368
+ re-evaluates instead of answering *"No changes detected"*. After promoting anything, re-sync each live
369
+ branch.
370
+
371
+ **The admin UI has the same surface**, which is what to point a non-CLI user at: the *owner* account page's
372
+ **Component branches** box (per-branch counts, drift, subscribers, and Preview / Sync / Retire), and the
373
+ *subscriber* account page's **Branch variants** box plus **GitHub → Fetch `<branch>` variants**. Both open
374
+ the same result panel — grouped plan, subscribers, a Monaco diff of trunk vs branch per changed field, and
375
+ a confirm-gated override when the removal guard refuses. Preview there is the same `--dry-run`.
376
+
377
+ ### Diagnosing a variant
378
+
379
+ - `remits-cli components branch <name> --json` — the stored overlay content and what it overrides.
380
+ - The compile signature `variant:<id>:<hash>` in `Using Cached BCD` logs — proves a variant actually ran.
381
+ - A test/tool response reports `testComponentSource` as `staged` | `variant` | `db`, so you can see which
382
+ layer the run resolved without reading logs.
383
+
384
+ > **A populated staging cache makes a variant look broken.** Anything carrying a CLI `TestMode` — a
385
+ > `remits-cli token` URL, `/s/<tokenKey>/...`, an `X-Auth-Token` request, a script-loader embed — resolves
386
+ > the STAGED layer, which outranks the variant. So with components staged under the same branch/user, a
387
+ > tokenized page reports `Component Branch: <branch>` but `Variant Applied: none` and renders trunk, while
388
+ > an anonymous `?account_id=` request to the same page reports the variant. That is the documented
389
+ > precedence (staged -> variant -> trunk) working correctly, and it reads exactly like "tokens break
390
+ > variant resolution". **Run `remits-cli components clear --all` before verifying variant resolution
391
+ > through any tokenized entry point**, or check `remits-cli components status` first.
@@ -0,0 +1,158 @@
1
+ # Local State: What remits-cli Knows and Where
2
+
3
+ > A `remits-cli` skill reference. **Load this when** a question is about repo discovery, authentication state, what a command actually sent or received, or where a large tool response went.
4
+ >
5
+ > The table of contents below carries **real line numbers** (`- L84 Some Heading`), resolved when
6
+ > this file is installed, so they are never stale. Read the head, pick your sections, and offset-read
7
+ > only those. The entry text is the heading verbatim, so it also greps.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Required Local Index Reads](#required-local-index-reads)
12
+ - [Big Picture: How remits-cli State Is Organized](#big-picture-how-remits-cli-state-is-organized)
13
+ - [Account Repository Index](#account-repository-index)
14
+ - [Local State Files](#local-state-files)
15
+
16
+ ## Required Local Index Reads
17
+
18
+ These files are decision inputs. Read them when the related decision depends on them.
19
+
20
+ - `~/.remits-cli/account-repos.json`
21
+ - The inventory of every local Remits repo. Account repos are keyed by numeric account id.
22
+ - It also contains the reserved **`platform`** entry: the local clone of the core Remits platform repo (`type:'PLATFORM_REPO'`, with its `directory` path). `remits-cli` clones it on first authenticated run if it is missing (default `~/remits`, override with `REMITS_PLATFORM_DIR`). Read this entry when you need to analyze a back-stage seam or open a platform-fix PR.
23
+ - Read before choosing a repo outside the current working directory.
24
+ - Read when a support ticket references an account and you need to locate the correct local repo.
25
+ - Read before concluding that a repo does not exist locally.
26
+ - `~/.remits-cli/config.json`
27
+ - Read when service lifecycle, dashboard, or preferred-agent behavior matters.
28
+ - `~/.remits-cli/service-state.json`
29
+ - Read when the local control center URL, current dashboard port, repo-scan summary, or websocket status matters.
30
+ - Read when the user asks whether the remits-cli service is running or where to open the browser view.
31
+ - `~/.remits-cli/agents.json`
32
+ - The agent sessions registered from THIS machine, each with the process it is anchored to.
33
+ - Read when `remits-cli agent work` says no agent is registered, or when several agents are
34
+ registered and a command needs `--agent-id`.
35
+ - `~/.remits-cli/activity.log`
36
+ - Read when diagnosing service lifecycle, websocket, agent registration, or ticket-routing failures.
37
+ - `~/.remits-cli/sessions.json`
38
+ - Read when authentication state, active accounts, base URLs, websocket topics, or per-account data mode matters.
39
+ - `./.remits-cli/current-session.txt`
40
+ - Read before opening repo-local session logs so you know which session file is current.
41
+ - `./.remits-cli/sessions/<current-session>.jsonl`
42
+ - Read when the question is about what HTTP calls the repo recently made through remits-cli, which payload was sent, or what response/error came back.
43
+ - `./.remits-cli/tool-responses/<callId>.json`
44
+ - Read when `remits-cli tool` says the full payload was stored externally.
45
+ - `./.remits-cli/tools/tools.json`
46
+ - Read when the question is about available tool names, cached schemas, or why a tool invocation shape may be invalid.
47
+ - `account-info.json`
48
+ - Read in the target repo before making component changes or assuming account ownership.
49
+ - `account-configurations.json`
50
+ - Read only when account configuration values matter. It is generated separately because configuration maps can be large and `account-info.json` deliberately omits them.
51
+
52
+ Do not rely on memory for these indexes. Read the file that governs the decision you are making.
53
+
54
+ ## Big Picture: How remits-cli State Is Organized
55
+
56
+ Think about remits-cli as two cooperating layers:
57
+
58
+ 1. **Global machine state** in `~/.remits-cli/`
59
+ - This is the cross-repo control plane.
60
+ - It answers questions like:
61
+ - which accounts are authenticated
62
+ - which repos exist locally
63
+ - whether the background service is running
64
+ - where the control center lives
65
+ - which agent sessions are registered from this machine
66
+
67
+ 2. **Per-repo state** in `./.remits-cli/`
68
+ - This is the request/response and cache layer for one specific working tree.
69
+ - It answers questions like:
70
+ - which repo-local session log is current
71
+ - which `/cli/*` calls were made from this repo
72
+ - where a large tool response was written
73
+ - which tool schemas were most recently cached here
74
+
75
+ When a user asks an indirect question, map it to the right layer first:
76
+
77
+ - "Why did this ticket open in the wrong repo?" → start in global state.
78
+ - "What exact payload did this tool call send?" → start in per-repo state.
79
+ - "Why is the dashboard showing stale repos?" → start in `service-state.json` and `account-repos.json`.
80
+ - "Why is the browser page not showing websocket activity?" → start in `service-state.json` and `activity.log`.
81
+ - "Why is my agent not getting tickets?" → start in `agents.json`, then `remits-cli agent list`.
82
+
83
+ Agents should use this mental model before guessing.
84
+
85
+ ## Account Repository Index
86
+
87
+ The account repository index file is located at: `{{ACCOUNT_REPO_INDEX_PATH}}`
88
+
89
+ This JSON file is automatically maintained by `remits-cli` and tracks all known Remits account repositories on this machine. It is updated whenever any `remits-cli` command runs from an account repo directory. The file maps account IDs to their metadata:
90
+
91
+ ```json
92
+ {
93
+ "37": {
94
+ "accountId": 37,
95
+ "name": "Acme Corp",
96
+ "directory": "/Users/you/Projects/remits-acme-corp",
97
+ "updatedAt": "2026-03-23T12:00:00.000Z"
98
+ }
99
+ }
100
+ ```
101
+
102
+ **Use this index to:**
103
+ - Discover which account repos exist on this machine when working from a different directory
104
+ - Navigate to another account's repo to read its `account-info.json` and component source
105
+ - Resolve account names and IDs without making remote API calls
106
+ - Support cross-account workflows where an agent in one repo needs context from another
107
+
108
+ If the index file doesn't exist yet, run any `remits-cli` command from an account repo to bootstrap it.
109
+
110
+ ## Local State Files
111
+
112
+ **Global** (`~/.remits-cli/`):
113
+ - `sessions.json`
114
+ - Source of truth for authenticated account sessions, keyed by `accountId + dataMode + baseUrl` so the same account can hold separate sessions for different environments (e.g., localhost vs production).
115
+ - Each entry contains auth token, user info, websocket topic, data mode, base URL, and timestamp.
116
+ - Read this when a question involves auth, account selection, websocket topic coverage, or which session a command should resolve.
117
+ - `config.json`
118
+ - Global CLI preferences.
119
+ - Currently most important for preferred agent selection, but treat it as the general machine-level config file.
120
+ - `service-state.json`
121
+ - Runtime snapshot for the currently running remits-cli service.
122
+ - Includes dashboard URL, chosen port, repo-discovery summary, and websocket connection state.
123
+ - This is the first file to read when the user asks "is remits-cli running?", "what port is the dashboard on?", or "why isn't the browser page showing my connections?"
124
+ - `account-repos.json`
125
+ - Index of all known account repositories on this machine.
126
+ - Built from `account-info.json` discovery plus best-effort updates when commands run inside an account repo.
127
+ - This is the repo-resolution file. Read it before deciding that a repo is unavailable locally.
128
+ - `listener.pid`
129
+ - PID of the background remits-cli service process.
130
+ - Use it only to confirm process presence; use `service-state.json` for richer service details.
131
+ - `agents.json`
132
+ - The agent sessions registered from this machine, each with the process it is anchored to.
133
+ - Read this when a command asks for `--agent-id`, or when an agent appears registered locally but
134
+ is missing from `remits-cli agent list` (that gap IS the "why am I not getting tickets" answer).
135
+ - `activity.log`
136
+ - Human-readable chronological event log for service lifecycle, websocket events, agent
137
+ registration/heartbeat, and ticket routing.
138
+ - This is usually the best forensic file for "what happened?" questions.
139
+
140
+ **Per-repo** (`./.remits-cli/`):
141
+ - `tools/tools.json`
142
+ - Cached tool definitions for this repo context.
143
+ - Read this when tool availability or input shape is unclear.
144
+ - `sessions/<name>.jsonl`
145
+ - Repo-local HTTP request/response log for `/cli/*` calls.
146
+ - Tokens are redacted and large content fields are summarized.
147
+ - This is the first file to inspect when the question is "what exactly did remits-cli send or receive from this repo?"
148
+ - `tool-responses/<callId>.json`
149
+ - Full payload for `remits-cli tool` responses that were too large for the session log.
150
+ - Prefer this over terminal summaries when investigating tool behavior.
151
+ - `current-session.txt`
152
+ - Pointer to the active repo-local session log name.
153
+ - Always read this before opening `sessions/<name>.jsonl`.
154
+
155
+ Reading rules:
156
+ - Read `current-session.txt` before opening a session log by name.
157
+ - When a tool call says the response was stored externally, open `tool-responses/<callId>.json` instead of inferring from the terminal summary.
158
+ - If a question spans both global and repo-local behavior, inspect both layers and explain which facts came from which layer.