@remits/remits-cli 0.1.127 → 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
- - L1296 Account Repos, Guide Sync, And Platform Repo
10
- - L1954 Component Discovery And HTTP Logging
11
- - L2444 Skill Delivery And TOC Resolution
12
- - L2754 Auth And Component Staging
13
- - L3352 Component Summaries, Status, And Sync Gates
14
- - L5107 Branches, Promotion, Commit, And Test Runs
15
- - L6231 Tokens, Tools, Verification, And Config
16
- - L7141 Service Dashboard And WebSocket Listener
17
- - L9236 Agent And Ticket Workflows
18
- - L12294 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
  /*
@@ -821,6 +821,22 @@ function fileEvidence(pathname) {
821
821
  };
822
822
  }
823
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
+
824
840
  function boundedToolExcerpt(data = {}) {
825
841
  const result = data.result && typeof data.result === 'object' ? data.result : {};
826
842
  const candidates = [
@@ -982,6 +998,35 @@ function readLocalVerificationEnvelope(cwd, envelopeId) {
982
998
  }
983
999
  }
984
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
+
985
1030
  function writeLocalVerificationPacket(cwd, envelopeId, packet) {
986
1031
  if (!envelopeId || !packet) return null;
987
1032
  const paths = verificationPaths(cwd, envelopeId);
@@ -1103,11 +1148,13 @@ function printVerificationSummaryLine(env) {
1103
1148
  const failures = env.currentFailureCount ? ' currentFailures=' + env.currentFailureCount : (env.failedCount ? ' failed=' + env.failedCount : '');
1104
1149
  const required = ' required=' + (env.satisfiedCount || 0) + '/' + (env.requiredCount || 0);
1105
1150
  const noManifest = env.noRequiredEvidence ? ' no-manifest' : '';
1151
+ const stale = env.staleCount ? ' stale=' + env.staleCount : '';
1152
+ const changed = env.requirementsChangedAfterEvidence ? ' requirements-changed' : '';
1106
1153
  const lane = [env.branchName || 'unknown-branch', env.workspace ? 'ws:' + env.workspace : 'shared', env.dataMode || null, env.sourceLayer || null]
1107
1154
  .filter(Boolean).join(' | ');
1108
- 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);
1109
1156
  if (env.summary) console.log(' ' + env.summary);
1110
- 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));
1111
1158
  }
1112
1159
 
1113
1160
  function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
@@ -1124,6 +1171,12 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
1124
1171
  if (evaluation.failedCount) {
1125
1172
  console.log('Failed packets:', evaluation.failedCount, 'current failures:', evaluation.currentFailureCount || 0);
1126
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
+ }
1127
1180
  const packets = Array.isArray(envelope.packets) ? envelope.packets : [];
1128
1181
  console.log('Packets:', packets.length);
1129
1182
  packets.slice(-20).forEach((packet) => {
@@ -1147,6 +1200,56 @@ function printAcceptanceWarnings(envelope) {
1147
1200
  });
1148
1201
  }
1149
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
+
1150
1253
  function buildCommandWorld(response, fallback = {}) {
1151
1254
  const source = (response && response.staging) || {};
1152
1255
  const resolution = (response && response.resolution) || {};
@@ -2395,21 +2498,23 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
2395
2498
  error = err;
2396
2499
  throw err;
2397
2500
  } finally {
2398
- const status = error ? 'error' : 'success';
2399
- const responseForLog = responseFile
2400
- ? { responseFile, note: 'Tool response stored externally' }
2401
- : sanitizeForLog(responseData);
2402
- appendSessionLog(cwd, {
2403
- ts: new Date().toISOString(),
2404
- requestId,
2405
- endpoint,
2406
- method: 'POST',
2407
- status,
2408
- durationMs: Date.now() - started,
2409
- request: sanitizeForLog(payload),
2410
- response: responseForLog,
2411
- error: error ? describeError(error) : null
2412
- });
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
+ }
2413
2518
  }
2414
2519
  }
2415
2520
 
@@ -3598,6 +3703,7 @@ function describeStageMode(mode) {
3598
3703
  function printStatusSummary(response, flags) {
3599
3704
  printBranchContext(response);
3600
3705
  printStagingLane(response.branchName, response.workspace, null);
3706
+ printStagingLaneOwnerNotice(response);
3601
3707
  printLaneSummary(response);
3602
3708
  printOrphanedWorkspaceWarning(response);
3603
3709
  printStagingFreshness(response.freshness);
@@ -3625,6 +3731,28 @@ function printStatusSummary(response, flags) {
3625
3731
  printComponentCommandResponse('Components staging', response, flags);
3626
3732
  }
3627
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
+
3628
3756
  function printLanesSummary(response, flags) {
3629
3757
  printBranchContext(response);
3630
3758
  const lanes = Array.isArray(response.lanes) ? response.lanes : (Array.isArray(response.accountLanes) ? response.accountLanes : []);
@@ -3999,19 +4127,25 @@ async function lanesComponentsCommand(flags) {
3999
4127
  const dataMode = resolveDataMode(flags, session);
4000
4128
  const api = buildAxios(baseUrl, session.token);
4001
4129
 
4002
- const response = await loggedPost(api, cwd, '/cli/components', {
4130
+ const requestPayload = {
4003
4131
  token: session.token,
4004
4132
  accountId,
4005
4133
  branchName,
4006
4134
  workspace,
4007
4135
  dataMode,
4008
4136
  mode: 'lanes'
4009
- }).then((r) => r.data);
4137
+ };
4138
+
4139
+ let response = await loggedPost(api, cwd, '/cli/components', requestPayload).then((r) => r.data);
4010
4140
 
4011
4141
  if (!response.success) {
4012
4142
  throw new Error(response.message || 'Staging lanes failed');
4013
4143
  }
4014
4144
 
4145
+ if (flagEnabled(flags['wait-change']) || flagEnabled(flags.waitChange)) {
4146
+ response = await waitForLaneChange(api, cwd, requestPayload, response, flags);
4147
+ }
4148
+
4015
4149
  if (flagEnabled(flags.json)) {
4016
4150
  console.log(JSON.stringify(response, null, 2));
4017
4151
  return response;
@@ -4024,6 +4158,55 @@ async function lanesComponentsCommand(flags) {
4024
4158
  return response;
4025
4159
  }
4026
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
+
4027
4210
  async function entriesComponentsCommand(flags) {
4028
4211
  const cwd = process.cwd();
4029
4212
  ensureLocalState(cwd);
@@ -4219,6 +4402,30 @@ async function syncComponentsCommand(rawFlags) {
4219
4402
  }
4220
4403
  throw new Error(mismatch + ' Nothing was synced.');
4221
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
+ }
4222
4429
  if (branchContextIsTrunk(branchContext)) {
4223
4430
  // Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
4224
4431
  // an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
@@ -4781,6 +4988,15 @@ function featureBranchCommitRefusal(branchContext, branchName, flags) {
4781
4988
  'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
4782
4989
  }
4783
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
+
4784
5000
  // What a verification envelope records as the world it verified. Taken from the platform's answer
4785
5001
  // (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
4786
5002
  // variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
@@ -5916,6 +6132,94 @@ function testCompileSignatures(status = {}) {
5916
6132
  .filter(Boolean);
5917
6133
  }
5918
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
+
5919
6223
  function printTestRunPivots(status = {}) {
5920
6224
  const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
5921
6225
  if (!tests.length) return;
@@ -6102,6 +6406,7 @@ async function testCommand(flags) {
6102
6406
  printSessionResolutionWarning(sessionContext);
6103
6407
  printResolvedBaseUrl(baseUrl);
6104
6408
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
6409
+ printStagingLaneOwnerNotice(start.staging || {});
6105
6410
  if (start.staging && Array.isArray(start.staging.accountLanes)) {
6106
6411
  printOrphanedWorkspaceWarning(start.staging);
6107
6412
  printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
@@ -6130,6 +6435,7 @@ async function testCommand(flags) {
6130
6435
  console.log(JSON.stringify(status, null, 2));
6131
6436
  } else {
6132
6437
  console.log('Final status:', JSON.stringify(status, null, 2));
6438
+ printStagingLaneOwnerNotice(status.staging || {});
6133
6439
  printTestRunPivots(status);
6134
6440
  }
6135
6441
 
@@ -6155,15 +6461,38 @@ async function testCommand(flags) {
6155
6461
  process.exitCode = 1;
6156
6462
  }
6157
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
+
6158
6485
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
6159
6486
  type: 'test_run',
6160
6487
  success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
6161
6488
  claim: 'Test run ' + String(testRef),
6162
6489
  world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
6163
- revision: Object.assign(collectVerificationSource(cwd, flags), {
6490
+ revision: Object.assign(sourceRevision, {
6164
6491
  compileSignatures: testCompileSignatures(status),
6165
- componentProvenance: testComponentProvenance(status)
6492
+ componentProvenance
6166
6493
  }),
6494
+ nondeterministic: nondeterminism ? true : undefined,
6495
+ nondeterminism: nondeterminism || undefined,
6167
6496
  test: {
6168
6497
  taskId: start.taskId,
6169
6498
  testId: status.test && status.test.id,
@@ -6176,7 +6505,9 @@ async function testCommand(flags) {
6176
6505
  unmatchedTestNames: unmatched
6177
6506
  },
6178
6507
  evidenceCategories: testEvidenceCategories(status, names),
6179
- limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
6508
+ limitations: []
6509
+ .concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
6510
+ .concat(nondeterminism ? [nondeterminism.message] : []),
6180
6511
  rawRefs: { testStatusKey: start.taskId },
6181
6512
  status
6182
6513
  }, { quiet: jsonOutput });
@@ -6564,8 +6895,7 @@ async function toolCommand(flags) {
6564
6895
  console.log('Data mode:', data.dataMode || dataMode);
6565
6896
  if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
6566
6897
  console.log('Session log:', data.sessionLog);
6567
- const responseEvidence = fileEvidence(data.responseFile);
6568
- console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
6898
+ printToolResponseFileLine('Tool response file', data.responseFile);
6569
6899
  if (polledFailed) {
6570
6900
  console.log('Tool error:', data.toolMessage);
6571
6901
  }
@@ -6636,8 +6966,7 @@ async function toolCommand(flags) {
6636
6966
  console.log('Component:', `${data.componentSource}${sig}`);
6637
6967
  }
6638
6968
  console.log('Session log:', data.sessionLog);
6639
- const responseEvidence = fileEvidence(data.responseFile);
6640
- console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
6969
+ printToolResponseFileLine('Tool response file', data.responseFile);
6641
6970
  }
6642
6971
 
6643
6972
  // Non-zero exit so scripted/agent callers that check status notice the refusal too.
@@ -6661,8 +6990,7 @@ async function toolCommand(flags) {
6661
6990
  evidenceResponseFile = finalStatus.responseFile;
6662
6991
  if (!jsonOutput) {
6663
6992
  console.log('Final status:', finalStatus.data.status);
6664
- const finalEvidence = fileEvidence(finalStatus.responseFile);
6665
- console.log('Tool response file:', finalStatus.responseFile + (finalEvidence.bytes ? ' (' + finalEvidence.bytes + ' bytes, sha256 ' + finalEvidence.sha256.slice(0, 12) + ')' : ''));
6993
+ printToolResponseFileLine('Tool response file', finalStatus.responseFile);
6666
6994
  }
6667
6995
  if (finalStatus.data.status !== 'completed') {
6668
6996
  process.exitCode = 1;
@@ -6673,6 +7001,7 @@ async function toolCommand(flags) {
6673
7001
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
6674
7002
  type: 'tool_call',
6675
7003
  success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
7004
+ pending: asyncMode && !waitForAsync,
6676
7005
  claim: 'Tool call ' + String(toolName),
6677
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 }),
6678
7007
  revision: Object.assign(collectVerificationSource(cwd, flags), {
@@ -6753,6 +7082,12 @@ async function verifyCommand(flags, subcommand) {
6753
7082
  if (flags.manifest || flags.file) {
6754
7083
  manifest = parseManifestFile(flags.manifest || flags.file);
6755
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
+ }
6756
7091
  let statusResponse = null;
6757
7092
  try {
6758
7093
  statusResponse = await loggedPost(api, cwd, '/cli/components', {
@@ -6788,6 +7123,7 @@ async function verifyCommand(flags, subcommand) {
6788
7123
  console.log('Verification envelope started:', envelope.envelopeId);
6789
7124
  console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
6790
7125
  printAcceptanceWarnings(envelope);
7126
+ printEnvelopeWarnings(envelope);
6791
7127
  console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
6792
7128
  return envelope;
6793
7129
  }
@@ -6829,6 +7165,7 @@ async function verifyCommand(flags, subcommand) {
6829
7165
  console.log('Manifest attached to envelope:', envelopeId);
6830
7166
  console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
6831
7167
  printAcceptanceWarnings(response.envelope);
7168
+ printEnvelopeWarnings(response.envelope);
6832
7169
  return response.envelope;
6833
7170
  }
6834
7171
 
@@ -12521,11 +12858,12 @@ function printComponentsHelp(subcommand) {
12521
12858
  return;
12522
12859
  }
12523
12860
  if (subcommand === 'lanes' || subcommand === 'lane') {
12524
- 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]');
12525
12862
  console.log('');
12526
12863
  console.log('Lists every indexed staging lane on the account, across users, branches, and workspaces.');
12527
12864
  console.log('This is read-only and uses the same lane registry that powers the admin Platforms view.');
12528
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.');
12529
12867
  return;
12530
12868
  }
12531
12869
  if (subcommand === 'entries' || subcommand === 'entry' || subcommand === 'staged') {
@@ -12564,7 +12902,7 @@ function printComponentsHelp(subcommand) {
12564
12902
  console.log('so several agents can iterate at once. See: remits-cli workspace --help');
12565
12903
  console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
12566
12904
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
12567
- 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]');
12568
12906
  console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
12569
12907
  console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
12570
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]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.127",
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
 
@@ -272,7 +275,10 @@ Existing commands also accept `--verify-envelope <id>` to attach to a specific e
272
275
  Failed `remits-cli test run` output includes compact pivots per failed case: duration, trace id,
273
276
  bounded `report(...)` diagnostics, live HTTP signals, and resolved component provenance when the
274
277
  platform returns it. Re-read an existing run with `remits-cli test status --task-id <taskId>` before
275
- rerunning a long suite.
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.
276
282
 
277
283
  Test-run requirements must name the suite/cases they mean:
278
284
 
@@ -281,23 +287,28 @@ Test-run requirements must name the suite/cases they mean:
281
287
  {"id":"one_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
282
288
  ```
283
289
 
284
- `verify start` and `verify manifest` warn when a requirement has a `packetType` but no discriminator.
285
- The evaluator treats pending async tool packets as pending, not passing; a later `tool status` packet is
286
- the proof. Tool packets store the response file path, byte size and hash rather than copying the whole
287
- tool payload into the envelope.
288
-
289
- Use `remits-cli verify list` to review open envelopes for the account without SQL. If a newer envelope
290
- replaces an older one, use `verify supersede`; if a duplicate or abandoned attempt should no longer read
291
- as live work, use `verify abandon`. Both keep the evidence history.
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.
292
304
 
293
305
  The manifest is the proof contract. It should name the world being exercised: repo account, host,
294
306
  git/component branch, workspace, data mode, source layer, user journeys, artifacts and hashes, and the
295
307
  evidence categories required before the final response may claim the work is done. If the manifest
296
308
  cannot be written because the workflow is ambiguous, ask before implementation. For machine-gated report
297
309
  requirements, prefer structured entries like `{"id":"actual_upload","packetType":"browser_step",
298
- "category":"browser_session.actual_upload"}`. Plain strings work when they match a packet type or an
299
- evidence category, but structured entries are clearer when several agents collect packets for one
300
- 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.
301
312
 
302
313
  Common packet meanings:
303
314
 
@@ -318,7 +329,13 @@ Final claims should come from `remits-cli verify report`. Treat `Verified` as th
318
329
  the report lists it under `Verified`. If the report says `partially_verified`, stale, or missing evidence,
319
330
  say that plainly instead of widening the claim. In particular, staged proof is not committed variant/trunk
320
331
  proof, a token is not browser proof, and a direct DOM or Alpine state mutation is not the same as a user
321
- 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.
322
339
 
323
340
  For tests specifically:
324
341
  - If `--data-mode` is omitted, `remits-cli test run` uses `test` and sends `dataModeSource:"cliDefault"`.
@@ -160,7 +160,10 @@ TestMode still uses the committed DB source. Staging remains a dev/verification
160
160
  - **Inspect another lane without impersonating it:** `remits-cli components lanes` lists every indexed
161
161
  lane on the account; `remits-cli components entries --lane-id <id>` reads the authoritative staged
162
162
  files for one lane. These are read-only review surfaces. They do not switch your workspace, clear
163
- 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.
164
167
  - **Clear only your own lane:** `remits-cli components clear` removes entries when you intentionally want
165
168
  to fall back to DB source. `--all` is scoped to the command's account/user/branch/workspace lane, not
166
169
  every lane another agent may be using.
@@ -209,7 +212,9 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
209
212
  account's lane context in their responses. A shared current lane is printed loudly; sibling lanes are
210
213
  listed when they matter.
211
214
  - `remits-cli components lanes` is the review view across users/branches/workspaces, and
212
- `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.
213
218
  - `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
214
219
  - In lane rows, `currentLane` (also `mine`) marks THIS command's lane; `ownedByCaller` marks every lane
215
220
  staged by your CLI user — your other clones' agents included.
@@ -104,9 +104,13 @@ For Test requirements, be specific enough for the evaluator to know what a pass
104
104
  {"id":"duplicate_fee_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
105
105
  ```
106
106
 
107
- A requirement that says only `{"packetType":"test_run"}` is intentionally only a warning-worthy sketch:
108
- it will not turn a random Test packet into acceptance. Full-suite runs emit suite and passed-case
109
- categories automatically, so case-level requirements can be satisfied by a real full run.
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.
110
114
 
111
115
  ## Development Workflow
112
116