@remits/remits-cli 0.1.126 → 0.1.128

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
@@ -28,7 +28,7 @@ remits-cli components stage # FULL SNAPSHOT of the repo into t
28
28
  remits-cli components status
29
29
  remits-cli components clear
30
30
  remits-cli test run --test 45
31
- remits-cli test run --test "My New Test" --names "test case 1,test case 2"
31
+ remits-cli test run --test "My New Test" --names "test case 1|test case 2"
32
32
  git add -A
33
33
  git commit -m "sync passing changes"
34
34
  git push
@@ -98,7 +98,8 @@ remits-cli install --skills --overwrite true
98
98
  - Branch defaults to the current local git branch.
99
99
  - Data mode defaults to `test`. For `remits-cli test run`, that default is now enforced even if the most-recent authenticated session for the account is `prod`; a production test run therefore requires an explicit `--data-mode prod` on the command line. Use `remits-cli data-mode set prod` only for production investigation.
100
100
  - 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.
101
- - 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 ...`.
101
+ - `--names` is `|`-delimited and may be repeated. A comma still splits a single value for compatibility,
102
+ so a case name containing a comma should be passed with `|` or by repeating `--names`.
102
103
  - Nested help is available before required-argument validation, including `remits-cli test run --help`, `remits-cli components sync --help`, and `remits-cli tool --help`.
103
104
  - `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`.
104
105
  - `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".
package/index.js CHANGED
@@ -6,16 +6,16 @@
6
6
  - L22 Runtime Bootstrap And Shared State
7
7
  - L116 Sessions, Account Resolution, And Production Guards
8
8
  - L595 Local State, Workspaces, And Verification Evidence
9
- - L1234 Account Repos, Guide Sync, And Platform Repo
10
- - L1892 Component Discovery And HTTP Logging
11
- - L2382 Skill Delivery And TOC Resolution
12
- - L2692 Auth And Component Staging
13
- - L3280 Component Summaries, Status, And Sync Gates
14
- - L4991 Branches, Promotion, Commit, And Test Runs
15
- - L5988 Tokens, Tools, Verification, And Config
16
- - L6889 Service Dashboard And WebSocket Listener
17
- - L8984 Agent And Ticket Workflows
18
- - L12042 Help, Auto Update, And Command Dispatch
9
+ - L1399 Account Repos, Guide Sync, And Platform Repo
10
+ - L2057 Component Discovery And HTTP Logging
11
+ - L2549 Skill Delivery And TOC Resolution
12
+ - L2859 Auth And Component Staging
13
+ - L3457 Component Summaries, Status, And Sync Gates
14
+ - L5323 Branches, Promotion, Commit, And Test Runs
15
+ - L6562 Tokens, Tools, Verification, And Config
16
+ - L7478 Service Dashboard And WebSocket Listener
17
+ - L9573 Agent And Ticket Workflows
18
+ - L12631 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -811,6 +811,58 @@ function writeToolResponseFile(cwd, callId, responsePayload) {
811
811
  return file;
812
812
  }
813
813
 
814
+ function fileEvidence(pathname) {
815
+ if (!pathname || !fs.existsSync(pathname)) return {};
816
+ const body = fs.readFileSync(pathname);
817
+ return {
818
+ responseFile: pathname,
819
+ bytes: body.length,
820
+ sha256: sha256(body)
821
+ };
822
+ }
823
+
824
+ const LARGE_TOOL_RESPONSE_BYTES = 200 * 1024;
825
+
826
+ function formatToolResponseFile(pathname) {
827
+ const evidence = fileEvidence(pathname);
828
+ if (!evidence.bytes) return String(pathname || '');
829
+ return pathname + ' (' + evidence.bytes + ' bytes, sha256 ' + evidence.sha256.slice(0, 12) + ')';
830
+ }
831
+
832
+ function printToolResponseFileLine(label, pathname) {
833
+ console.log(label + ':', formatToolResponseFile(pathname));
834
+ const evidence = fileEvidence(pathname);
835
+ if (evidence.bytes && evidence.bytes >= LARGE_TOOL_RESPONSE_BYTES) {
836
+ console.log('Large response: inspect selectively with jq or targeted offset reads instead of opening the whole file.');
837
+ }
838
+ }
839
+
840
+ function boundedToolExcerpt(data = {}) {
841
+ const result = data.result && typeof data.result === 'object' ? data.result : {};
842
+ const candidates = [
843
+ data.message,
844
+ data.toolMessage,
845
+ data.error,
846
+ result.message,
847
+ result.error,
848
+ result.reason
849
+ ].filter(Boolean).map((value) => String(value));
850
+ const excerpt = candidates.join(' | ').trim();
851
+ return excerpt ? excerpt.slice(0, 1200) : null;
852
+ }
853
+
854
+ function toolEvidenceSummary(data = {}, responseFile) {
855
+ return Object.assign({
856
+ callId: data.callId,
857
+ name: data.name,
858
+ async: data.async,
859
+ status: data.status,
860
+ toolSuccess: data.toolSuccess,
861
+ threadGroupingId: data.threadGroupingId,
862
+ message: boundedToolExcerpt(data)
863
+ }, fileEvidence(responseFile));
864
+ }
865
+
814
866
  function readToolResponseFile(cwd, callId) {
815
867
  if (!callId) return null;
816
868
  const paths = ensureLocalState(cwd);
@@ -946,6 +998,35 @@ function readLocalVerificationEnvelope(cwd, envelopeId) {
946
998
  }
947
999
  }
948
1000
 
1001
+ function readLocalVerificationPackets(cwd, envelopeId, filters = {}) {
1002
+ const paths = verificationPaths(cwd, envelopeId);
1003
+ const packets = [];
1004
+ if (fs.existsSync(paths.packetsDir)) {
1005
+ fs.readdirSync(paths.packetsDir)
1006
+ .filter((name) => name.endsWith('.json'))
1007
+ .forEach((name) => {
1008
+ try {
1009
+ const parsed = JSON.parse(fs.readFileSync(path.join(paths.packetsDir, name), 'utf8'));
1010
+ if (parsed && typeof parsed === 'object') packets.push(parsed);
1011
+ } catch (_) {
1012
+ // Ignore one malformed local packet; the rest of the envelope remains useful.
1013
+ }
1014
+ });
1015
+ }
1016
+ if (!packets.length) {
1017
+ const envelope = readLocalVerificationEnvelope(cwd, envelopeId);
1018
+ if (envelope && Array.isArray(envelope.packets)) packets.push(...envelope.packets);
1019
+ }
1020
+ const type = filters.type || filters.packetType;
1021
+ const suite = filters.suite || filters.testName;
1022
+ const max = parsePositiveInt(filters.max, 50);
1023
+ const filtered = packets
1024
+ .filter((packet) => !type || packet.type === type)
1025
+ .filter((packet) => !suite || testRunSuiteNameFromPacket(packet) === String(suite))
1026
+ .sort((a, b) => Number(a.createdAt || 0) - Number(b.createdAt || 0));
1027
+ return filtered.slice(Math.max(0, filtered.length - max));
1028
+ }
1029
+
949
1030
  function writeLocalVerificationPacket(cwd, envelopeId, packet) {
950
1031
  if (!envelopeId || !packet) return null;
951
1032
  const paths = verificationPaths(cwd, envelopeId);
@@ -1067,11 +1148,13 @@ function printVerificationSummaryLine(env) {
1067
1148
  const failures = env.currentFailureCount ? ' currentFailures=' + env.currentFailureCount : (env.failedCount ? ' failed=' + env.failedCount : '');
1068
1149
  const required = ' required=' + (env.satisfiedCount || 0) + '/' + (env.requiredCount || 0);
1069
1150
  const noManifest = env.noRequiredEvidence ? ' no-manifest' : '';
1151
+ const stale = env.staleCount ? ' stale=' + env.staleCount : '';
1152
+ const changed = env.requirementsChangedAfterEvidence ? ' requirements-changed' : '';
1070
1153
  const lane = [env.branchName || 'unknown-branch', env.workspace ? 'ws:' + env.workspace : 'shared', env.dataMode || null, env.sourceLayer || null]
1071
1154
  .filter(Boolean).join(' | ');
1072
- console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + noManifest);
1155
+ console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + stale + noManifest + changed);
1073
1156
  if (env.summary) console.log(' ' + env.summary);
1074
- console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
1157
+ console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' lastPacket=' + formatTime(env.lastPacketAtMs || env.latestPacketAtMs) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
1075
1158
  }
1076
1159
 
1077
1160
  function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
@@ -1088,6 +1171,12 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
1088
1171
  if (evaluation.failedCount) {
1089
1172
  console.log('Failed packets:', evaluation.failedCount, 'current failures:', evaluation.currentFailureCount || 0);
1090
1173
  }
1174
+ if (evaluation.requirementsChangedAfterEvidence) {
1175
+ console.log('Requirements changed after evidence was collected.');
1176
+ }
1177
+ if (Array.isArray(evaluation.stale) && evaluation.stale.length) {
1178
+ console.log('Stale packets:', evaluation.stale.length);
1179
+ }
1091
1180
  const packets = Array.isArray(envelope.packets) ? envelope.packets : [];
1092
1181
  console.log('Packets:', packets.length);
1093
1182
  packets.slice(-20).forEach((packet) => {
@@ -1100,12 +1189,78 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
1100
1189
  }
1101
1190
  }
1102
1191
 
1192
+ function printAcceptanceWarnings(envelope) {
1193
+ const warnings = envelope && envelope.acceptance && Array.isArray(envelope.acceptance.warnings)
1194
+ ? envelope.acceptance.warnings
1195
+ : [];
1196
+ if (!warnings.length) return;
1197
+ console.log('Manifest warnings:');
1198
+ warnings.forEach((warning) => {
1199
+ console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
1200
+ });
1201
+ }
1202
+
1203
+ function printEnvelopeWarnings(envelope) {
1204
+ const warnings = envelope && Array.isArray(envelope.warnings) ? envelope.warnings : [];
1205
+ if (!warnings.length) return;
1206
+ console.log('Envelope warnings:');
1207
+ warnings.forEach((warning) => {
1208
+ console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
1209
+ if (Array.isArray(warning.envelopes) && warning.envelopes.length) {
1210
+ warning.envelopes.forEach((env) => {
1211
+ console.log(' ' + (env.envelopeId || '(no id)') + (env.status ? ' ' + env.status : ''));
1212
+ });
1213
+ }
1214
+ });
1215
+ }
1216
+
1217
+ function manifestRequiredEvidence(manifest = {}) {
1218
+ const required = [];
1219
+ if (Array.isArray(manifest.requiredEvidence)) required.push(...manifest.requiredEvidence);
1220
+ const journeys = Array.isArray(manifest.journeys) ? manifest.journeys :
1221
+ (Array.isArray(manifest.requestedJourneys) ? manifest.requestedJourneys : []);
1222
+ journeys.forEach((journey) => {
1223
+ if (journey && Array.isArray(journey.requiredEvidence)) required.push(...journey.requiredEvidence);
1224
+ });
1225
+ return required;
1226
+ }
1227
+
1228
+ async function readVerificationEnvelopeForDiagnostics(api, cwd, session, accountId, envelopeId) {
1229
+ if (!envelopeId) return null;
1230
+ try {
1231
+ const response = await postVerificationCommand(api, cwd, session, accountId, 'show', { envelopeId });
1232
+ if (response.envelope) {
1233
+ writeLocalVerificationEnvelope(cwd, response.envelope);
1234
+ return response.envelope;
1235
+ }
1236
+ } catch (_) {
1237
+ // Local evidence is still useful for diagnostics when the platform read fails.
1238
+ }
1239
+ return readLocalVerificationEnvelope(cwd, envelopeId);
1240
+ }
1241
+
1242
+ async function readVerificationPacketsForDiagnostics(api, cwd, session, accountId, envelopeId, filters = {}) {
1243
+ if (!envelopeId) return [];
1244
+ try {
1245
+ const response = await postVerificationCommand(api, cwd, session, accountId, 'packets', Object.assign({ envelopeId }, filters));
1246
+ if (Array.isArray(response.packets)) return response.packets;
1247
+ } catch (_) {
1248
+ // Local full-fidelity packet files remain the fallback when the platform read is unavailable.
1249
+ }
1250
+ return readLocalVerificationPackets(cwd, envelopeId, filters);
1251
+ }
1252
+
1103
1253
  function buildCommandWorld(response, fallback = {}) {
1104
1254
  const source = (response && response.staging) || {};
1105
1255
  const resolution = (response && response.resolution) || {};
1256
+ const result = (response && response.result) || {};
1257
+ const laneHash = laneContentHashFrom(response) || fallback.laneContentHash || fallback.stagedOverlayHash;
1258
+ const executionAccountId = response && (response.executionAccountId || result.accountId || source.executionAccountId || response.accountId) ||
1259
+ resolution.executionAccountId ||
1260
+ fallback.executionAccountId;
1106
1261
  return {
1107
1262
  accountId: response && (response.accountId || response.ownerAccountId) || fallback.accountId,
1108
- executionAccountId: response && (response.executionAccountId || response.accountId) || resolution.executionAccountId || fallback.executionAccountId,
1263
+ executionAccountId,
1109
1264
  addressedAccountId: resolution.addressedAccountId || fallback.addressedAccountId,
1110
1265
  host: fallback.host,
1111
1266
  dataMode: response && response.dataMode || fallback.dataMode,
@@ -1113,16 +1268,26 @@ function buildCommandWorld(response, fallback = {}) {
1113
1268
  branchName: response && response.branchName || fallback.branchName,
1114
1269
  workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
1115
1270
  stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
1271
+ laneContentHash: laneHash,
1272
+ stagedOverlayHash: laneHash,
1116
1273
  // `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
1117
1274
  // subscription); a bare `variantBranch` is null whenever the subscription decided.
1118
1275
  componentBranch: resolution.componentBranch ||
1119
1276
  response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
1120
1277
  fallback.componentBranch,
1278
+ variantBranchSource: response && (response.variantBranchSource || (response.variantWorld && response.variantWorld.variantBranchSource)) || fallback.variantBranchSource,
1121
1279
  variantId: resolution.variantId || fallback.variantId,
1122
1280
  sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
1123
1281
  };
1124
1282
  }
1125
1283
 
1284
+ function laneContentHashFrom(response) {
1285
+ if (!response) return null;
1286
+ const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
1287
+ (response.componentStatus && response.componentStatus.laneSummary);
1288
+ return laneSummary && laneSummary.contentHash ? laneSummary.contentHash : null;
1289
+ }
1290
+
1126
1291
  function normalizeName(name) {
1127
1292
  return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
1128
1293
  }
@@ -2333,21 +2498,23 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
2333
2498
  error = err;
2334
2499
  throw err;
2335
2500
  } finally {
2336
- const status = error ? 'error' : 'success';
2337
- const responseForLog = responseFile
2338
- ? { responseFile, note: 'Tool response stored externally' }
2339
- : sanitizeForLog(responseData);
2340
- appendSessionLog(cwd, {
2341
- ts: new Date().toISOString(),
2342
- requestId,
2343
- endpoint,
2344
- method: 'POST',
2345
- status,
2346
- durationMs: Date.now() - started,
2347
- request: sanitizeForLog(payload),
2348
- response: responseForLog,
2349
- error: error ? describeError(error) : null
2350
- });
2501
+ if (!options.skipSessionLog) {
2502
+ const status = error ? 'error' : 'success';
2503
+ const responseForLog = responseFile
2504
+ ? { responseFile, note: 'Tool response stored externally' }
2505
+ : sanitizeForLog(responseData);
2506
+ appendSessionLog(cwd, {
2507
+ ts: new Date().toISOString(),
2508
+ requestId,
2509
+ endpoint,
2510
+ method: 'POST',
2511
+ status,
2512
+ durationMs: Date.now() - started,
2513
+ request: sanitizeForLog(payload),
2514
+ response: responseForLog,
2515
+ error: error ? describeError(error) : null
2516
+ });
2517
+ }
2351
2518
  }
2352
2519
  }
2353
2520
 
@@ -3057,6 +3224,7 @@ async function pushComponentsCommand(flags) {
3057
3224
  rawRefs: { command: 'components stage' },
3058
3225
  stage: (err.responseBody || {}).stage,
3059
3226
  compileValidation: (err.responseBody || {}).compileValidation,
3227
+ refusalDetails: stageRefusalDetails(err.responseBody || {}),
3060
3228
  changedFromWorkingTree: changedFromWorkingTree || [],
3061
3229
  unrepresentableChanges: unstageable
3062
3230
  }, { quiet: true });
@@ -3091,6 +3259,7 @@ async function pushComponentsCommand(flags) {
3091
3259
  rawRefs: { command: 'components stage' },
3092
3260
  stage: response.stage,
3093
3261
  laneSummary: response.laneSummary,
3262
+ compileValidation: response.compileValidation || null,
3094
3263
  changedFromWorkingTree: response.changedFromWorkingTree,
3095
3264
  unrepresentableChanges: response.unrepresentableChanges
3096
3265
  }, { quiet: flagEnabled(flags.json) });
@@ -3276,6 +3445,14 @@ async function stageOrRefuse(api, cwd, payload) {
3276
3445
  }
3277
3446
  }
3278
3447
 
3448
+ function stageRefusalDetails(body = {}) {
3449
+ const details = {};
3450
+ ['duplicates', 'compileValidation', 'policy', 'editLease', 'skipped', 'reconcile', 'laneHoldsRejectedSource'].forEach((key) => {
3451
+ if (body[key] !== undefined) details[key] = sanitizeForLog(body[key]);
3452
+ });
3453
+ return Object.keys(details).length ? details : undefined;
3454
+ }
3455
+
3279
3456
  /*
3280
3457
  ## Component Summaries, Status, And Sync Gates
3281
3458
  */
@@ -3331,6 +3508,10 @@ function printCompileValidation(validation, options) {
3331
3508
  // A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
3332
3509
  // component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
3333
3510
  const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
3511
+ const warnings = Array.isArray(validation.warnings) ? validation.warnings : [];
3512
+ warnings.forEach((warning) => {
3513
+ emit(' Warning: ' + (warning.message || JSON.stringify(warning)));
3514
+ });
3334
3515
  if (unchecked.length) {
3335
3516
  emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
3336
3517
  (entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
@@ -3495,6 +3676,9 @@ function printStageSummary(response, flags) {
3495
3676
  // The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
3496
3677
  // different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
3497
3678
  printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
3679
+ if (typeof printOrphanedWorkspaceWarning === 'function') {
3680
+ printOrphanedWorkspaceWarning(response);
3681
+ }
3498
3682
  if (typeof printAccountLanes === 'function') {
3499
3683
  printAccountLanes(response);
3500
3684
  }
@@ -3519,7 +3703,9 @@ function describeStageMode(mode) {
3519
3703
  function printStatusSummary(response, flags) {
3520
3704
  printBranchContext(response);
3521
3705
  printStagingLane(response.branchName, response.workspace, null);
3706
+ printStagingLaneOwnerNotice(response);
3522
3707
  printLaneSummary(response);
3708
+ printOrphanedWorkspaceWarning(response);
3523
3709
  printStagingFreshness(response.freshness);
3524
3710
  printRepositoryCheck(response.repositoryCheck);
3525
3711
  printComponentTypeCounts(response.entries || []);
@@ -3545,6 +3731,28 @@ function printStatusSummary(response, flags) {
3545
3731
  printComponentCommandResponse('Components staging', response, flags);
3546
3732
  }
3547
3733
 
3734
+ function stagingLaneOwnerNotice(response = {}) {
3735
+ const owner = response.laneOwnerAccountId != null ? response.laneOwnerAccountId :
3736
+ (response.staging && response.staging.laneOwnerAccountId);
3737
+ const checkout = response.checkoutAccountId != null ? response.checkoutAccountId :
3738
+ (response.accountId != null ? response.accountId : (response.staging && response.staging.checkoutAccountId));
3739
+ const stagedCount = Number(response.stagedCount != null ? response.stagedCount :
3740
+ (response.laneSummary && response.laneSummary.stagedCount != null ? response.laneSummary.stagedCount :
3741
+ (response.staging && response.staging.stagedCount != null ? response.staging.stagedCount :
3742
+ (response.staging && response.staging.laneSummary && response.staging.laneSummary.stagedCount))));
3743
+ if (owner == null || checkout == null || String(owner) === String(checkout) || !(stagedCount > 0)) {
3744
+ return [];
3745
+ }
3746
+ return [
3747
+ 'WARNING: staged entries shown here are under account ' + owner + ', not checkout account ' + checkout + '.',
3748
+ ' clear with: remits-cli components clear --all --account-id ' + owner
3749
+ ];
3750
+ }
3751
+
3752
+ function printStagingLaneOwnerNotice(response = {}) {
3753
+ stagingLaneOwnerNotice(response).forEach((line) => console.log(line));
3754
+ }
3755
+
3548
3756
  function printLanesSummary(response, flags) {
3549
3757
  printBranchContext(response);
3550
3758
  const lanes = Array.isArray(response.lanes) ? response.lanes : (Array.isArray(response.accountLanes) ? response.accountLanes : []);
@@ -3678,6 +3886,36 @@ function printAccountLanes(response) {
3678
3886
  console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
3679
3887
  }
3680
3888
 
3889
+ function printOrphanedWorkspaceWarning(response) {
3890
+ const currentCount = Number((response && response.stagedCount) ||
3891
+ (response && response.laneSummary && response.laneSummary.stagedCount) || 0);
3892
+ if (currentCount > 0) return;
3893
+ const lanes = Array.isArray(response && response.accountLanes) ? response.accountLanes : [];
3894
+ const currentBranch = response && response.branchName;
3895
+ const currentWorkspace = response && response.workspace;
3896
+ const orphan = lanes.find((lane) => lane && lane.sameWorkspaceOtherBranch === true);
3897
+ if (!orphan) return;
3898
+ const age = orphan.updatedAtMs ? formatAge(Date.now() - Number(orphan.updatedAtMs)) + ' ago' : 'recently';
3899
+ console.log('');
3900
+ console.log('WARNING: Your workspace "' + (currentWorkspace || orphan.workspace || 'shared') + '" has ' +
3901
+ (orphan.stagedCountAsOf || orphan.stagedCount || 0) + ' staged entr' +
3902
+ ((orphan.stagedCountAsOf || orphan.stagedCount || 0) === 1 ? 'y' : 'ies') +
3903
+ ' on branch "' + orphan.branchName + '" (staged ' + age + '); this checkout is now on "' +
3904
+ (currentBranch || 'unknown') + '". Re-stage here, or switch back.');
3905
+ }
3906
+
3907
+ function formatAge(ms) {
3908
+ const value = Number(ms);
3909
+ if (!Number.isFinite(value) || value < 0) return 'unknown age';
3910
+ const seconds = Math.round(value / 1000);
3911
+ if (seconds < 90) return seconds + 's';
3912
+ const minutes = Math.round(seconds / 60);
3913
+ if (minutes < 90) return minutes + 'm';
3914
+ const hours = Math.round(minutes / 60);
3915
+ if (hours < 48) return hours + 'h';
3916
+ return Math.round(hours / 24) + 'd';
3917
+ }
3918
+
3681
3919
  // The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
3682
3920
  // resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
3683
3921
  // non-trunk — never guessed to be a variant branch. Pure.
@@ -3846,6 +4084,7 @@ async function statusComponentsCommand(flags) {
3846
4084
  // supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
3847
4085
  response.freshness = stagingFreshness({
3848
4086
  head: safeGitValue(cwd, 'git rev-parse HEAD'),
4087
+ originHead: safeGitValue(cwd, 'git rev-parse ' + shellQuote('origin/' + branchName)),
3849
4088
  lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
3850
4089
  entries: response.entries,
3851
4090
  laneSummary: response.laneSummary,
@@ -3888,19 +4127,25 @@ async function lanesComponentsCommand(flags) {
3888
4127
  const dataMode = resolveDataMode(flags, session);
3889
4128
  const api = buildAxios(baseUrl, session.token);
3890
4129
 
3891
- const response = await loggedPost(api, cwd, '/cli/components', {
4130
+ const requestPayload = {
3892
4131
  token: session.token,
3893
4132
  accountId,
3894
4133
  branchName,
3895
4134
  workspace,
3896
4135
  dataMode,
3897
4136
  mode: 'lanes'
3898
- }).then((r) => r.data);
4137
+ };
4138
+
4139
+ let response = await loggedPost(api, cwd, '/cli/components', requestPayload).then((r) => r.data);
3899
4140
 
3900
4141
  if (!response.success) {
3901
4142
  throw new Error(response.message || 'Staging lanes failed');
3902
4143
  }
3903
4144
 
4145
+ if (flagEnabled(flags['wait-change']) || flagEnabled(flags.waitChange)) {
4146
+ response = await waitForLaneChange(api, cwd, requestPayload, response, flags);
4147
+ }
4148
+
3904
4149
  if (flagEnabled(flags.json)) {
3905
4150
  console.log(JSON.stringify(response, null, 2));
3906
4151
  return response;
@@ -3913,6 +4158,55 @@ async function lanesComponentsCommand(flags) {
3913
4158
  return response;
3914
4159
  }
3915
4160
 
4161
+ function laneWaitSignature(response, laneId) {
4162
+ const lanes = Array.isArray(response && response.lanes)
4163
+ ? response.lanes
4164
+ : (Array.isArray(response && response.accountLanes) ? response.accountLanes : []);
4165
+ const selected = laneId ? lanes.filter((lane) => String(lane.laneId || '') === String(laneId)) : lanes;
4166
+ return stableStringify(selected.map((lane) => ({
4167
+ laneId: lane.laneId,
4168
+ accountId: lane.accountId,
4169
+ branchName: lane.branchName,
4170
+ workspace: lane.workspace || null,
4171
+ stagedCount: lane.stagedCountAsOf || lane.stagedCount || 0,
4172
+ contentHash: lane.contentHash || (lane.laneSummary && lane.laneSummary.contentHash) || null,
4173
+ updatedAtMs: lane.updatedAtMs || null
4174
+ })));
4175
+ }
4176
+
4177
+ async function waitForLaneChange(api, cwd, requestPayload, initialResponse, flags) {
4178
+ const laneId = flags['lane-id'] || flags.laneId || flags.lane;
4179
+ const timeoutSeconds = parsePositiveInt(flags.timeout || flags['timeout-seconds'] || flags.timeoutSeconds, 600);
4180
+ const pollMs = parsePositiveInt(flags['poll-ms'] || flags.pollMs, 2000);
4181
+ const started = Date.now();
4182
+ const initialSignature = laneWaitSignature(initialResponse, laneId);
4183
+ if (!flagEnabled(flags.json)) {
4184
+ console.log('Waiting for staging lane change' + (laneId ? ' on ' + laneId : '') + ' for up to ' + timeoutSeconds + 's...');
4185
+ }
4186
+ while (Date.now() - started < timeoutSeconds * 1000) {
4187
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
4188
+ const next = await loggedPost(api, cwd, '/cli/components', requestPayload, { skipSessionLog: true }).then((r) => r.data);
4189
+ if (!next.success) {
4190
+ throw new Error(next.message || 'Staging lanes failed while waiting for a change');
4191
+ }
4192
+ if (laneWaitSignature(next, laneId) !== initialSignature) {
4193
+ next.waitChange = {
4194
+ changed: true,
4195
+ laneId: laneId || null,
4196
+ waitedMs: Date.now() - started
4197
+ };
4198
+ return next;
4199
+ }
4200
+ }
4201
+ initialResponse.waitChange = {
4202
+ changed: false,
4203
+ laneId: laneId || null,
4204
+ waitedMs: Date.now() - started,
4205
+ timeoutSeconds
4206
+ };
4207
+ return initialResponse;
4208
+ }
4209
+
3916
4210
  async function entriesComponentsCommand(flags) {
3917
4211
  const cwd = process.cwd();
3918
4212
  ensureLocalState(cwd);
@@ -4108,6 +4402,30 @@ async function syncComponentsCommand(rawFlags) {
4108
4402
  }
4109
4403
  throw new Error(mismatch + ' Nothing was synced.');
4110
4404
  }
4405
+ const featureRefusal = !namesOnly ? featureBranchSyncRefusal(branchContext, branchName, flags) : null;
4406
+ if (featureRefusal) {
4407
+ if (flagEnabled(flags.json)) {
4408
+ const refusal = {
4409
+ success: false,
4410
+ mode: 'sync',
4411
+ dataMode,
4412
+ accountId,
4413
+ branchName,
4414
+ workspace,
4415
+ dryRun,
4416
+ safe,
4417
+ branchContext,
4418
+ repositoryCheck: preflightRepoCheck,
4419
+ refusal: 'feature_branch_landing',
4420
+ gateViolations: [featureRefusal],
4421
+ message: featureRefusal
4422
+ };
4423
+ console.log(JSON.stringify(refusal, null, 2));
4424
+ process.exitCode = 1;
4425
+ return refusal;
4426
+ }
4427
+ throw new Error(featureRefusal + ' Nothing was synced.');
4428
+ }
4111
4429
  if (branchContextIsTrunk(branchContext)) {
4112
4430
  // Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
4113
4431
  // an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
@@ -4670,6 +4988,15 @@ function featureBranchCommitRefusal(branchContext, branchName, flags) {
4670
4988
  'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
4671
4989
  }
4672
4990
 
4991
+ function featureBranchSyncRefusal(branchContext, branchName, flags) {
4992
+ if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
4993
+ if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
4994
+ return 'components sync refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
4995
+ 'subscribers). ' + featureBranchLandingHint(branchContext) + ' Syncing here would write overlays for \'' +
4996
+ branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
4997
+ 'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
4998
+ }
4999
+
4673
5000
  // What a verification envelope records as the world it verified. Taken from the platform's answer
4674
5001
  // (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
4675
5002
  // variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
@@ -4759,7 +5086,7 @@ function gitIsAncestor(cwd, ancestor, descendant) {
4759
5086
  * `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
4760
5087
  * injected as `isAncestor(a, b)` — so it can be tested without a repository.
4761
5088
  */
4762
- function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
5089
+ function stagingFreshness({ head, originHead, lastSyncedSha, entries, laneSummary, isAncestor }) {
4763
5090
  const rows = Array.isArray(entries) ? entries : [];
4764
5091
  const staged = rows.filter((entry) => entry && entry.stageBaseSha);
4765
5092
  const memo = new Map();
@@ -4785,7 +5112,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
4785
5112
 
4786
5113
  return {
4787
5114
  head: head || null,
5115
+ originHead: originHead || null,
4788
5116
  lastSyncedSha: lastSyncedSha || null,
5117
+ freshnessUnknownReason: !originHead ? 'branch has no upstream' : (!lastSyncedSha ? 'platform has no last synced SHA for this branch' : null),
4789
5118
  landedSinceHead,
4790
5119
  entriesBehindLastSync: behindLastSync.length,
4791
5120
  entriesFromOtherCommits: fromOtherCommits.length,
@@ -4799,6 +5128,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
4799
5128
  function printStagingFreshness(freshness) {
4800
5129
  if (!freshness) return;
4801
5130
  const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
5131
+ if (freshness.freshnessUnknownReason) {
5132
+ console.log('Freshness unknown:', freshness.freshnessUnknownReason + '.');
5133
+ }
4802
5134
  if (freshness.landedSinceHead === true) {
4803
5135
  console.log('');
4804
5136
  console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
@@ -5759,6 +6091,172 @@ async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dat
5759
6091
  }
5760
6092
  }
5761
6093
 
6094
+ function testEvidenceCategories(status = {}, selectedNames = []) {
6095
+ const categories = new Set(['test_run']);
6096
+ const test = status.test || {};
6097
+ if (test.name) categories.add('test_run.suite:' + String(test.name));
6098
+ if (test.id !== undefined && test.id !== null) categories.add('test_run.suite_id:' + String(test.id));
6099
+ (Array.isArray(selectedNames) ? selectedNames : []).forEach((name) => {
6100
+ if (name) categories.add('test_run.' + String(name));
6101
+ });
6102
+ const results = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
6103
+ results.forEach((result) => {
6104
+ if (result && result.passed === true && result.name) {
6105
+ categories.add('test_run.case:' + String(result.name));
6106
+ categories.add('test_run.' + String(result.name));
6107
+ }
6108
+ });
6109
+ return Array.from(categories);
6110
+ }
6111
+
6112
+ function testComponentProvenance(status = {}) {
6113
+ const direct = status.resolvedComponentProvenance || (status.result && status.result.resolvedComponentProvenance) ||
6114
+ (status.staging && status.staging.resolvedComponentProvenance);
6115
+ if (Array.isArray(direct)) return direct;
6116
+ const staging = status.staging || {};
6117
+ if (staging.testComponentSignature) {
6118
+ return [{
6119
+ type: 'test',
6120
+ id: status.test && status.test.id,
6121
+ name: status.test && status.test.name,
6122
+ signature: staging.testComponentSignature,
6123
+ source: staging.testComponentSource || null
6124
+ }];
6125
+ }
6126
+ return [];
6127
+ }
6128
+
6129
+ function testCompileSignatures(status = {}) {
6130
+ return testComponentProvenance(status)
6131
+ .map((entry) => entry && entry.signature)
6132
+ .filter(Boolean);
6133
+ }
6134
+
6135
+ function testRunSuiteNameFromStatus(status = {}) {
6136
+ return status.test && status.test.name ? String(status.test.name) : null;
6137
+ }
6138
+
6139
+ function testRunSuiteNameFromPacket(packet = {}) {
6140
+ return packet.test && packet.test.testName
6141
+ ? String(packet.test.testName)
6142
+ : (packet.status && packet.status.test && packet.status.test.name ? String(packet.status.test.name) : null);
6143
+ }
6144
+
6145
+ function testCaseOutcomesFromStatusLike(status = {}) {
6146
+ const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests :
6147
+ (status.test && Array.isArray(status.test.tests) ? status.test.tests : []);
6148
+ return tests
6149
+ .filter((test) => test && test.name && typeof test.passed === 'boolean')
6150
+ .map((test) => ({ name: String(test.name), passed: test.passed === true }));
6151
+ }
6152
+
6153
+ function provenanceSignature(value) {
6154
+ const rows = Array.isArray(value) ? value : [];
6155
+ return sha256(stableStringify(rows.map((entry) => ({
6156
+ type: entry && entry.type || null,
6157
+ id: entry && entry.id || null,
6158
+ name: entry && entry.name || null,
6159
+ signature: entry && entry.signature || null,
6160
+ source: entry && entry.source || null
6161
+ }))));
6162
+ }
6163
+
6164
+ function packetEvidenceIdentity(packet = {}) {
6165
+ const world = packet.world || {};
6166
+ const revision = packet.revision || {};
6167
+ const laneSummary = packet.laneSummary || (packet.componentStatus && packet.componentStatus.laneSummary) || {};
6168
+ return {
6169
+ laneContentHash: world.laneContentHash || world.stagedOverlayHash || revision.laneContentHash ||
6170
+ revision.stagedOverlayHash || laneSummary.contentHash || null,
6171
+ gitHead: revision.gitHead || null
6172
+ };
6173
+ }
6174
+
6175
+ function sameNondeterminismIdentity(left = {}, right = {}) {
6176
+ return !!(left.laneContentHash && right.laneContentHash && left.gitHead && right.gitHead &&
6177
+ String(left.laneContentHash) === String(right.laneContentHash) &&
6178
+ String(left.gitHead) === String(right.gitHead));
6179
+ }
6180
+
6181
+ function detectNondeterministicTestRun(priorPackets, currentStatus, currentProvenance, currentIdentity = {}) {
6182
+ const packets = Array.isArray(priorPackets) ? priorPackets : [];
6183
+ const suiteName = testRunSuiteNameFromStatus(currentStatus);
6184
+ const currentCases = testCaseOutcomesFromStatusLike(currentStatus);
6185
+ if (!suiteName || !currentCases.length) return null;
6186
+ if (!Array.isArray(currentProvenance) || !currentProvenance.length) return null;
6187
+ if (!currentIdentity.laneContentHash || !currentIdentity.gitHead) return null;
6188
+
6189
+ const currentSignature = provenanceSignature(currentProvenance);
6190
+ const currentByName = new Map(currentCases.map((outcome) => [outcome.name, outcome]));
6191
+ for (let i = packets.length - 1; i >= 0; i -= 1) {
6192
+ const packet = packets[i];
6193
+ if (!packet || packet.type !== 'test_run') continue;
6194
+ if (testRunSuiteNameFromPacket(packet) !== suiteName) continue;
6195
+ const previousProvenance = packet.revision && packet.revision.componentProvenance;
6196
+ if (!Array.isArray(previousProvenance) || !previousProvenance.length) continue;
6197
+ if (!sameNondeterminismIdentity(currentIdentity, packetEvidenceIdentity(packet))) continue;
6198
+ const previousSignature = provenanceSignature(previousProvenance);
6199
+ if (previousSignature !== currentSignature) continue;
6200
+ const previousCases = testCaseOutcomesFromStatusLike(packet.status || packet);
6201
+ const flips = previousCases
6202
+ .map((previous) => {
6203
+ const current = currentByName.get(previous.name);
6204
+ return current && current.passed !== previous.passed
6205
+ ? { caseName: previous.name, previousPassed: previous.passed, currentPassed: current.passed, previousPacketId: packet.packetId }
6206
+ : null;
6207
+ })
6208
+ .filter(Boolean);
6209
+ if (flips.length) {
6210
+ return {
6211
+ nondeterministic: true,
6212
+ message: 'Outcome changed with no source change - likely nondeterministic (AI/live data).',
6213
+ suite: suiteName,
6214
+ provenanceSignature: currentSignature,
6215
+ previousPacketId: packet.packetId,
6216
+ flips
6217
+ };
6218
+ }
6219
+ }
6220
+ return null;
6221
+ }
6222
+
6223
+ function printTestRunPivots(status = {}) {
6224
+ const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
6225
+ if (!tests.length) return;
6226
+ const slowest = tests.slice().sort((a, b) => Number(b.duration || 0) - Number(a.duration || 0))[0];
6227
+ if (slowest && slowest.duration != null) {
6228
+ console.log('Slowest case:', (slowest.name || '(unnamed)') + ' in ' + formatDurationMs(slowest.duration));
6229
+ }
6230
+ tests.filter((test) => test && test.passed === false).forEach((test) => {
6231
+ console.log('Pivots for failed case:', test.name || '(unnamed)');
6232
+ if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
6233
+ if (test.threadGroupingId || test.threadGroupId || test.traceId) {
6234
+ console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
6235
+ }
6236
+ if (test.error) console.log(' Error:', String(test.error).slice(0, 500));
6237
+ const diagnostics = test.diagnostics && typeof test.diagnostics === 'object' ? test.diagnostics : null;
6238
+ if (diagnostics && Object.keys(diagnostics).length) {
6239
+ console.log(' Diagnostics:', JSON.stringify(diagnostics).slice(0, 1200));
6240
+ }
6241
+ if (Array.isArray(test.resolvedComponentProvenance) && test.resolvedComponentProvenance.length) {
6242
+ console.log(' Components:', test.resolvedComponentProvenance.slice(0, 8).map((entry) =>
6243
+ (entry.type || 'component') + ' ' + (entry.id || entry.name || '?') +
6244
+ (entry.source ? ' [' + entry.source + ']' : '') +
6245
+ (entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
6246
+ }
6247
+ if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
6248
+ console.log(' Live HTTP calls:', test.liveHttpCalls.length);
6249
+ }
6250
+ });
6251
+ }
6252
+
6253
+ function formatDurationMs(value) {
6254
+ const ms = Number(value);
6255
+ if (!Number.isFinite(ms)) return String(value);
6256
+ if (ms < 1000) return ms + 'ms';
6257
+ return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
6258
+ }
6259
+
5762
6260
  async function waitForToolStatus(api, cwd, options) {
5763
6261
  const started = Date.now();
5764
6262
  let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
@@ -5908,7 +6406,9 @@ async function testCommand(flags) {
5908
6406
  printSessionResolutionWarning(sessionContext);
5909
6407
  printResolvedBaseUrl(baseUrl);
5910
6408
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
6409
+ printStagingLaneOwnerNotice(start.staging || {});
5911
6410
  if (start.staging && Array.isArray(start.staging.accountLanes)) {
6411
+ printOrphanedWorkspaceWarning(start.staging);
5912
6412
  printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
5913
6413
  }
5914
6414
  console.log('Test run started:', start.taskId);
@@ -5935,6 +6435,8 @@ async function testCommand(flags) {
5935
6435
  console.log(JSON.stringify(status, null, 2));
5936
6436
  } else {
5937
6437
  console.log('Final status:', JSON.stringify(status, null, 2));
6438
+ printStagingLaneOwnerNotice(status.staging || {});
6439
+ printTestRunPivots(status);
5938
6440
  }
5939
6441
 
5940
6442
  // A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
@@ -5959,14 +6461,38 @@ async function testCommand(flags) {
5959
6461
  process.exitCode = 1;
5960
6462
  }
5961
6463
 
6464
+ const verificationContext = activeVerificationContext(cwd, flags, session, accountId);
6465
+ const activeEnvelopeId = verificationEnvelopeIdForCommand(cwd, flags, verificationContext);
6466
+ const componentProvenance = testComponentProvenance(status);
6467
+ const sourceRevision = collectVerificationSource(cwd, flags);
6468
+ const priorPackets = await readVerificationPacketsForDiagnostics(api, cwd, session, accountId, activeEnvelopeId, {
6469
+ packetType: 'test_run',
6470
+ suite: status.test && status.test.name,
6471
+ max: 50
6472
+ });
6473
+ const nondeterminism = detectNondeterministicTestRun(priorPackets, status, componentProvenance, {
6474
+ laneContentHash: status.staging && status.staging.laneSummary && status.staging.laneSummary.contentHash,
6475
+ gitHead: sourceRevision.gitHead
6476
+ });
6477
+ if (nondeterminism && !jsonOutput) {
6478
+ console.log('Nondeterministic signal:', nondeterminism.message);
6479
+ nondeterminism.flips.slice(0, 6).forEach((flip) => {
6480
+ console.log(' - ' + flip.caseName + ': ' + (flip.previousPassed ? 'passed' : 'failed') + ' -> ' + (flip.currentPassed ? 'passed' : 'failed') +
6481
+ ' (previous packet ' + (flip.previousPacketId || 'unknown') + ')');
6482
+ });
6483
+ }
6484
+
5962
6485
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
5963
6486
  type: 'test_run',
5964
6487
  success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
5965
6488
  claim: 'Test run ' + String(testRef),
5966
6489
  world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
5967
- revision: Object.assign(collectVerificationSource(cwd, flags), {
5968
- compileSignatures: status.staging && status.staging.testComponentSignature ? [status.staging.testComponentSignature] : []
6490
+ revision: Object.assign(sourceRevision, {
6491
+ compileSignatures: testCompileSignatures(status),
6492
+ componentProvenance
5969
6493
  }),
6494
+ nondeterministic: nondeterminism ? true : undefined,
6495
+ nondeterminism: nondeterminism || undefined,
5970
6496
  test: {
5971
6497
  taskId: start.taskId,
5972
6498
  testId: status.test && status.test.id,
@@ -5974,16 +6500,64 @@ async function testCommand(flags) {
5974
6500
  selectedCases: names,
5975
6501
  passed: status.result && status.result.passed,
5976
6502
  failed: status.result && status.result.failed,
6503
+ tests: status.result && status.result.tests,
5977
6504
  dataModeSource: status.dataModeSource || dataModeSource,
5978
6505
  unmatchedTestNames: unmatched
5979
6506
  },
5980
- evidenceCategories: ['test_run'].concat(names.map((name) => 'test_run.' + name)),
5981
- limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
6507
+ evidenceCategories: testEvidenceCategories(status, names),
6508
+ limitations: []
6509
+ .concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
6510
+ .concat(nondeterminism ? [nondeterminism.message] : []),
5982
6511
  rawRefs: { testStatusKey: start.taskId },
5983
6512
  status
5984
6513
  }, { quiet: jsonOutput });
5985
6514
  }
5986
6515
 
6516
+ async function testStatusCommand(flags) {
6517
+ const cwd = process.cwd();
6518
+ ensureLocalState(cwd);
6519
+ const sessionContext = resolveSessionContext(cwd, flags);
6520
+ const { session, accountId } = sessionContext;
6521
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
6522
+ const branchName = flags.branch || currentBranch(cwd);
6523
+ const dataMode = hasExplicitDataModeFlag(flags)
6524
+ ? resolveDataMode(flags, null)
6525
+ : DEFAULT_DATA_MODE;
6526
+ const taskId = flags['task-id'] || flags.taskId || flags.id || (flags._ && flags._[2]);
6527
+ const jsonOutput = flagEnabled(flags.json);
6528
+ if (!taskId) throw new Error('Missing --task-id <taskId>');
6529
+
6530
+ const api = buildAxios(baseUrl, session.token);
6531
+ const status = await loggedPost(api, cwd, '/cli/test', {
6532
+ token: session.token,
6533
+ command: 'status',
6534
+ accountId,
6535
+ branchName,
6536
+ taskId,
6537
+ dataMode
6538
+ }).then((r) => r.data);
6539
+
6540
+ if (jsonOutput) {
6541
+ console.log(JSON.stringify(status, null, 2));
6542
+ return status;
6543
+ }
6544
+ printSessionResolutionWarning(sessionContext);
6545
+ printResolvedBaseUrl(baseUrl);
6546
+ console.log('Test run status:', status.status || 'unknown');
6547
+ console.log('Task ID:', taskId);
6548
+ console.log('Data mode:', status.dataMode || dataMode);
6549
+ if (status.result) {
6550
+ console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
6551
+ }
6552
+ if (status.error || status.message) {
6553
+ console.log('Message:', status.error || status.message);
6554
+ }
6555
+ if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
6556
+ process.exitCode = 1;
6557
+ }
6558
+ return status;
6559
+ }
6560
+
5987
6561
  /*
5988
6562
  ## Tokens, Tools, Verification, And Config
5989
6563
  */
@@ -6308,8 +6882,7 @@ async function toolCommand(flags) {
6308
6882
  revision: collectVerificationSource(cwd, flags),
6309
6883
  evidenceCategories: ['tool_call'],
6310
6884
  rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
6311
- tool: { callId: requestedCallId, status: data.status, name: stored && stored.name },
6312
- result: data
6885
+ tool: Object.assign(toolEvidenceSummary(data, statusResponse.responseFile), { callId: requestedCallId, name: stored && stored.name })
6313
6886
  }, { quiet: jsonOutput });
6314
6887
  if (jsonOutput) {
6315
6888
  console.log(JSON.stringify(data, null, 2));
@@ -6322,7 +6895,7 @@ async function toolCommand(flags) {
6322
6895
  console.log('Data mode:', data.dataMode || dataMode);
6323
6896
  if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
6324
6897
  console.log('Session log:', data.sessionLog);
6325
- console.log('Tool response file:', data.responseFile);
6898
+ printToolResponseFileLine('Tool response file', data.responseFile);
6326
6899
  if (polledFailed) {
6327
6900
  console.log('Tool error:', data.toolMessage);
6328
6901
  }
@@ -6373,6 +6946,9 @@ async function toolCommand(flags) {
6373
6946
  data.sessionLog = sessionJsonlFile(cwd);
6374
6947
  if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
6375
6948
 
6949
+ let evidenceData = data;
6950
+ let evidenceResponseFile = response.responseFile;
6951
+
6376
6952
  if (!jsonOutput) {
6377
6953
  printSessionResolutionWarning(sessionContext);
6378
6954
  printResolvedBaseUrl(baseUrl);
@@ -6390,7 +6966,7 @@ async function toolCommand(flags) {
6390
6966
  console.log('Component:', `${data.componentSource}${sig}`);
6391
6967
  }
6392
6968
  console.log('Session log:', data.sessionLog);
6393
- console.log('Tool response file:', data.responseFile);
6969
+ printToolResponseFileLine('Tool response file', data.responseFile);
6394
6970
  }
6395
6971
 
6396
6972
  // Non-zero exit so scripted/agent callers that check status notice the refusal too.
@@ -6410,9 +6986,11 @@ async function toolCommand(flags) {
6410
6986
 
6411
6987
  finalStatus.data.responseFile = finalStatus.responseFile;
6412
6988
  finalStatus.data.sessionLog = sessionJsonlFile(cwd);
6989
+ evidenceData = finalStatus.data;
6990
+ evidenceResponseFile = finalStatus.responseFile;
6413
6991
  if (!jsonOutput) {
6414
6992
  console.log('Final status:', finalStatus.data.status);
6415
- console.log('Tool response file:', finalStatus.responseFile);
6993
+ printToolResponseFileLine('Tool response file', finalStatus.responseFile);
6416
6994
  }
6417
6995
  if (finalStatus.data.status !== 'completed') {
6418
6996
  process.exitCode = 1;
@@ -6422,20 +7000,20 @@ async function toolCommand(flags) {
6422
7000
 
6423
7001
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
6424
7002
  type: 'tool_call',
6425
- success: !toolFailed && (!asyncMode || !waitForAsync || process.exitCode !== 1),
7003
+ success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
7004
+ pending: asyncMode && !waitForAsync,
6426
7005
  claim: 'Tool call ' + String(toolName),
6427
- world: buildCommandWorld(data, { accountId: data.accountId || accountId, dataMode: data.dataMode || dataMode, branchName: data.branchName || branchName, workspace: resolveWorkspace(cwd, flags), host: normalizeBaseUrl(baseUrl), sourceLayer: data.componentSource }),
7006
+ world: buildCommandWorld(evidenceData, { accountId: evidenceData.accountId || accountId, dataMode: evidenceData.dataMode || dataMode, branchName: evidenceData.branchName || branchName, workspace: resolveWorkspace(cwd, flags), host: normalizeBaseUrl(baseUrl), sourceLayer: evidenceData.componentSource }),
6428
7007
  revision: Object.assign(collectVerificationSource(cwd, flags), {
6429
- compileSignatures: data.componentSignature ? [data.componentSignature] : []
7008
+ compileSignatures: evidenceData.componentSignature ? [evidenceData.componentSignature] : []
6430
7009
  }),
6431
7010
  evidenceCategories: ['tool_call'],
6432
7011
  dependencies: {
6433
7012
  mocks: [],
6434
- liveHttpCalls: data.liveHttpCalls || []
7013
+ liveHttpCalls: evidenceData.liveHttpCalls || []
6435
7014
  },
6436
- rawRefs: { toolResponsePath: response.responseFile, callId },
6437
- tool: { callId, name: String(toolName), async: asyncMode, status: data.status, toolSuccess: data.toolSuccess },
6438
- result: data
7015
+ rawRefs: { toolResponsePath: evidenceResponseFile, callId },
7016
+ tool: Object.assign(toolEvidenceSummary(evidenceData, evidenceResponseFile), { callId, name: String(toolName), async: asyncMode })
6439
7017
  }, { quiet: jsonOutput });
6440
7018
  if (jsonOutput) {
6441
7019
  console.log(JSON.stringify(data, null, 2));
@@ -6504,6 +7082,12 @@ async function verifyCommand(flags, subcommand) {
6504
7082
  if (flags.manifest || flags.file) {
6505
7083
  manifest = parseManifestFile(flags.manifest || flags.file);
6506
7084
  }
7085
+ if (dataMode === 'prod' && !manifestRequiredEvidence(manifest).length && !flagEnabled(flags['no-contract'])) {
7086
+ console.error('');
7087
+ console.error('Warning: starting a prod-data verification envelope with no required evidence contract.');
7088
+ console.error('Attach a manifest with requiredEvidence before destructive work, or pass --no-contract when this is intentionally evidence-only.');
7089
+ console.error('');
7090
+ }
6507
7091
  let statusResponse = null;
6508
7092
  try {
6509
7093
  statusResponse = await loggedPost(api, cwd, '/cli/components', {
@@ -6522,7 +7106,8 @@ async function verifyCommand(flags, subcommand) {
6522
7106
  source.componentBranch = verifiedWorld.componentBranch;
6523
7107
  source.sourceLayer = verifiedWorld.sourceLayer;
6524
7108
  if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
6525
- source.stagedOverlayHash = statusResponse.laneSummary ? sha256(stableStringify(statusResponse.laneSummary)) : null;
7109
+ source.laneContentHash = laneContentHashFrom(statusResponse);
7110
+ source.stagedOverlayHash = source.laneContentHash;
6526
7111
  }
6527
7112
 
6528
7113
  const response = await postVerificationCommand(api, cwd, session, accountId, 'start', {
@@ -6537,6 +7122,8 @@ async function verifyCommand(flags, subcommand) {
6537
7122
  writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
6538
7123
  console.log('Verification envelope started:', envelope.envelopeId);
6539
7124
  console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
7125
+ printAcceptanceWarnings(envelope);
7126
+ printEnvelopeWarnings(envelope);
6540
7127
  console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
6541
7128
  return envelope;
6542
7129
  }
@@ -6577,6 +7164,8 @@ async function verifyCommand(flags, subcommand) {
6577
7164
  writeLocalVerificationEnvelope(cwd, response.envelope);
6578
7165
  console.log('Manifest attached to envelope:', envelopeId);
6579
7166
  console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
7167
+ printAcceptanceWarnings(response.envelope);
7168
+ printEnvelopeWarnings(response.envelope);
6580
7169
  return response.envelope;
6581
7170
  }
6582
7171
 
@@ -12269,11 +12858,12 @@ function printComponentsHelp(subcommand) {
12269
12858
  return;
12270
12859
  }
12271
12860
  if (subcommand === 'lanes' || subcommand === 'lane') {
12272
- console.log('Usage: remits-cli components lanes [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--json]');
12861
+ console.log('Usage: remits-cli components lanes [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
12273
12862
  console.log('');
12274
12863
  console.log('Lists every indexed staging lane on the account, across users, branches, and workspaces.');
12275
12864
  console.log('This is read-only and uses the same lane registry that powers the admin Platforms view.');
12276
12865
  console.log('Use `components entries --lane-id <id>` to inspect the actual staged files in a lane.');
12866
+ console.log('Use --wait-change to wait until the lane registry changes instead of polling in a loop.');
12277
12867
  return;
12278
12868
  }
12279
12869
  if (subcommand === 'entries' || subcommand === 'entry' || subcommand === 'staged') {
@@ -12312,7 +12902,7 @@ function printComponentsHelp(subcommand) {
12312
12902
  console.log('so several agents can iterate at once. See: remits-cli workspace --help');
12313
12903
  console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
12314
12904
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
12315
- console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--json]');
12905
+ console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
12316
12906
  console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
12317
12907
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
12318
12908
  console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
@@ -12337,11 +12927,13 @@ function printComponentsHelp(subcommand) {
12337
12927
 
12338
12928
  function printTestHelp() {
12339
12929
  console.log('Usage: remits-cli test run --test <id|name> [--base-url URL] [--branch stagingScope] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none] [--json]');
12930
+ console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
12340
12931
  console.log('');
12341
12932
  console.log('Runs a Test component against the staged/variant world for this checkout.');
12342
12933
  console.log('Examples:');
12343
12934
  console.log(' remits-cli test run --test 11');
12344
12935
  console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
12936
+ console.log(' remits-cli test status --task-id 7a1b...');
12345
12937
  console.log('');
12346
12938
  console.log('Notes:');
12347
12939
  console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
@@ -12728,6 +13320,10 @@ async function main() {
12728
13320
  await testCommand(args);
12729
13321
  return;
12730
13322
  }
13323
+ if (command === 'test' && subcommand === 'status') {
13324
+ await testStatusCommand(args);
13325
+ return;
13326
+ }
12731
13327
 
12732
13328
  if (command === 'token') {
12733
13329
  await tokenCommand(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.126",
3
+ "version": "0.1.128",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -72,6 +72,11 @@ Think about remits-cli as two cooperating layers:
72
72
  - where a large tool response was written
73
73
  - which tool schemas were most recently cached here
74
74
 
75
+ Tool response files under `./.remits-cli/tool-responses/` are the full CLI results. The CLI may print a
76
+ size/hash hint for large files so you can inspect them selectively with `jq`, `rg`, or byte-range reads,
77
+ but it does not truncate the local JSON file. Do not confuse this with the in-platform OpenRouter client,
78
+ which can offload large tool results inside the model conversation and inject a read-back tool for the AI.
79
+
75
80
  When a user asks an indirect question, map it to the right layer first:
76
81
 
77
82
  - "Why did this ticket open in the wrong repo?" → start in global state.
@@ -85,7 +85,10 @@ deterministically:
85
85
  remits-cli tool --name "mcp_run_action" --input '{"accountId":49,"actionId":200,"executionMode":"async","actionRunId":"my-stable-run-id","actionInput":{"sourceDocumentId":"..."}}' --data-mode prod
86
86
  ```
87
87
 
88
- Every tool response is saved to `./.remits-cli/tool-responses/<callId>.json`.
88
+ Every tool response is saved in full to `./.remits-cli/tool-responses/<callId>.json`. Large responses may
89
+ print an early size/hash line and a selective-read hint before any preview text, but the saved CLI file is
90
+ not truncated or offloaded. That is separate from the platform's OpenRouter model loop, where the AI
91
+ client may offload large tool results before feeding context back to the model.
89
92
 
90
93
  ### Hierarchy-scoped tool reads
91
94
 
@@ -207,6 +210,7 @@ remits-cli components branch <name> --subscribe <accountId> [--parent-account <i
207
210
  remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
208
211
  remits-cli components branch <name> --retire [--force] # delete the branch's overlays
209
212
  remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>] [--json]
213
+ remits-cli test status --task-id <taskId> [--branch <stagingScope>] [--data-mode test|prod] [--json]
210
214
  remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
211
215
  remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
212
216
  remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
@@ -268,18 +272,43 @@ remits-cli verify report
268
272
  Existing commands also accept `--verify-envelope <id>` to attach to a specific envelope and
269
273
  `--no-verify-envelope` to suppress automatic attachment for one command.
270
274
 
271
- Use `remits-cli verify list` to review open envelopes for the account without SQL. If a newer envelope
272
- replaces an older one, use `verify supersede`; if a duplicate or abandoned attempt should no longer read
273
- as live work, use `verify abandon`. Both keep the evidence history.
275
+ Failed `remits-cli test run` output includes compact pivots per failed case: duration, trace id,
276
+ bounded `report(...)` diagnostics, live HTTP signals, and resolved component provenance when the
277
+ platform returns it. Re-read an existing run with `remits-cli test status --task-id <taskId>` before
278
+ rerunning a long suite. If the same suite/case flips pass/fail while the resolved component provenance
279
+ signature is unchanged, the CLI prints a nondeterministic signal and records `nondeterministic:true` on
280
+ the `test_run` packet. Treat that as an AI/live-data/flaky-fixture investigation cue, not as proof that
281
+ source changed.
282
+
283
+ Test-run requirements must name the suite/cases they mean:
284
+
285
+ ```json
286
+ {"id":"api_flow", "packetType":"test_run", "suite":"Statement API Flow", "allCases":true}
287
+ {"id":"one_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
288
+ ```
289
+
290
+ A suite-only Test requirement is treated as `allCases:true`. A category-only Test requirement matches only
291
+ a run carrying that exact evidence category, or the case whose name the category names; unrelated Test
292
+ runs cannot replace it. `verify start` and `verify manifest` warn when a requirement has a `packetType` but
293
+ no discriminator. The evaluator treats pending async tool packets as pending, not passing; a later
294
+ `tool status` packet is the proof. Tool packets store the response file path, byte size and hash rather
295
+ than copying the whole tool payload into the envelope.
296
+
297
+ Use `remits-cli verify list` to review open envelopes for the account without SQL. The list shows stale
298
+ counts, requirements-changed markers, and last packet time. `verify start` warns when another open envelope
299
+ with the same summary/account/world exists; use `verify use <id>` to continue that contract or
300
+ `verify supersede` when a newer envelope replaces it. If a duplicate or abandoned attempt should no longer
301
+ read as live work, use `verify abandon`. Both keep the evidence history. Starting a prod-data envelope
302
+ without required evidence prints a loud no-contract warning; add a manifest unless the envelope is
303
+ intentionally evidence-only.
274
304
 
275
305
  The manifest is the proof contract. It should name the world being exercised: repo account, host,
276
306
  git/component branch, workspace, data mode, source layer, user journeys, artifacts and hashes, and the
277
307
  evidence categories required before the final response may claim the work is done. If the manifest
278
308
  cannot be written because the workflow is ambiguous, ask before implementation. For machine-gated report
279
309
  requirements, prefer structured entries like `{"id":"actual_upload","packetType":"browser_step",
280
- "category":"browser_session.actual_upload"}`. Plain strings work when they match a packet type or an
281
- evidence category, but structured entries are clearer when several agents collect packets for one
282
- envelope.
310
+ "category":"browser_session.actual_upload"}`. Plain strings work for evidence categories, but do not use
311
+ plain `"test_run"` as acceptance: it cannot say which Test or case proved the behavior.
283
312
 
284
313
  Common packet meanings:
285
314
 
@@ -300,7 +329,13 @@ Final claims should come from `remits-cli verify report`. Treat `Verified` as th
300
329
  the report lists it under `Verified`. If the report says `partially_verified`, stale, or missing evidence,
301
330
  say that plainly instead of widening the claim. In particular, staged proof is not committed variant/trunk
302
331
  proof, a token is not browser proof, and a direct DOM or Alpine state mutation is not the same as a user
303
- click/upload/reload flow.
332
+ action.
333
+
334
+ Evidence from the wrong world is excluded before it can satisfy a requirement. When the manifest declares
335
+ fields such as `repoAccountId`, `gitBranch`, `componentBranch`, `workspace`, or `dataMode`, the report names
336
+ the mismatch (`expected X got Y`) under missing evidence. Later stage/sync/source facts can stale earlier
337
+ packets; explicit invalidations stale only packets collected before the invalidation, so a fresh rerun can
338
+ verify the same requirement again.
304
339
 
305
340
  For tests specifically:
306
341
  - If `--data-mode` is omitted, `remits-cli test run` uses `test` and sends `dataModeSource:"cliDefault"`.
@@ -57,9 +57,10 @@ source-layer facts the command observed. A staged test packet is proof of the st
57
57
  is proof of a source transition; a token packet is proof of the browser token's resolution tuple. Those
58
58
  are not interchangeable.
59
59
 
60
- Use `remits-cli verify report` before summarizing the work. It will naturally say when the evidence only
61
- covered staged source, when committed variant/trunk proof is missing, or when evidence became stale after
62
- the git head, overlay, or platform sync moved.
60
+ Use `remits-cli verify report` before summarizing the work. It will say when the evidence only covered
61
+ staged source, when committed variant/trunk proof is missing, when the manifest world does not match the
62
+ packet world, or when later packet facts made an earlier proof stale. Packets carry a lane content hash,
63
+ git head, workspace, data lane, and variant-world facts so a re-stage or branch move is visible.
63
64
 
64
65
  ### Staging cache key format
65
66
 
@@ -159,7 +160,10 @@ TestMode still uses the committed DB source. Staging remains a dev/verification
159
160
  - **Inspect another lane without impersonating it:** `remits-cli components lanes` lists every indexed
160
161
  lane on the account; `remits-cli components entries --lane-id <id>` reads the authoritative staged
161
162
  files for one lane. These are read-only review surfaces. They do not switch your workspace, clear
162
- anything, or change what your own test/token/tool runs resolve.
163
+ anything, or change what your own test/token/tool runs resolve. Use
164
+ `remits-cli components lanes --wait-change [--lane-id ID] [--timeout 600]` when you need to wait for a
165
+ sibling lane to move; it watches the lane registry's `updatedAtMs`/content hash instead of making you
166
+ poll in a loop.
163
167
  - **Clear only your own lane:** `remits-cli components clear` removes entries when you intentionally want
164
168
  to fall back to DB source. `--all` is scoped to the command's account/user/branch/workspace lane, not
165
169
  every lane another agent may be using.
@@ -208,10 +212,15 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
208
212
  account's lane context in their responses. A shared current lane is printed loudly; sibling lanes are
209
213
  listed when they matter.
210
214
  - `remits-cli components lanes` is the review view across users/branches/workspaces, and
211
- `remits-cli components entries --lane-id <id>` is the drill-down into actual staged files.
215
+ `remits-cli components entries --lane-id <id>` is the drill-down into actual staged files. When you are
216
+ coordinating with another agent, prefer `components lanes --wait-change --lane-id <id>` to repeated
217
+ status checks.
212
218
  - `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
213
219
  - In lane rows, `currentLane` (also `mine`) marks THIS command's lane; `ownedByCaller` marks every lane
214
220
  staged by your CLI user — your other clones' agents included.
221
+ - If your current lane is empty but the same workspace has staged entries on another branch, the CLI
222
+ prints a warning. That usually means the checkout switched branches after staging; re-stage on this
223
+ branch or switch back.
215
224
  - **A landing clears only the lander's lane.** Every stage records the commit it came from
216
225
  (`stageBaseSha`), and `components status` compares that with the last commit the platform synced for the
217
226
  branch (`branchContext.lastSyncedSha`) using your local git:
@@ -97,6 +97,21 @@ remits-cli verify report
97
97
  The report is the final-response source. It separates verified claims, missing evidence, stale packets,
98
98
  and the source/account/lane tuple, so do not replace it with a generic "verified" sentence.
99
99
 
100
+ For Test requirements, be specific enough for the evaluator to know what a pass means:
101
+
102
+ ```json
103
+ {"id":"api_flow", "packetType":"test_run", "suite":"Statement API Flow", "allCases":true}
104
+ {"id":"duplicate_fee_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
105
+ ```
106
+
107
+ A suite-only Test requirement is treated as `allCases:true`. A category-only Test requirement matches only
108
+ a run carrying that exact evidence category, or the case whose name the category names. A requirement that
109
+ says only `{"packetType":"test_run"}` is intentionally only a warning-worthy sketch: it will not turn a
110
+ random Test packet into acceptance. Full-suite runs emit suite and passed-case categories automatically,
111
+ so case-level requirements can be satisfied by a real full run. The evaluator excludes packets collected
112
+ in the wrong account/data lane/git branch/component branch/workspace before satisfying requirements, and
113
+ pending async packets do not count until a result packet arrives.
114
+
100
115
  ## Development Workflow
101
116
 
102
117
  ### The Golden Rule: Writing Code Is Not Finishing the Job
@@ -327,9 +342,13 @@ This applies to:
327
342
  ```bash
328
343
  remits-cli test run --test <TEST_ID_OR_NAME>
329
344
  remits-cli test run --test "Invoice Tests" --names "specific test case"
345
+ remits-cli test status --task-id <TASK_ID>
330
346
  ```
331
347
 
332
- Tests run on the platform against your staged snapshot. They stream results in real-time. If they fail, fix the code, re-stage, and re-run.
348
+ Tests run on the platform against your staged snapshot. They stream results in real-time. If they fail,
349
+ read the printed pivots first: failed cases include timing, trace ids, bounded `report(...)`
350
+ diagnostics, live HTTP signals, and resolved component provenance when available. Fix the code,
351
+ re-stage, and re-run only after those pivots explain the failure.
333
352
 
334
353
  Important test-runner constraints:
335
354
  - `remits-cli test run` now defaults to `test` dataMode unless you explicitly pass `--data-mode prod`.