@remits/remits-cli 0.1.115 → 0.1.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,9 @@ remits-cli install --skills
22
22
  remits-cli tools
23
23
  remits-cli tool --base-url http://localhost:8080 --name "My Tool" --input '{"foo":"bar"}'
24
24
  remits-cli tool --name mcp_firestore_search --input '{"collection":"statements","documentId":"1234"}' --scope children
25
- remits-cli components stage
25
+ remits-cli workspace use --auto # once per checkout: your own staging lane
26
+ remits-cli components stage --workset # normal iteration: stage exactly what you changed
27
+ remits-cli components stage # FULL SNAPSHOT of the repo into the lane
26
28
  remits-cli components status
27
29
  remits-cli components clear
28
30
  remits-cli test run --test 45
@@ -30,6 +32,7 @@ remits-cli test run --test "My New Test" --names "test case 1,test case 2"
30
32
  git add -A
31
33
  git commit -m "sync passing changes"
32
34
  git push
35
+ remits-cli components sync --safe # variant branch: gated, dry-runs first, refuses surprises
33
36
  remits-cli components sync
34
37
  remits-cli components sync --branch feature_branch --dry-run
35
38
  remits-cli components sync --branch feature_branch --dry-run --summary
@@ -61,11 +64,17 @@ remits-cli install --skills --overwrite true
61
64
 
62
65
  ## How It Works
63
66
 
64
- - `components stage` uploads the current working tree into the staging cache used by test-mode execution. It replaces the branch/user staging scope with the current manifest, so stale aliases from prior stages are removed.
67
+ - `components stage` has three modes, and the difference decides what a run in the lane resolves:
68
+ - **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
+ - **`--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.
70
+ - **`--changed-only`** — uploads only the changed components and MERGES, leaving every other staged entry in place. It therefore cannot shrink a lane inherited from an earlier full snapshot; the command warns when it retains entries that way.
71
+ - An **empty workset never clears a lane**: `--workset` on a clean working tree stages nothing and leaves the lane as it is. Pass `--empty-workset clear` to opt in, or use `components clear --all`.
72
+ - A **deleted component file cannot be represented in Redis staging** — clearing a staged entry falls back to the committed row, so the component still resolves. Those changes are reported as NOT REPRESENTABLE. On a non-trunk variant branch, prove a deletion through `components sync --dry-run --summary --fail-on-errors`; on trunk there is no dry-run plan, so use the full pre-sync safety check before any mutating reconcile.
65
73
  - Schema `.meta.yml` sidecars can stage/sync the schema flags: `enableTrigger`, `enableFullText`, `enableRAG`, `enableRevisions`, `enableBigQuerySync`, `enableRules`, `anchor`, and `auxiliary`.
66
74
  - `components status` shows the branch/user staging entries that can shadow DB components during CLI-scoped test-mode execution.
67
75
  - `components clear` clears staged entries. Scope it with `--component-type` and/or `--component-id`. Component ids are type-local, so an id alone clears that one component when the id is staged in only one family; if the same id is staged across multiple families it returns an ambiguity error asking you to add `--component-type`. With no filter it clears every staged entry for the current branch; pass `--all` to force the full-branch wipe explicitly.
68
- - `components stage`, `components status`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response. `components stage` separates local working-tree component deltas from the full materialized staging cache count.
76
+ - `components stage`, `components status`, and `components clear` print concise summaries by default. Add `--json` or `--verbose` to print the full server response.
77
+ - `components stage` and `components status` report three separate numbers, and they answer three different questions: the **workset** (components git reports this working tree changed), what was **submitted**, and the materialized **overlay** the lane now holds — which is what a run resolves. A number that could not be established prints as `unknown`, never as `0`.
69
78
  - `components sync` performs a server-side sync from the git remote into the Remits platform for the selected branch. It does not run local git commands. After a successful non-dry-run sync, the platform clears the branch/user staging scope so staged aliases cannot keep shadowing the newly synced DB rows.
70
79
  - On trunk, `components sync` performs the full repo-to-DB reconcile. On a non-trunk branch, it writes `ComponentVariant` overlays only and may refresh branch-local `account-info.json`, `account-hierarchy.json`, and `account-configurations.json` for the subscribing account that initiated the sync. `--dry-run` is accepted only for non-trunk variant syncs and reports overrides/additions/tombstones without writing variants, caching the sync SHA, updating metadata, or clearing staging. Add `--summary` to dry-run output when you only need counts, removals/tombstones, errors, skipped items, and warnings. `--force-tombstones` is accepted only for non-trunk variant syncs and should be used only when missing trunk component files are intentional tombstone overrides.
71
80
  - `components commit` is a convenience wrapper that performs local git commit/push, then `components sync`, then local `git fetch`/`git pull --ff-only`.
@@ -86,6 +95,7 @@ remits-cli install --skills --overwrite true
86
95
  - If the same account is authenticated against more than one host and you omit `--base-url`, the CLI auto-resolves the best matching session and now prints the resolved host. Pass `--base-url` explicitly whenever the target host matters.
87
96
  - Avoid commas in individual test names. The `--names` filter is comma-delimited, so a single test case whose name contains commas cannot be targeted cleanly through `remits-cli test run --names ...`.
88
97
  - Nested help is available before required-argument validation, including `remits-cli test run --help`, `remits-cli components sync --help`, and `remits-cli tool --help`.
98
+ - `components sync --safe` is the recommended agent path on a variant branch. It expands to `--summary --changed-only --fail-on-errors --fail-on-removed`, resolves `--changed-since` from the branch's merge base with trunk when you did not name one (local refs only; it never runs an implicit `git fetch`), and prints the planned writes before mutating unless `--yes` is passed. On trunk there is no plan to gate, so it states what a trunk reconcile does and requires `--yes`.
89
99
  - `components sync` has fail-closed safety gates for unattended/agent use. Each exits non-zero instead of printing a wall of JSON: `--changed-only` (fail unless every planned write is a component this checkout edited), `--names-only` (print only `BUCKET type:id name` lines), `--fail-on-removed`, `--fail-on-errors`, and `--expected-removed <type:id>` (repeatable or comma-delimited; implies `--fail-on-removed`, so any removal you did not name fails). `--changed-only` also fails closed when the checkout is not a git working tree, because "git could not answer" must never be read as "nothing changed".
90
100
  - Any command that can touch production prints a `PROD DATA` banner naming the operation, the resolved account, and the host, and distinguishes a live **write** from a live **read** (and from a dry run). This covers `tool`, `test run`, and `components sync`.
91
101
  - When a tool call fails inside the platform runtime rather than inside the tool — Groovy reflective dispatch of a runtime-compiled component, an empty connection pool, a Redis reconnect, a lock-wait timeout — the server returns `503` with `failureClass: "transient_infrastructure"` and `retryable: true`, and the CLI prints `TRANSIENT INFRASTRUCTURE FAILURE (retryable)`. A genuine tool error stays a `500` with `failureClass: "tool_error"`. Retry the first; do not retry the second.
@@ -197,7 +207,7 @@ tail -n 200 ~/.remits-cli/tmux-activity.log
197
207
  - `tmux` must be installed for agent dispatch to work. Without it, websocket dispatch is received but agent panes cannot be created.
198
208
  - 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.
199
209
  - 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.
200
- - Recommended durable update flow for agents: `components stage` for testing, then `git add/commit/push`, then `remits-cli components sync`, then `git pull --ff-only` to confirm platform-generated files like `account-info.json`.
210
+ - 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.
201
211
  - In sandboxed agent environments, local git writes may require a single approval step. If you want to minimize approval churn, `remits-cli components commit` consolidates the local git and sync phases into one CLI command.
202
212
  - If account repo discovery fails for a websocket message, dispatch is skipped because the listener does not know which local directory to open for that account.
203
213
  - If you authenticate a new account while the listener is already running, the listener now refreshes its websocket clients automatically instead of requiring a manual restart.
package/index.js CHANGED
@@ -2458,12 +2458,29 @@ async function pushComponentsCommand(flags) {
2458
2458
  const mode = requestedMode === 'push' ? 'stage' : requestedMode;
2459
2459
  const changedFromWorkingTree = changedComponentsFromWorkingTree(cwd);
2460
2460
 
2461
- // --changed-only stages just the components this working tree edited, instead of re-uploading all of
2462
- // them. Two reasons it matters here: it is what makes an iteration loop cheap on a large repo, and it
2463
- // deliberately does NOT send `replace`, so the server's reconcile pass cannot delete the entries for
2464
- // components this partial stage did not mention.
2465
- const changedOnly = flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly);
2461
+ // WHAT this stage is. Three shapes, and the difference between them is the difference between a lane
2462
+ // that reads as "7 components in flight" and one that reads as "115 staged":
2463
+ //
2464
+ // full snapshot the whole repo manifest, lane reconciled to it `components stage`
2465
+ // workset replace only the git-changed components, lane reconciled `components stage --workset`
2466
+ // workset merge only the git-changed components, merged into the lane `components stage --changed-only`
2467
+ //
2468
+ // `--changed-only` keeps its old merge semantics deliberately — changing what an existing flag does
2469
+ // silently is how a safe command becomes a destructive one for somebody's script. The exact-lane
2470
+ // behavior is the new flag.
2471
+ const worksetReplace = flagEnabled(flags.workset) ||
2472
+ ((flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) &&
2473
+ (flagEnabled(flags['replace-lane']) || flagEnabled(flags.replaceLane)));
2474
+ const changedOnly = worksetReplace || flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly);
2475
+ const stageMode = worksetReplace ? 'workset-replace' : (changedOnly ? 'workset-merge' : 'full-snapshot');
2476
+ const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
2477
+
2466
2478
  let components = collectComponents(cwd);
2479
+ // Changes git reported that a component payload cannot carry — a deleted component file has nothing to
2480
+ // stage, and Redis staging has no way to say "hide this during CLI-scoped runs". Removing its staged
2481
+ // entry falls back to the committed row, so the component still resolves. Reported out loud rather than
2482
+ // dropped, because "my delete did not take effect" otherwise looks like a platform bug.
2483
+ const unstageable = unrepresentableChanges(changedFromWorkingTree, components);
2467
2484
  if (changedOnly) {
2468
2485
  if (changedFromWorkingTree === null) {
2469
2486
  throw new Error('--changed-only needs a git working tree to establish the changed set, and this directory is not one.');
@@ -2472,10 +2489,20 @@ async function pushComponentsCommand(flags) {
2472
2489
  entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase())));
2473
2490
  components = components.filter((component) => wanted.has(
2474
2491
  component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
2475
- if (!components.length) {
2476
- console.log('--changed-only: this working tree has no uncommitted component edits; nothing to stage.');
2492
+ if (!components.length && !(worksetReplace && emptyWorksetPolicy === 'clear')) {
2493
+ // Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
2494
+ // is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
2495
+ // manifest would delete the overlay its next test run depends on.
2496
+ console.log('Stage mode:', stageMode);
2497
+ console.log('Workset detected from git: 0 component(s) — nothing was staged and the lane was left as it is.');
2498
+ if (worksetReplace) {
2499
+ console.log(' The lane was NOT cleared. Clearing stays explicit: `remits-cli components clear --all`,');
2500
+ console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
2501
+ }
2477
2502
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
2478
- return { success: true, mode, updated: 0, unchanged: 0, skipped: [], changedOnly: true };
2503
+ printUnrepresentableChanges(unstageable);
2504
+ return { success: true, mode, updated: 0, unchanged: 0, skipped: [], changedOnly: true, stageMode,
2505
+ worksetCount: 0, staged: false };
2479
2506
  }
2480
2507
  }
2481
2508
 
@@ -2503,13 +2530,27 @@ async function pushComponentsCommand(flags) {
2503
2530
  ...runContextPayload(flags),
2504
2531
  agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
2505
2532
  acknowledge: flags.acknowledge || undefined,
2506
- // A partial (--changed-only) stage must not trigger the server's reconcile pass: reconcile deletes
2507
- // every staged entry absent from the manifest, which for a partial manifest is nearly all of them.
2533
+ // What this stage IS, stated rather than implied. The server derives the lane behavior from the mode
2534
+ // (CliComponentStaging.stageRequest) instead of from `replace`, which was overloaded: a partial
2535
+ // manifest with `replace: true` would make the reconcile pass delete nearly the whole lane, so
2536
+ // `--changed-only` had to send `replace: false` and could therefore never shrink a lane it inherited.
2537
+ stageMode,
2538
+ manifestScope: changedOnly ? 'working-tree' : 'repo',
2539
+ emptyWorksetPolicy,
2540
+ // Whether git could answer at all. A zero rendered for an unavailable answer is exactly the kind of
2541
+ // confident wrong number this whole surface exists to remove.
2542
+ worksetKnown: changedFromWorkingTree !== null,
2543
+ // The git evidence, per component: which files changed and with what status. It is what lets the
2544
+ // console split a lane's "active workset" from the entries a previous full snapshot left behind.
2545
+ changedSet: (changedFromWorkingTree || []).concat(unstageable),
2546
+ // Kept for a platform that predates stageMode. Same meaning it always had.
2508
2547
  replace: !changedOnly,
2509
2548
  components
2510
2549
  });
2511
2550
  response.changedFromWorkingTree = changedFromWorkingTree || [];
2512
2551
  response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
2552
+ response.requestedStageMode = stageMode;
2553
+ response.unrepresentableChanges = unstageable;
2513
2554
 
2514
2555
  if (flagEnabled(flags.json)) {
2515
2556
  console.log(JSON.stringify(response, null, 2));
@@ -2520,11 +2561,53 @@ async function pushComponentsCommand(flags) {
2520
2561
  printResolvedBaseUrl(baseUrl);
2521
2562
  printStagingLane(response.branchName || branchName, response.workspace || workspace, workspaceSource(cwd, flags));
2522
2563
  console.log('Data mode:', response.dataMode || dataMode);
2523
- console.log('Mode:', response.mode || mode + (changedOnly ? ' (--changed-only)' : ''));
2524
2564
  printComponentPolicy(response);
2525
2565
  printStageSummary(response, flags);
2526
2566
  }
2527
2567
 
2568
+ /** `--empty-workset refuse|clear`. Refusing is the default, and the only safe one. */
2569
+ function normalizeEmptyWorksetPolicy(flags) {
2570
+ const raw = flags['empty-workset'] != null ? flags['empty-workset'] : flags.emptyWorkset;
2571
+ const value = raw === true ? '' : String(raw == null ? '' : raw).trim().toLowerCase();
2572
+ if (value === 'clear' || value === 'clears-lane') return 'clear';
2573
+ return 'refuse';
2574
+ }
2575
+
2576
+ /**
2577
+ * Git-reported component changes that a stage payload cannot carry.
2578
+ *
2579
+ * A deleted component file has no content to send, and Redis staging has no way to express "hide this
2580
+ * during CLI-scoped runs" — clearing its staged entry falls back to the committed row, so the component
2581
+ * still resolves. An agent that deletes a file, stages, and sees the component still running would read
2582
+ * that as a platform defect. Durable deletion lives in variant sync (tombstones), not here.
2583
+ *
2584
+ * Returns [] when git could not answer, because "nothing is unrepresentable" would be a claim.
2585
+ */
2586
+ function unrepresentableChanges(changedFromWorkingTree, collected) {
2587
+ if (!Array.isArray(changedFromWorkingTree)) return [];
2588
+ const present = new Set((collected || []).map((component) =>
2589
+ component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
2590
+ return changedFromWorkingTree
2591
+ .filter((entry) => !present.has(entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase())))
2592
+ .map((entry) => Object.assign({}, entry, {
2593
+ unrepresentable: true,
2594
+ reason: (entry.statuses || []).some((s) => String(s).includes('D')) ? 'deleted' : 'no-local-files'
2595
+ }));
2596
+ }
2597
+
2598
+ function printUnrepresentableChanges(unstageable) {
2599
+ if (!unstageable || !unstageable.length) return;
2600
+ console.log('');
2601
+ console.log('NOT REPRESENTABLE IN REDIS STAGING — ' + unstageable.length + ' change(s):');
2602
+ unstageable.slice(0, 20).forEach((entry) => {
2603
+ console.log(' ' + entry.type + ' ' + (entry.id || entry.name || '(unknown)') +
2604
+ ' (' + entry.reason + ') ' + (entry.paths || []).join(', '));
2605
+ });
2606
+ console.log(' A staged entry cannot hide a component. Clearing it falls back to the committed row, so');
2607
+ console.log(' the component still resolves in a CLI-scoped run. Prove a deletion through the durable');
2608
+ console.log(' plan instead: remits-cli components sync --dry-run --summary --fail-on-errors');
2609
+ }
2610
+
2528
2611
  /**
2529
2612
  * A policy refusal is an ANSWER, not a transport failure.
2530
2613
  *
@@ -2602,15 +2685,55 @@ function printComponentCommandResponse(label, response, flags) {
2602
2685
  printFullResponseHint();
2603
2686
  }
2604
2687
 
2688
+ /**
2689
+ * What this stage did, in the terms a human is actually asking about.
2690
+ *
2691
+ * Three numbers, and they are three different questions:
2692
+ *
2693
+ * WORKSET how many components this working tree changed, per git
2694
+ * SUBMITTED how many this command uploaded
2695
+ * OVERLAY how many staged entries the lane now holds, which is what a run resolves
2696
+ *
2697
+ * They used to be reported as one number — the overlay — so an agent that edited 5 components in a repo
2698
+ * of 115 produced "Updated: 115" and a console badge reading "STAGED 115". Nobody could tell 115 edits
2699
+ * from one full snapshot, and those want opposite actions.
2700
+ */
2605
2701
  function printStageSummary(response, flags) {
2606
- // `stage` always uploads EVERY component in the repo, so the server's Updated/Unchanged counts
2607
- // describe the whole staging overlay, not what this checkout edited. That is what made "Updated: 75"
2608
- // read as "I changed 75 components". Lead with the working-tree delta, which is the number an agent
2609
- // is actually asking about, and label the server counts as the overlay they describe.
2610
2702
  const changed = Array.isArray(response.changedFromWorkingTree) ? response.changedFromWorkingTree : [];
2611
2703
  const tracked = response.changedFromWorkingTreeAvailable !== false;
2704
+ const stage = response.stage || {};
2705
+ const lane = response.laneSummary || {};
2706
+ // What we ASKED for, and what the platform says it did. Kept apart deliberately: comparing the server's
2707
+ // answer against a value that already fell back to it can never disagree, so the mismatch note below
2708
+ // could never fire.
2709
+ const requestedMode = response.requestedStageMode || null;
2710
+ const stageMode = stage.mode || requestedMode || 'full-snapshot';
2711
+
2712
+ console.log('Stage mode:', describeStageMode(stageMode));
2713
+
2714
+ // A platform that predates stage modes ignores `stageMode` and reads `replace`, which a workset stage
2715
+ // sends as false — so it MERGES where the caller asked for a lane replacement, and returns no `stage`
2716
+ // block to say so. Silence there is the worst outcome available: the agent believes the lane holds
2717
+ // exactly its workset while the platform left an inherited overlay in place. Detect it by what came
2718
+ // back rather than by a version number.
2719
+ if (stage.mode == null && response.staging) {
2720
+ console.log('NOTE: this platform predates stage modes. It read the legacy `replace` flag, so a');
2721
+ console.log(' --workset stage MERGED into the lane instead of reconciling it, and the workset/');
2722
+ console.log(' overlay split below is not available. Use `components clear --all` first if you need');
2723
+ console.log(' the lane to hold exactly your workset.');
2724
+ } else if (stage.mode && requestedMode && stage.mode !== requestedMode) {
2725
+ console.log('NOTE: asked the platform for `' + requestedMode + '`; it applied `' + stage.mode + '`.');
2726
+ }
2727
+
2728
+ if (response.sharedLane === true) {
2729
+ console.log('Lane: SHARED (no workspace) — a full stage REPLACES it rather than merging, so a second');
2730
+ console.log(' agent staging here overwrites what you are testing. `remits-cli workspace use --auto`.');
2731
+ }
2732
+
2733
+ // "git could not answer" and "git says nothing changed" are different facts, and only one of them is a
2734
+ // number. Never render the missing answer as zero.
2612
2735
  if (tracked) {
2613
- console.log('Components edited in this working tree:', changed.length);
2736
+ console.log('Workset detected from git:', changed.length, 'component(s)');
2614
2737
  changed.slice(0, 20).forEach((entry) => {
2615
2738
  const label = (entry.type || 'component') + ' ' + (entry.id || entry.name || '(unknown)');
2616
2739
  const name = entry.name && entry.id ? ' ' + entry.name : '';
@@ -2623,23 +2746,66 @@ function printStageSummary(response, flags) {
2623
2746
  if (!changed.length) {
2624
2747
  console.log(' (no uncommitted component edits — already-committed edits are staged but not listed here)');
2625
2748
  }
2749
+ } else {
2750
+ console.log('Workset detected from git: UNKNOWN (this is not a git working tree)');
2626
2751
  }
2627
- console.log('Staging overlay — newly written:', response.updated || 0);
2628
- console.log('Staging overlay — already current:', response.unchanged || 0);
2629
- console.log('Skipped:', Array.isArray(response.skipped) ? response.skipped.length : 0);
2630
- if (response.reconcile) {
2631
- console.log('Reconciled stale keys:', response.reconcile.removedCount || 0);
2752
+
2753
+ const submitted = stage.submittedCount != null
2754
+ ? stage.submittedCount
2755
+ : ((response.updated || 0) + (response.unchanged || 0));
2756
+ console.log('Submitted to the platform:', submitted, 'component(s) (' +
2757
+ (response.updated || 0) + ' newly written, ' + (response.unchanged || 0) + ' already current)');
2758
+ if (Array.isArray(response.skipped) && response.skipped.length) {
2759
+ console.log('Skipped (no type/hash/identity):', response.skipped.length);
2632
2760
  }
2633
- if (response.staging) {
2634
- console.log('Total staged entries for this branch/user:', response.staging.remainingCount || 0);
2761
+ if (stage.removedRetainedCount) {
2762
+ console.log('Removed from the lane by this stage:', stage.removedRetainedCount, 'staged entry(ies)');
2763
+ } else if (response.reconcile) {
2764
+ console.log('Removed from the lane by this stage:', response.reconcile.removedCount || 0, 'staged entry(ies)');
2635
2765
  }
2766
+
2767
+ const overlay = lane.stagedCount != null
2768
+ ? lane.stagedCount
2769
+ : (response.staging ? response.staging.remainingCount : null);
2770
+ if (overlay != null) {
2771
+ console.log('Materialized overlay now held by the lane:', overlay, 'component(s)');
2772
+ }
2773
+
2774
+ // The trap `--changed-only` leaves behind: it merges, so a lane inherited from an earlier full snapshot
2775
+ // keeps every one of those entries resolving ahead of committed source. Silence here is what made
2776
+ // "I staged 7 components" and "this lane holds 115" look like the same statement.
2777
+ const retained = lane.retainedCount;
2778
+ if (retained > 0) {
2779
+ console.log('');
2780
+ console.log('WARNING: ' + retained + ' staged component(s) in this lane were NOT submitted by this stage.');
2781
+ console.log(' They came from an earlier stage and still shadow committed source for every run in this lane.');
2782
+ console.log(' Make the lane exactly your workset: remits-cli components stage --workset');
2783
+ console.log(' Or drop the overlay entirely: remits-cli components clear --all');
2784
+ }
2785
+
2786
+ printUnrepresentableChanges(response.unrepresentableChanges || (stage.unrepresentable || []));
2636
2787
  printComponentCommandResponse('Components stage', response, flags);
2637
2788
  }
2638
2789
 
2790
+ function describeStageMode(mode) {
2791
+ switch (mode) {
2792
+ case 'full-snapshot':
2793
+ return 'full snapshot (whole repo manifest; the lane is reconciled to it)';
2794
+ case 'workset-replace':
2795
+ return 'workset replace (only the components git reports changed; the lane is reconciled to exactly them)';
2796
+ case 'workset-merge':
2797
+ return 'workset merge (only the components git reports changed, MERGED into whatever the lane already held)';
2798
+ case 'partial':
2799
+ return 'partial (single-component field edit)';
2800
+ default:
2801
+ return mode || 'unknown';
2802
+ }
2803
+ }
2804
+
2639
2805
  function printStatusSummary(response, flags) {
2640
2806
  printBranchContext(response);
2641
2807
  printStagingLane(response.branchName, response.workspace, null);
2642
- console.log('Staged count:', response.stagedCount || 0);
2808
+ printLaneSummary(response);
2643
2809
  printComponentTypeCounts(response.entries || []);
2644
2810
 
2645
2811
  // Every lane staged on this branch, not just this one. An agent that believes it is working alone can
@@ -2663,6 +2829,48 @@ function printStatusSummary(response, flags) {
2663
2829
  printComponentCommandResponse('Components staging', response, flags);
2664
2830
  }
2665
2831
 
2832
+ /**
2833
+ * What THIS lane holds, separated into the work in flight and the overlay carried along with it.
2834
+ *
2835
+ * Read authoritatively from the staging keys (not the lane index), so the numbers here are the ones a run
2836
+ * in this lane will actually resolve.
2837
+ */
2838
+ function printLaneSummary(response) {
2839
+ const lane = response.laneSummary || {};
2840
+ const overlay = lane.stagedCount != null ? lane.stagedCount : (response.stagedCount || 0);
2841
+
2842
+ console.log('Materialized overlay (what a run in this lane resolves):', overlay, 'component(s)');
2843
+ // An EMPTY lane has no last stage, so "Last stage: unknown" would invite a reader to go looking for a
2844
+ // stage that never happened. Nothing staged is clean state, not a missing answer.
2845
+ if (!overlay) {
2846
+ console.log('Nothing is staged in this lane; runs here resolve committed source.');
2847
+ } else if (lane.stageMode && lane.stageMode !== 'unknown') {
2848
+ console.log('Last stage:', describeStageMode(lane.stageMode));
2849
+ if (lane.submittedCount != null) {
2850
+ console.log(' submitted by that stage:', lane.submittedCount, 'component(s)');
2851
+ }
2852
+ // null is "not established", never zero. A lane staged by a build that predates stage provenance has
2853
+ // an unknown workset, and printing 0 would assert that nobody is working in it.
2854
+ if (lane.worksetKnown && lane.worksetCount != null) {
2855
+ console.log(' of which git reported as edited:', lane.worksetCount, 'component(s)');
2856
+ } else {
2857
+ console.log(' of which git reported as edited: unknown (that stage reported no git evidence — an');
2858
+ console.log(' older CLI, a non-git checkout, or a single-field MCP edit)');
2859
+ }
2860
+ if (lane.retainedCount > 0) {
2861
+ console.log(' retained from EARLIER stages:', lane.retainedCount, 'component(s) — these still shadow');
2862
+ console.log(' committed source. `components stage --workset` makes the lane exactly your workset.');
2863
+ }
2864
+ } else {
2865
+ console.log('Last stage: unknown (staged before stage modes existed, or by another door)');
2866
+ }
2867
+
2868
+ if (response.sharedLane === true) {
2869
+ console.log('Lane isolation: SHARED — no workspace. A full stage here REPLACES the lane rather than');
2870
+ console.log(' merging, so a second agent overwrites what you are testing. `remits-cli workspace use --auto`.');
2871
+ }
2872
+ }
2873
+
2666
2874
  // Every staging lane on the ACCOUNT, across branches.
2667
2875
  //
2668
2876
  // The block above only ever covered lanes on the SAME git branch, because that is all its key pattern
@@ -2689,8 +2897,14 @@ function printAccountLanes(response) {
2689
2897
  const who = lane.userEmail ? ' ' + lane.userEmail : '';
2690
2898
  const ttl = lane.expiresInSeconds != null
2691
2899
  ? ' expires in ~' + Math.round(lane.expiresInSeconds / 60) + 'm' : '';
2900
+ // Overlay first (it is what a run resolves), then the work the last stage actually submitted. A lane
2901
+ // showing "115 overlay / 7 workset" is one agent iterating on 7 components, not 115 edits in flight.
2902
+ const workset = (lane.worksetKnown && lane.worksetCountAsOf != null)
2903
+ ? ', workset ' + lane.worksetCountAsOf : '';
2904
+ const stageMode = lane.stageMode && lane.stageMode !== 'unknown' ? ' [' + lane.stageMode + ']' : '';
2905
+ const shared = lane.sharedLane === true ? ' SHARED LANE' : '';
2692
2906
  console.log(' ' + (lane.mine ? '* ' : ' ') + lane.branchName + ws
2693
- + ' (' + world + ') — ' + (lane.stagedCountAsOf || 0) + ' component(s)' + who + ttl);
2907
+ + ' (' + world + ') — overlay ' + (lane.stagedCountAsOf || 0) + workset + stageMode + shared + who + ttl);
2694
2908
  });
2695
2909
  console.log(' (* = this command\'s lane)');
2696
2910
  console.log(' A variant-branch lane layers over that branch\'s ComponentVariant overlays, and a commit');
@@ -2887,8 +3101,13 @@ async function clearComponentsCommand(flags) {
2887
3101
  return response;
2888
3102
  }
2889
3103
 
2890
- async function syncComponentsCommand(flags) {
3104
+ async function syncComponentsCommand(rawFlags) {
2891
3105
  const cwd = process.cwd();
3106
+ // `--safe` is a NAME for the gate combination that should be the default agent path, not a new gate.
3107
+ // Spelling out --summary --changed-only --fail-on-errors --fail-on-removed correctly every time is the
3108
+ // step agents skip, and skipping it is how a stale branch writes 34 unrelated ComponentVariant rows.
3109
+ const safe = flagEnabled(rawFlags.safe);
3110
+ const flags = safe ? expandSafeSyncFlags(rawFlags) : rawFlags;
2892
3111
  ensureLocalState(cwd);
2893
3112
  const sessionContext = resolveSessionContext(cwd, flags);
2894
3113
  const { session, accountId } = sessionContext;
@@ -2935,6 +3154,10 @@ async function syncComponentsCommand(flags) {
2935
3154
  };
2936
3155
 
2937
3156
  let preflightGate = null;
3157
+ // The flags the gates are actually evaluated with. `--safe` may resolve `--changed-since` for the
3158
+ // caller, and it must be the SAME value the preflight and the post-sync evaluation use, or the two
3159
+ // could disagree about the same plan.
3160
+ let gateFlags = flags;
2938
3161
  if (preflightRequested && !dryRun) {
2939
3162
  const statusResponse = await loggedPost(api, cwd, '/cli/components', {
2940
3163
  token: session.token,
@@ -2947,31 +3170,63 @@ async function syncComponentsCommand(flags) {
2947
3170
 
2948
3171
  const branchContext = statusResponse && statusResponse.branchContext;
2949
3172
  if (!branchContext || branchContext.onTrunk) {
2950
- throw new Error('components sync safety preflight requires a non-trunk variant branch because trunk sync has no dry-run plan. Re-run without safety flags only if you intend a full trunk reconcile.');
2951
- }
3173
+ // Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
3174
+ // an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
3175
+ // and make the caller assert it.
3176
+ if (!safe) {
3177
+ throw new Error('components sync safety preflight requires a non-trunk variant branch because trunk sync has no dry-run plan. Re-run without safety flags only if you intend a full trunk reconcile.');
3178
+ }
3179
+ console.log('');
3180
+ console.log('TRUNK SYNC — there is no dry-run plan for trunk, so --safe cannot gate this.');
3181
+ console.log(' This is an AUTHORITATIVE full repo-to-DB reconcile of ' + (branchContext ? branchContext.trunkBranch || branchName : branchName) + ':');
3182
+ console.log(' every component row is rewritten from the pushed repo, and any live component');
3183
+ console.log(' missing from the repo is DELETED.');
3184
+ console.log(' For a gated plan, work on a variant branch instead.');
3185
+ if (!flagEnabled(flags.yes)) {
3186
+ throw new Error('components sync --safe on trunk requires --yes, because the plan cannot be previewed. ' +
3187
+ 'Re-run with --yes to accept the full trunk reconcile, or switch to a variant branch for a gated sync.');
3188
+ }
3189
+ // Drop the gates that can only be evaluated against a plan. Leaving them on would fail the command
3190
+ // AFTER an authoritative reconcile had already been written, which is the worst of both.
3191
+ gateFlags = Object.assign({}, flags, {
3192
+ 'changed-only': false, changedOnly: false,
3193
+ 'fail-on-removed': false, failOnRemoved: false
3194
+ });
3195
+ } else {
3196
+ gateFlags = resolveChangedSinceFlags(flags, cwd, branchContext, safe);
2952
3197
 
2953
- const previewResponse = await loggedPost(api, cwd, '/cli/components', Object.assign({}, syncPayload, {
2954
- dryRun: true
2955
- })).then((r) => r.data);
3198
+ const previewResponse = await loggedPost(api, cwd, '/cli/components', Object.assign({}, syncPayload, {
3199
+ dryRun: true
3200
+ })).then((r) => r.data);
2956
3201
 
2957
- if (!previewResponse.success) {
2958
- throw new Error(previewResponse.message || 'Server sync dry-run failed');
2959
- }
3202
+ if (!previewResponse.success) {
3203
+ throw new Error(previewResponse.message || 'Server sync dry-run failed');
3204
+ }
2960
3205
 
2961
- preflightGate = evaluateSyncGates(previewResponse, flags, changedFromWorkingTree, cwd);
3206
+ preflightGate = evaluateSyncGates(previewResponse, gateFlags, changedFromWorkingTree, cwd);
3207
+
3208
+ if (namesOnly) {
3209
+ printSessionResolutionWarning(sessionContext);
3210
+ printResolvedBaseUrl(baseUrl);
3211
+ console.log('Data mode:', previewResponse.dataMode || dataMode);
3212
+ console.log('Mode:', 'sync dry-run');
3213
+ console.log('Dry run: no variants were written and staging was not cleared.');
3214
+ printSyncNames(previewResponse);
3215
+ failOnSyncGate(preflightGate);
3216
+ return previewResponse;
3217
+ }
2962
3218
 
2963
- if (namesOnly) {
2964
- printSessionResolutionWarning(sessionContext);
2965
- printResolvedBaseUrl(baseUrl);
2966
- console.log('Data mode:', previewResponse.dataMode || dataMode);
2967
- console.log('Mode:', 'sync dry-run');
2968
- console.log('Dry run: no variants were written and staging was not cleared.');
2969
- printSyncNames(previewResponse);
2970
3219
  failOnSyncGate(preflightGate);
2971
- return previewResponse;
2972
- }
2973
3220
 
2974
- failOnSyncGate(preflightGate);
3221
+ // The plan passed every gate. Show it before mutating anyway: a gate proves the plan matches the
3222
+ // changed set, not that the changed set is what the author meant.
3223
+ if (safe && !flagEnabled(flags.yes)) {
3224
+ console.log('');
3225
+ console.log('Planned durable writes (gates passed):');
3226
+ printSyncNames(previewResponse);
3227
+ console.log('');
3228
+ }
3229
+ }
2975
3230
  }
2976
3231
 
2977
3232
  // Through the same refusal-aware door as `stage`: the landing lease and the account's commit rules both
@@ -2983,7 +3238,7 @@ async function syncComponentsCommand(flags) {
2983
3238
  }
2984
3239
 
2985
3240
  const summary = buildSyncSummary(response);
2986
- const gate = preflightGate || evaluateSyncGates(response, flags, changedFromWorkingTree, cwd);
3241
+ const gate = preflightGate || evaluateSyncGates(response, gateFlags, changedFromWorkingTree, cwd);
2987
3242
  summary.gates = gate.checks;
2988
3243
 
2989
3244
  if (flagEnabled(flags.json)) {
@@ -3093,8 +3348,73 @@ function printPromotionSignals(response) {
3093
3348
  });
3094
3349
  }
3095
3350
 
3351
+ /**
3352
+ * `--safe`: the gate combination that should be the normal agent path for a variant-branch sync.
3353
+ *
3354
+ * Named rather than remembered. The individual flags have existed for a while and the failure they exist
3355
+ * to stop — a stale branch whose old copies of untouched files become unrelated ComponentVariant
3356
+ * overrides — happens precisely when somebody forgets one of them.
3357
+ *
3358
+ * `--expected-removed` already implies fail-on-removed and NAMES the removals the caller accepts, so
3359
+ * --safe must not layer a blanket --fail-on-removed on top of it and contradict the narrower answer.
3360
+ */
3361
+ function expandSafeSyncFlags(flags) {
3362
+ const expectedRemoved = flags['expected-removed'] != null ? flags['expected-removed'] : flags.expectedRemoved;
3363
+ return Object.assign({}, flags, {
3364
+ summary: true,
3365
+ 'fail-on-errors': true,
3366
+ 'changed-only': true,
3367
+ 'fail-on-removed': expectedRemoved != null ? flags['fail-on-removed'] : true
3368
+ });
3369
+ }
3370
+
3371
+ /**
3372
+ * The base ref the `--changed-only` gate compares the server plan against.
3373
+ *
3374
+ * `--changed-only` alone reads UNCOMMITTED edits, and the documented flow commits and pushes BEFORE the
3375
+ * sync (the server syncs by pulling the remote, so it cannot see uncommitted work at all). So at the
3376
+ * moment the gate runs, the working tree is usually clean and the changed set is empty — which used to
3377
+ * make the gate refuse every plan.
3378
+ *
3379
+ * Under --safe the ref is derived from the branch's own merge base with trunk when the caller did not
3380
+ * name one. Local refs only: no implicit `git fetch`, because a status/gate command that quietly reaches
3381
+ * the network is a command whose cost and failure modes the caller did not agree to. When nothing local
3382
+ * can answer, this refuses with the command to run rather than gating against an empty set.
3383
+ */
3384
+ function resolveChangedSinceFlags(flags, cwd, branchContext, safe) {
3385
+ const explicit = flags['changed-since'] || flags.changedSince;
3386
+ if (explicit || !safe) {
3387
+ return flags;
3388
+ }
3389
+ const trunk = (branchContext && branchContext.trunkBranch) || 'main';
3390
+ const candidates = ['origin/' + trunk, trunk];
3391
+ for (const candidate of candidates) {
3392
+ let base = null;
3393
+ try {
3394
+ base = execSync('git merge-base HEAD ' + JSON.stringify(candidate), {
3395
+ cwd,
3396
+ stdio: ['ignore', 'pipe', 'pipe']
3397
+ }).toString().trim();
3398
+ } catch (_) {
3399
+ continue;
3400
+ }
3401
+ if (base) {
3402
+ console.log('--safe: comparing the sync plan against everything this branch added since ' +
3403
+ candidate + ' (' + base.slice(0, 12) + ').');
3404
+ return Object.assign({}, flags, { 'changed-since': base });
3405
+ }
3406
+ }
3407
+ throw new Error(
3408
+ 'components sync --safe needs a base ref to compare the plan against, and neither origin/' + trunk +
3409
+ ' nor ' + trunk + ' could be resolved locally.\n' +
3410
+ ' Run `git fetch origin ' + trunk + '` and retry, or pass it yourself:\n' +
3411
+ ' remits-cli components sync --safe --changed-since origin/' + trunk
3412
+ );
3413
+ }
3414
+
3096
3415
  function syncPreflightRequested(flags) {
3097
3416
  return Boolean(
3417
+ flagEnabled(flags.safe) ||
3098
3418
  flagEnabled(flags['names-only']) ||
3099
3419
  flagEnabled(flags.namesOnly) ||
3100
3420
  flagEnabled(flags['changed-only']) ||
@@ -3214,7 +3534,16 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
3214
3534
  ? '\n The changed set is EMPTY: --changed-only reads UNCOMMITTED edits, and the documented flow ' +
3215
3535
  'commits and pushes before syncing. Pass --changed-since <ref> (e.g. the commit you branched ' +
3216
3536
  'from, or origin/main) so the gate can see committed work.'
3217
- : '';
3537
+ // A plan that reaches well beyond a real changed set is almost always a STALE BRANCH: it still
3538
+ // physically carries old copies of files nobody on it touched, and a variant sync turns each of
3539
+ // those into an unrelated override. Naming the cause is the difference between a gate that
3540
+ // stops the damage and a gate that also tells you how to clear it.
3541
+ : (changedSet.length && unexpected.length > changedSet.length)
3542
+ ? '\n Most likely cause: this branch is BEHIND trunk and still carries old copies of files ' +
3543
+ 'it never changed. A variant sync turns each of those into an unrelated override.' +
3544
+ '\n Next step: merge trunk into this branch, push, then re-run the same command:' +
3545
+ '\n git merge <trunk> && git push origin <branch>'
3546
+ : '';
3218
3547
  violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this checkout did not change: ' +
3219
3548
  unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : '') + hint);
3220
3549
  }
@@ -3717,6 +4046,14 @@ async function commitComponentsCommand(flags) {
3717
4046
  }
3718
4047
 
3719
4048
  console.log('Phase 2/3: server sync');
4049
+ if (flagEnabled(flags.safe)) {
4050
+ // The honest limit, stated where it applies. `components commit` runs git add/commit/push BEFORE the
4051
+ // server sync, so a --safe gate here cannot stop the push — it stops the PLATFORM from writing bad
4052
+ // overlays. The branch may already be pushed when the gate refuses; that is recoverable, an
4053
+ // unintended ComponentVariant tree is much less so.
4054
+ console.log(' --safe: the branch is already pushed. The gate below stops the platform from WRITING a');
4055
+ console.log(' surprising plan; it cannot un-push. Use `components sync --safe` alone for a pre-push gate.');
4056
+ }
3720
4057
  const syncResponse = await syncComponentsCommand({
3721
4058
  ...flags,
3722
4059
  branch: branchName,
@@ -9854,6 +10191,13 @@ function printComponentsHelp(subcommand) {
9854
10191
  console.log('--summary prints compact counts, removals/tombstones, errors, skipped items, and warnings.');
9855
10192
  console.log('');
9856
10193
  console.log('Agent safety gates (each exits non-zero instead of printing a wall of JSON):');
10194
+ console.log(' --safe THE RECOMMENDED PATH on a variant branch. Expands to');
10195
+ console.log(' --summary --changed-only --fail-on-errors --fail-on-removed,');
10196
+ console.log(' resolves --changed-since from the merge base with trunk when you');
10197
+ console.log(' did not name one (local refs only, no implicit fetch), and prints');
10198
+ console.log(' the planned writes before mutating unless --yes is passed.');
10199
+ console.log(' On TRUNK there is no plan to gate, so it explains what a trunk');
10200
+ console.log(' reconcile does and requires --yes.');
9857
10201
  console.log(' --changed-only dry-run first; fail unless every planned write is a component this checkout changed');
9858
10202
  console.log(' --changed-since <ref> pair with --changed-only after committing: the changed set becomes');
9859
10203
  console.log(' the components touched between <ref> and HEAD, plus uncommitted edits');
@@ -9865,14 +10209,29 @@ function printComponentsHelp(subcommand) {
9865
10209
  return;
9866
10210
  }
9867
10211
  if (subcommand === 'stage' || subcommand === 'push') {
9868
- console.log('Usage: remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--changed-only] [--data-mode test|prod] [--json|--verbose]');
10212
+ console.log('Usage: remits-cli components stage [--workset | --changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--data-mode test|prod] [--json|--verbose]');
9869
10213
  console.log('');
9870
10214
  console.log('Stages local component files into the Redis staging cache. It never writes the database or git.');
9871
- console.log('Terminal output separates local working-tree component deltas from the full materialized staging cache count.');
10215
+ console.log('');
10216
+ console.log('Three stage modes, and the difference decides what a run in this lane resolves:');
10217
+ console.log(' (default) FULL SNAPSHOT — uploads the whole repo manifest and reconciles the lane');
10218
+ console.log(' to it. Correct as a complete snapshot and as a "what is stale?" reset;');
10219
+ console.log(' a poor progress signal, because the lane then holds every component.');
10220
+ console.log(' --workset WORKSET REPLACE — uploads only the components git reports changed and');
10221
+ console.log(' reconciles the lane to exactly them. THE NORMAL ITERATION MODE: the');
10222
+ console.log(' lane holds your work and nothing else, so the console shows what you');
10223
+ console.log(' are actually doing. An empty workset is refused, never a silent clear.');
10224
+ console.log(' --changed-only WORKSET MERGE — uploads only the changed components and leaves every');
10225
+ console.log(' other staged entry in place. Keeps its long-standing semantics, so it');
10226
+ console.log(' cannot shrink a lane inherited from an earlier full snapshot; the');
10227
+ console.log(' command warns when entries are retained that way.');
9872
10228
  console.log('');
9873
10229
  console.log(' --workspace NAME stage into an isolated lane on this branch (see `remits-cli workspace`).');
9874
- console.log(' --changed-only stage only the components this working tree edited, and do NOT reconcile');
9875
- console.log(' (entries for untouched components are left alone rather than deleted).');
10230
+ console.log(' --empty-workset clear with --workset, let an empty workset clear the lane (default: refuse).');
10231
+ console.log(' --replace-lane with --changed-only, the explicit spelling of --workset.');
10232
+ console.log('');
10233
+ console.log('Output separates three different numbers: the WORKSET git reports, what was SUBMITTED, and');
10234
+ console.log('the materialized OVERLAY the lane now holds — which is the one a run resolves.');
9876
10235
  return;
9877
10236
  }
9878
10237
  if (subcommand === 'status') {
@@ -9893,21 +10252,26 @@ function printComponentsHelp(subcommand) {
9893
10252
  return;
9894
10253
  }
9895
10254
  if (subcommand === 'commit') {
9896
- console.log('Usage: remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
10255
+ console.log('Usage: remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
9897
10256
  console.log('');
9898
10257
  console.log('Runs local git add/commit/push, then server sync. Prefer explicit git + components sync when you need inspectable phases.');
9899
10258
  console.log('components commit does not support --dry-run.');
10259
+ console.log('');
10260
+ console.log('--safe passes the sync gates through to phase 2. The honest limit: git push happens in phase 1,');
10261
+ console.log('so the gate stops the PLATFORM from writing a surprising plan — it cannot un-push the branch.');
10262
+ console.log('For a gate that runs before anything leaves your machine, push yourself and use');
10263
+ console.log('`remits-cli components sync --safe`.');
9900
10264
  return;
9901
10265
  }
9902
10266
  console.log('Usage: remits-cli components <stage|status|clear|sync|commit|promotion|branches|branch>');
9903
10267
  console.log('');
9904
10268
  console.log('All staging subcommands accept --workspace NAME to isolate a lane on the same branch,');
9905
10269
  console.log('so several agents can iterate at once. See: remits-cli workspace --help');
9906
- console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
10270
+ console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
9907
10271
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
9908
10272
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
9909
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
9910
- console.log(' remits-cli components commit [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
10273
+ console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
10274
+ console.log(' remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
9911
10275
  console.log('');
9912
10276
  console.log(' --force-tombstones applies only on non-trunk variant syncs, when a missing trunk');
9913
10277
  console.log(' component file is intentionally being recorded as a tombstone override.');
@@ -10057,10 +10421,10 @@ async function main() {
10057
10421
  console.log(' remits-cli tool --name <toolName> [--base-url URL] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--scope self|children|hierarchy] [--target-account-id ID] [--account-ids 1,2,3] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
10058
10422
  console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
10059
10423
  console.log(' remits-cli workspace [show|use <name>|use --auto|clear] # isolate staging when several agents share a repo');
10060
- console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--changed-only] [--data-mode test|prod] [--json|--verbose]');
10424
+ console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--data-mode test|prod] [--json|--verbose]');
10061
10425
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE --component-id ID] [--json|--verbose]');
10062
10426
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
10063
- console.log(' remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
10427
+ console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
10064
10428
  console.log(' remits-cli components commit [--message \"msg\"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
10065
10429
  console.log(' remits-cli components promotion [<branch>] [--json] [--no-fail] # promotion readiness + ordered next steps');
10066
10430
  console.log(' remits-cli components branches [--json] # committed branch variants for this account');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -60,6 +60,19 @@ reference named after it.
60
60
  - **edit → stage → run, every time.** The platform executes whatever is in the staging cache at the
61
61
  moment a run starts. Edit a file, run a test without `remits-cli components stage`, and the test runs
62
62
  the OLD code. This is the single most common mistake. (`development-loop.md`)
63
+ - **Stage your WORKSET, not the whole repo: `remits-cli components stage --workset`.** It uploads only
64
+ the components git reports changed and makes the lane hold exactly them. A plain `components stage` is
65
+ a FULL SNAPSHOT — it puts every component in the repo into the lane, so "115 staged" tells a human
66
+ nothing about what you are working on, and every one of those entries shadows committed source until
67
+ it expires. Keep the full stage for a deliberate complete snapshot or a "what is stale here?" reset.
68
+ (`development-loop.md`)
69
+ - **Give each agent its own lane: `remits-cli workspace use --auto`.** Without a workspace you are in
70
+ the SHARED lane, where a full stage replaces what another agent is testing rather than merging with
71
+ it. (`component-resolution.md`)
72
+ - **On a variant branch, sync with `remits-cli components sync --safe`.** It dry-runs first and refuses
73
+ a plan that would write components this checkout did not change — which is what a branch that is
74
+ behind trunk produces, because it still physically carries old copies of files nobody touched.
75
+ (`component-integrity.md`, `branch-variants.md`)
63
76
  - **`stage` is always safe. `components commit` on trunk is the most dangerous command in the CLI.**
64
77
  It is not a convenience wrapper: it `git add -A`, commits, pushes, and then reconciles the whole
65
78
  pushed repo into the live component database — creating, updating, renaming, and **hard-deleting**
@@ -84,6 +97,10 @@ reference named after it.
84
97
  - **Resolution order is staged → variant → trunk, and each layers over the one beneath.** A populated
85
98
  staging cache makes a committed variant look broken through any tokenized entry point; clear it before
86
99
  verifying variant resolution. (`component-resolution.md`)
100
+ - **A lane's staged count is the OVERLAY, not your workset.** The overlay is every staged entry the lane
101
+ holds — what a run resolves. The workset is what git says you changed. A full stage makes them differ
102
+ by the size of the repo, and `--changed-only` merges, so it can never shrink an overlay it inherited.
103
+ `components status` prints all three numbers; so does the console. (`component-resolution.md`)
87
104
  - **Host and data mode are two independent decisions.** `--base-url` picks the Remits host,
88
105
  `--data-mode` picks the data segment on it. Neither implies the other. (`command-reference.md`)
89
106
  - **`test run` ignores the stored session data lane.** It defaults to `test` even when `whoami` shows
@@ -139,7 +156,8 @@ reference named after it.
139
156
 
140
157
  ```bash
141
158
  remits-cli whoami # account, user, branch, data mode, host for the NEXT tool call
142
- remits-cli components status # trunk or variant checkout, staging lane, what is staged
159
+ remits-cli workspace use --auto # your own staging lane, named after this checkout
160
+ remits-cli components status # trunk or variant checkout, staging lane, workset vs overlay, who else is staging
143
161
  remits-cli tools # which tools this account actually has (tools are per-account)
144
162
  ```
145
163
 
@@ -15,6 +15,7 @@
15
15
  - [Hierarchy-scoped tool reads](#hierarchy-scoped-tool-reads)
16
16
  - [Data Mode](#data-mode)
17
17
  - [Command Reference](#command-reference)
18
+ - [Staging modes: workset vs full snapshot](#staging-modes-workset-vs-full-snapshot)
18
19
  - [Prod banners and retryable failures](#prod-banners-and-retryable-failures)
19
20
 
20
21
  ## Getting Started
@@ -189,12 +190,12 @@ remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--
189
190
  remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]
190
191
  remits-cli listen [stop|status] [--foreground true] # compatibility alias
191
192
  remits-cli data-mode [set test|prod]
192
- remits-cli components stage [--branch <name>] [--workspace <name>] [--changed-only] [--data-mode test|prod] [--json|--verbose]
193
+ remits-cli components stage [--workset | --changed-only] [--branch <name>] [--workspace <name>] [--empty-workset clear] [--data-mode test|prod] [--json|--verbose] # default = FULL SNAPSHOT of the repo; --workset = only what git says changed, lane reconciled to it
193
194
  remits-cli workspace [show | use <name> | use --auto | clear]
194
195
  remits-cli components status [--branch <name>] [--component-type <type>] [--component-id <id>] [--json|--verbose]
195
196
  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
196
- remits-cli components sync [--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
197
- remits-cli components commit [--message "msg"] [--data-mode test|prod] [--force-tombstones]
197
+ 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
198
+ remits-cli components commit [--safe] [--message "msg"] [--data-mode test|prod] [--force-tombstones] # --safe gates phase 2; it cannot un-push phase 1
198
199
  remits-cli components branches [--json] # branches carrying committed variants, with counts + drift
199
200
  remits-cli components branch <name> [--json] # one branch: overridden / added / removed, drift flags, subscribers
200
201
  remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]
@@ -251,8 +252,50 @@ For tests specifically:
251
252
  - `--names-only` — dry-run and print only `BUCKET type:id name` lines for the planned writes, then stop
252
253
  without writing overlays.
253
254
 
255
+ - `--safe` — the NAME for that combination, and the recommended agent path on a variant branch. It
256
+ expands to `--summary --changed-only --fail-on-errors --fail-on-removed`, resolves `--changed-since`
257
+ from this branch's merge base with trunk when you did not name one (local refs only — it never runs
258
+ an implicit `git fetch`, and refuses with the fetch command when nothing local can answer), and
259
+ prints the planned writes before mutating unless `--yes` is passed. On TRUNK there is no plan to
260
+ gate, so it states what a trunk reconcile does (every row rewritten from the repo; any live component
261
+ missing from the repo DELETED) and requires `--yes`. It does not override a narrower
262
+ `--expected-removed`.
263
+
254
264
  A good default for an unattended promotion is:
255
- `remits-cli components sync --summary --changed-only --fail-on-errors`
265
+ `remits-cli components sync --safe`
266
+
267
+ When `--changed-only` refuses a plan far larger than your changed set, the usual cause is a branch that
268
+ is BEHIND trunk: it still physically carries old copies of files nobody on it touched, and a variant
269
+ sync turns each of those into an unrelated override. The refusal says so. Merge trunk in, push, re-run.
270
+
271
+ ### Staging modes: workset vs full snapshot
272
+
273
+ `components stage` reports three numbers, and they answer three different questions:
274
+
275
+ | Number | Question |
276
+ |---|---|
277
+ | **workset** | how many components git reports this working tree changed |
278
+ | **submitted** | how many this command uploaded |
279
+ | **overlay** | how many staged entries the lane now holds — **what a run resolves** |
280
+
281
+ | Mode | Uploads | Lane afterwards |
282
+ |---|---|---|
283
+ | `components stage` (default) | the whole repository manifest | reconciled to the whole repo — a FULL SNAPSHOT |
284
+ | `components stage --workset` | only the git-changed components | reconciled to exactly those |
285
+ | `components stage --changed-only` | only the git-changed components | merged; earlier entries are left in place |
286
+
287
+ `--workset` is the iteration mode. `--changed-only` keeps its long-standing merge semantics, so it cannot
288
+ shrink a lane inherited from an earlier full stage; the command warns when it retains entries that way.
289
+ `--changed-only --replace-lane` is the explicit spelling of `--workset`.
290
+
291
+ An empty workset never clears a lane: `--workset` on a clean tree stages nothing and leaves the lane as
292
+ it is. `--empty-workset clear` opts into the clear; `components clear --all` is the direct way.
293
+
294
+ A deleted component file cannot be represented in Redis staging — clearing a staged entry falls back to
295
+ the committed row, so the component still resolves. The command reports those changes as NOT
296
+ REPRESENTABLE. On a non-trunk variant branch, prove a deletion through
297
+ `components sync --dry-run --summary --fail-on-errors`; on trunk there is no dry-run plan, so use the
298
+ full pre-sync safety check before any mutating reconcile.
256
299
 
257
300
  ### Prod banners and retryable failures
258
301
 
@@ -15,6 +15,7 @@
15
15
  - [When staged overrides apply](#when-staged-overrides-apply)
16
16
  - [Diagnosing which version is in play](#diagnosing-which-version-is-in-play)
17
17
  - [Working alongside other agents: the staging WORKSPACE](#working-alongside-other-agents-the-staging-workspace)
18
+ - [A lane holds an OVERLAY; your workset is a different number](#a-lane-holds-an-overlay-your-workset-is-a-different-number)
18
19
  - [Stage / sync / clear with remits-cli](#stage--sync--clear-with-remits-cli)
19
20
  - [Stale after sync / commit (the in-memory compile cache)](#stale-after-sync--commit-the-in-memory-compile-cache)
20
21
 
@@ -184,19 +185,60 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
184
185
  - Every stage / test run / token / clear prints its `Staging lane:` — if a change seems to have had no
185
186
  effect, check that line FIRST. A mismatched lane resolves committed source, which looks identical to
186
187
  "the stage did not work".
187
- - `remits-cli components status` lists every lane staged on the branch, so you can see whether another
188
- agent is working alongside you.
188
+ - `remits-cli components status` lists every lane staged on the branch AND every lane on the account, so
189
+ you can see whether another agent is working alongside you. Each row names its world (trunk or variant
190
+ branch), its overlay, its workset where known, and whether it is the SHARED lane.
189
191
  - `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
190
192
 
193
+ ### A lane holds an OVERLAY; your workset is a different number
194
+
195
+ This is the distinction that decides whether a lane is legible to anyone but you.
196
+
197
+ - The **overlay** is every staged entry the lane currently holds. It is what a CLI-scoped run resolves,
198
+ and it is the number the console shows as "staged".
199
+ - The **workset** is what git reports this working tree changed. It is the work in flight.
200
+
201
+ A plain `components stage` is a FULL SNAPSHOT: it uploads the whole repository manifest and reconciles
202
+ the lane to it, so on a 115-component repo the overlay is 115 whether you edited five components or all
203
+ of them. That is safe — it is a complete, known state — but it is a poor signal. Everyone reading the
204
+ console sees a lane that looks like 115 edits in flight, and all 115 entries shadow committed source for
205
+ every run in that lane until they expire.
206
+
207
+ `components stage --workset` uploads only the changed components and reconciles the lane to exactly
208
+ them, so the overlay IS the workset. That is the mode to iterate in.
209
+
210
+ `components stage --changed-only` uploads the same narrow set but MERGES: it deliberately leaves every
211
+ other staged entry alone. So it can never shrink a lane inherited from an earlier full snapshot — the
212
+ overlay stays at 115 while you work on seven. The command warns when entries are retained that way.
213
+
214
+ Every count is `unknown` rather than `0` when it cannot be established. "git could not answer" and "git
215
+ says nothing changed" are different facts and only one of them is a number.
216
+
217
+ **Deletion is not expressible here.** There is no staged removal: clearing a staged entry falls back to
218
+ the committed row, so the component still resolves. `stage --workset` reports a deleted component file as
219
+ NOT REPRESENTABLE rather than quietly omitting it. On a non-trunk variant branch, prove a deletion
220
+ through the durable variant plan — `remits-cli components sync --dry-run --summary --fail-on-errors` —
221
+ and read the removed/tombstone bucket. On trunk there is no dry-run plan; a deletion is only proven by
222
+ the full pre-sync safety check before a mutating reconcile.
223
+
224
+ **An empty workset never clears the lane.** `--workset` on a clean tree stages nothing and leaves the
225
+ lane as it is; reconciling to an empty manifest would delete the overlay the next run depends on.
226
+ Clearing stays explicit (`components clear --all`), or `--empty-workset clear` if that really is what you
227
+ meant.
228
+
191
229
  ### Stage / sync / clear with remits-cli
192
230
 
193
- - `remits-cli components stage` writes local file changes into the Redis staging cache for the current
194
- branch/user/workspace scope. This is the normal edit/test loop.
195
- - `remits-cli components stage --changed-only` stages just the components this working tree edited. It
196
- does NOT reconcile, so entries for components it did not mention are left alone rather than deleted.
197
- Useful on a large repo; a full stage is still the default and the safest.
198
- - `remits-cli components status` shows which branch/variant world the checkout resolves, plus staged entries,
199
- staged fields, aliases, hashes, and TTLs. Use `--json` or `--verbose` for the full staged-entry payload.
231
+ - `remits-cli components stage --workset` stages exactly what git says this working tree changed and
232
+ reconciles the lane to it. **The normal iteration mode.**
233
+ - `remits-cli components stage` stages the whole repository manifest (a full snapshot). Use it for a
234
+ deliberate complete snapshot, when a `.meta.yml` key you deleted must be reconciled against the whole
235
+ repo, or as a "what is stale in here?" reset — then clear when you are done.
236
+ - `remits-cli components stage --changed-only` stages just the changed components and does NOT reconcile,
237
+ so entries it did not mention are left alone rather than deleted. Kept as-is for compatibility;
238
+ `--workset` is the same narrow upload with the lane reconciled.
239
+ - `remits-cli components status` shows which branch/variant world the checkout resolves, whether the lane
240
+ is shared, the overlay/workset/retained split with the last stage's mode, plus staged entries, staged
241
+ fields, aliases, hashes, and TTLs. Use `--json` or `--verbose` for the full staged-entry payload.
200
242
  - **A full `stage` makes the `.meta.yml` AUTHORITATIVE.** Staging layers a payload over what is already
201
243
  staged, which is what lets `mcp_component_edit` write a single field without blanking the others. But a
202
244
  `remits-cli components stage` sends the whole sidecar, so a key you DELETE from a sidecar is removed from
@@ -16,11 +16,14 @@
16
16
  - [Step 1: Understand the Request](#step-1-understand-the-request)
17
17
  - [Step 2: Make the Change](#step-2-make-the-change)
18
18
  - [Step 3: Stage to Platform](#step-3-stage-to-platform)
19
+ - [Stage your workset, not the whole repo](#stage-your-workset-not-the-whole-repo)
20
+ - [Three numbers, three questions](#three-numbers-three-questions)
19
21
  - [Step 4: Verify the Change](#step-4-verify-the-change)
20
22
  - [Step 5: Iterate If Needed](#step-5-iterate-if-needed)
21
23
  - [Step 6: Update Documentation](#step-6-update-documentation)
22
24
  - [Temporary Experiment Workflow](#temporary-experiment-workflow)
23
25
  - [Step 7: Commit and Durable Sync](#step-7-commit-and-durable-sync)
26
+ - [Verifying the COMMITTED variant, not your staging](#verifying-the-committed-variant-not-your-staging)
24
27
  - [Step 8: Close the Ticket](#step-8-close-the-ticket)
25
28
  - [User Confirmation Preferences](#user-confirmation-preferences)
26
29
 
@@ -174,10 +177,63 @@ overlay instead of pruning cleanly.
174
177
  #### Step 3: Stage to Platform
175
178
 
176
179
  ```bash
177
- remits-cli components stage
180
+ remits-cli workspace use --auto # once per checkout: your own lane
181
+ remits-cli components stage --workset # every edit: stage what you changed
178
182
  ```
179
183
 
180
- This uploads your local file changes to the platform's staging cache (Redis, 240-minute TTL). It does NOT commit anything. The platform cannot see your local edits until you stage them.
184
+ This uploads your local component changes to the platform's staging cache (Redis, 240-minute TTL). It does NOT commit anything. The platform cannot see your local edits until you stage them.
185
+
186
+ ##### Stage your workset, not the whole repo
187
+
188
+ There are three stage modes, and the difference decides what a run in your lane resolves and what a
189
+ human watching the console sees:
190
+
191
+ | Command | What it uploads | What the lane holds afterwards |
192
+ |---|---|---|
193
+ | `components stage --workset` | only the components git reports changed | **exactly those** — the lane is reconciled to your workset |
194
+ | `components stage` | the whole repository manifest | **every component in the repo** (a full snapshot) |
195
+ | `components stage --changed-only` | only the changed components | the changed ones **merged into whatever was already there** |
196
+
197
+ **Use `--workset` for normal iteration.** A full stage is correct and safe, but on a real repository it
198
+ puts a hundred-plus components into your lane, and every one of them then shadows committed source for
199
+ any run in that lane until it expires. A human looking at `/admin/platforms` sees "115 staged" and cannot
200
+ tell whether you edited 115 components or five.
201
+
202
+ `--changed-only` MERGES. It cannot shrink a lane it inherited from an earlier full stage, so a lane can
203
+ sit at 115 overlay entries while you are working on seven. The command warns when that happens; the fix
204
+ is `--workset` (or `components clear --all` once, then keep using `--workset`).
205
+
206
+ **When a full stage is the right answer:**
207
+
208
+ - you deliberately want a complete snapshot of the repo in the lane;
209
+ - you changed a `.meta.yml` sidecar and want removed keys reconciled against the whole repo;
210
+ - you cannot tell what is stale in the lane and want a clean, known state (then `components clear --all`
211
+ when you are done).
212
+
213
+ **An empty workset never clears your lane.** `--workset` on a clean working tree stages nothing and
214
+ leaves the lane alone — reconciling to an empty manifest would delete the overlay your next test run
215
+ depends on. Clearing stays explicit: `components clear --all`.
216
+
217
+ **Deleting a component file cannot be verified by staging.** There is no staged "removal": clearing a
218
+ staged entry falls back to the committed row, so the component still resolves. `stage --workset` reports
219
+ those changes as NOT REPRESENTABLE.
220
+
221
+ On a non-trunk variant branch, prove the removal through the durable variant plan:
222
+ `remits-cli components sync --dry-run --summary --fail-on-errors` and read the removed/tombstone bucket.
223
+ On trunk there is no dry-run plan; treat deletion as a high-risk durable reconcile and pass the full
224
+ pre-sync safety check before running any mutating sync.
225
+
226
+ ##### Three numbers, three questions
227
+
228
+ `components stage` and `components status` report all three, and so does the admin console. They are not
229
+ interchangeable:
230
+
231
+ - **workset** — components git reports this working tree changed. The work in flight.
232
+ - **submitted** — what this command uploaded.
233
+ - **overlay** — every staged entry the lane now holds. **This is what a run resolves.**
234
+
235
+ A missing answer is printed as `unknown`, never as `0`: "git could not answer" and "git says nothing
236
+ changed" are different facts and only one of them is a number.
181
237
 
182
238
  **THE STAGE-BEFORE-RUN RULE:** You MUST run `remits-cli components stage` after EVERY file edit and BEFORE any test run or verification. The platform executes whatever version is in the staging cache at the moment the test starts. If you edit a file and run a test without staging first, the test runs the OLD code — not your changes. This is the single most common mistake. Never skip staging. The sequence is always: **edit → stage → run**.
183
239
 
@@ -310,13 +366,16 @@ Redis cache can affect later test/tool runs, so always clear it after restoring
310
366
 
311
367
  ```bash
312
368
  # make temporary local edit
313
- remits-cli components stage
369
+ remits-cli components stage --workset
314
370
  remits-cli test run --test <id-or-name> --names "<case name>"
315
371
  git restore <file>
316
372
  remits-cli components clear --all
317
373
  remits-cli components status
318
374
  ```
319
375
 
376
+ With `--workset` the restore-and-clear is belt and braces rather than the only thing standing between
377
+ the experiment and a later run: the lane only ever held the component you were experimenting on.
378
+
320
379
  For narrower cleanup when only one staged component should be cleared:
321
380
 
322
381
  ```bash
@@ -352,10 +411,41 @@ IDs or when unexpected deletes/renumbers are present.
352
411
  Only after those checks pass, and only when the user intends to promote the repo to the platform database:
353
412
 
354
413
  ```bash
414
+ # On a VARIANT branch — the recommended path. Dry-runs first and refuses a surprising plan.
415
+ remits-cli components sync --safe
416
+ git pull --ff-only origin <branch>
417
+
418
+ # On TRUNK — there is no plan to gate, so --safe explains what a trunk reconcile does and needs --yes.
355
419
  remits-cli components sync
356
420
  git pull --ff-only origin <branch>
357
421
  ```
358
422
 
423
+ `--safe` expands to `--summary --changed-only --fail-on-errors --fail-on-removed`, resolves the
424
+ comparison base from this branch's merge base with trunk when you did not pass `--changed-since`, and
425
+ prints the planned writes before mutating unless you pass `--yes`. It refuses when the plan would write
426
+ components this checkout did not change — which is exactly what a branch that is BEHIND trunk produces,
427
+ because it still physically carries old copies of files nobody on it touched, and a variant sync turns
428
+ each of those into an unrelated override. When it refuses that way, merge trunk into your branch, push,
429
+ and re-run the same command.
430
+
431
+ When a removal is intended, name it rather than disabling the gate:
432
+
433
+ ```bash
434
+ remits-cli components sync --safe --expected-removed action:50
435
+ ```
436
+
437
+ `--force-tombstones` stays explicit and human-owned. Never pass it to get past a refusal.
438
+
439
+ ##### Verifying the COMMITTED variant, not your staging
440
+
441
+ After a sync, staged entries still win for CLI-scoped runs, so a test that passes may be testing your
442
+ staging rather than what you just committed. Clear the lane first:
443
+
444
+ ```bash
445
+ remits-cli components clear --all
446
+ remits-cli test run --test <id-or-name> --as-account <subscriber-id>
447
+ ```
448
+
359
449
  `remits-cli components sync` is the authoritative platform-sync step. It does not perform local git operations,
360
450
  and it is capable of reconciling creates/deletes/renames from the remote repository into the database. Treat it
361
451
  as a gated promote/reconciliation command, not as an exploratory command or fallback.