@remits/remits-cli 0.1.121 → 0.1.122

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/index.js CHANGED
@@ -19,6 +19,9 @@ const openBrowser = (openModule && typeof openModule === 'function')
19
19
 
20
20
  //const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'http://localhost:8080';
21
21
  const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'https://remits-529558023549.us-east5.run.app';
22
+
23
+ // How many compile failures print their full message before the rest are listed by identifier only.
24
+ const COMPILE_FAILURES_SHOWN = 5;
22
25
  const SESSION_DIR = path.join(os.homedir(), '.remits-cli');
23
26
  const SESSIONS_FILE = path.join(SESSION_DIR, 'sessions.json');
24
27
  const CONFIG_FILE = path.join(SESSION_DIR, 'config.json');
@@ -2817,7 +2820,7 @@ async function pushComponentsCommand(flags) {
2817
2820
  // or, worse, conclude staging was broken and go on reading trunk. Scaled by payload size, floored at
2818
2821
  // the old default, and overridable.
2819
2822
  const api = buildAxios(baseUrl, session.token, stageTimeoutMs(components, flags));
2820
- const response = await stageOrRefuse(api, cwd, {
2823
+ const stagePayload = {
2821
2824
  token: session.token,
2822
2825
  accountId,
2823
2826
  branchName,
@@ -2846,11 +2849,50 @@ async function pushComponentsCommand(flags) {
2846
2849
  // Kept for a platform that predates stageMode. Same meaning it always had.
2847
2850
  replace: !changedOnly,
2848
2851
  components
2849
- });
2852
+ };
2853
+
2854
+ let response;
2855
+ try {
2856
+ response = await stageOrRefuse(api, cwd, stagePayload);
2857
+ } catch (err) {
2858
+ // A REFUSED stage is evidence too. Recording it only on the success path is how a lane ends up with
2859
+ // an envelope that says nothing happened between two packets, when in fact a stage was attempted and
2860
+ // the platform rejected it. The packet is best-effort: a failing stage must surface the STAGE error,
2861
+ // never an error from writing the record of it.
2862
+ try {
2863
+ await appendVerificationPacket(api, cwd, session, accountId, flags, {
2864
+ type: 'stage',
2865
+ success: false,
2866
+ claim: 'Components refused at staging',
2867
+ world: buildCommandWorld(err.responseBody || {}, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: 'staged' }),
2868
+ revision: collectVerificationSource(cwd, flags),
2869
+ assertions: [
2870
+ { id: 'stage', stageMode },
2871
+ { id: 'stage_refused', value: err.message }
2872
+ ],
2873
+ evidenceCategories: ['stage'],
2874
+ rawRefs: { command: 'components stage' },
2875
+ stage: (err.responseBody || {}).stage,
2876
+ compileValidation: (err.responseBody || {}).compileValidation,
2877
+ changedFromWorkingTree: changedFromWorkingTree || [],
2878
+ unrepresentableChanges: unstageable
2879
+ }, { quiet: true });
2880
+ } catch (packetError) {
2881
+ // Deliberately swallowed and named, not silent.
2882
+ console.error('Could not record the refused-stage verification packet: ' + packetError.message);
2883
+ }
2884
+ throw err;
2885
+ }
2850
2886
  response.changedFromWorkingTree = changedFromWorkingTree || [];
2851
2887
  response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
2852
2888
  response.requestedStageMode = stageMode;
2853
2889
  response.unrepresentableChanges = unstageable;
2890
+ if (response.success === false) {
2891
+ // A 200 that says `success:false`. The 422 path throws out of stageOrRefuse above and never lands
2892
+ // here, so this stays as the belt-and-braces case — print whatever detail came with it first.
2893
+ printCompileValidation(response.compileValidation, { stderr: true });
2894
+ throw new Error(response.message || 'Server stage failed');
2895
+ }
2854
2896
 
2855
2897
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
2856
2898
  type: 'stage',
@@ -2947,6 +2989,17 @@ async function stageOrRefuse(api, cwd, payload) {
2947
2989
  refusal.refused = true;
2948
2990
  throw refusal;
2949
2991
  }
2992
+ if (body && body.message) {
2993
+ // The server's `message` is ONE line derived from the FIRST failure. The rest of the detail is in
2994
+ // the body, and the caller cannot see the body — an agent handed "compile failed for Reader 41"
2995
+ // over a stage with nine broken components fixes one, re-stages, and pays nine round trips. Render
2996
+ // the whole list HERE, where the body is still in hand, then throw the one-line summary.
2997
+ printCompileValidation(body.compileValidation, { stderr: true });
2998
+ const failure = new Error(body.message);
2999
+ failure.responseBody = body;
3000
+ failure.status = status;
3001
+ throw failure;
3002
+ }
2950
3003
  throw err;
2951
3004
  }
2952
3005
  }
@@ -2967,6 +3020,60 @@ function printComponentPolicy(response) {
2967
3020
  (policy.overrides || []).forEach((id) => console.log('Policy override recorded for rule: ' + id));
2968
3021
  }
2969
3022
 
3023
+ /**
3024
+ * What the platform compiled, and what it could not.
3025
+ *
3026
+ * Prints on BOTH outcomes, including `attempted: 0`. A silent pass and a pass that validated nothing look
3027
+ * identical otherwise, and they are not the same claim: when git cannot identify the changed set the
3028
+ * server has no workset to validate and says so (`skipped[].reason`). Reporting that as silence would be
3029
+ * the same confident-wrong-number this surface exists to remove.
3030
+ */
3031
+ function printCompileValidation(validation, options) {
3032
+ // Failures go to stderr so `--json` stdout stays parseable: a caller that asked for JSON is a program,
3033
+ // and a refusal must not put prose in the middle of its document.
3034
+ const emit = (options && options.stderr) ? console.error : console.log;
3035
+ if (!validation || validation.attempted == null) return;
3036
+ const failures = Array.isArray(validation.failures) ? validation.failures : [];
3037
+ if (validation.attempted === 0) {
3038
+ const reasons = (Array.isArray(validation.skipped) ? validation.skipped : [])
3039
+ .map((entry) => (entry && entry.reason) || '')
3040
+ .filter((reason) => reason && reason !== 'no-compilable-source');
3041
+ if (reasons.length) {
3042
+ emit('Compile validation: NOT RUN — ' + reasons.join(', ') +
3043
+ '. Nothing in this stage was compile-checked.');
3044
+ if (reasons.includes('workset-unknown')) {
3045
+ emit(' git could not identify the changed set, so there was no workset to validate.');
3046
+ emit(' Stage from a git working tree, or re-stage the component explicitly, to get the check.');
3047
+ }
3048
+ }
3049
+ return;
3050
+ }
3051
+ const status = validation.success === true ? 'passed' : 'failed';
3052
+ emit('Compile validation: ' + status + ' — ' + validation.passed + '/' + validation.attempted +
3053
+ ' compiled in ' + validation.durationMs + 'ms' +
3054
+ (validation.concurrency ? ' (' + validation.concurrency + ' parallel)' : ''));
3055
+ const shown = failures.slice(0, COMPILE_FAILURES_SHOWN);
3056
+ shown.forEach((failure) => {
3057
+ emit(' ' + (failure.type || 'component') + ' ' + (failure.id || failure.name || '') +
3058
+ ': ' + (failure.message || 'compile failed'));
3059
+ });
3060
+ if (failures.length > shown.length) {
3061
+ // Named, not counted. "3 more failure(s)" sends an agent back for another round trip to learn WHICH
3062
+ // three; the identifiers are already in hand and cost one line.
3063
+ emit(' ' + (failures.length - shown.length) + ' more failed: ' +
3064
+ failures.slice(COMPILE_FAILURES_SHOWN)
3065
+ .map((failure) => (failure.type || 'component') + ' ' + (failure.id || failure.name || '?'))
3066
+ .join(', '));
3067
+ emit(' Re-run with --json for every compile message.');
3068
+ }
3069
+ if (validation.success !== true) {
3070
+ // The lane is written BEFORE it is validated, because the compile has to resolve through the staged
3071
+ // overlay to see the source it is judging. So a refusal does not mean "nothing was staged".
3072
+ emit(' The rejected source IS in the lane: staging writes, then compiles what it wrote.');
3073
+ emit(' A run in this lane will resolve the broken component until you fix it and re-stage.');
3074
+ }
3075
+ }
3076
+
2970
3077
  function shouldPrintFullComponentResponse(flags) {
2971
3078
  return flagEnabled(flags.verbose);
2972
3079
  }
@@ -3088,6 +3195,7 @@ function printStageSummary(response, flags) {
3088
3195
  if (overlay != null) {
3089
3196
  console.log('Materialized overlay now held by the lane:', overlay, 'component(s)');
3090
3197
  }
3198
+ printCompileValidation(response.compileValidation);
3091
3199
 
3092
3200
  // The trap `--changed-only` leaves behind: it merges, so a lane inherited from an earlier full snapshot
3093
3201
  // keeps every one of those entries resolving ahead of committed source. Silence here is what made
@@ -4501,7 +4609,22 @@ async function commitComponentsCommand(flags) {
4501
4609
  const landing = await acquireLandingLease(flags, accountId, branchName);
4502
4610
  try {
4503
4611
  if (!skipGit) {
4504
- console.log('Phase 1/3: local git commit/push');
4612
+ console.log('Phase 1/4: staged compile validation');
4613
+ // MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
4614
+ // lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
4615
+ // validation pass must not be able to delete another agent's staged work as a side effect of somebody
4616
+ // running `components commit`. This adds the changed components to the lane and leaves the rest alone.
4617
+ await pushComponentsCommand(Object.assign({}, flags, {
4618
+ branch: branchName,
4619
+ 'account-id': accountId,
4620
+ mode: 'stage',
4621
+ workset: false,
4622
+ 'replace-lane': false,
4623
+ replaceLane: false,
4624
+ 'changed-only': true
4625
+ }));
4626
+
4627
+ console.log('Phase 2/4: local git commit/push');
4505
4628
  const status = runGit(cwd, 'git status --porcelain');
4506
4629
  if (status || allowEmpty) {
4507
4630
  runGit(cwd, 'git add -A');
@@ -4528,14 +4651,12 @@ async function commitComponentsCommand(flags) {
4528
4651
  console.log('Skipping local git phase. Prefer `remits-cli components sync` if you only need the server sync step.');
4529
4652
  }
4530
4653
 
4531
- console.log('Phase 2/3: server sync');
4654
+ console.log('Phase 3/4: server sync');
4532
4655
  if (flagEnabled(flags.safe)) {
4533
- // The honest limit, stated where it applies. `components commit` runs git add/commit/push BEFORE the
4534
- // server sync, so a --safe gate here cannot stop the push — it stops the PLATFORM from writing bad
4535
- // overlays. The branch may already be pushed when the gate refuses; that is recoverable, an
4536
- // unintended ComponentVariant tree is much less so.
4537
- console.log(' --safe: the branch is already pushed. The gate below stops the platform from WRITING a');
4538
- console.log(' surprising plan; it cannot un-push. Use `components sync --safe` alone for a pre-push gate.');
4656
+ // Compile validation already ran before the push. The sync safety gates still run after the push
4657
+ // because they need the server's remote-branch plan.
4658
+ console.log(' --safe: compile validation ran before git push; the sync plan gate below runs before');
4659
+ console.log(' the platform writes trunk rows or ComponentVariant overlays.');
4539
4660
  }
4540
4661
  const syncResponse = await syncComponentsCommand({
4541
4662
  ...flags,
@@ -4544,7 +4665,7 @@ async function commitComponentsCommand(flags) {
4544
4665
  });
4545
4666
 
4546
4667
  if (!skipGit) {
4547
- console.log('Phase 3/3: local fast-forward pull');
4668
+ console.log('Phase 4/4: local fast-forward pull');
4548
4669
  runGit(cwd, 'git fetch origin ' + shellQuote(branchName));
4549
4670
  const expectedSha = syncResponse && syncResponse.sync && syncResponse.sync.postSyncSha;
4550
4671
  if (expectedSha) {
@@ -11144,13 +11265,13 @@ function printComponentsHelp(subcommand) {
11144
11265
  if (subcommand === 'commit') {
11145
11266
  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]');
11146
11267
  console.log('');
11147
- console.log('Runs local git add/commit/push, then server sync. Prefer explicit git + components sync when you need inspectable phases.');
11268
+ console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
11269
+ console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
11148
11270
  console.log('components commit does not support --dry-run.');
11149
11271
  console.log('');
11150
- console.log('--safe passes the sync gates through to phase 2. The honest limit: git push happens in phase 1,');
11151
- console.log('so the gate stops the PLATFORM from writing a surprising plan — it cannot un-push the branch.');
11152
- console.log('For a gate that runs before anything leaves your machine, push yourself and use');
11153
- console.log('`remits-cli components sync --safe`.');
11272
+ console.log('Phase 1 merge-stages the git-changed components and compile-validates their runtime source.');
11273
+ console.log('It uses --changed-only semantics, so it never reconciles a lane you share with another agent.');
11274
+ console.log('--safe also gates the server sync plan before any trunk rows or ComponentVariant overlays are written.');
11154
11275
  return;
11155
11276
  }
11156
11277
  console.log('Usage: remits-cli components <stage|status|lanes|entries|clear|sync|commit|promotion|branches|branch>');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.121",
3
+ "version": "0.1.122",
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,12 @@ 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
+ - **Staging and sync fail early on broken runtime-compiled Groovy.** Stage validates changed source in the
64
+ active lane; sync validates changed/new compiled components before writing DB rows or branch variants.
65
+ Agent files are Utility components internally. A stage refused BY COMPILE VALIDATION still wrote the
66
+ lane — the compile has to resolve through the staged overlay to see the source it is judging — so fix and
67
+ re-stage before running anything in that lane. (A policy or edit-lease refusal is the opposite: it
68
+ returns before any write.) (`development-loop.md`, `component-integrity.md`, `branch-variants.md`)
63
69
  - **Stage your WORKSET, not the whole repo: `remits-cli components stage --workset`.** It uploads only
64
70
  the components git reports changed and makes the lane hold exactly them. A plain `components stage` is
65
71
  a FULL SNAPSHOT — it puts every component in the repo into the lane, so "115 staged" tells a human
@@ -222,6 +222,12 @@ remits-cli components sync --dry-run # inspect overrides/additions/tombstones wi
222
222
  remits-cli components sync # writes ComponentVariant overlays ONLY
223
223
  ```
224
224
 
225
+ `components stage` validates changed runtime-compiled source through the active branch/workspace lane,
226
+ and `components sync` validates changed/new Groovy source before writing a `ComponentVariant`. This is
227
+ intentional: a branch overlay that cannot compile should fail at staging/sync time, not later when the
228
+ subscriber's workflow first resolves it. `Agent` files in `components/agents/` are the `Utility`
229
+ component kind internally; the CLI and validator normalize both names to the same target.
230
+
225
231
  Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
226
232
  for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
227
233
  `features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
@@ -330,7 +336,8 @@ The analogous hazard is different, and you must still respect it:
330
336
  `removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
331
337
  staging. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
332
338
  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.
339
+ `components commit --dry-run` is unsupported because `commit` performs compile validation and local git
340
+ writes before syncing.
334
341
  - **The sync refuses a wholesale removal.** Above roughly a third of a kind — or **100% of a kind at any
335
342
  size** — it aborts that kind, reports why, and points at a rebase. Rebasing is almost always the real
336
343
  fix. Only when the removals are genuinely deliberate, re-run with `--force-tombstones`.
@@ -198,7 +198,7 @@ remits-cli components lanes [--json] # every indexed sta
198
198
  remits-cli components entries --lane-id <id> [--json|--verbose] # authoritative staged files for one lane
199
199
  remits-cli components clear [--branch <name>] [--component-type <type>] [--component-id <id>] [--all] [--json|--verbose] # id alone scopes to one component when unambiguous (ids are type-local; add --component-type if the same id is staged in multiple families); no filter clears the whole branch scope; --all forces the full wipe
200
200
  remits-cli components sync [--safe [--yes]] [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--changed-only [--changed-since <ref>]] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
201
- remits-cli components commit [--safe] [--message "msg"] [--data-mode test|prod] [--force-tombstones] # --safe gates phase 2; it cannot un-push phase 1
201
+ remits-cli components commit [--safe] [--message "msg"] [--data-mode test|prod] [--force-tombstones] # phase 1 merge-stages + compile-validates changed source (never reconciles the lane); --safe gates sync writes
202
202
  remits-cli components branches [--json] # branches carrying committed variants, with counts + drift
203
203
  remits-cli components branch <name> [--json] # one branch: overridden / added / removed, drift flags, subscribers
204
204
  remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]
@@ -87,21 +87,41 @@ every live component present at its real id, and nothing extra.
87
87
 
88
88
  | Command | What it touches | Danger |
89
89
  |---|---|---|
90
- | `remits-cli components stage` (alias: deprecated `push`) | **Redis staging cache only.** Never mutates the DB or git. The safe iteration surface. | none |
90
+ | `remits-cli components stage` (alias: deprecated `push`) | **Redis staging cache only.** Never mutates the DB or git. The safe iteration surface. A plain stage or `--workset` RECONCILES the lane (entries outside the manifest are dropped); `--changed-only` merges. | none for the DB; a lane you share with another agent is reconciled by the first two |
91
91
  | `remits-cli components status` | Reads this lane's staging scope (account/user/branch/workspace) and branch resolution, and lists every other lane on the branch. | none |
92
92
  | `remits-cli components clear` | Clears THIS lane's staging cache without changing DB or git. Never touches another workspace lane. | none |
93
93
  | `remits-cli components sync` **on trunk** | **Server-side git→DB reconcile of the whole account** (create/update/**delete**/rename). Reads the pushed remote; ignores local files. | **high** |
94
94
  | `remits-cli components sync` **on a variant branch** | Writes `ComponentVariant` overlays for that branch only. Never touches trunk rows or the account's trunk branch. When the checkout identifies a subscribing account, the branch-local `account-info.json` is refreshed for that subscriber; `--dry-run` reports the plan without writes. | medium (a missing file becomes a **tombstone** that hides the component from subscribers) |
95
- | `remits-cli components commit` | **One shot:** `git add -A` + commit + `git push` + **`components sync`** + `git pull --ff-only`. Blindly stages the *entire* working tree (including any drift) and reconciles it into prod. Inherits the danger of whichever sync mode the branch selects. | **highest on trunk** |
95
+ | `remits-cli components commit` | **One shot:** changed-source compile validation (a `--changed-only` MERGE stage, so it never reconciles the lane) + `git add -A` + commit + `git push` + **`components sync`** + `git pull --ff-only`. Blindly stages the *entire* working tree (including any drift) and reconciles it into prod. Inherits the danger of whichever sync mode the branch selects. | **highest on trunk** |
96
96
 
97
97
  Key implications:
98
- - **`stage` is always safe** — stage and test as much as you want; it never reconciles or deletes.
99
- - **`components commit` is the most dangerous command**, not a mere convenience wrapper: it `git add -A`
100
- commits and pushes whatever is in the working tree, then immediately syncs. Never run it while the tree
101
- contains drift or unexplained changes. Prefer the explicit, observable
102
- `git commit → git push → components sync → git pull` sequence so each phase can be inspected.
98
+ - **`stage` never touches the DB or git** — stage and test as much as you want. It is safe *for your
99
+ components*; it is not inert *for the lane*. A full snapshot and `--workset` both reconcile the lane to
100
+ their manifest, so on a lane shared with another agent they drop that agent's staged entries. Use
101
+ `--changed-only` when you mean "add mine, leave theirs".
102
+ - **A COMPILE-VALIDATION refusal still wrote the lane; a policy refusal does not.** Read the status code,
103
+ because the two refusals leave opposite states behind:
104
+ - **422 (compile validation)** — the entries are already stored. Validation runs after the write because
105
+ the compile has to resolve through the staged overlay to see the source it is judging. The response
106
+ says so with `laneHoldsRejectedSource`. Until you fix the source and re-stage, a run in that lane
107
+ resolves the broken component.
108
+ - **409 (account policy, or another agent's edit lease)** — refused before anything was written. The lane
109
+ is exactly as it was; nothing to undo.
110
+ - **"Compile validation: NOT RUN" is a real answer.** The check keys off the git changed set. If git
111
+ cannot identify one — you are not in a working tree, or git failed — there is no workset to validate and
112
+ the CLI says `workset-unknown` rather than implying a pass.
113
+ - **`components commit` is the most dangerous command**, not a mere convenience wrapper: it first
114
+ merge-stages and compile-validates the changed runtime source, then `git add -A`, commits, pushes, and
115
+ immediately syncs. Never run it while the tree contains drift or unexplained changes. Prefer the explicit,
116
+ observable `components stage → git commit → git push → components sync → git pull` sequence so each
117
+ phase can be inspected.
103
118
  - **`components sync` acts on the pushed remote**, so local edits are invisible to it until committed **and
104
119
  pushed**, and a drifted **remote** is dangerous even when your local tree looks fine.
120
+ - **Runtime-compiled component source is validated before durable writes.** When a trunk sync or variant
121
+ sync sees changed/new Groovy source for a `Reader`, `Action`, `Embeddable`, `HtmlTemplate`, `Rule`,
122
+ `Test`, `Agent`/`Utility`, or `Tool`, the platform compiles it before accepting the DB row or
123
+ `ComponentVariant` overlay. A compile failure lands in `syncResults.errors` and the branch SHA cache is
124
+ not advanced, so fix the source and retry the same sync.
105
125
  - After a successful non-dry-run `components sync` / `components commit`, the server clears the full
106
126
  staging scope for that lane (account/user/branch/workspace). This is the expected clean state: old Redis aliases should not keep shadowing
107
127
  the newly synced DB rows. `components sync --dry-run` intentionally leaves staging untouched.
@@ -218,6 +218,28 @@ remits-cli components stage --workset # every edit: stage what you changed
218
218
 
219
219
  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.
220
220
 
221
+ For runtime-compiled components (`Reader`, `Action`, `Embeddable`, `HtmlTemplate`, `Rule`, `Test`,
222
+ `Agent`/`Utility`, and `Tool`), staging also validates the Groovy source that is part of the submitted
223
+ workset. Validation resolves through the same branch/workspace staging lane a later run will use and
224
+ compiles candidates in a small bounded pool, so a syntax/compile error is reported by `components stage`
225
+ instead of waiting for the next workflow to trip over it. A full repository stage does not compile every
226
+ component in the repo; it validates only entries known to be in the current workset, plus explicit partial
227
+ source updates.
228
+
229
+ Two things to read correctly when it refuses:
230
+
231
+ - **The lane already holds what it rejected — on a 422.** The compile has to resolve through the staged
232
+ overlay to see the source it is judging, so the entries are written first and compiled second. `422`
233
+ means "staged, and rejected", not "nothing happened" — fix the source and re-stage before running
234
+ anything in that lane. A `409` is the other kind of refusal (account policy, or another agent's edit
235
+ lease) and writes nothing at all.
236
+ - **Every failure is printed, not just the first.** The one-line error is the first failure; the full list
237
+ (identified by type and id) follows it. Fix them in one pass rather than one round trip each.
238
+
239
+ If git cannot identify the changed set — you are not in a working tree, or git failed — there is no
240
+ workset to validate, and the CLI prints `Compile validation: NOT RUN — workset-unknown` rather than
241
+ silently implying a pass.
242
+
221
243
  ##### Stage your workset, not the whole repo
222
244
 
223
245
  There are three stage modes, and the difference decides what a run in your lane resolves and what a