@remits/remits-cli 0.1.126 → 0.1.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -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
+ - 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
19
19
  */
20
20
 
21
21
  /*
@@ -811,6 +811,42 @@ 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
+ function boundedToolExcerpt(data = {}) {
825
+ const result = data.result && typeof data.result === 'object' ? data.result : {};
826
+ const candidates = [
827
+ data.message,
828
+ data.toolMessage,
829
+ data.error,
830
+ result.message,
831
+ result.error,
832
+ result.reason
833
+ ].filter(Boolean).map((value) => String(value));
834
+ const excerpt = candidates.join(' | ').trim();
835
+ return excerpt ? excerpt.slice(0, 1200) : null;
836
+ }
837
+
838
+ function toolEvidenceSummary(data = {}, responseFile) {
839
+ return Object.assign({
840
+ callId: data.callId,
841
+ name: data.name,
842
+ async: data.async,
843
+ status: data.status,
844
+ toolSuccess: data.toolSuccess,
845
+ threadGroupingId: data.threadGroupingId,
846
+ message: boundedToolExcerpt(data)
847
+ }, fileEvidence(responseFile));
848
+ }
849
+
814
850
  function readToolResponseFile(cwd, callId) {
815
851
  if (!callId) return null;
816
852
  const paths = ensureLocalState(cwd);
@@ -1100,12 +1136,28 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
1100
1136
  }
1101
1137
  }
1102
1138
 
1139
+ function printAcceptanceWarnings(envelope) {
1140
+ const warnings = envelope && envelope.acceptance && Array.isArray(envelope.acceptance.warnings)
1141
+ ? envelope.acceptance.warnings
1142
+ : [];
1143
+ if (!warnings.length) return;
1144
+ console.log('Manifest warnings:');
1145
+ warnings.forEach((warning) => {
1146
+ console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
1147
+ });
1148
+ }
1149
+
1103
1150
  function buildCommandWorld(response, fallback = {}) {
1104
1151
  const source = (response && response.staging) || {};
1105
1152
  const resolution = (response && response.resolution) || {};
1153
+ const result = (response && response.result) || {};
1154
+ const laneHash = laneContentHashFrom(response) || fallback.laneContentHash || fallback.stagedOverlayHash;
1155
+ const executionAccountId = response && (response.executionAccountId || result.accountId || source.executionAccountId || response.accountId) ||
1156
+ resolution.executionAccountId ||
1157
+ fallback.executionAccountId;
1106
1158
  return {
1107
1159
  accountId: response && (response.accountId || response.ownerAccountId) || fallback.accountId,
1108
- executionAccountId: response && (response.executionAccountId || response.accountId) || resolution.executionAccountId || fallback.executionAccountId,
1160
+ executionAccountId,
1109
1161
  addressedAccountId: resolution.addressedAccountId || fallback.addressedAccountId,
1110
1162
  host: fallback.host,
1111
1163
  dataMode: response && response.dataMode || fallback.dataMode,
@@ -1113,16 +1165,26 @@ function buildCommandWorld(response, fallback = {}) {
1113
1165
  branchName: response && response.branchName || fallback.branchName,
1114
1166
  workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
1115
1167
  stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
1168
+ laneContentHash: laneHash,
1169
+ stagedOverlayHash: laneHash,
1116
1170
  // `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
1117
1171
  // subscription); a bare `variantBranch` is null whenever the subscription decided.
1118
1172
  componentBranch: resolution.componentBranch ||
1119
1173
  response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
1120
1174
  fallback.componentBranch,
1175
+ variantBranchSource: response && (response.variantBranchSource || (response.variantWorld && response.variantWorld.variantBranchSource)) || fallback.variantBranchSource,
1121
1176
  variantId: resolution.variantId || fallback.variantId,
1122
1177
  sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
1123
1178
  };
1124
1179
  }
1125
1180
 
1181
+ function laneContentHashFrom(response) {
1182
+ if (!response) return null;
1183
+ const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
1184
+ (response.componentStatus && response.componentStatus.laneSummary);
1185
+ return laneSummary && laneSummary.contentHash ? laneSummary.contentHash : null;
1186
+ }
1187
+
1126
1188
  function normalizeName(name) {
1127
1189
  return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
1128
1190
  }
@@ -3057,6 +3119,7 @@ async function pushComponentsCommand(flags) {
3057
3119
  rawRefs: { command: 'components stage' },
3058
3120
  stage: (err.responseBody || {}).stage,
3059
3121
  compileValidation: (err.responseBody || {}).compileValidation,
3122
+ refusalDetails: stageRefusalDetails(err.responseBody || {}),
3060
3123
  changedFromWorkingTree: changedFromWorkingTree || [],
3061
3124
  unrepresentableChanges: unstageable
3062
3125
  }, { quiet: true });
@@ -3091,6 +3154,7 @@ async function pushComponentsCommand(flags) {
3091
3154
  rawRefs: { command: 'components stage' },
3092
3155
  stage: response.stage,
3093
3156
  laneSummary: response.laneSummary,
3157
+ compileValidation: response.compileValidation || null,
3094
3158
  changedFromWorkingTree: response.changedFromWorkingTree,
3095
3159
  unrepresentableChanges: response.unrepresentableChanges
3096
3160
  }, { quiet: flagEnabled(flags.json) });
@@ -3276,6 +3340,14 @@ async function stageOrRefuse(api, cwd, payload) {
3276
3340
  }
3277
3341
  }
3278
3342
 
3343
+ function stageRefusalDetails(body = {}) {
3344
+ const details = {};
3345
+ ['duplicates', 'compileValidation', 'policy', 'editLease', 'skipped', 'reconcile', 'laneHoldsRejectedSource'].forEach((key) => {
3346
+ if (body[key] !== undefined) details[key] = sanitizeForLog(body[key]);
3347
+ });
3348
+ return Object.keys(details).length ? details : undefined;
3349
+ }
3350
+
3279
3351
  /*
3280
3352
  ## Component Summaries, Status, And Sync Gates
3281
3353
  */
@@ -3331,6 +3403,10 @@ function printCompileValidation(validation, options) {
3331
3403
  // A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
3332
3404
  // component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
3333
3405
  const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
3406
+ const warnings = Array.isArray(validation.warnings) ? validation.warnings : [];
3407
+ warnings.forEach((warning) => {
3408
+ emit(' Warning: ' + (warning.message || JSON.stringify(warning)));
3409
+ });
3334
3410
  if (unchecked.length) {
3335
3411
  emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
3336
3412
  (entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
@@ -3495,6 +3571,9 @@ function printStageSummary(response, flags) {
3495
3571
  // The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
3496
3572
  // different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
3497
3573
  printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
3574
+ if (typeof printOrphanedWorkspaceWarning === 'function') {
3575
+ printOrphanedWorkspaceWarning(response);
3576
+ }
3498
3577
  if (typeof printAccountLanes === 'function') {
3499
3578
  printAccountLanes(response);
3500
3579
  }
@@ -3520,6 +3599,7 @@ function printStatusSummary(response, flags) {
3520
3599
  printBranchContext(response);
3521
3600
  printStagingLane(response.branchName, response.workspace, null);
3522
3601
  printLaneSummary(response);
3602
+ printOrphanedWorkspaceWarning(response);
3523
3603
  printStagingFreshness(response.freshness);
3524
3604
  printRepositoryCheck(response.repositoryCheck);
3525
3605
  printComponentTypeCounts(response.entries || []);
@@ -3678,6 +3758,36 @@ function printAccountLanes(response) {
3678
3758
  console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
3679
3759
  }
3680
3760
 
3761
+ function printOrphanedWorkspaceWarning(response) {
3762
+ const currentCount = Number((response && response.stagedCount) ||
3763
+ (response && response.laneSummary && response.laneSummary.stagedCount) || 0);
3764
+ if (currentCount > 0) return;
3765
+ const lanes = Array.isArray(response && response.accountLanes) ? response.accountLanes : [];
3766
+ const currentBranch = response && response.branchName;
3767
+ const currentWorkspace = response && response.workspace;
3768
+ const orphan = lanes.find((lane) => lane && lane.sameWorkspaceOtherBranch === true);
3769
+ if (!orphan) return;
3770
+ const age = orphan.updatedAtMs ? formatAge(Date.now() - Number(orphan.updatedAtMs)) + ' ago' : 'recently';
3771
+ console.log('');
3772
+ console.log('WARNING: Your workspace "' + (currentWorkspace || orphan.workspace || 'shared') + '" has ' +
3773
+ (orphan.stagedCountAsOf || orphan.stagedCount || 0) + ' staged entr' +
3774
+ ((orphan.stagedCountAsOf || orphan.stagedCount || 0) === 1 ? 'y' : 'ies') +
3775
+ ' on branch "' + orphan.branchName + '" (staged ' + age + '); this checkout is now on "' +
3776
+ (currentBranch || 'unknown') + '". Re-stage here, or switch back.');
3777
+ }
3778
+
3779
+ function formatAge(ms) {
3780
+ const value = Number(ms);
3781
+ if (!Number.isFinite(value) || value < 0) return 'unknown age';
3782
+ const seconds = Math.round(value / 1000);
3783
+ if (seconds < 90) return seconds + 's';
3784
+ const minutes = Math.round(seconds / 60);
3785
+ if (minutes < 90) return minutes + 'm';
3786
+ const hours = Math.round(minutes / 60);
3787
+ if (hours < 48) return hours + 'h';
3788
+ return Math.round(hours / 24) + 'd';
3789
+ }
3790
+
3681
3791
  // The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
3682
3792
  // resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
3683
3793
  // non-trunk — never guessed to be a variant branch. Pure.
@@ -3846,6 +3956,7 @@ async function statusComponentsCommand(flags) {
3846
3956
  // supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
3847
3957
  response.freshness = stagingFreshness({
3848
3958
  head: safeGitValue(cwd, 'git rev-parse HEAD'),
3959
+ originHead: safeGitValue(cwd, 'git rev-parse ' + shellQuote('origin/' + branchName)),
3849
3960
  lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
3850
3961
  entries: response.entries,
3851
3962
  laneSummary: response.laneSummary,
@@ -4759,7 +4870,7 @@ function gitIsAncestor(cwd, ancestor, descendant) {
4759
4870
  * `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
4760
4871
  * injected as `isAncestor(a, b)` — so it can be tested without a repository.
4761
4872
  */
4762
- function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
4873
+ function stagingFreshness({ head, originHead, lastSyncedSha, entries, laneSummary, isAncestor }) {
4763
4874
  const rows = Array.isArray(entries) ? entries : [];
4764
4875
  const staged = rows.filter((entry) => entry && entry.stageBaseSha);
4765
4876
  const memo = new Map();
@@ -4785,7 +4896,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
4785
4896
 
4786
4897
  return {
4787
4898
  head: head || null,
4899
+ originHead: originHead || null,
4788
4900
  lastSyncedSha: lastSyncedSha || null,
4901
+ freshnessUnknownReason: !originHead ? 'branch has no upstream' : (!lastSyncedSha ? 'platform has no last synced SHA for this branch' : null),
4789
4902
  landedSinceHead,
4790
4903
  entriesBehindLastSync: behindLastSync.length,
4791
4904
  entriesFromOtherCommits: fromOtherCommits.length,
@@ -4799,6 +4912,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
4799
4912
  function printStagingFreshness(freshness) {
4800
4913
  if (!freshness) return;
4801
4914
  const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
4915
+ if (freshness.freshnessUnknownReason) {
4916
+ console.log('Freshness unknown:', freshness.freshnessUnknownReason + '.');
4917
+ }
4802
4918
  if (freshness.landedSinceHead === true) {
4803
4919
  console.log('');
4804
4920
  console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
@@ -5759,6 +5875,84 @@ async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dat
5759
5875
  }
5760
5876
  }
5761
5877
 
5878
+ function testEvidenceCategories(status = {}, selectedNames = []) {
5879
+ const categories = new Set(['test_run']);
5880
+ const test = status.test || {};
5881
+ if (test.name) categories.add('test_run.suite:' + String(test.name));
5882
+ if (test.id !== undefined && test.id !== null) categories.add('test_run.suite_id:' + String(test.id));
5883
+ (Array.isArray(selectedNames) ? selectedNames : []).forEach((name) => {
5884
+ if (name) categories.add('test_run.' + String(name));
5885
+ });
5886
+ const results = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
5887
+ results.forEach((result) => {
5888
+ if (result && result.passed === true && result.name) {
5889
+ categories.add('test_run.case:' + String(result.name));
5890
+ categories.add('test_run.' + String(result.name));
5891
+ }
5892
+ });
5893
+ return Array.from(categories);
5894
+ }
5895
+
5896
+ function testComponentProvenance(status = {}) {
5897
+ const direct = status.resolvedComponentProvenance || (status.result && status.result.resolvedComponentProvenance) ||
5898
+ (status.staging && status.staging.resolvedComponentProvenance);
5899
+ if (Array.isArray(direct)) return direct;
5900
+ const staging = status.staging || {};
5901
+ if (staging.testComponentSignature) {
5902
+ return [{
5903
+ type: 'test',
5904
+ id: status.test && status.test.id,
5905
+ name: status.test && status.test.name,
5906
+ signature: staging.testComponentSignature,
5907
+ source: staging.testComponentSource || null
5908
+ }];
5909
+ }
5910
+ return [];
5911
+ }
5912
+
5913
+ function testCompileSignatures(status = {}) {
5914
+ return testComponentProvenance(status)
5915
+ .map((entry) => entry && entry.signature)
5916
+ .filter(Boolean);
5917
+ }
5918
+
5919
+ function printTestRunPivots(status = {}) {
5920
+ const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
5921
+ if (!tests.length) return;
5922
+ const slowest = tests.slice().sort((a, b) => Number(b.duration || 0) - Number(a.duration || 0))[0];
5923
+ if (slowest && slowest.duration != null) {
5924
+ console.log('Slowest case:', (slowest.name || '(unnamed)') + ' in ' + formatDurationMs(slowest.duration));
5925
+ }
5926
+ tests.filter((test) => test && test.passed === false).forEach((test) => {
5927
+ console.log('Pivots for failed case:', test.name || '(unnamed)');
5928
+ if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
5929
+ if (test.threadGroupingId || test.threadGroupId || test.traceId) {
5930
+ console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
5931
+ }
5932
+ if (test.error) console.log(' Error:', String(test.error).slice(0, 500));
5933
+ const diagnostics = test.diagnostics && typeof test.diagnostics === 'object' ? test.diagnostics : null;
5934
+ if (diagnostics && Object.keys(diagnostics).length) {
5935
+ console.log(' Diagnostics:', JSON.stringify(diagnostics).slice(0, 1200));
5936
+ }
5937
+ if (Array.isArray(test.resolvedComponentProvenance) && test.resolvedComponentProvenance.length) {
5938
+ console.log(' Components:', test.resolvedComponentProvenance.slice(0, 8).map((entry) =>
5939
+ (entry.type || 'component') + ' ' + (entry.id || entry.name || '?') +
5940
+ (entry.source ? ' [' + entry.source + ']' : '') +
5941
+ (entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
5942
+ }
5943
+ if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
5944
+ console.log(' Live HTTP calls:', test.liveHttpCalls.length);
5945
+ }
5946
+ });
5947
+ }
5948
+
5949
+ function formatDurationMs(value) {
5950
+ const ms = Number(value);
5951
+ if (!Number.isFinite(ms)) return String(value);
5952
+ if (ms < 1000) return ms + 'ms';
5953
+ return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
5954
+ }
5955
+
5762
5956
  async function waitForToolStatus(api, cwd, options) {
5763
5957
  const started = Date.now();
5764
5958
  let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
@@ -5909,6 +6103,7 @@ async function testCommand(flags) {
5909
6103
  printResolvedBaseUrl(baseUrl);
5910
6104
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
5911
6105
  if (start.staging && Array.isArray(start.staging.accountLanes)) {
6106
+ printOrphanedWorkspaceWarning(start.staging);
5912
6107
  printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
5913
6108
  }
5914
6109
  console.log('Test run started:', start.taskId);
@@ -5935,6 +6130,7 @@ async function testCommand(flags) {
5935
6130
  console.log(JSON.stringify(status, null, 2));
5936
6131
  } else {
5937
6132
  console.log('Final status:', JSON.stringify(status, null, 2));
6133
+ printTestRunPivots(status);
5938
6134
  }
5939
6135
 
5940
6136
  // A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
@@ -5965,7 +6161,8 @@ async function testCommand(flags) {
5965
6161
  claim: 'Test run ' + String(testRef),
5966
6162
  world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
5967
6163
  revision: Object.assign(collectVerificationSource(cwd, flags), {
5968
- compileSignatures: status.staging && status.staging.testComponentSignature ? [status.staging.testComponentSignature] : []
6164
+ compileSignatures: testCompileSignatures(status),
6165
+ componentProvenance: testComponentProvenance(status)
5969
6166
  }),
5970
6167
  test: {
5971
6168
  taskId: start.taskId,
@@ -5974,16 +6171,62 @@ async function testCommand(flags) {
5974
6171
  selectedCases: names,
5975
6172
  passed: status.result && status.result.passed,
5976
6173
  failed: status.result && status.result.failed,
6174
+ tests: status.result && status.result.tests,
5977
6175
  dataModeSource: status.dataModeSource || dataModeSource,
5978
6176
  unmatchedTestNames: unmatched
5979
6177
  },
5980
- evidenceCategories: ['test_run'].concat(names.map((name) => 'test_run.' + name)),
6178
+ evidenceCategories: testEvidenceCategories(status, names),
5981
6179
  limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
5982
6180
  rawRefs: { testStatusKey: start.taskId },
5983
6181
  status
5984
6182
  }, { quiet: jsonOutput });
5985
6183
  }
5986
6184
 
6185
+ async function testStatusCommand(flags) {
6186
+ const cwd = process.cwd();
6187
+ ensureLocalState(cwd);
6188
+ const sessionContext = resolveSessionContext(cwd, flags);
6189
+ const { session, accountId } = sessionContext;
6190
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
6191
+ const branchName = flags.branch || currentBranch(cwd);
6192
+ const dataMode = hasExplicitDataModeFlag(flags)
6193
+ ? resolveDataMode(flags, null)
6194
+ : DEFAULT_DATA_MODE;
6195
+ const taskId = flags['task-id'] || flags.taskId || flags.id || (flags._ && flags._[2]);
6196
+ const jsonOutput = flagEnabled(flags.json);
6197
+ if (!taskId) throw new Error('Missing --task-id <taskId>');
6198
+
6199
+ const api = buildAxios(baseUrl, session.token);
6200
+ const status = await loggedPost(api, cwd, '/cli/test', {
6201
+ token: session.token,
6202
+ command: 'status',
6203
+ accountId,
6204
+ branchName,
6205
+ taskId,
6206
+ dataMode
6207
+ }).then((r) => r.data);
6208
+
6209
+ if (jsonOutput) {
6210
+ console.log(JSON.stringify(status, null, 2));
6211
+ return status;
6212
+ }
6213
+ printSessionResolutionWarning(sessionContext);
6214
+ printResolvedBaseUrl(baseUrl);
6215
+ console.log('Test run status:', status.status || 'unknown');
6216
+ console.log('Task ID:', taskId);
6217
+ console.log('Data mode:', status.dataMode || dataMode);
6218
+ if (status.result) {
6219
+ console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
6220
+ }
6221
+ if (status.error || status.message) {
6222
+ console.log('Message:', status.error || status.message);
6223
+ }
6224
+ if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
6225
+ process.exitCode = 1;
6226
+ }
6227
+ return status;
6228
+ }
6229
+
5987
6230
  /*
5988
6231
  ## Tokens, Tools, Verification, And Config
5989
6232
  */
@@ -6308,8 +6551,7 @@ async function toolCommand(flags) {
6308
6551
  revision: collectVerificationSource(cwd, flags),
6309
6552
  evidenceCategories: ['tool_call'],
6310
6553
  rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
6311
- tool: { callId: requestedCallId, status: data.status, name: stored && stored.name },
6312
- result: data
6554
+ tool: Object.assign(toolEvidenceSummary(data, statusResponse.responseFile), { callId: requestedCallId, name: stored && stored.name })
6313
6555
  }, { quiet: jsonOutput });
6314
6556
  if (jsonOutput) {
6315
6557
  console.log(JSON.stringify(data, null, 2));
@@ -6322,7 +6564,8 @@ async function toolCommand(flags) {
6322
6564
  console.log('Data mode:', data.dataMode || dataMode);
6323
6565
  if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
6324
6566
  console.log('Session log:', data.sessionLog);
6325
- console.log('Tool response file:', data.responseFile);
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) + ')' : ''));
6326
6569
  if (polledFailed) {
6327
6570
  console.log('Tool error:', data.toolMessage);
6328
6571
  }
@@ -6373,6 +6616,9 @@ async function toolCommand(flags) {
6373
6616
  data.sessionLog = sessionJsonlFile(cwd);
6374
6617
  if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
6375
6618
 
6619
+ let evidenceData = data;
6620
+ let evidenceResponseFile = response.responseFile;
6621
+
6376
6622
  if (!jsonOutput) {
6377
6623
  printSessionResolutionWarning(sessionContext);
6378
6624
  printResolvedBaseUrl(baseUrl);
@@ -6390,7 +6636,8 @@ async function toolCommand(flags) {
6390
6636
  console.log('Component:', `${data.componentSource}${sig}`);
6391
6637
  }
6392
6638
  console.log('Session log:', data.sessionLog);
6393
- console.log('Tool response file:', data.responseFile);
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) + ')' : ''));
6394
6641
  }
6395
6642
 
6396
6643
  // Non-zero exit so scripted/agent callers that check status notice the refusal too.
@@ -6410,9 +6657,12 @@ async function toolCommand(flags) {
6410
6657
 
6411
6658
  finalStatus.data.responseFile = finalStatus.responseFile;
6412
6659
  finalStatus.data.sessionLog = sessionJsonlFile(cwd);
6660
+ evidenceData = finalStatus.data;
6661
+ evidenceResponseFile = finalStatus.responseFile;
6413
6662
  if (!jsonOutput) {
6414
6663
  console.log('Final status:', finalStatus.data.status);
6415
- console.log('Tool response file:', finalStatus.responseFile);
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) + ')' : ''));
6416
6666
  }
6417
6667
  if (finalStatus.data.status !== 'completed') {
6418
6668
  process.exitCode = 1;
@@ -6422,20 +6672,19 @@ async function toolCommand(flags) {
6422
6672
 
6423
6673
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
6424
6674
  type: 'tool_call',
6425
- success: !toolFailed && (!asyncMode || !waitForAsync || process.exitCode !== 1),
6675
+ success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
6426
6676
  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 }),
6677
+ 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
6678
  revision: Object.assign(collectVerificationSource(cwd, flags), {
6429
- compileSignatures: data.componentSignature ? [data.componentSignature] : []
6679
+ compileSignatures: evidenceData.componentSignature ? [evidenceData.componentSignature] : []
6430
6680
  }),
6431
6681
  evidenceCategories: ['tool_call'],
6432
6682
  dependencies: {
6433
6683
  mocks: [],
6434
- liveHttpCalls: data.liveHttpCalls || []
6684
+ liveHttpCalls: evidenceData.liveHttpCalls || []
6435
6685
  },
6436
- rawRefs: { toolResponsePath: response.responseFile, callId },
6437
- tool: { callId, name: String(toolName), async: asyncMode, status: data.status, toolSuccess: data.toolSuccess },
6438
- result: data
6686
+ rawRefs: { toolResponsePath: evidenceResponseFile, callId },
6687
+ tool: Object.assign(toolEvidenceSummary(evidenceData, evidenceResponseFile), { callId, name: String(toolName), async: asyncMode })
6439
6688
  }, { quiet: jsonOutput });
6440
6689
  if (jsonOutput) {
6441
6690
  console.log(JSON.stringify(data, null, 2));
@@ -6522,7 +6771,8 @@ async function verifyCommand(flags, subcommand) {
6522
6771
  source.componentBranch = verifiedWorld.componentBranch;
6523
6772
  source.sourceLayer = verifiedWorld.sourceLayer;
6524
6773
  if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
6525
- source.stagedOverlayHash = statusResponse.laneSummary ? sha256(stableStringify(statusResponse.laneSummary)) : null;
6774
+ source.laneContentHash = laneContentHashFrom(statusResponse);
6775
+ source.stagedOverlayHash = source.laneContentHash;
6526
6776
  }
6527
6777
 
6528
6778
  const response = await postVerificationCommand(api, cwd, session, accountId, 'start', {
@@ -6537,6 +6787,7 @@ async function verifyCommand(flags, subcommand) {
6537
6787
  writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
6538
6788
  console.log('Verification envelope started:', envelope.envelopeId);
6539
6789
  console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
6790
+ printAcceptanceWarnings(envelope);
6540
6791
  console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
6541
6792
  return envelope;
6542
6793
  }
@@ -6577,6 +6828,7 @@ async function verifyCommand(flags, subcommand) {
6577
6828
  writeLocalVerificationEnvelope(cwd, response.envelope);
6578
6829
  console.log('Manifest attached to envelope:', envelopeId);
6579
6830
  console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
6831
+ printAcceptanceWarnings(response.envelope);
6580
6832
  return response.envelope;
6581
6833
  }
6582
6834
 
@@ -12337,11 +12589,13 @@ function printComponentsHelp(subcommand) {
12337
12589
 
12338
12590
  function printTestHelp() {
12339
12591
  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]');
12592
+ console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
12340
12593
  console.log('');
12341
12594
  console.log('Runs a Test component against the staged/variant world for this checkout.');
12342
12595
  console.log('Examples:');
12343
12596
  console.log(' remits-cli test run --test 11');
12344
12597
  console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
12598
+ console.log(' remits-cli test status --task-id 7a1b...');
12345
12599
  console.log('');
12346
12600
  console.log('Notes:');
12347
12601
  console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
@@ -12728,6 +12982,10 @@ async function main() {
12728
12982
  await testCommand(args);
12729
12983
  return;
12730
12984
  }
12985
+ if (command === 'test' && subcommand === 'status') {
12986
+ await testStatusCommand(args);
12987
+ return;
12988
+ }
12731
12989
 
12732
12990
  if (command === 'token') {
12733
12991
  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.127",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -207,6 +207,7 @@ remits-cli components branch <name> --subscribe <accountId> [--parent-account <i
207
207
  remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
208
208
  remits-cli components branch <name> --retire [--force] # delete the branch's overlays
209
209
  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]
210
+ remits-cli test status --task-id <taskId> [--branch <stagingScope>] [--data-mode test|prod] [--json]
210
211
  remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
211
212
  remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
212
213
  remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
@@ -268,6 +269,23 @@ remits-cli verify report
268
269
  Existing commands also accept `--verify-envelope <id>` to attach to a specific envelope and
269
270
  `--no-verify-envelope` to suppress automatic attachment for one command.
270
271
 
272
+ Failed `remits-cli test run` output includes compact pivots per failed case: duration, trace id,
273
+ bounded `report(...)` diagnostics, live HTTP signals, and resolved component provenance when the
274
+ platform returns it. Re-read an existing run with `remits-cli test status --task-id <taskId>` before
275
+ rerunning a long suite.
276
+
277
+ Test-run requirements must name the suite/cases they mean:
278
+
279
+ ```json
280
+ {"id":"api_flow", "packetType":"test_run", "suite":"Statement API Flow", "allCases":true}
281
+ {"id":"one_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
282
+ ```
283
+
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
+
271
289
  Use `remits-cli verify list` to review open envelopes for the account without SQL. If a newer envelope
272
290
  replaces an older one, use `verify supersede`; if a duplicate or abandoned attempt should no longer read
273
291
  as live work, use `verify abandon`. Both keep the evidence history.
@@ -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
 
@@ -212,6 +213,9 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
212
213
  - `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
213
214
  - In lane rows, `currentLane` (also `mine`) marks THIS command's lane; `ownedByCaller` marks every lane
214
215
  staged by your CLI user — your other clones' agents included.
216
+ - If your current lane is empty but the same workspace has staged entries on another branch, the CLI
217
+ prints a warning. That usually means the checkout switched branches after staging; re-stage on this
218
+ branch or switch back.
215
219
  - **A landing clears only the lander's lane.** Every stage records the commit it came from
216
220
  (`stageBaseSha`), and `components status` compares that with the last commit the platform synced for the
217
221
  branch (`branchContext.lastSyncedSha`) using your local git:
@@ -97,6 +97,17 @@ 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 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.
110
+
100
111
  ## Development Workflow
101
112
 
102
113
  ### The Golden Rule: Writing Code Is Not Finishing the Job
@@ -327,9 +338,13 @@ This applies to:
327
338
  ```bash
328
339
  remits-cli test run --test <TEST_ID_OR_NAME>
329
340
  remits-cli test run --test "Invoice Tests" --names "specific test case"
341
+ remits-cli test status --task-id <TASK_ID>
330
342
  ```
331
343
 
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.
344
+ Tests run on the platform against your staged snapshot. They stream results in real-time. If they fail,
345
+ read the printed pivots first: failed cases include timing, trace ids, bounded `report(...)`
346
+ diagnostics, live HTTP signals, and resolved component provenance when available. Fix the code,
347
+ re-stage, and re-run only after those pivots explain the failure.
333
348
 
334
349
  Important test-runner constraints:
335
350
  - `remits-cli test run` now defaults to `test` dataMode unless you explicitly pass `--data-mode prod`.