@remits/remits-cli 0.1.129 → 0.1.132

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
@@ -4,18 +4,18 @@
4
4
  ## Table of Contents
5
5
 
6
6
  - L22 Runtime Bootstrap And Shared State
7
- - L116 Sessions, Account Resolution, And Production Guards
8
- - L595 Local State, Workspaces, And Verification Evidence
9
- - L1448 Account Repos, Guide Sync, And Platform Repo
10
- - L2106 Component Discovery And HTTP Logging
11
- - L2598 Skill Delivery And TOC Resolution
12
- - L2908 Auth And Component Staging
13
- - L3506 Component Summaries, Status, And Sync Gates
14
- - L5457 Branches, Promotion, Commit, And Test Runs
15
- - L6682 Tokens, Tools, Verification, And Config
16
- - L7598 Service Dashboard And WebSocket Listener
17
- - L9693 Agent And Ticket Workflows
18
- - L12751 Help, Auto Update, And Command Dispatch
7
+ - L119 Sessions, Account Resolution, And Production Guards
8
+ - L603 Local State, Workspaces, And Verification Evidence
9
+ - L2155 Account Repos, Guide Sync, And Platform Repo
10
+ - L2813 Component Discovery And HTTP Logging
11
+ - L3389 Skill Delivery And TOC Resolution
12
+ - L3699 Auth And Component Staging
13
+ - L4312 Component Summaries, Status, And Sync Gates
14
+ - L6375 Branches, Promotion, Commit, And Test Runs
15
+ - L8036 Tokens, Tools, Verification, And Config
16
+ - L9251 Service Dashboard And WebSocket Listener
17
+ - L11367 Agent And Ticket Workflows
18
+ - L14559 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -102,9 +102,12 @@ const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
102
102
  'tmp'
103
103
  ]);
104
104
 
105
+ const LOCAL_ACTOR_MAX_LENGTH = 64;
106
+ let LOCAL_ACTOR_CONTEXT = null;
107
+
105
108
  // Flags that are legitimately repeatable accumulate into an array instead of last-wins. Every other
106
109
  // flag keeps last-wins so existing callers are unaffected.
107
- const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names']);
110
+ const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names', 'tag', 'tags', 'key', 'keys', 'case-key']);
108
111
 
109
112
  // `-m "msg"` is how git spells a commit message, and agents type it from habit. Before this the parser took a
110
113
  // single-dash token as the VALUE of whatever flag preceded it: `--json -m "x"` silently turned JSON mode
@@ -405,6 +408,7 @@ function resolveSessionIdentity(cwd, flags = {}) {
405
408
  branchName,
406
409
  workspace,
407
410
  workspaceSource: workspaceSource(cwd, flags),
411
+ localState: localStateSummary(cwd, flags),
408
412
  dataMode,
409
413
  // `test run` resolves its lane from the FLAG ONLY (testRunCommand), never the stored session, so that
410
414
  // an account whose session is parked on prod cannot have a test run silently follow it. Report both
@@ -433,6 +437,8 @@ function printSessionIdentity(identity, options = {}) {
433
437
  console.log('Data mode (test run):', identity.testRunDataMode || DEFAULT_DATA_MODE,
434
438
  '[' + (identity.testRunDataModeSource || 'cliDefault') + ']');
435
439
  console.log('Base URL:', identity.baseUrl);
440
+ console.log('Local actor:', identity.localState.actor + ' (from ' + identity.localState.actorSource + ')');
441
+ console.log('Local state:', identity.localState.stateDir);
436
442
  return;
437
443
  }
438
444
 
@@ -456,6 +462,8 @@ function printSessionIdentity(identity, options = {}) {
456
462
  console.log(' Branch:', identity.branchName);
457
463
  console.log(' Workspace:', (identity.workspace || 'none (shared default lane)') +
458
464
  (identity.workspaceSource ? ' [' + identity.workspaceSource + ']' : ''));
465
+ console.log(' Local actor:', identity.localState.actor + ' (from ' + identity.localState.actorSource + ')');
466
+ console.log(' Local state:', identity.localState.stateDir);
459
467
  console.log(' Data mode:', identity.dataMode, '(applies to tool/tools/token)');
460
468
  // `test run` deliberately ignores the stored session lane and defaults to test, so a session sitting on
461
469
  // prod would otherwise read as "prod" here while the next test run went to the test lane. Say so rather
@@ -603,16 +611,316 @@ function ensureDir(dirPath) {
603
611
 
604
612
  function localStatePaths(cwd) {
605
613
  const base = path.join(cwd, '.remits-cli');
614
+ const actor = resolveLocalActor();
615
+ const actorDir = path.join(base, 'actors', actor.id);
616
+ const legacyVerificationDir = path.join(base, 'verification');
606
617
  return {
607
618
  base,
608
- toolsDir: path.join(base, 'tools'),
609
- sessionsDir: path.join(base, 'sessions'),
610
- toolResponsesDir: path.join(base, 'tool-responses'),
611
- verificationDir: path.join(base, 'verification'),
612
- activeVerificationFile: path.join(base, 'verification', 'active'),
613
- activeVerificationContextsFile: path.join(base, 'verification', 'active-contexts.json'),
614
- currentSessionFile: path.join(base, 'current-session.txt'),
615
- workspaceFile: path.join(base, 'workspace')
619
+ actor,
620
+ actorsDir: path.join(base, 'actors'),
621
+ actorDir,
622
+ sharedDir: path.join(base, 'shared'),
623
+ toolsDir: path.join(base, 'shared', 'tools'),
624
+ sessionsDir: path.join(actorDir, 'sessions'),
625
+ toolResponsesDir: path.join(actorDir, 'tool-responses'),
626
+ verificationDir: path.join(actorDir, 'verification'),
627
+ diagnosticsDir: path.join(actorDir, 'diagnostics'),
628
+ activeVerificationFile: path.join(actorDir, 'verification', 'active'),
629
+ activeVerificationContextsFile: path.join(actorDir, 'verification', 'active-contexts.json'),
630
+ currentSessionFile: path.join(actorDir, 'current-session.txt'),
631
+ workspaceFile: path.join(base, 'workspace'),
632
+ stateVersionFile: path.join(base, 'state-version.json'),
633
+ legacy: {
634
+ toolsDir: path.join(base, 'tools'),
635
+ sessionsDir: path.join(base, 'sessions'),
636
+ toolResponsesDir: path.join(base, 'tool-responses'),
637
+ verificationDir: legacyVerificationDir,
638
+ activeVerificationFile: path.join(legacyVerificationDir, 'active'),
639
+ activeVerificationContextsFile: path.join(legacyVerificationDir, 'active-contexts.json'),
640
+ currentSessionFile: path.join(base, 'current-session.txt')
641
+ }
642
+ };
643
+ }
644
+
645
+ function normalizeLocalActor(value) {
646
+ if (value === undefined || value === null) return null;
647
+ const trimmed = String(value).trim();
648
+ if (!trimmed) return null;
649
+ let normalized = trimmed
650
+ .toLowerCase()
651
+ .replace(/[^a-z0-9._-]/g, '-')
652
+ .replace(/-+/g, '-')
653
+ .replace(/^[-.]+/, '')
654
+ .replace(/[-.]+$/, '');
655
+ if (normalized.length > LOCAL_ACTOR_MAX_LENGTH) {
656
+ normalized = normalized.slice(0, LOCAL_ACTOR_MAX_LENGTH).replace(/[-.]+$/, '');
657
+ }
658
+ return normalized || null;
659
+ }
660
+
661
+ // The generated fallback id must be STABLE for a given user and checkout. It used to carry process.pid, so every
662
+ // single CLI invocation minted a fresh actor directory: 85 of them accumulated in one account repo, each holding a
663
+ // partial copy of the session and tool-response state, the "other actor state exists" warning grew to a ~50-name
664
+ // wall, and — because the active-verification pointer lives in the actor directory — no invocation could ever see
665
+ // the envelope the previous one opened, so everything silently fell back to the legacy flat state.
666
+ // Genuine concurrent agents still isolate themselves explicitly, with --actor / --local-agent / REMITS_AGENT_ID
667
+ // (which `agent serve` exports into each child), and that path is unchanged.
668
+ function generatedLocalActorId() {
669
+ const base = 'local-' + os.userInfo().username + '-' + path.basename(process.cwd());
670
+ return normalizeLocalActor(base) || 'local-default';
671
+ }
672
+
673
+ function resolveLocalActor(flags = {}) {
674
+ if (LOCAL_ACTOR_CONTEXT && LOCAL_ACTOR_CONTEXT.id) return LOCAL_ACTOR_CONTEXT;
675
+ const explicit = flags['local-agent'] || flags.localAgent || flags['actor'] || flags.actor;
676
+ const envValue = process.env.REMITS_AGENT_ID || process.env.REMITS_LOCAL_AGENT_ID;
677
+ const explicitId = normalizeLocalActor(explicit);
678
+ if (explicitId) return { id: explicitId, source: flags['local-agent'] || flags.localAgent ? '--local-agent' : '--actor' };
679
+ const envId = normalizeLocalActor(envValue);
680
+ if (envId) return { id: envId, source: process.env.REMITS_AGENT_ID ? 'REMITS_AGENT_ID' : 'REMITS_LOCAL_AGENT_ID' };
681
+ return { id: generatedLocalActorId(), source: 'generated from process and checkout' };
682
+ }
683
+
684
+ function configureLocalActor(flags = {}) {
685
+ const explicit = flags['local-agent'] || flags.localAgent || flags['actor'] || flags.actor;
686
+ const explicitId = normalizeLocalActor(explicit);
687
+ if (explicitId) {
688
+ LOCAL_ACTOR_CONTEXT = { id: explicitId, source: flags['local-agent'] || flags.localAgent ? '--local-agent' : '--actor' };
689
+ } else if (!LOCAL_ACTOR_CONTEXT) {
690
+ LOCAL_ACTOR_CONTEXT = resolveLocalActor({});
691
+ }
692
+ return LOCAL_ACTOR_CONTEXT;
693
+ }
694
+
695
+ function localActorId(flags = {}) {
696
+ return resolveLocalActor(flags).id;
697
+ }
698
+
699
+ function printLocalActor(cwd, flags = {}) {
700
+ const paths = localStatePaths(cwd);
701
+ console.log('Local actor:', paths.actor.id + ' (from ' + paths.actor.source + ')');
702
+ console.log('Local state:', paths.actorDir);
703
+ }
704
+
705
+ function listActorDirectories(cwd) {
706
+ const paths = localStatePaths(cwd);
707
+ try {
708
+ if (!fs.existsSync(paths.actorsDir)) return [];
709
+ return fs.readdirSync(paths.actorsDir, { withFileTypes: true })
710
+ .filter((entry) => entry.isDirectory())
711
+ .map((entry) => {
712
+ const actorDir = path.join(paths.actorsDir, entry.name);
713
+ const stat = fileStat(actorDir);
714
+ return {
715
+ actor: entry.name,
716
+ path: actorDir,
717
+ current: entry.name === paths.actor.id,
718
+ updatedAt: stat ? stat.mtime.toISOString() : null
719
+ };
720
+ })
721
+ .sort((a, b) => String(a.actor).localeCompare(String(b.actor)));
722
+ } catch (_) {
723
+ return [];
724
+ }
725
+ }
726
+
727
+ function gitTrackedRemitsCliFiles(cwd) {
728
+ try {
729
+ const output = runGit(cwd, 'git ls-files .remits-cli');
730
+ return output ? output.split(/\r?\n/).filter(Boolean) : [];
731
+ } catch (_) {
732
+ return [];
733
+ }
734
+ }
735
+
736
+ function fileRowsUnder(dir, suffixPattern = null, max = 50) {
737
+ const rows = [];
738
+ function walk(current) {
739
+ if (rows.length >= max || !fs.existsSync(current)) return;
740
+ let entries = [];
741
+ try {
742
+ entries = fs.readdirSync(current, { withFileTypes: true });
743
+ } catch (_) {
744
+ return;
745
+ }
746
+ entries.forEach((entry) => {
747
+ if (rows.length >= max) return;
748
+ const entryPath = path.join(current, entry.name);
749
+ if (entry.isDirectory()) {
750
+ walk(entryPath);
751
+ } else if (!suffixPattern || suffixPattern.test(entry.name)) {
752
+ const stat = fileStat(entryPath);
753
+ rows.push({
754
+ path: entryPath,
755
+ size: stat ? stat.size : 0,
756
+ updatedAt: stat ? stat.mtime.toISOString() : null
757
+ });
758
+ }
759
+ });
760
+ }
761
+ walk(dir);
762
+ return rows;
763
+ }
764
+
765
+ function likelyRawPayloadSessionLog(file) {
766
+ const entries = readJsonLinesFile(file, 120);
767
+ return entries.some((entry) => {
768
+ if (!entry || typeof entry !== 'object') return false;
769
+ if (entry.unsafePayloadLogging === true) return true;
770
+ const request = entry.request;
771
+ if (!request || typeof request !== 'object') return false;
772
+ const text = JSON.stringify(request);
773
+ return /statementText|ocrText|messages|actionInput|source|html|schema/i.test(text) &&
774
+ text.length > 2000;
775
+ });
776
+ }
777
+
778
+ function localStateReport(cwd, flags = {}) {
779
+ const paths = ensureLocalState(cwd);
780
+ const actors = listActorDirectories(cwd);
781
+ const legacySessionLogs = fileRowsUnder(paths.legacy.sessionsDir, /\.jsonl$/);
782
+ const actorSessionLogs = fileRowsUnder(paths.sessionsDir, /\.jsonl$/);
783
+ const legacyToolResponses = fileRowsUnder(paths.legacy.toolResponsesDir, /\.json$/);
784
+ const actorToolResponses = fileRowsUnder(paths.toolResponsesDir, /\.json$/);
785
+ const largest = actorSessionLogs.concat(legacySessionLogs, actorToolResponses, legacyToolResponses)
786
+ .sort((a, b) => b.size - a.size)
787
+ .slice(0, 10);
788
+ return {
789
+ cwd,
790
+ actor: paths.actor,
791
+ activeActorStateDir: paths.actorDir,
792
+ workspace: resolveWorkspace(cwd, flags),
793
+ workspaceSource: workspaceSource(cwd, flags),
794
+ workspaceFile: paths.workspaceFile,
795
+ trackedFiles: gitTrackedRemitsCliFiles(cwd),
796
+ actors,
797
+ foreignActors: actors.filter((actor) => !actor.current),
798
+ legacy: {
799
+ currentSessionFile: paths.legacy.currentSessionFile,
800
+ currentSessionExists: fs.existsSync(paths.legacy.currentSessionFile),
801
+ sessionLogs: legacySessionLogs,
802
+ toolResponses: legacyToolResponses,
803
+ likelyRawPayloadLogs: legacySessionLogs.filter((row) => likelyRawPayloadSessionLog(row.path))
804
+ },
805
+ current: {
806
+ currentSessionFile: paths.currentSessionFile,
807
+ sessionLogs: actorSessionLogs,
808
+ toolResponses: actorToolResponses,
809
+ likelyRawPayloadLogs: actorSessionLogs.filter((row) => likelyRawPayloadSessionLog(row.path))
810
+ },
811
+ largestFiles: largest
812
+ };
813
+ }
814
+
815
+ const FOREIGN_ACTOR_RECENT_MS = 7 * 24 * 60 * 60 * 1000;
816
+
817
+ // Directories left by the old per-process generated id ('session-<pid>-<user>-<checkout>'). Nothing mints that
818
+ // shape any more, so however recently one was written it is debris from a finished process, never a live agent:
819
+ // it is prunable regardless of age, which matters because the defect produced dozens of them in a single day.
820
+ const LEGACY_PER_PROCESS_ACTOR = /^session-\d+-/;
821
+
822
+ function isDisposableActor(actor, cutoff) {
823
+ if (!actor || actor.current) return false;
824
+ if (LEGACY_PER_PROCESS_ACTOR.test(String(actor.actor))) return true;
825
+ return !(actor.updatedAt && Date.parse(actor.updatedAt) >= cutoff);
826
+ }
827
+
828
+ // Remove actor state directories that are neither the current actor nor recently active. Only ever touches
829
+ // directories under .remits-cli/actors, and never the active one, so the worst case is discarding old session
830
+ // logs and tool responses that nothing reads.
831
+ function pruneStaleActorDirectories(cwd) {
832
+ const paths = localStatePaths(cwd);
833
+ const cutoff = Date.now() - FOREIGN_ACTOR_RECENT_MS;
834
+ const removed = [];
835
+ const failed = [];
836
+ let kept = 0;
837
+ listActorDirectories(cwd).forEach((actor) => {
838
+ if (!isDisposableActor(actor, cutoff)) {
839
+ kept += 1;
840
+ return;
841
+ }
842
+ if (path.dirname(actor.path) !== paths.actorsDir) return;
843
+ try {
844
+ fs.rmSync(actor.path, { recursive: true, force: true });
845
+ removed.push(actor.actor);
846
+ } catch (_) {
847
+ failed.push(actor.actor);
848
+ }
849
+ });
850
+ return { removed, failed, kept };
851
+ }
852
+
853
+ function localStateWarnings(cwd, flags = {}) {
854
+ const report = localStateReport(cwd, flags);
855
+ const warnings = [];
856
+ if (!report.workspace) {
857
+ warnings.push('Using the shared/default workspace lane. Run `remits-cli workspace use --auto` for isolated staging.');
858
+ }
859
+ if (report.trackedFiles.length) {
860
+ warnings.push('.remits-cli is tracked by git (' + report.trackedFiles.length + ' file(s)); untrack it before committing.');
861
+ }
862
+ if (report.actor.source && report.actor.source.startsWith('generated') && report.foreignActors.length) {
863
+ // Only RECENTLY active other actors are a signal. Every generated actor leaves a directory behind forever,
864
+ // so listing them all printed a ~50-name wall on every command that drowned the warnings worth reading.
865
+ const cutoff = Date.now() - FOREIGN_ACTOR_RECENT_MS;
866
+ const recent = report.foreignActors
867
+ .filter((a) => !isDisposableActor(a, cutoff))
868
+ .sort((a, b) => Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0));
869
+ const shown = recent.slice(0, 3).map((a) => a.actor);
870
+ const stale = report.foreignActors.length - recent.length;
871
+ const setOne = 'Name this one with --actor <name> (or REMITS_AGENT_ID) if another agent shares this checkout.';
872
+ const pruneHint = stale
873
+ ? stale + ' spent actor dir(s) (idle, or left behind by finished processes) can be cleared with `remits-cli doctor local-state --prune-actors`.'
874
+ : '';
875
+ if (shown.length) {
876
+ warnings.push('Other actor state is live here: ' + shown.join(', ') +
877
+ (recent.length > shown.length ? ' (+' + (recent.length - shown.length) + ' more)' : '') + '. ' + setOne +
878
+ (pruneHint ? ' ' + pruneHint : ''));
879
+ } else if (pruneHint) {
880
+ warnings.push(pruneHint);
881
+ }
882
+ }
883
+ if (report.legacy.currentSessionExists || report.legacy.sessionLogs.length || report.legacy.toolResponses.length) {
884
+ warnings.push('Legacy flat .remits-cli state exists; new writes use actor-scoped state and legacy files are read only as fallback.');
885
+ }
886
+ if (report.current.likelyRawPayloadLogs.length || report.legacy.likelyRawPayloadLogs.length) {
887
+ warnings.push('At least one session log appears to contain raw or unsafe payload-shaped fields.');
888
+ }
889
+ return { report, warnings };
890
+ }
891
+
892
+ function printLocalStateWarnings(cwd, flags = {}) {
893
+ const { warnings } = localStateWarnings(cwd, flags);
894
+ if (!warnings.length) return;
895
+ console.log('Local state warnings:');
896
+ warnings.forEach((warning) => console.log(' - ' + warning));
897
+ }
898
+
899
+ function printLocalCommandContext(cwd, flags = {}, context = {}) {
900
+ const paths = localStatePaths(cwd);
901
+ const branchName = context.branchName || flags.branch || safeGitValue(cwd, 'git rev-parse --abbrev-ref HEAD') || 'unknown';
902
+ const workspace = context.workspace !== undefined ? context.workspace : resolveWorkspace(cwd, flags);
903
+ const dataMode = context.dataMode || null;
904
+ const accountId = context.accountId || null;
905
+ console.log('Local actor:', paths.actor.id + ' (from ' + paths.actor.source + ')');
906
+ console.log('Local state:', paths.actorDir);
907
+ const pieces = [];
908
+ if (accountId) pieces.push('account=' + accountId);
909
+ pieces.push('branch=' + branchName);
910
+ pieces.push('workspace=' + (workspace || 'shared'));
911
+ if (dataMode) pieces.push('dataMode=' + dataMode);
912
+ console.log('Command context:', pieces.join(', '));
913
+ }
914
+
915
+ function localStateSummary(cwd, flags = {}) {
916
+ const paths = localStatePaths(cwd);
917
+ return {
918
+ actor: paths.actor.id,
919
+ actorSource: paths.actor.source,
920
+ stateDir: paths.actorDir,
921
+ workspace: resolveWorkspace(cwd, flags),
922
+ workspaceSource: workspaceSource(cwd, flags),
923
+ sharedWorkspace: !resolveWorkspace(cwd, flags)
616
924
  };
617
925
  }
618
926
 
@@ -713,10 +1021,21 @@ function printStagingLane(branchName, workspace, source) {
713
1021
  function ensureLocalState(cwd) {
714
1022
  const paths = localStatePaths(cwd);
715
1023
  ensureDir(paths.base);
1024
+ ensureDir(paths.actorsDir);
1025
+ ensureDir(paths.actorDir);
1026
+ ensureDir(paths.sharedDir);
716
1027
  ensureDir(paths.toolsDir);
717
1028
  ensureDir(paths.sessionsDir);
718
1029
  ensureDir(paths.toolResponsesDir);
719
1030
  ensureDir(paths.verificationDir);
1031
+ ensureDir(paths.diagnosticsDir);
1032
+ if (!fs.existsSync(paths.stateVersionFile)) {
1033
+ atomicWriteFile(paths.stateVersionFile, JSON.stringify({
1034
+ version: 2,
1035
+ shape: 'actor-scoped',
1036
+ updatedAt: new Date().toISOString()
1037
+ }, null, 2));
1038
+ }
720
1039
  return paths;
721
1040
  }
722
1041
 
@@ -740,8 +1059,14 @@ function sessionJsonlFile(cwd) {
740
1059
  }
741
1060
 
742
1061
  const CONTENT_FIELDS = new Set(['source', 'html', 'javascript', 'css', 'schema', 'inputSchema', 'previewData', 'messages']);
1062
+ const LARGE_LOG_STRING_BYTES = 256;
1063
+ const MAX_LOG_SUMMARY_DEPTH = 2;
743
1064
  const SECRET_LOG_FIELDS = new Set([
744
1065
  'token',
1066
+ 'embedtokenkey',
1067
+ 'embeddableurl',
1068
+ 'liveurl',
1069
+ 'shortbasepath',
745
1070
  'tokeninput',
746
1071
  'tokenkey',
747
1072
  'authtoken',
@@ -751,6 +1076,26 @@ const SECRET_LOG_FIELDS = new Set([
751
1076
  'x-remits-token',
752
1077
  'authorization'
753
1078
  ]);
1079
+ const CONTENT_LOG_FIELDS = new Set([
1080
+ 'actioninput',
1081
+ 'content',
1082
+ 'css',
1083
+ 'file',
1084
+ 'filecontents',
1085
+ 'html',
1086
+ 'input',
1087
+ 'inputschema',
1088
+ 'javascript',
1089
+ 'messages',
1090
+ 'ocrtext',
1091
+ 'payload',
1092
+ 'previewdata',
1093
+ 'prompt',
1094
+ 'schema',
1095
+ 'source',
1096
+ 'statementtext',
1097
+ 'text'
1098
+ ]);
754
1099
 
755
1100
  function sanitizeForLog(value) {
756
1101
  if (value === null || value === undefined) {
@@ -775,6 +1120,173 @@ function sanitizeForLog(value) {
775
1120
  return out;
776
1121
  }
777
1122
 
1123
+ function estimatedJsonBytes(value) {
1124
+ try {
1125
+ return Buffer.byteLength(JSON.stringify(value));
1126
+ } catch (_) {
1127
+ return Buffer.byteLength(String(value));
1128
+ }
1129
+ }
1130
+
1131
+ function safeHashValue(value) {
1132
+ try {
1133
+ return sha256(stableStringify(value));
1134
+ } catch (_) {
1135
+ return sha256(String(value));
1136
+ }
1137
+ }
1138
+
1139
+ function fieldLooksSensitive(key) {
1140
+ return SECRET_LOG_FIELDS.has(String(key || '').toLowerCase());
1141
+ }
1142
+
1143
+ function fieldLooksContent(key) {
1144
+ return CONTENT_LOG_FIELDS.has(String(key || '').toLowerCase());
1145
+ }
1146
+
1147
+ function summarizeLogValue(value, key = null, state = { nestedKeyCount: 0, redactions: [] }, depth = 0) {
1148
+ const keyText = key == null ? '' : String(key);
1149
+ const nestedPayloadValue = depth > 0;
1150
+ if (fieldLooksSensitive(keyText)) {
1151
+ state.redactions.push(keyText + ' redacted');
1152
+ return { type: 'redacted', redacted: true };
1153
+ }
1154
+ if (value === null || value === undefined) {
1155
+ return { type: value === null ? 'null' : 'undefined' };
1156
+ }
1157
+ if (typeof value === 'string') {
1158
+ const bytes = Buffer.byteLength(value);
1159
+ if (nestedPayloadValue || fieldLooksContent(keyText) || bytes > LARGE_LOG_STRING_BYTES) {
1160
+ state.redactions.push((keyText || 'string') + ' omitted by default');
1161
+ return { type: 'string', bytes, sha256: sha256(value), omitted: true };
1162
+ }
1163
+ return { type: 'string', bytes, value };
1164
+ }
1165
+ if (typeof value === 'number' || typeof value === 'boolean') {
1166
+ if (nestedPayloadValue) {
1167
+ state.redactions.push((keyText || typeof value) + ' omitted by default');
1168
+ return { type: typeof value, sha256: safeHashValue(value), omitted: true };
1169
+ }
1170
+ return { type: typeof value, value };
1171
+ }
1172
+ if (Array.isArray(value)) {
1173
+ if (depth >= MAX_LOG_SUMMARY_DEPTH) {
1174
+ state.redactions.push((keyText || 'array') + ' nested array omitted by default');
1175
+ return {
1176
+ type: 'array',
1177
+ length: value.length,
1178
+ estimatedBytes: estimatedJsonBytes(value),
1179
+ sha256: safeHashValue(value),
1180
+ omitted: true
1181
+ };
1182
+ }
1183
+ const sample = value.slice(0, 5).map((entry) => summarizeLogValue(entry, keyText, state, depth + 1));
1184
+ if (value.length > 5) {
1185
+ state.redactions.push((keyText || 'array') + ' sample capped at 5 of ' + value.length);
1186
+ }
1187
+ return {
1188
+ type: 'array',
1189
+ length: value.length,
1190
+ estimatedBytes: estimatedJsonBytes(value),
1191
+ sha256: safeHashValue(value),
1192
+ sample
1193
+ };
1194
+ }
1195
+ if (typeof value === 'object') {
1196
+ const keys = Object.keys(value).sort();
1197
+ state.nestedKeyCount += keys.length;
1198
+ if (depth >= MAX_LOG_SUMMARY_DEPTH) {
1199
+ state.redactions.push((keyText || 'object') + ' nested object omitted by default');
1200
+ return {
1201
+ type: 'object',
1202
+ keyCount: keys.length,
1203
+ keys,
1204
+ estimatedBytes: estimatedJsonBytes(value),
1205
+ sha256: safeHashValue(value),
1206
+ omitted: true
1207
+ };
1208
+ }
1209
+ const fields = {};
1210
+ keys.slice(0, 50).forEach((childKey) => {
1211
+ fields[childKey] = summarizeLogValue(value[childKey], childKey, state, depth + 1);
1212
+ });
1213
+ if (keys.length > 50) {
1214
+ state.redactions.push((keyText || 'object') + ' keys capped at 50 of ' + keys.length);
1215
+ }
1216
+ return {
1217
+ type: 'object',
1218
+ keyCount: keys.length,
1219
+ keys,
1220
+ estimatedBytes: estimatedJsonBytes(value),
1221
+ sha256: safeHashValue(value),
1222
+ fields
1223
+ };
1224
+ }
1225
+ return { type: typeof value, bytes: estimatedJsonBytes(value), sha256: safeHashValue(value), omitted: true };
1226
+ }
1227
+
1228
+ function summarizePayloadFieldsForLog(payload, defaultRedaction) {
1229
+ const input = payload && typeof payload === 'object' ? payload : { value: payload };
1230
+ const state = { nestedKeyCount: 0, redactions: [] };
1231
+ const topLevelKeys = Object.keys(input).sort();
1232
+ const fieldSummary = {};
1233
+ topLevelKeys.forEach((key) => {
1234
+ fieldSummary[key] = summarizeLogValue(input[key], key, state);
1235
+ });
1236
+ if (state.redactions.length === 0 && defaultRedaction) {
1237
+ state.redactions.push(defaultRedaction);
1238
+ }
1239
+ return { input, state, topLevelKeys, fieldSummary };
1240
+ }
1241
+
1242
+ function summarizeRequestForLog(payload = {}) {
1243
+ const { input, state, topLevelKeys, fieldSummary } = summarizePayloadFieldsForLog(
1244
+ payload,
1245
+ 'payload summarized; nested values omitted by default'
1246
+ );
1247
+ return {
1248
+ accountId: input.accountId !== undefined ? input.accountId : null,
1249
+ dataMode: input.dataMode || null,
1250
+ branchName: input.branchName || null,
1251
+ workspace: input.workspace || null,
1252
+ command: input.command || null,
1253
+ toolName: input.name || input.toolName || null,
1254
+ callId: input.callId || null,
1255
+ topLevelKeys,
1256
+ nestedKeyCount: state.nestedKeyCount,
1257
+ estimatedBytes: estimatedJsonBytes(input),
1258
+ sha256: safeHashValue(input),
1259
+ fields: fieldSummary,
1260
+ redactions: Array.from(new Set(state.redactions))
1261
+ };
1262
+ }
1263
+
1264
+ function summarizeResponseForLog(payload = {}) {
1265
+ const { input, state, topLevelKeys, fieldSummary } = summarizePayloadFieldsForLog(
1266
+ payload,
1267
+ 'response summarized; nested values omitted by default'
1268
+ );
1269
+ return {
1270
+ topLevelKeys,
1271
+ nestedKeyCount: state.nestedKeyCount,
1272
+ estimatedBytes: estimatedJsonBytes(input),
1273
+ sha256: safeHashValue(input),
1274
+ fields: fieldSummary,
1275
+ redactions: Array.from(new Set(state.redactions))
1276
+ };
1277
+ }
1278
+
1279
+ function unsafePayloadLoggingEnabled() {
1280
+ return process.env.REMITS_CLI_UNSAFE_LOG_PAYLOADS === '1';
1281
+ }
1282
+
1283
+ function requestPayloadForSessionLog(payload) {
1284
+ if (unsafePayloadLoggingEnabled()) {
1285
+ return sanitizeForLog(payload);
1286
+ }
1287
+ return undefined;
1288
+ }
1289
+
778
1290
  function appendSessionLog(cwd, entry) {
779
1291
  const file = sessionJsonlFile(cwd);
780
1292
  fs.appendFileSync(file, JSON.stringify(entry) + '\n');
@@ -866,13 +1378,24 @@ function toolEvidenceSummary(data = {}, responseFile) {
866
1378
  function readToolResponseFile(cwd, callId) {
867
1379
  if (!callId) return null;
868
1380
  const paths = ensureLocalState(cwd);
869
- const file = path.join(paths.toolResponsesDir, callId + '.json');
870
- if (!fs.existsSync(file)) return null;
871
- try {
872
- return JSON.parse(fs.readFileSync(file, 'utf8'));
873
- } catch (_) {
874
- return null;
1381
+ const candidates = [
1382
+ path.join(paths.toolResponsesDir, callId + '.json'),
1383
+ path.join(paths.legacy.toolResponsesDir, callId + '.json')
1384
+ ];
1385
+ for (const file of candidates) {
1386
+ if (!fs.existsSync(file)) continue;
1387
+ try {
1388
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1389
+ if (parsed && typeof parsed === 'object' && file.includes(path.sep + 'tool-responses' + path.sep)) {
1390
+ parsed._localStateFile = file;
1391
+ parsed._legacyLocalState = file.startsWith(paths.legacy.toolResponsesDir);
1392
+ }
1393
+ return parsed;
1394
+ } catch (_) {
1395
+ return null;
1396
+ }
875
1397
  }
1398
+ return null;
876
1399
  }
877
1400
 
878
1401
  function verificationPaths(cwd, envelopeId) {
@@ -888,24 +1411,51 @@ function verificationPaths(cwd, envelopeId) {
888
1411
  };
889
1412
  }
890
1413
 
1414
+ // The active envelope is bound to the checkout world an agent WORKS in: host, account, branch, workspace - the
1415
+ // same identity as its staging lane. The data lane is deliberately NOT part of it. Commands resolve their lane
1416
+ // differently (`test run` defaults to test; stage/token/tool/sync follow the session), so a lane in this key made
1417
+ // one agent see a different envelope - or none - from one command to the next: evidence silently never attached,
1418
+ // and `verify report` answered "No verification envelope selected" for the envelope it had just started. Whether
1419
+ // a command's lane fits the envelope is the platform's call (`verificationPreflight`), and it is made out loud.
891
1420
  function verificationContextKey(context = {}) {
892
1421
  const baseUrl = normalizeBaseUrl(context.baseUrl || context.host || DEFAULT_BASE_URL);
893
1422
  const accountId = context.accountId != null ? String(context.accountId) : 'unknown-account';
894
1423
  const branchName = context.branchName || context.branch || 'unknown-branch';
895
1424
  const workspace = normalizeWorkspace(context.workspace) || 'shared';
896
- const dataMode = normalizeDataMode(context.dataMode);
897
- return [baseUrl, accountId, branchName, workspace, dataMode].join('|');
1425
+ return [baseUrl, accountId, branchName, workspace].join('|');
1426
+ }
1427
+
1428
+ // Pointers written while the data lane was still part of the key end in `|test` / `|prod`. They name the same
1429
+ // checkout world, so read them (newest first) rather than orphan an envelope an older CLI selected.
1430
+ function legacyVerificationContextKeys(key) {
1431
+ return [key + '|test', key + '|prod'];
1432
+ }
1433
+
1434
+ function activeVerificationContextEntry(contexts, key) {
1435
+ const normalize = (entry) => (typeof entry === 'string' ? { envelopeId: entry } : entry);
1436
+ const current = contexts && contexts[key];
1437
+ if (current) return normalize(current);
1438
+ const legacy = legacyVerificationContextKeys(key)
1439
+ .map((legacyKey) => contexts && contexts[legacyKey])
1440
+ .filter((entry) => entry && (typeof entry === 'string' || entry.envelopeId))
1441
+ .map(normalize)
1442
+ .sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || '')));
1443
+ return legacy[0] || null;
898
1444
  }
899
1445
 
900
1446
  function readActiveVerificationContexts(cwd) {
901
- const file = localStatePaths(cwd).activeVerificationContextsFile;
902
- if (!fs.existsSync(file)) return {};
903
- try {
904
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
905
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
906
- } catch (_) {
907
- return {};
1447
+ const paths = localStatePaths(cwd);
1448
+ const candidates = [paths.activeVerificationContextsFile, paths.legacy.activeVerificationContextsFile];
1449
+ for (const file of candidates) {
1450
+ if (!fs.existsSync(file)) continue;
1451
+ try {
1452
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1453
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
1454
+ } catch (_) {
1455
+ return {};
1456
+ }
908
1457
  }
1458
+ return {};
909
1459
  }
910
1460
 
911
1461
  function writeActiveVerificationContexts(cwd, contexts) {
@@ -925,20 +1475,21 @@ function activeVerificationContext(cwd, flags = {}, session = null, accountId =
925
1475
 
926
1476
  function readActiveVerificationEnvelope(cwd, context = null) {
927
1477
  if (context) {
928
- const contexts = readActiveVerificationContexts(cwd);
929
- const entry = contexts[verificationContextKey(context)];
930
- if (entry && typeof entry === 'object') return entry.envelopeId || null;
931
- if (typeof entry === 'string') return entry || null;
932
- return null;
1478
+ const entry = activeVerificationContextEntry(readActiveVerificationContexts(cwd), verificationContextKey(context));
1479
+ return entry && entry.envelopeId ? String(entry.envelopeId) : null;
933
1480
  }
934
- const file = verificationPaths(cwd).activeFile;
935
- try {
936
- if (!fs.existsSync(file)) return null;
937
- const id = fs.readFileSync(file, 'utf8').trim();
938
- return id || null;
939
- } catch (_) {
940
- return null;
1481
+ const paths = verificationPaths(cwd);
1482
+ const candidates = [paths.activeFile, localStatePaths(cwd).legacy.activeVerificationFile];
1483
+ for (const file of candidates) {
1484
+ try {
1485
+ if (!fs.existsSync(file)) continue;
1486
+ const id = fs.readFileSync(file, 'utf8').trim();
1487
+ return id || null;
1488
+ } catch (_) {
1489
+ return null;
1490
+ }
941
1491
  }
1492
+ return null;
942
1493
  }
943
1494
 
944
1495
  function writeActiveVerificationEnvelope(cwd, envelopeId, context = null) {
@@ -946,6 +1497,7 @@ function writeActiveVerificationEnvelope(cwd, envelopeId, context = null) {
946
1497
  if (context) {
947
1498
  const contexts = readActiveVerificationContexts(cwd);
948
1499
  const key = verificationContextKey(context);
1500
+ legacyVerificationContextKeys(key).forEach((legacyKey) => { delete contexts[legacyKey]; });
949
1501
  if (!envelopeId) {
950
1502
  delete contexts[key];
951
1503
  } else {
@@ -968,10 +1520,15 @@ function writeActiveVerificationEnvelope(cwd, envelopeId, context = null) {
968
1520
  return String(envelopeId);
969
1521
  }
970
1522
 
1523
+ function explicitVerificationEnvelopeId(flags = {}) {
1524
+ const explicit = flags['verify-envelope'] || flags.verifyEnvelope || flags.envelope;
1525
+ return explicit !== undefined && explicit !== null && explicit !== true ? (String(explicit).trim() || null) : null;
1526
+ }
1527
+
971
1528
  function verificationEnvelopeIdForCommand(cwd, flags = {}, context = null) {
972
1529
  if (flagEnabled(flags['no-verify-envelope']) || flagEnabled(flags.noVerifyEnvelope)) return null;
973
- const explicit = flags['verify-envelope'] || flags.verifyEnvelope || flags.envelope;
974
- if (explicit !== undefined && explicit !== null && explicit !== true) return String(explicit).trim() || null;
1530
+ const explicit = explicitVerificationEnvelopeId(flags);
1531
+ if (explicit) return explicit;
975
1532
  return context ? readActiveVerificationEnvelope(cwd, context) : readActiveVerificationEnvelope(cwd);
976
1533
  }
977
1534
 
@@ -980,7 +1537,10 @@ function writeLocalVerificationEnvelope(cwd, envelope) {
980
1537
  const paths = verificationPaths(cwd, envelope.envelopeId);
981
1538
  ensureDir(paths.base);
982
1539
  ensureDir(paths.packetsDir);
983
- atomicWriteFile(paths.envelopeFile, JSON.stringify(envelope, null, 2));
1540
+ // Attach and status answer with a PACKETLESS header. Layer it over the mirror instead of replacing it, or the
1541
+ // offline fallback loses the packets and evaluation the last full read stored.
1542
+ const mirrored = Array.isArray(envelope.packets) ? envelope : Object.assign({}, readLocalVerificationEnvelope(cwd, envelope.envelopeId) || {}, envelope);
1543
+ atomicWriteFile(paths.envelopeFile, JSON.stringify(mirrored, null, 2));
984
1544
  if (envelope.acceptance) {
985
1545
  atomicWriteFile(paths.manifestFile, JSON.stringify(envelope.acceptance, null, 2));
986
1546
  }
@@ -990,29 +1550,36 @@ function writeLocalVerificationEnvelope(cwd, envelope) {
990
1550
 
991
1551
  function readLocalVerificationEnvelope(cwd, envelopeId) {
992
1552
  const paths = verificationPaths(cwd, envelopeId);
993
- if (!fs.existsSync(paths.envelopeFile)) return null;
994
- try {
995
- return JSON.parse(fs.readFileSync(paths.envelopeFile, 'utf8'));
996
- } catch (_) {
997
- return null;
1553
+ const legacyBase = path.join(localStatePaths(cwd).legacy.verificationDir, String(envelopeId));
1554
+ const candidates = [paths.envelopeFile, path.join(legacyBase, 'envelope.json')];
1555
+ for (const file of candidates) {
1556
+ if (!fs.existsSync(file)) continue;
1557
+ try {
1558
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1559
+ } catch (_) {
1560
+ return null;
1561
+ }
998
1562
  }
1563
+ return null;
999
1564
  }
1000
1565
 
1001
1566
  function readLocalVerificationPackets(cwd, envelopeId, filters = {}) {
1002
1567
  const paths = verificationPaths(cwd, envelopeId);
1568
+ const legacyPacketsDir = path.join(localStatePaths(cwd).legacy.verificationDir, String(envelopeId), 'packets');
1003
1569
  const packets = [];
1004
- if (fs.existsSync(paths.packetsDir)) {
1005
- fs.readdirSync(paths.packetsDir)
1570
+ [paths.packetsDir, legacyPacketsDir].forEach((packetsDir) => {
1571
+ if (!fs.existsSync(packetsDir)) return;
1572
+ fs.readdirSync(packetsDir)
1006
1573
  .filter((name) => name.endsWith('.json'))
1007
1574
  .forEach((name) => {
1008
1575
  try {
1009
- const parsed = JSON.parse(fs.readFileSync(path.join(paths.packetsDir, name), 'utf8'));
1576
+ const parsed = JSON.parse(fs.readFileSync(path.join(packetsDir, name), 'utf8'));
1010
1577
  if (parsed && typeof parsed === 'object') packets.push(parsed);
1011
1578
  } catch (_) {
1012
1579
  // Ignore one malformed local packet; the rest of the envelope remains useful.
1013
1580
  }
1014
1581
  });
1015
- }
1582
+ });
1016
1583
  if (!packets.length) {
1017
1584
  const envelope = readLocalVerificationEnvelope(cwd, envelopeId);
1018
1585
  if (envelope && Array.isArray(envelope.packets)) packets.push(...envelope.packets);
@@ -1144,6 +1711,135 @@ function syncEvidencePayload(sync) {
1144
1711
  return Object.assign({}, sync, { syncResults: trimmedResults });
1145
1712
  }
1146
1713
 
1714
+ // The flag that would put a command into the envelope's world, per mismatched field. Rendering only: WHETHER a
1715
+ // field mismatches is the platform's answer (`compatibility`), never recomputed here.
1716
+ function verificationWorldHints(diffs = []) {
1717
+ return (Array.isArray(diffs) ? diffs : []).map((diff) => {
1718
+ const expected = diff && diff.expected;
1719
+ if (expected === undefined || expected === null || expected === '') return null;
1720
+ switch (diff.field) {
1721
+ case 'dataMode': return '--data-mode ' + expected;
1722
+ case 'workspace': return '--workspace ' + expected;
1723
+ case 'repoAccountId': return '--account-id ' + expected;
1724
+ case 'accountId': return '--as-account ' + expected;
1725
+ case 'componentBranch':
1726
+ case 'variantBranch': return '--variant-branch ' + expected;
1727
+ case 'gitBranch':
1728
+ case 'branchName': return 'run from git branch ' + expected;
1729
+ default: return null;
1730
+ }
1731
+ }).filter(Boolean);
1732
+ }
1733
+
1734
+ function verificationDiffSummary(diffs = []) {
1735
+ return (Array.isArray(diffs) ? diffs : []).map((diff) =>
1736
+ diff.field + ': envelope requires ' + diff.expected + ', this command has ' + (diff.command == null ? 'none (shared lane)' : diff.command)).join('; ');
1737
+ }
1738
+
1739
+ function verificationRefusalLines(result = {}, commandLabel = 'this command') {
1740
+ const lines = [result.message || ('Evidence from ' + commandLabel + ' would not count for verification envelope ' + result.envelopeId + '.')];
1741
+ const hints = verificationWorldHints(result.diffs);
1742
+ lines.push('Nothing ran. Pick one and re-run ' + commandLabel + ':');
1743
+ if (hints.length) lines.push(" - prove it in the envelope's world: " + hints.join(' '));
1744
+ lines.push(' - run it without recording evidence: --no-verify-envelope');
1745
+ lines.push(' - it is different work: remits-cli verify start --summary "..." (selects a new envelope for this checkout)');
1746
+ lines.push(' - record it anyway as context (reported wrong-world): --allow-wrong-world-evidence');
1747
+ return lines;
1748
+ }
1749
+
1750
+ function flagsWithoutVerificationEnvelope(flags = {}) {
1751
+ return Object.assign({}, flags, { 'no-verify-envelope': true });
1752
+ }
1753
+
1754
+ // Asks the platform - by the evaluator's own world rule - whether evidence from this command would count for the
1755
+ // selected envelope, BEFORE the command runs. The CLI used to answer this with its own copy of the rule, which had
1756
+ // drifted (shared lane vs a named workspace, the execution account, packet-type exemptions) and so blessed
1757
+ // evidence the report then rejected.
1758
+ //
1759
+ // `probe` is `{ packetType, world, test? }`, with world keys as a packet records them. Resolves to
1760
+ // `{ envelopeId, verdict, evidenceFlags }`: pass `evidenceFlags` to every appendVerificationPacket of the command.
1761
+ // attach - same world: evidence attaches to exactly this envelope (pinned by id, no second lookup).
1762
+ // refuse - a requirement could use this packet but the world differs: THROWS a refusal, nothing runs.
1763
+ // detach - differs, but nothing in the manifest could use it (or the envelope is closed): runs, attaches
1764
+ // nothing, and says so once. Refusing there would cost the agent a turn and protect nothing.
1765
+ // Idempotent within one invocation: a caller that already asked passes the answer in `__verificationPreflight`.
1766
+ async function verificationPreflight(api, cwd, session, accountId, flags = {}, probe = {}, options = {}) {
1767
+ if (flags.__verificationPreflight) return flags.__verificationPreflight;
1768
+ const label = options.command || 'this command';
1769
+ const context = activeVerificationContext(cwd, flags, session, accountId);
1770
+ const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
1771
+ if (!envelopeId) return { envelopeId: null, verdict: 'none', evidenceFlags: flags };
1772
+ const pinned = Object.assign({}, flags, { 'verify-envelope': envelopeId });
1773
+ let result;
1774
+ try {
1775
+ result = await postVerificationCommand(api, cwd, session, accountId, 'compatibility', { envelopeId, probe });
1776
+ } catch (err) {
1777
+ const status = err && err.response && err.response.status;
1778
+ if (status === 404) {
1779
+ if (explicitVerificationEnvelopeId(flags)) {
1780
+ const refusal = new Error('Verification envelope ' + envelopeId + ' does not exist on ' + context.baseUrl +
1781
+ '. Run `remits-cli verify list` to find the right id, or drop --verify-envelope.');
1782
+ refusal.refused = true;
1783
+ throw refusal;
1784
+ }
1785
+ console.error('Verification envelope ' + envelopeId + ' (selected for this checkout) no longer exists; evidence is not attached. ' +
1786
+ 'Run `remits-cli verify list`, then `verify use <id>` or `verify start`.');
1787
+ return { envelopeId: null, verdict: 'detach', evidenceFlags: flagsWithoutVerificationEnvelope(flags) };
1788
+ }
1789
+ // A platform without `compatibility`, or one that is unreachable: attach as before. The report still judges
1790
+ // the world; this only loses the early answer.
1791
+ return { envelopeId, verdict: 'unchecked', evidenceFlags: pinned };
1792
+ }
1793
+ const verdict = result.verdict === 'refuse' && options.neverRefuse ? 'detach' : (result.verdict || 'attach');
1794
+ const allowWrongWorld = flagEnabled(flags['allow-wrong-world-evidence']) || flagEnabled(flags.allowWrongWorldEvidence);
1795
+ if (verdict === 'refuse' && !allowWrongWorld) {
1796
+ const refusal = new Error(verificationRefusalLines(result, label).join('\n'));
1797
+ refusal.refused = true;
1798
+ refusal.compatibility = result;
1799
+ throw refusal;
1800
+ }
1801
+ if (verdict === 'refuse') {
1802
+ console.error('Attaching wrong-world evidence as context (--allow-wrong-world-evidence): ' + result.message);
1803
+ return { envelopeId, verdict, compatibility: result, evidenceFlags: pinned };
1804
+ }
1805
+ if (verdict === 'detach') {
1806
+ if (!options.quiet && result.message) {
1807
+ const hints = verificationWorldHints(result.diffs);
1808
+ console.error('Verification: ' + result.message + (hints.length ? ' (It would attach, as context only, with: ' + hints.join(' ') + ')' : ''));
1809
+ }
1810
+ return { envelopeId: null, verdict, compatibility: result, evidenceFlags: flagsWithoutVerificationEnvelope(flags) };
1811
+ }
1812
+ return { envelopeId, verdict, compatibility: result, evidenceFlags: pinned };
1813
+ }
1814
+
1815
+ // "Would a plain `test run` from this checkout count for this envelope?" - printed where an agent selects or
1816
+ // inspects an envelope, so a mismatch is learned before the first run rather than from a refusal after it.
1817
+ async function printVerificationRunProbe(api, cwd, session, accountId, envelopeId, world = {}, options = {}) {
1818
+ if (!envelopeId) return null;
1819
+ let result;
1820
+ try {
1821
+ result = await postVerificationCommand(api, cwd, session, accountId, 'compatibility', {
1822
+ envelopeId,
1823
+ probe: { packetType: 'test_run', world }
1824
+ });
1825
+ } catch (_) {
1826
+ return null;
1827
+ }
1828
+ if (options.onlyMismatch && result.verdict === 'attach') return result;
1829
+ if (result.comparisonWorld) {
1830
+ console.log('Proof world (' + (result.comparisonWorldSource || 'unknown') + '):', JSON.stringify(result.comparisonWorld));
1831
+ }
1832
+ if (result.verdict === 'attach') {
1833
+ console.log('A `test run` from this checkout (dataMode=' + (world.dataMode || DEFAULT_DATA_MODE) + ') attaches to this envelope.');
1834
+ } else if (result.verdict === 'refuse' || result.verdict === 'detach') {
1835
+ const hints = verificationWorldHints(result.diffs);
1836
+ console.log('A `test run` from this checkout (dataMode=' + (world.dataMode || DEFAULT_DATA_MODE) + ') would ' +
1837
+ (result.verdict === 'refuse' ? 'be REFUSED' : 'NOT attach') + ' - ' + verificationDiffSummary(result.diffs) + '.');
1838
+ if (hints.length) console.log(' to prove it for this envelope, pass: ' + hints.join(' '));
1839
+ }
1840
+ return result;
1841
+ }
1842
+
1147
1843
  async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
1148
1844
  const context = activeVerificationContext(cwd, flags, session, accountId);
1149
1845
  const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
@@ -1159,11 +1855,22 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
1159
1855
  try {
1160
1856
  const response = await postVerificationCommand(api, cwd, session, accountId, 'packet', {
1161
1857
  envelopeId,
1162
- packet: finalPacket
1858
+ packet: finalPacket,
1859
+ // Packetless answer: this runs on every command while an envelope is active.
1860
+ compact: true
1163
1861
  });
1164
1862
  if (response.packet) writeLocalVerificationPacket(cwd, envelopeId, response.packet);
1165
1863
  if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
1166
- if (!options.quiet) console.log('Verification envelope:', envelopeId, '(packet ' + finalPacket.type + ' attached)');
1864
+ if (!options.quiet) {
1865
+ console.log('Verification envelope:', envelopeId, '(packet ' + finalPacket.type + ' attached)');
1866
+ // The platform's own verdict on the stored packet, so this line and `verify report` cannot disagree.
1867
+ const diffs = Array.isArray(response.worldDiffs) ? response.worldDiffs : [];
1868
+ if (diffs.length) {
1869
+ console.log(' WARNING: this packet is from a different world than the envelope requires - ' +
1870
+ diffs.map((diff) => diff.field + ' expected ' + diff.expected + ' got ' + (diff.packet == null ? 'none' : diff.packet)).join('; ') +
1871
+ '. It is attached, but it cannot satisfy a requirement.');
1872
+ }
1873
+ }
1167
1874
  return response;
1168
1875
  } catch (err) {
1169
1876
  if (!options.quiet) {
@@ -2114,11 +2821,75 @@ function resolveSessionContext(cwd, flags) {
2114
2821
  * file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
2115
2822
  * identity is the only thing both the payload and the git changed set agree on.
2116
2823
  */
2117
- function withFileKey(target, key) {
2824
+ function withFileKey(target, key, filePaths) {
2118
2825
  Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
2826
+ if (filePaths) {
2827
+ addHiddenFilePaths(target, filePaths);
2828
+ }
2829
+ return target;
2830
+ }
2831
+
2832
+ function addHiddenFilePaths(target, filePaths) {
2833
+ if (!target || !filePaths) return target;
2834
+ const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
2835
+ if (!Object.prototype.hasOwnProperty.call(target, 'filePaths')) {
2836
+ Object.defineProperty(target, 'filePaths', { value: [], enumerable: false, configurable: true });
2837
+ }
2838
+ for (const filePath of paths) {
2839
+ const normalized = String(filePath || '').replace(/\\/g, '/');
2840
+ if (normalized && !target.filePaths.includes(normalized)) target.filePaths.push(normalized);
2841
+ }
2119
2842
  return target;
2120
2843
  }
2121
2844
 
2845
+ function isRootReadmeComponent(component) {
2846
+ return component &&
2847
+ component.type === 'prompt' &&
2848
+ component.fileKey === 'prompt:name:readme' &&
2849
+ normalizeName(String(component.name || '')).toLowerCase() === 'readme' &&
2850
+ String(component.purpose || '').toUpperCase() === 'README' &&
2851
+ (component.path === 'README.md' || (component.filePaths || []).includes('README.md'));
2852
+ }
2853
+
2854
+ function isLegacyReadmePromptComponent(component) {
2855
+ if (!component || component.type !== 'prompt' ||
2856
+ normalizeName(String(component.name || '')).toLowerCase() !== 'readme') {
2857
+ return false;
2858
+ }
2859
+ return [component.path].concat(component.filePaths || []).some((filePath) =>
2860
+ /^components\/prompts\/[^/]+_README\.md$/i.test(String(filePath || '')));
2861
+ }
2862
+
2863
+ function canonicalizeReadmePromptComponents(components) {
2864
+ const list = Array.isArray(components) ? components : [];
2865
+ const rootReadme = list.find(isRootReadmeComponent);
2866
+ if (!rootReadme) {
2867
+ return { components: list, warnings: [] };
2868
+ }
2869
+
2870
+ const warnings = [];
2871
+ const canonical = [];
2872
+ for (const component of list) {
2873
+ if (component !== rootReadme && isLegacyReadmePromptComponent(component)) {
2874
+ const legacyPaths = [component.path].concat(component.filePaths || [])
2875
+ .filter((filePath) => /^components\/prompts\/[^/]+_README\.md$/i.test(String(filePath || '')))
2876
+ .sort();
2877
+ warnings.push({
2878
+ type: 'legacy-readme-prompt-suppressed',
2879
+ canonicalPath: 'README.md',
2880
+ legacyPaths,
2881
+ id: component.id == null ? null : component.id,
2882
+ name: component.name || 'README',
2883
+ message: 'Skipped legacy README prompt file ' + (legacyPaths.join(', ') || 'components/prompts/*_README.md') +
2884
+ ' because root README.md owns the reserved README prompt identity.'
2885
+ });
2886
+ continue;
2887
+ }
2888
+ canonical.push(component);
2889
+ }
2890
+ return { components: canonical, warnings };
2891
+ }
2892
+
2122
2893
  function collectComponents(cwd) {
2123
2894
  const mapping = {
2124
2895
  schemas: 'schema',
@@ -2170,6 +2941,7 @@ function collectComponents(cwd) {
2170
2941
  }
2171
2942
  const component = byKey.get(key);
2172
2943
  const filePath = path.join(dir, fileName);
2944
+ addHiddenFilePaths(component, path.relative(cwd, filePath).replace(/\\/g, '/'));
2173
2945
  const content = fs.readFileSync(filePath, 'utf8');
2174
2946
 
2175
2947
  if (ext === 'meta') {
@@ -2233,7 +3005,7 @@ function collectComponents(cwd) {
2233
3005
  category: 'default',
2234
3006
  prompt: fs.readFileSync(rootReadme, 'utf8'),
2235
3007
  metadataAuthoritative: true
2236
- }, 'prompt:name:readme');
3008
+ }, 'prompt:name:readme', 'README.md');
2237
3009
  const fingerprint = {};
2238
3010
  Object.keys(component).sort().forEach((key) => {
2239
3011
  if (key !== 'hash') {
@@ -2244,7 +3016,13 @@ function collectComponents(cwd) {
2244
3016
  components.push(component);
2245
3017
  }
2246
3018
 
2247
- return components;
3019
+ const canonicalized = canonicalizeReadmePromptComponents(components);
3020
+ Object.defineProperty(canonicalized.components, 'discoveryWarnings', {
3021
+ value: canonicalized.warnings,
3022
+ enumerable: false,
3023
+ configurable: true
3024
+ });
3025
+ return canonicalized.components;
2248
3026
  }
2249
3027
 
2250
3028
  function componentPathInfo(cwd, filePath) {
@@ -2534,6 +3312,11 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
2534
3312
  let responseData = null;
2535
3313
  let error = null;
2536
3314
  let responseFile = null;
3315
+ const actor = resolveLocalActor();
3316
+ const unsafePayloadLog = unsafePayloadLoggingEnabled();
3317
+ if (unsafePayloadLog && !options.skipSessionLog) {
3318
+ console.error('WARNING: REMITS_CLI_UNSAFE_LOG_PAYLOADS=1 is enabled; session logs may contain request payload values.');
3319
+ }
2537
3320
 
2538
3321
  try {
2539
3322
  responseData = await api.post(endpoint, payload).then((r) => r.data);
@@ -2551,15 +3334,19 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
2551
3334
  const status = error ? 'error' : 'success';
2552
3335
  const responseForLog = responseFile
2553
3336
  ? { responseFile, note: 'Tool response stored externally' }
2554
- : sanitizeForLog(responseData);
3337
+ : summarizeResponseForLog(responseData);
2555
3338
  appendSessionLog(cwd, {
2556
3339
  ts: new Date().toISOString(),
2557
3340
  requestId,
2558
3341
  endpoint,
2559
3342
  method: 'POST',
3343
+ actor: actor.id,
3344
+ actorSource: actor.source,
2560
3345
  status,
2561
3346
  durationMs: Date.now() - started,
2562
- request: sanitizeForLog(payload),
3347
+ requestSummary: summarizeRequestForLog(payload),
3348
+ unsafePayloadLogging: unsafePayloadLog,
3349
+ request: requestPayloadForSessionLog(payload),
2563
3350
  response: responseForLog,
2564
3351
  error: error ? describeError(error) : null
2565
3352
  });
@@ -2572,6 +3359,7 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
2572
3359
  const started = Date.now();
2573
3360
  let responseData = null;
2574
3361
  let error = null;
3362
+ const actor = resolveLocalActor();
2575
3363
  try {
2576
3364
  responseData = await api.get(endpoint, { params }).then((r) => r.data);
2577
3365
  return { data: responseData, requestId };
@@ -2585,10 +3373,13 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
2585
3373
  requestId,
2586
3374
  endpoint,
2587
3375
  method: 'GET',
3376
+ actor: actor.id,
3377
+ actorSource: actor.source,
2588
3378
  status: error ? 'error' : 'success',
2589
3379
  durationMs: Date.now() - started,
2590
- request: sanitizeForLog(params),
2591
- response: sanitizeForLog(responseData),
3380
+ requestSummary: summarizeRequestForLog(params),
3381
+ request: requestPayloadForSessionLog(params),
3382
+ response: summarizeResponseForLog(responseData),
2592
3383
  error: error ? describeError(error) : null
2593
3384
  });
2594
3385
  }
@@ -3128,6 +3919,7 @@ async function pushComponentsCommand(flags) {
3128
3919
  const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
3129
3920
 
3130
3921
  let components = collectComponents(cwd);
3922
+ const componentDiscoveryWarnings = Array.isArray(components.discoveryWarnings) ? components.discoveryWarnings : [];
3131
3923
  // Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
3132
3924
  const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
3133
3925
  // Changes git reported that a component payload cannot carry — a deleted component file has nothing to
@@ -3187,7 +3979,9 @@ async function pushComponentsCommand(flags) {
3187
3979
  : null,
3188
3980
  changedFromWorkingTree: [],
3189
3981
  changedFromWorkingTreeAvailable: true,
3190
- unrepresentableChanges: unstageable
3982
+ unrepresentableChanges: unstageable,
3983
+ componentDiscoveryWarnings,
3984
+ localState: localStateSummary(cwd, flags)
3191
3985
  };
3192
3986
  if (flagEnabled(flags.json)) {
3193
3987
  console.log(JSON.stringify(emptyResponse, null, 2));
@@ -3199,6 +3993,9 @@ async function pushComponentsCommand(flags) {
3199
3993
  console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
3200
3994
  }
3201
3995
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
3996
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
3997
+ printLocalStateWarnings(cwd, flags);
3998
+ printComponentDiscoveryWarnings(componentDiscoveryWarnings);
3202
3999
  printUnrepresentableChanges(unstageable);
3203
4000
  }
3204
4001
  return emptyResponse;
@@ -3216,6 +4013,10 @@ async function pushComponentsCommand(flags) {
3216
4013
  // or, worse, conclude staging was broken and go on reading trunk. Scaled by payload size, floored at
3217
4014
  // the old default, and overridable.
3218
4015
  const api = buildAxios(baseUrl, session.token, stageTimeoutMs(components, flags));
4016
+ const verification = await verificationPreflight(api, cwd, session, accountId, flags, {
4017
+ packetType: 'stage',
4018
+ world: { accountId, dataMode, branchName, workspace }
4019
+ }, { command: 'components stage' });
3219
4020
  const stagePayload = {
3220
4021
  token: session.token,
3221
4022
  accountId,
@@ -3259,7 +4060,7 @@ async function pushComponentsCommand(flags) {
3259
4060
  // the platform rejected it. The packet is best-effort: a failing stage must surface the STAGE error,
3260
4061
  // never an error from writing the record of it.
3261
4062
  try {
3262
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
4063
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
3263
4064
  type: 'stage',
3264
4065
  success: false,
3265
4066
  claim: 'Components refused at staging',
@@ -3287,6 +4088,8 @@ async function pushComponentsCommand(flags) {
3287
4088
  response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
3288
4089
  response.requestedStageMode = stageMode;
3289
4090
  response.unrepresentableChanges = unstageable;
4091
+ response.componentDiscoveryWarnings = componentDiscoveryWarnings;
4092
+ response.localState = localStateSummary(cwd, flags);
3290
4093
  if (response.success === false) {
3291
4094
  // A 200 that says `success:false`. The 422 path throws out of stageOrRefuse above and never lands
3292
4095
  // here, so this stays as the belt-and-braces case — print whatever detail came with it first.
@@ -3294,7 +4097,7 @@ async function pushComponentsCommand(flags) {
3294
4097
  throw new Error(response.message || 'Server stage failed');
3295
4098
  }
3296
4099
 
3297
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
4100
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
3298
4101
  type: 'stage',
3299
4102
  success: response.success !== false,
3300
4103
  claim: 'Components staged for verification',
@@ -3320,6 +4123,9 @@ async function pushComponentsCommand(flags) {
3320
4123
 
3321
4124
  printSessionResolutionWarning(sessionContext);
3322
4125
  printResolvedBaseUrl(baseUrl);
4126
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
4127
+ printLocalStateWarnings(cwd, flags);
4128
+ printComponentDiscoveryWarnings(componentDiscoveryWarnings);
3323
4129
  printStagingLane(response.branchName || branchName, response.workspace || workspace, workspaceSource(cwd, flags));
3324
4130
  console.log('Data mode:', response.dataMode || dataMode);
3325
4131
  printComponentPolicy(response);
@@ -3522,6 +4328,15 @@ function printComponentPolicy(response) {
3522
4328
  (policy.overrides || []).forEach((id) => console.log('Policy override recorded for rule: ' + id));
3523
4329
  }
3524
4330
 
4331
+ function printComponentDiscoveryWarnings(warnings) {
4332
+ const list = Array.isArray(warnings) ? warnings : [];
4333
+ if (!list.length) return;
4334
+ console.log('Component discovery warnings:');
4335
+ list.forEach((warning) => {
4336
+ console.log(' - ' + (warning.message || JSON.stringify(warning)));
4337
+ });
4338
+ }
4339
+
3525
4340
  /**
3526
4341
  * What the platform compiled, and what it could not.
3527
4342
  *
@@ -4020,6 +4835,8 @@ function printBranchContext(response) {
4020
4835
  ('this account resolves the branch through an ancestor owned by account ' + ctx.resolvingOwnerAccountId + '.')));
4021
4836
  }
4022
4837
 
4838
+ variantBranchIdentityLines(response).forEach((line) => console.log(line));
4839
+
4023
4840
  if (!ctx.onTrunk) {
4024
4841
  console.log(' variants stored on this branch: ' + (ctx.variantCount || 0));
4025
4842
  const subs = ctx.subscribers || [];
@@ -4033,6 +4850,29 @@ function printBranchContext(response) {
4033
4850
  console.log('');
4034
4851
  }
4035
4852
 
4853
+ // A variant branch whose checkout identifies as its OWNER (account-info.json names the owner) runs every command as
4854
+ // the owner, and the owner subscribes to nothing — so a feature branch cut from this checkout resolves TRUNK, and no
4855
+ // run exercises what the subscriber actually gets. With exactly one subscriber that identity is repairable, and the
4856
+ // agent should hear it here rather than infer it from a page that rendered trunk. Pure.
4857
+ function variantBranchIdentityLines(response) {
4858
+ const ctx = response && response.branchContext;
4859
+ if (!ctx || ctx.onTrunk || ctx.variantBranchSource === 'subscription-fallback') return [];
4860
+ const subs = Array.isArray(ctx.subscribers) ? ctx.subscribers : [];
4861
+ const checkoutAccountId = response.accountId;
4862
+ if (subs.length !== 1 || checkoutAccountId == null || String(subs[0].accountId) === String(checkoutAccountId)) return [];
4863
+ if (ctx.commitOwnerAccountId != null && String(ctx.commitOwnerAccountId) !== String(checkoutAccountId)) return [];
4864
+ const sub = subs[0];
4865
+ const label = sub.accountId + (sub.accountName ? ' (' + sub.accountName + ')' : '');
4866
+ return [
4867
+ '',
4868
+ ' CHECKOUT IDENTITY: this branch has one subscriber, account ' + label + ', but this checkout runs as the owner,',
4869
+ ' account ' + checkoutAccountId + ' (account-info.json names it). Feature branches cut from here run as ' + checkoutAccountId +
4870
+ ' too, which subscribes to nothing, so they resolve trunk.',
4871
+ ' run as the subscriber: --as-account ' + sub.accountId,
4872
+ ' repair the identity: remits-cli components sync --account-id ' + sub.accountId + ' then git pull --ff-only'
4873
+ ];
4874
+ }
4875
+
4036
4876
  function printClearSummary(response, flags) {
4037
4877
  console.log('Cleared keys:', response.clearedCount || 0);
4038
4878
  console.log('Remaining staged count:', response.remainingCount || 0);
@@ -4091,13 +4931,83 @@ async function workspaceCommand(flags) {
4091
4931
  const branchName = flags.branch || currentBranch(cwd);
4092
4932
  const workspace = resolveWorkspace(cwd, flags);
4093
4933
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
4934
+ printLocalActor(cwd, flags);
4935
+ printLocalStateWarnings(cwd, flags);
4936
+ console.log('');
4937
+ console.log('Precedence: --workspace > REMITS_WORKSPACE > .remits-cli/workspace > shared default lane');
4938
+ console.log('');
4939
+ console.log(' remits-cli workspace use <name> set this checkout\'s lane');
4940
+ console.log(' remits-cli workspace use --auto name it after this directory (' + normalizeWorkspace(path.basename(cwd)) + ')');
4941
+ console.log(' remits-cli workspace clear go back to the shared default lane');
4942
+ console.log(' remits-cli components status see every lane staged on this branch');
4943
+ }
4944
+
4945
+ function printFileRows(label, rows, max = 5) {
4946
+ console.log(label + ':', rows.length);
4947
+ rows.slice(0, max).forEach((row) => {
4948
+ console.log(' - ' + row.path + ' (' + row.size + ' bytes' + (row.updatedAt ? ', updated ' + row.updatedAt : '') + ')');
4949
+ });
4950
+ if (rows.length > max) {
4951
+ console.log(' ...' + (rows.length - max) + ' more');
4952
+ }
4953
+ }
4954
+
4955
+ async function doctorCommand(flags, subcommand) {
4956
+ const sub = String(subcommand || (flags._ && flags._[1]) || 'local-state').toLowerCase();
4957
+ if (sub !== 'local-state' && sub !== 'state') {
4958
+ throw new Error('Unknown doctor subcommand "' + sub + '". Expected local-state.');
4959
+ }
4960
+ const cwd = process.cwd();
4961
+ if (flagEnabled(flags['prune-actors'])) {
4962
+ const pruned = pruneStaleActorDirectories(cwd);
4963
+ console.log('Pruned ' + pruned.removed.length + ' idle actor dir(s)' +
4964
+ (pruned.kept ? ', kept ' + pruned.kept + ' (current or active in the last 7 days)' : '') + '.');
4965
+ pruned.removed.slice(0, 20).forEach((name) => console.log(' - ' + name));
4966
+ if (pruned.removed.length > 20) console.log(' ...' + (pruned.removed.length - 20) + ' more');
4967
+ if (pruned.failed.length) console.log(' Could not remove: ' + pruned.failed.join(', '));
4968
+ }
4969
+ const { report, warnings } = localStateWarnings(cwd, flags);
4970
+ if (flagEnabled(flags.json)) {
4971
+ console.log(JSON.stringify(report, null, 2));
4972
+ return report;
4973
+ }
4974
+ console.log('Local state doctor');
4975
+ console.log('Working directory:', cwd);
4976
+ console.log('Local actor:', report.actor.id + ' (from ' + report.actor.source + ')');
4977
+ console.log('Active actor state:', report.activeActorStateDir);
4978
+ console.log('Workspace:', report.workspace || 'shared default lane', ' [' + report.workspaceSource + ']');
4979
+ console.log('Workspace file:', report.workspaceFile);
4980
+ if (warnings.length) {
4981
+ console.log('');
4982
+ console.log('Warnings:');
4983
+ warnings.forEach((warning) => console.log(' - ' + warning));
4984
+ }
4985
+ console.log('');
4986
+ console.log('Actor directories:', report.actors.length);
4987
+ const listedActors = report.actors
4988
+ .slice()
4989
+ .sort((a, b) => (a.current ? -1 : b.current ? 1 : Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0)));
4990
+ listedActors.slice(0, 15).forEach((actor) => {
4991
+ console.log(' ' + (actor.current ? '* ' : ' ') + actor.actor + ' ' + actor.path + (actor.updatedAt ? ' updated ' + actor.updatedAt : ''));
4992
+ });
4993
+ if (listedActors.length > 15) console.log(' ...' + (listedActors.length - 15) + ' more (newest first; --prune-actors clears the idle ones)');
4994
+ console.log(' (* = active actor)');
4094
4995
  console.log('');
4095
- console.log('Precedence: --workspace > REMITS_WORKSPACE > .remits-cli/workspace > shared default lane');
4996
+ console.log('.remits-cli tracked by git:', report.trackedFiles.length ? 'YES' : 'no');
4997
+ report.trackedFiles.slice(0, 20).forEach((file) => console.log(' - ' + file));
4998
+ if (report.trackedFiles.length > 20) console.log(' ...' + (report.trackedFiles.length - 20) + ' more');
4096
4999
  console.log('');
4097
- console.log(' remits-cli workspace use <name> set this checkout\'s lane');
4098
- console.log(' remits-cli workspace use --auto name it after this directory (' + normalizeWorkspace(path.basename(cwd)) + ')');
4099
- console.log(' remits-cli workspace clear go back to the shared default lane');
4100
- console.log(' remits-cli components status see every lane staged on this branch');
5000
+ printFileRows('Current actor session logs', report.current.sessionLogs);
5001
+ printFileRows('Current actor tool responses', report.current.toolResponses);
5002
+ printFileRows('Legacy session logs', report.legacy.sessionLogs);
5003
+ printFileRows('Legacy tool responses', report.legacy.toolResponses);
5004
+ printFileRows('Largest local-state files', report.largestFiles, 10);
5005
+ const suspicious = report.current.likelyRawPayloadLogs.concat(report.legacy.likelyRawPayloadLogs);
5006
+ if (suspicious.length) {
5007
+ console.log('');
5008
+ printFileRows('Logs with raw/unsafe payload-shaped fields', suspicious, 10);
5009
+ }
5010
+ return report;
4101
5011
  }
4102
5012
 
4103
5013
  async function statusComponentsCommand(flags) {
@@ -4140,6 +5050,7 @@ async function statusComponentsCommand(flags) {
4140
5050
  isAncestor: (a, b) => gitIsAncestor(cwd, a, b)
4141
5051
  });
4142
5052
  response.repositoryCheck = repositoryCheck(cwd, response.branchContext);
5053
+ response.localState = localStateSummary(cwd, flags);
4143
5054
 
4144
5055
  await appendVerificationPacket(api, cwd, session, accountId, flags, {
4145
5056
  type: 'component_status',
@@ -4159,6 +5070,8 @@ async function statusComponentsCommand(flags) {
4159
5070
 
4160
5071
  printSessionResolutionWarning(sessionContext);
4161
5072
  printResolvedBaseUrl(baseUrl);
5073
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
5074
+ printLocalStateWarnings(cwd, flags);
4162
5075
  console.log('Data mode:', response.dataMode || dataMode);
4163
5076
  console.log('Mode:', response.mode || 'status');
4164
5077
  printStatusSummary(response, flags);
@@ -4376,6 +5289,11 @@ async function syncComponentsCommand(rawFlags) {
4376
5289
  const preflightRequested = syncPreflightRequested(flags);
4377
5290
  const namesOnly = flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly);
4378
5291
 
5292
+ const verification = await verificationPreflight(api, cwd, session, accountId, flags, {
5293
+ packetType: dryRun || namesOnly ? 'sync_dry_run' : 'sync_mutation',
5294
+ world: { accountId, dataMode, branchName, workspace }
5295
+ }, { command: 'components sync' });
5296
+
4379
5297
  printProdDataBanner({
4380
5298
  dataMode,
4381
5299
  accountId,
@@ -4587,7 +5505,7 @@ async function syncComponentsCommand(rawFlags) {
4587
5505
  summary.gates = gate.checks;
4588
5506
  summary.gateViolations = gate.violations;
4589
5507
 
4590
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
5508
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
4591
5509
  type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
4592
5510
  success: response.success !== false,
4593
5511
  claim: response.sync && response.sync.dryRun ? 'Component sync dry-run observed' : 'Component sync completed',
@@ -5812,10 +6730,21 @@ function short(sha) {
5812
6730
  return sha ? String(sha).slice(0, 8) : 'unknown';
5813
6731
  }
5814
6732
 
6733
+ // A component signature is structured, not a bare hash: 'version:12:ab34cd', 'content:<sha>', 'cli:<hash>'.
6734
+ // Truncating the whole thing to 8 characters rendered the commonest case as the literal string 'version:' —
6735
+ // the prefix exactly fills the budget and the part that identifies the code is what got cut.
6736
+ function shortSignature(signature) {
6737
+ if (!signature) return 'unknown';
6738
+ const parts = String(signature).split(':');
6739
+ if (parts.length < 2) return short(signature);
6740
+ const last = parts.pop();
6741
+ return parts.concat(short(last)).join(':');
6742
+ }
6743
+
5815
6744
  async function commitComponentsCommand(flags) {
5816
6745
  const cwd = process.cwd();
5817
6746
  ensureLocalState(cwd);
5818
- const { accountId } = resolveSessionContext(cwd, flags);
6747
+ const { session, accountId } = resolveSessionContext(cwd, flags);
5819
6748
  const branchName = flags.branch || currentBranch(cwd);
5820
6749
  const commitMessage = String(flags.message || ('remits-cli commit sync ' + new Date().toISOString()));
5821
6750
  const allowEmpty = flags['allow-empty'] === true || flags['allow-empty'] === 'true';
@@ -5828,6 +6757,15 @@ async function commitComponentsCommand(flags) {
5828
6757
  if (await refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, skipGit)) {
5829
6758
  return;
5830
6759
  }
6760
+ // The sync's verification answer, asked BEFORE `git push`. Asked inside the sync it could refuse only after the
6761
+ // push, leaving a pushed-but-unsynced branch - the one state this command exists to avoid. The sync below
6762
+ // reuses this answer instead of asking again.
6763
+ const commitVerification = await verificationPreflight(
6764
+ buildAxios(flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL, session.token),
6765
+ cwd, session, accountId, Object.assign({}, flags, { branch: branchName }), {
6766
+ packetType: 'sync_mutation',
6767
+ world: { accountId, dataMode: resolveDataMode(flags, session), branchName, workspace: resolveWorkspace(cwd, flags) }
6768
+ }, { command: 'components commit' });
5831
6769
 
5832
6770
  // Landing is serial and nothing can make it concurrent: `git add -A` sweeps a shared checkout, and the
5833
6771
  // platform pushes a regenerated `account-info.json` back to the branch during sync, so two commits
@@ -5923,7 +6861,8 @@ async function commitComponentsCommand(flags) {
5923
6861
  const syncResponse = await withStdoutRoutedToStderr(jsonOutput, () => syncComponentsCommand({
5924
6862
  ...flags,
5925
6863
  branch: branchName,
5926
- 'account-id': accountId
6864
+ 'account-id': accountId,
6865
+ __verificationPreflight: commitVerification
5927
6866
  }));
5928
6867
  outcome.sync = syncResponse && flagEnabled(flags.summary) ? buildSyncSummary(syncResponse) : (syncResponse || null);
5929
6868
  // Stop here when the sync did not succeed. In --json mode a refused sync gate RETURNS (with a non-zero
@@ -6363,7 +7302,12 @@ function printTestRunPivots(status = {}) {
6363
7302
  }
6364
7303
  tests.filter((test) => test && test.passed === false).forEach((test) => {
6365
7304
  console.log('Pivots for failed case:', test.name || '(unnamed)');
7305
+ if (test.outcome) console.log(' Outcome:', test.outcome + (test.outcomeReason && test.outcomeReason !== test.error ? ' - ' + String(test.outcomeReason).slice(0, 300) : ''));
6366
7306
  if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
7307
+ if (test.ai && (test.ai.calls || test.ai.liveCalls)) {
7308
+ console.log(' AI:', (test.ai.liveCalls || 0) + ' live ' + formatUsd(test.ai.cost) + ', ' + (test.ai.mockedCalls || 0) + ' mocked' +
7309
+ ((test.ai.errors || []).length ? ', errors: ' + test.ai.errors.map((e) => (e.classification || 'error') + ' ' + (e.model || '')).join('; ') : ''));
7310
+ }
6367
7311
  if (test.threadGroupingId || test.threadGroupId || test.traceId) {
6368
7312
  console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
6369
7313
  }
@@ -6379,7 +7323,8 @@ function printTestRunPivots(status = {}) {
6379
7323
  (entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
6380
7324
  }
6381
7325
  if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
6382
- console.log(' Live HTTP calls:', test.liveHttpCalls.length);
7326
+ const intentional = test.liveHttpCalls.filter((call) => call && call.intentional === true).length;
7327
+ console.log(' Live HTTP calls:', test.liveHttpCalls.length + (intentional ? ' (' + intentional + ' intentional, inside withAiBudget)' : ''));
6383
7328
  }
6384
7329
  });
6385
7330
  }
@@ -6391,6 +7336,276 @@ function formatDurationMs(value) {
6391
7336
  return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
6392
7337
  }
6393
7338
 
7339
+ // A local corpus manifest -> case entries, reading expected/source JSON files and artifact paths RELATIVE to the
7340
+ // manifest. Artifacts are described (path, size) and only base64-encoded at send time, per batch.
7341
+ function corpusManifestEntries(manifest, manifestDir) {
7342
+ const root = manifest && typeof manifest === 'object' ? manifest : {};
7343
+ const cases = Array.isArray(root.cases) ? root.cases : [];
7344
+ const readJson = (file) => JSON.parse(fs.readFileSync(path.resolve(manifestDir, file), 'utf8'));
7345
+ return cases.map((raw, index) => {
7346
+ const entry = raw && typeof raw === 'object' ? raw : {};
7347
+ if (!entry.caseKey) throw new Error('cases[' + index + '] has no caseKey');
7348
+ const out = { caseKey: String(entry.caseKey) };
7349
+ ['split', 'tags', 'expected', 'expectedStatus', 'source', 'notes', 'retired'].forEach((key) => {
7350
+ if (entry[key] !== undefined) out[key] = entry[key];
7351
+ });
7352
+ if (typeof entry.expectedFile === 'string') out.expected = readJson(entry.expectedFile);
7353
+ if (typeof entry.sourceFile === 'string') out.source = readJson(entry.sourceFile);
7354
+ out.artifacts = (Array.isArray(entry.artifacts) ? entry.artifacts : []).map((artifact, artifactIndex) => {
7355
+ const spec = typeof artifact === 'string' ? { path: artifact } : (artifact || {});
7356
+ if (!spec.path) throw new Error('cases[' + index + '].artifacts[' + artifactIndex + '] has no path');
7357
+ const resolved = path.resolve(manifestDir, spec.path);
7358
+ if (!fs.existsSync(resolved)) throw new Error('cases[' + index + '] artifact not found: ' + resolved);
7359
+ return {
7360
+ name: spec.name || (artifactIndex === 0 ? 'source' : path.basename(resolved).replace(/[^A-Za-z0-9._-]+/g, '_')),
7361
+ path: resolved,
7362
+ fileName: spec.fileName || path.basename(resolved),
7363
+ contentType: spec.contentType || undefined,
7364
+ bytes: fs.statSync(resolved).size
7365
+ };
7366
+ });
7367
+ return out;
7368
+ });
7369
+ }
7370
+
7371
+ // Split corpus case entries into requests under the server's per-request limits (artifact bytes and count).
7372
+ function batchCorpusEntries(entries, maxBytes = 40 * 1024 * 1024, maxCount = 100) {
7373
+ const batches = [];
7374
+ let current = [];
7375
+ let currentBytes = 0;
7376
+ (entries || []).forEach((entry) => {
7377
+ const bytes = (entry.artifacts || []).reduce((sum, a) => sum + Number(a.bytes || 0), 0);
7378
+ if (current.length && (current.length >= maxCount || currentBytes + bytes > maxBytes)) {
7379
+ batches.push(current);
7380
+ current = [];
7381
+ currentBytes = 0;
7382
+ }
7383
+ current.push(entry);
7384
+ currentBytes += bytes;
7385
+ });
7386
+ if (current.length) batches.push(current);
7387
+ return batches;
7388
+ }
7389
+
7390
+ // The corpus_import verification packet: case keys, statuses, changed KEYS, artifact ids/hashes/sizes. Never an
7391
+ // expected answer, a provenance value or a byte of artifact content.
7392
+ function corpusImportPacketBody(responses) {
7393
+ const cases = [];
7394
+ let world = null;
7395
+ let corpus = null;
7396
+ let ownerAccountId = null;
7397
+ (responses || []).forEach((response) => {
7398
+ if (!response) return;
7399
+ if (response.world && !world) world = response.world;
7400
+ corpus = corpus || response.corpus;
7401
+ ownerAccountId = ownerAccountId || response.ownerAccountId;
7402
+ (Array.isArray(response.cases) ? response.cases : []).forEach((c) => {
7403
+ cases.push({
7404
+ caseKey: c.caseKey,
7405
+ status: c.status,
7406
+ changedKeys: c.changedKeys,
7407
+ artifacts: (c.artifacts || []).map((a) => ({ name: a.name, objectId: a.objectId, sha256: a.sha256, bytes: a.bytes, created: a.created })),
7408
+ message: c.status === 'failed' ? c.message : undefined
7409
+ });
7410
+ });
7411
+ });
7412
+ const counts = cases.reduce((acc, c) => { acc[c.status] = (acc[c.status] || 0) + 1; return acc; }, {});
7413
+ return { world, corpus, ownerAccountId, counts, cases, success: cases.length > 0 && !cases.some((c) => c.status === 'failed') };
7414
+ }
7415
+
7416
+ function parseCorpusFilterValues(flags, names) {
7417
+ const values = [];
7418
+ (names || []).forEach((name) => {
7419
+ const raw = flags[name];
7420
+ if (raw === undefined || raw === null || raw === true) return;
7421
+ (Array.isArray(raw) ? raw : [raw]).forEach((value) => {
7422
+ String(value).split(',').forEach((part) => {
7423
+ const trimmed = part.trim();
7424
+ if (trimmed) values.push(trimmed);
7425
+ });
7426
+ });
7427
+ });
7428
+ return Array.from(new Set(values));
7429
+ }
7430
+
7431
+ function testRunDependencies(status = {}) {
7432
+ const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
7433
+ const liveHttpCalls = [];
7434
+ tests.forEach((test) => (Array.isArray(test.liveHttpCalls) ? test.liveHttpCalls : []).forEach((call) => {
7435
+ liveHttpCalls.push(Object.assign({ caseName: test.name }, call));
7436
+ }));
7437
+ const ai = status.result && status.result.summary && status.result.summary.ai;
7438
+ return {
7439
+ liveHttpCalls: liveHttpCalls.slice(0, 50),
7440
+ unintendedLiveHttpCalls: liveHttpCalls.filter((call) => call.intentional !== true).length,
7441
+ liveAi: ai ? {
7442
+ liveCalls: ai.liveCalls || 0,
7443
+ mockedCalls: ai.mockedCalls || 0,
7444
+ cost: ai.cost,
7445
+ intentional: (ai.intentionalLiveCalls || 0) > 0 && (ai.intentionalLiveCalls || 0) >= (ai.liveCalls || 0),
7446
+ intentionalLiveCalls: ai.intentionalLiveCalls || 0,
7447
+ budgets: ai.budgets || []
7448
+ } : null
7449
+ };
7450
+ }
7451
+
7452
+ function formatUsd(value) {
7453
+ const n = Number(value);
7454
+ if (!Number.isFinite(n)) return 'unknown';
7455
+ // Real spend must never render as $0.00. Per-call costs of a few microdollars are normal for small models,
7456
+ // and a live run printing "$0.0000" reads as "this cost nothing", which is how a live run gets mistaken for
7457
+ // a replay — the exact confusion the AI-mode labelling exists to prevent.
7458
+ if (n !== 0 && Math.abs(n) < 0.0001) return (n < 0 ? '-' : '') + '<$0.0001';
7459
+ return '$' + n.toFixed(n !== 0 && Math.abs(n) < 0.01 ? 4 : 2);
7460
+ }
7461
+
7462
+ // The world a run executes in, as the lines printed BEFORE the result: host, who it runs as, the data lane,
7463
+ // the component world, the staging lane with its content hash, and the platform sync against the local HEAD.
7464
+ // One block, stated once, so an agent never has to assemble "which world was that?" from five other outputs —
7465
+ // the localhost-vs-prod mismatch that sent account 21 in circles was two of these lines disagreeing.
7466
+ function runWorldLines(world, context = {}) {
7467
+ if (!world || typeof world !== 'object') return [];
7468
+ const lines = ['World:'];
7469
+ if (context.host) lines.push(' host: ' + context.host);
7470
+ const accounts = ['account ' + (world.executionAccountId || context.accountId || 'unknown')];
7471
+ if (world.checkoutAccountId && world.checkoutAccountId !== world.executionAccountId) accounts.push('checkout ' + world.checkoutAccountId);
7472
+ if (world.componentOwnerAccountId && world.componentOwnerAccountId !== world.executionAccountId) accounts.push('component owner ' + world.componentOwnerAccountId);
7473
+ lines.push(' runs as: ' + accounts.join(', '));
7474
+ lines.push(' data lane: ' + (world.dataMode || context.dataMode || 'unknown'));
7475
+ const contextVariantWorld = context.variantWorld && typeof context.variantWorld === 'object' ? context.variantWorld : {};
7476
+ const subscribedComponentBranch = world.subscribedComponentBranch || contextVariantWorld.subscribedComponentBranch;
7477
+ const componentBranch = world.componentBranch || subscribedComponentBranch || contextVariantWorld.componentBranch || world.variantBranch;
7478
+ const branchSource = componentBranch && subscribedComponentBranch && String(componentBranch) === String(subscribedComponentBranch)
7479
+ ? 'subscription'
7480
+ : (world.componentBranchSource || contextVariantWorld.componentBranchSource || world.variantBranchSource || contextVariantWorld.variantBranchSource);
7481
+ const branchLabel = componentBranch
7482
+ ? "componentBranch=" + componentBranch + (branchSource ? ' via ' + branchSource : '')
7483
+ : (branchSource || '');
7484
+ const branchSuffix = branchLabel ? ' [' + branchLabel + ']' : '';
7485
+ lines.push(' component world: ' + (world.resolves || (componentBranch ? "trunk + the '" + componentBranch + "' variant overlays" : 'trunk')) + branchSuffix);
7486
+ const laneParts = [world.stagingLane || (world.branchName ? world.branchName + (world.workspace ? ' [ws:' + world.workspace + ']' : '') : 'none')];
7487
+ if (world.sharedLane) laneParts.push('(shared lane)');
7488
+ if (world.laneContentHash) laneParts.push('content ' + String(world.laneContentHash).slice(0, 12));
7489
+ if (world.stagedCount !== undefined && world.stagedCount !== null) laneParts.push(world.stagedCount + ' staged');
7490
+ lines.push(' staging lane: ' + laneParts.join(' '));
7491
+ if (world.laneId) lines.push(' lane id: ' + world.laneId);
7492
+ const localHead = context.gitHead ? String(context.gitHead).slice(0, 8) : null;
7493
+ const synced = world.platformSyncSha ? String(world.platformSyncSha).slice(0, 8) : null;
7494
+ if (synced || localHead) {
7495
+ let syncLine = ' platform sync: ' + (synced || 'unknown') + (localHead ? ' local HEAD ' + localHead : '');
7496
+ if (synced && localHead && !String(context.gitHead).startsWith(String(world.platformSyncSha).slice(0, 8))) {
7497
+ syncLine += ' (differ: committed components run as last synced; only STAGED components reflect this checkout)';
7498
+ }
7499
+ lines.push(syncLine);
7500
+ }
7501
+ if (world.testComponentSource) {
7502
+ lines.push(' test component: ' + world.testComponentSource + (world.testComponentSignature ? ' ' + shortSignature(world.testComponentSignature) : '') +
7503
+ (world.testComponentBranch ? " variant '" + world.testComponentBranch + "'" : ''));
7504
+ }
7505
+ lines.push(...stagedMaskedByVariantLines(world.stagedMaskedByVariant, world.executionAccountId || context.accountId));
7506
+ return lines;
7507
+ }
7508
+
7509
+ // Staged edits from a TRUNK lane that the execution account will not run, because its branch overrides those
7510
+ // components with committed variants. Production behaves the same way (the variant keeps shadowing trunk), so this
7511
+ // is drift to report, not a bug to route around. Pure.
7512
+ function stagedMaskedByVariantLines(masked, accountId) {
7513
+ if (!Array.isArray(masked) || !masked.length) return [];
7514
+ const lines = [' NOT RUN FOR THIS ACCOUNT: ' + masked.length + ' staged trunk edit(s) are shadowed by account ' +
7515
+ (accountId || '?') + "'s committed variant, exactly as in production:"];
7516
+ masked.slice(0, 10).forEach((entry) => {
7517
+ lines.push(' - ' + entry.type + ' ' + (entry.componentId != null ? entry.componentId + ' ' : '') + (entry.componentName || '') +
7518
+ " (variant " + (entry.variantId || '?') + " on '" + entry.branch + "')");
7519
+ });
7520
+ if (masked.length > 10) lines.push(' ...' + (masked.length - 10) + ' more');
7521
+ lines.push(" To change what this account runs, edit the component on its variant branch ('" + masked[0].branch +
7522
+ "') or promote the trunk change to that branch.");
7523
+ return lines;
7524
+ }
7525
+
7526
+ // The corpus-style roll-up printed after a run: outcome counts, case duration percentiles, AI usage split
7527
+ // live/mocked, and the durable record. Built only from the result the platform returned.
7528
+ function testRunSummaryLines(status = {}) {
7529
+ const result = status.result || {};
7530
+ const summary = result.summary || {};
7531
+ const lines = [];
7532
+ const outcomes = summary.outcomes && typeof summary.outcomes === 'object' ? summary.outcomes : null;
7533
+ if (outcomes && Object.keys(outcomes).length) {
7534
+ lines.push('Outcomes: ' + Object.keys(outcomes).map((key) => key + ' ' + outcomes[key]).join(', '));
7535
+ }
7536
+ if (summary.durationMs) {
7537
+ lines.push('Case duration: median ' + formatDurationMs(summary.durationMs.median) + ', p95 ' +
7538
+ formatDurationMs(summary.durationMs.p95) + ', max ' + formatDurationMs(summary.durationMs.max));
7539
+ }
7540
+ const ai = summary.ai;
7541
+ if (ai && (ai.calls || ai.liveCalls || ai.mockedCalls)) {
7542
+ let aiLine = 'AI: ' + (ai.liveCalls || 0) + ' live call(s) ' + formatUsd(ai.cost) + ', ' + (ai.mockedCalls || 0) + ' mocked';
7543
+ if (ai.uncostedLiveCalls) aiLine += ', ' + ai.uncostedLiveCalls + ' live call(s) unpriced (cost is a floor)';
7544
+ if (ai.intentionalLiveCalls) aiLine += ', ' + ai.intentionalLiveCalls + ' intentional (withAiBudget)';
7545
+ lines.push(aiLine);
7546
+ const models = Array.isArray(ai.models) ? ai.models : [];
7547
+ if (models.length) {
7548
+ lines.push('AI models: ' + models.slice(0, 6).map((m) => (m.model || 'unknown') + ' ' + (m.liveCalls || 0) + ' live/' + (m.mockedCalls || 0) + ' mocked').join('; '));
7549
+ }
7550
+ if (Array.isArray(ai.budgets) && ai.budgets.length) lines.push('AI budgets: ' + ai.budgets.join(', '));
7551
+ }
7552
+ const unintended = (Array.isArray(result.tests) ? result.tests : [])
7553
+ .reduce((sum, test) => sum + (Array.isArray(test.liveHttpCalls) ? test.liveHttpCalls.filter((c) => c && c.intentional !== true).length : 0), 0);
7554
+ if (unintended) {
7555
+ lines.push('Unmocked live HTTP calls: ' + unintended + ' (missing mocks, or live AI outside withAiBudget)');
7556
+ }
7557
+ if (result.durableRecord && result.durableRecord.stored) {
7558
+ lines.push('Durable record: kept after the live status expires — remits-cli test status --task-id ' + result.durableRecord.taskId);
7559
+ } else if (status.durable) {
7560
+ lines.push('Durable record: ' + (status.durableNote || 'read from the durable test run record'));
7561
+ }
7562
+ return lines;
7563
+ }
7564
+
7565
+ // Per-case comparison of two runs of the same suite: which cases changed outcome, and how cost and duration
7566
+ // moved. The answer to "did this revision do better than the last one" without reconstructing either run.
7567
+ function compareTestRunResults(baseResult = {}, headResult = {}) {
7568
+ const outcomeOf = (test) => test.outcome || (test.passed === true ? 'passed' : (test.passed === false ? 'failed' : 'unknown'));
7569
+ const byName = (result) => {
7570
+ const map = new Map();
7571
+ (Array.isArray(result.tests) ? result.tests : []).forEach((test) => { if (test && test.name) map.set(test.name, test); });
7572
+ return map;
7573
+ };
7574
+ const base = byName(baseResult);
7575
+ const head = byName(headResult);
7576
+ const names = Array.from(new Set([...base.keys(), ...head.keys()]));
7577
+ const cases = names.map((name) => {
7578
+ const b = base.get(name);
7579
+ const h = head.get(name);
7580
+ const entry = {
7581
+ name,
7582
+ base: b ? outcomeOf(b) : 'absent',
7583
+ head: h ? outcomeOf(h) : 'absent',
7584
+ baseCost: b && b.ai ? Number(b.ai.cost || 0) : null,
7585
+ headCost: h && h.ai ? Number(h.ai.cost || 0) : null,
7586
+ baseDurationMs: b ? b.duration : null,
7587
+ headDurationMs: h ? h.duration : null,
7588
+ headReason: h && h.outcome !== 'passed' ? (h.outcomeReason || h.error || null) : null
7589
+ };
7590
+ entry.changed = entry.base !== entry.head;
7591
+ entry.direction = !entry.changed ? 'same' : (entry.head === 'passed' ? 'improved' : (entry.base === 'passed' ? 'regressed' : 'changed'));
7592
+ return entry;
7593
+ });
7594
+ const count = (result, outcome) => (Array.isArray(result.tests) ? result.tests : []).filter((t) => outcomeOf(t) === outcome).length;
7595
+ const aiCost = (result) => (result.summary && result.summary.ai && Number(result.summary.ai.cost)) || 0;
7596
+ return {
7597
+ base: { taskId: baseResult.taskId, passed: count(baseResult, 'passed'), total: (baseResult.tests || []).length, cost: aiCost(baseResult) },
7598
+ head: { taskId: headResult.taskId, passed: count(headResult, 'passed'), total: (headResult.tests || []).length, cost: aiCost(headResult) },
7599
+ improved: cases.filter((c) => c.direction === 'improved').map((c) => c.name),
7600
+ regressed: cases.filter((c) => c.direction === 'regressed').map((c) => c.name),
7601
+ changed: cases.filter((c) => c.changed),
7602
+ cases
7603
+ };
7604
+ }
7605
+
7606
+
7607
+
7608
+
6394
7609
  async function waitForToolStatus(api, cwd, options) {
6395
7610
  const started = Date.now();
6396
7611
  let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
@@ -6422,6 +7637,19 @@ async function waitForToolStatus(api, cwd, options) {
6422
7637
  }
6423
7638
  }
6424
7639
 
7640
+ // One compact progress line for a streamed TestSuite frame: a finished case, or the suite roll-up.
7641
+ function testSuiteProgressLine(payload = {}) {
7642
+ if (payload.total != null || Array.isArray(payload.tests)) {
7643
+ const total = payload.total != null ? payload.total : (payload.tests || []).length;
7644
+ const passed = payload.passed != null ? payload.passed : (payload.tests || []).filter((t) => t && t.passed).length;
7645
+ return '[ws] ' + (payload.testName || 'suite') + ': ' + passed + '/' + total + ' passed';
7646
+ }
7647
+ const mark = payload.passed === false ? 'FAIL' : 'pass';
7648
+ const detail = payload.outcome && payload.outcome !== 'passed' ? ' [' + payload.outcome + ']' : '';
7649
+ return '[ws] ' + mark + ' ' + (payload.name || 'case') + detail +
7650
+ (payload.duration != null ? ' ' + payload.duration + 'ms' : '');
7651
+ }
7652
+
6425
7653
  function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
6426
7654
  const emit = options.stderr ? console.error : console.log;
6427
7655
  const client = new Client({
@@ -6442,7 +7670,11 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
6442
7670
  return;
6443
7671
  }
6444
7672
  if (payload.tests || payload.name || payload.total != null) {
6445
- emit('[ws][TestSuite]', JSON.stringify(payload));
7673
+ // Progress, not the payload. This used to print the whole suite result — every case, every
7674
+ // stacktrace — as one enormous line immediately before the readable summary said the same thing,
7675
+ // which buried the run world, the outcomes and the pivots an agent actually reads. --verbose still
7676
+ // gets the raw frame.
7677
+ emit(options.verbose ? '[ws][TestSuite] ' + JSON.stringify(payload) : testSuiteProgressLine(payload));
6446
7678
  }
6447
7679
  }
6448
7680
  } catch (err) {
@@ -6493,6 +7725,22 @@ async function testCommand(flags) {
6493
7725
  // --variant-branch explicitly probes a committed variant branch. Normally omitted: variants resolve
6494
7726
  // from the account's subscription edge, which is what production does.
6495
7727
  const variantBranch = flags['variant-branch'];
7728
+ // Asked BEFORE the run: a run whose evidence could only fail the envelope's contract is refused here, not
7729
+ // discovered at `verify report` after the minutes it took.
7730
+ const numericTestRef = /^\d+$/.test(String(testRef));
7731
+ const verification = await verificationPreflight(api, cwd, session, accountId, flags, {
7732
+ packetType: 'test_run',
7733
+ world: {
7734
+ accountId,
7735
+ executionAccountId: asAccountId ? Number(asAccountId) : Number(accountId),
7736
+ dataMode,
7737
+ branchName,
7738
+ workspace,
7739
+ componentBranch: variantBranch || undefined
7740
+ },
7741
+ test: numericTestRef ? { testId: Number(testRef) } : { testName: String(testRef) }
7742
+ }, { command: 'test run' });
7743
+ const activeEnvelopeId = verification.envelopeId;
6496
7744
 
6497
7745
  withStdoutRoutedToStderr(jsonOutput, () => {
6498
7746
  printProdDataBanner({
@@ -6525,12 +7773,21 @@ async function testCommand(flags) {
6525
7773
  withStdoutRoutedToStderr(jsonOutput, () => {
6526
7774
  printSessionResolutionWarning(sessionContext);
6527
7775
  printResolvedBaseUrl(baseUrl);
7776
+ printLocalCommandContext(cwd, flags, { accountId: asAccountId || accountId, branchName, workspace, dataMode });
7777
+ printLocalStateWarnings(cwd, flags);
6528
7778
  printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
6529
7779
  printStagingLaneOwnerNotice(start.staging || {});
6530
7780
  if (start.staging && Array.isArray(start.staging.accountLanes)) {
6531
7781
  printOrphanedWorkspaceWarning(start.staging);
6532
7782
  printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
6533
7783
  }
7784
+ runWorldLines(start.world, {
7785
+ host: normalizeBaseUrl(baseUrl),
7786
+ accountId: asAccountId || accountId,
7787
+ dataMode,
7788
+ variantWorld: start.variantWorld,
7789
+ gitHead: safeGitValue(cwd, 'git rev-parse HEAD')
7790
+ }).forEach((line) => console.log(line));
6534
7791
  console.log('Test run started:', start.taskId);
6535
7792
  });
6536
7793
  runtimeState.currentTestTaskId = start.taskId;
@@ -6539,11 +7796,12 @@ async function testCommand(flags) {
6539
7796
  if (flags.watch !== 'false') {
6540
7797
  const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
6541
7798
  if (topic) {
6542
- stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput });
7799
+ stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
6543
7800
  }
6544
7801
  }
6545
7802
 
6546
7803
  const status = await waitForStatus(api, cwd, accountId, branchName, start.taskId, session.token, dataMode);
7804
+ status.localState = localStateSummary(cwd, flags);
6547
7805
  if (stopWs) {
6548
7806
  stopWs();
6549
7807
  }
@@ -6556,6 +7814,7 @@ async function testCommand(flags) {
6556
7814
  } else {
6557
7815
  console.log('Final status:', JSON.stringify(status, null, 2));
6558
7816
  printStagingLaneOwnerNotice(status.staging || {});
7817
+ testRunSummaryLines(status).forEach((line) => console.log(line));
6559
7818
  printTestRunPivots(status);
6560
7819
  }
6561
7820
 
@@ -6581,8 +7840,6 @@ async function testCommand(flags) {
6581
7840
  process.exitCode = 1;
6582
7841
  }
6583
7842
 
6584
- const verificationContext = activeVerificationContext(cwd, flags, session, accountId);
6585
- const activeEnvelopeId = verificationEnvelopeIdForCommand(cwd, flags, verificationContext);
6586
7843
  const componentProvenance = testComponentProvenance(status);
6587
7844
  const sourceRevision = collectVerificationSource(cwd, flags);
6588
7845
  const priorPackets = await readVerificationPacketsForDiagnostics(api, cwd, session, accountId, activeEnvelopeId, {
@@ -6602,7 +7859,7 @@ async function testCommand(flags) {
6602
7859
  });
6603
7860
  }
6604
7861
 
6605
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
7862
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
6606
7863
  type: 'test_run',
6607
7864
  success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
6608
7865
  claim: 'Test run ' + String(testRef),
@@ -6625,10 +7882,14 @@ async function testCommand(flags) {
6625
7882
  unmatchedTestNames: unmatched
6626
7883
  },
6627
7884
  evidenceCategories: testEvidenceCategories(status, names),
7885
+ // What the run spent and whether that live AI was declared (withAiBudget), so an envelope can tell a
7886
+ // measurement run from a suite with missing mocks without re-reading every case.
7887
+ dependencies: testRunDependencies(status),
7888
+ summary: status.result && status.result.summary,
6628
7889
  limitations: []
6629
7890
  .concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
6630
7891
  .concat(nondeterminism ? [nondeterminism.message] : []),
6631
- rawRefs: { testStatusKey: start.taskId },
7892
+ rawRefs: { testStatusKey: start.taskId, durableTestRun: status.result && status.result.durableRecord ? start.taskId : undefined },
6632
7893
  status
6633
7894
  }, { quiet: jsonOutput });
6634
7895
  }
@@ -6665,6 +7926,11 @@ async function testStatusCommand(flags) {
6665
7926
  printResolvedBaseUrl(baseUrl);
6666
7927
  console.log('Test run status:', status.status || 'unknown');
6667
7928
  console.log('Task ID:', taskId);
7929
+ // Say where this answer came from. A durable record is a finished snapshot rebuilt from the database after the
7930
+ // live status was gone; without this line a reconstructed run is indistinguishable from one still being watched.
7931
+ console.log('Source:', status.durable
7932
+ ? 'durable run record (the live status has expired; this run is final)'
7933
+ : 'live run status');
6668
7934
  console.log('Data mode:', status.dataMode || dataMode);
6669
7935
  if (status.result) {
6670
7936
  console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
@@ -6672,12 +7938,100 @@ async function testStatusCommand(flags) {
6672
7938
  if (status.error || status.message) {
6673
7939
  console.log('Message:', status.error || status.message);
6674
7940
  }
7941
+ if (status.world) runWorldLines(status.world, { host: normalizeBaseUrl(baseUrl) }).forEach((line) => console.log(line));
7942
+ testRunSummaryLines(status).forEach((line) => console.log(line));
7943
+ printTestRunPivots(status);
6675
7944
  if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
6676
7945
  process.exitCode = 1;
6677
7946
  }
6678
7947
  return status;
6679
7948
  }
6680
7949
 
7950
+ // `test runs --test X`: the durable history of a suite, newest first. With --compare, the latest run is
7951
+ // compared case-by-case with the one before it.
7952
+ async function testRunsCommand(flags) {
7953
+ const cwd = process.cwd();
7954
+ ensureLocalState(cwd);
7955
+ const sessionContext = resolveSessionContext(cwd, flags);
7956
+ const { session, accountId } = sessionContext;
7957
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
7958
+ const api = buildAxios(baseUrl, session.token);
7959
+ const testRef = flags.test || flags['test-id'] || flags.name;
7960
+ const data = await loggedPost(api, cwd, '/cli/testRuns', {
7961
+ token: session.token,
7962
+ accountId,
7963
+ asAccountId: flags['as-account'] || flags['as-account-id'],
7964
+ testId: testRef && /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
7965
+ testName: testRef && !/^\d+$/.test(String(testRef)) ? String(testRef) : undefined,
7966
+ max: flags.limit || flags.max || 20
7967
+ }).then((r) => r.data);
7968
+ if (!data.success) throw new Error(data.message || 'Could not list test runs');
7969
+
7970
+ if (flagEnabled(flags.compare)) {
7971
+ const runs = data.runs || [];
7972
+ if (runs.length < 2) throw new Error('--compare needs at least two recorded runs' + (testRef ? ' of ' + testRef : '') + '; found ' + runs.length);
7973
+ return testCompareCommand(Object.assign({}, flags, { base: runs[1].taskId, head: runs[0].taskId }));
7974
+ }
7975
+ if (flagEnabled(flags.json)) {
7976
+ console.log(JSON.stringify(data, null, 2));
7977
+ return data;
7978
+ }
7979
+ printSessionResolutionWarning(sessionContext);
7980
+ printResolvedBaseUrl(baseUrl);
7981
+ console.log('Recorded test runs' + (testRef ? ' for ' + testRef : '') + ' (account ' + data.accountId + '), newest first:');
7982
+ (data.runs || []).forEach((run) => {
7983
+ console.log('- ' + run.taskId + ' ' + (run.status || 'unknown') + ' ' + (run.passed || 0) + '/' + (run.total || 0) + ' passed' +
7984
+ ' ' + corpusAiLabel(run) +
7985
+ (run.durationMs ? ' ' + formatDurationMs(run.durationMs) : '') +
7986
+ ' ' + (run.dataMode || '?') + ' ' + (run.branchName || '?') + (run.workspace ? ' [ws:' + run.workspace + ']' : '') +
7987
+ (run.laneContentHash ? ' lane ' + String(run.laneContentHash).slice(0, 12) : '') +
7988
+ ' ' + formatTime(run.completedAt) + ' ' + (run.testName || ''));
7989
+ });
7990
+ if (!(data.runs || []).length) console.log('(none recorded)');
7991
+ return data;
7992
+ }
7993
+
7994
+ // `test compare --base <taskId> --head <taskId>`: per-case outcome, cost and duration deltas between two runs,
7995
+ // read from the durable records.
7996
+ async function testCompareCommand(flags) {
7997
+ const cwd = process.cwd();
7998
+ ensureLocalState(cwd);
7999
+ const sessionContext = resolveSessionContext(cwd, flags);
8000
+ const { session, accountId } = sessionContext;
8001
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
8002
+ const api = buildAxios(baseUrl, session.token);
8003
+ const baseTask = flags.base;
8004
+ const headTask = flags.head;
8005
+ if (!baseTask || !headTask) throw new Error('test compare needs --base <taskId> and --head <taskId> (or: test runs --test X --compare)');
8006
+ const fetchStatus = (taskId) => loggedPost(api, cwd, '/cli/test', {
8007
+ token: session.token, command: 'status', accountId, branchName: flags.branch || currentBranch(cwd), taskId
8008
+ }).then((r) => r.data);
8009
+ const [baseStatus, headStatus] = await Promise.all([fetchStatus(baseTask), fetchStatus(headTask)]);
8010
+ const comparison = compareTestRunResults(Object.assign({ taskId: baseTask }, baseStatus.result || {}), Object.assign({ taskId: headTask }, headStatus.result || {}));
8011
+ comparison.baseWorld = baseStatus.world || (baseStatus.result && baseStatus.result.world) || null;
8012
+ comparison.headWorld = headStatus.world || (headStatus.result && headStatus.result.world) || null;
8013
+ if (flagEnabled(flags.json)) {
8014
+ console.log(JSON.stringify(comparison, null, 2));
8015
+ return comparison;
8016
+ }
8017
+ printResolvedBaseUrl(baseUrl);
8018
+ console.log('Base ' + baseTask + ': ' + comparison.base.passed + '/' + comparison.base.total + ' passed, AI ' + formatUsd(comparison.base.cost));
8019
+ console.log('Head ' + headTask + ': ' + comparison.head.passed + '/' + comparison.head.total + ' passed, AI ' + formatUsd(comparison.head.cost));
8020
+ const worldDiff = ['laneContentHash', 'componentBranch', 'dataMode', 'workspace', 'platformSyncSha']
8021
+ .filter((key) => comparison.baseWorld && comparison.headWorld && String(comparison.baseWorld[key] || '') !== String(comparison.headWorld[key] || ''))
8022
+ .map((key) => key + ' ' + short(String(comparison.baseWorld[key] || 'none')) + ' -> ' + short(String(comparison.headWorld[key] || 'none')));
8023
+ if (worldDiff.length) console.log('World changed: ' + worldDiff.join('; '));
8024
+ if (comparison.improved.length) console.log('Improved (' + comparison.improved.length + '): ' + comparison.improved.join(', '));
8025
+ if (comparison.regressed.length) console.log('Regressed (' + comparison.regressed.length + '): ' + comparison.regressed.join(', '));
8026
+ comparison.changed.filter((c) => c.direction === 'changed').forEach((c) => console.log('Changed: ' + c.name + ' ' + c.base + ' -> ' + c.head));
8027
+ comparison.cases.filter((c) => c.head !== 'passed' && c.headReason).slice(0, 20).forEach((c) => {
8028
+ console.log(' ' + c.name + ' [' + c.head + ']: ' + String(c.headReason).slice(0, 300));
8029
+ });
8030
+ if (!comparison.changed.length) console.log('No case changed outcome.');
8031
+ return comparison;
8032
+ }
8033
+
8034
+
6681
8035
  /*
6682
8036
  ## Tokens, Tools, Verification, And Config
6683
8037
  */
@@ -6703,6 +8057,18 @@ async function tokenCommand(flags) {
6703
8057
  // Embeddable resolves that account's branch variants rather than the owner's trunk.
6704
8058
  // --variant-branch explicitly probes a committed variant branch before any edge subscribes.
6705
8059
  const variantBranch = flags['variant-branch'];
8060
+ const tokenAsAccountId = flags['as-account'] || flags['as-account-id'];
8061
+ const verification = await verificationPreflight(api, cwd, session, accountId, flags, {
8062
+ packetType: 'token_inspect',
8063
+ world: {
8064
+ accountId,
8065
+ executionAccountId: tokenAsAccountId ? Number(tokenAsAccountId) : Number(accountId),
8066
+ dataMode,
8067
+ branchName,
8068
+ workspace,
8069
+ componentBranch: variantBranch || undefined
8070
+ }
8071
+ }, { command: 'token' });
6706
8072
  // Send the path so the server can also mint the EMBEDDABLE-SCOPED token key. `tokenKey` opens a
6707
8073
  // tokenized URL in a browser; `embedTokenKey` lets the `<script>` embed loader resolve the component
6708
8074
  // from persisted token context because the loader request sends no path.
@@ -6747,7 +8113,8 @@ async function tokenCommand(flags) {
6747
8113
  accountLanes: data.accountLanes,
6748
8114
  dataMode: data.dataMode || dataMode,
6749
8115
  shortBasePath: data.shortBasePath,
6750
- embeddableUrl
8116
+ embeddableUrl,
8117
+ localState: localStateSummary(cwd, flags)
6751
8118
  };
6752
8119
 
6753
8120
  // What the token will RESOLVE AS. `accountId` says which account it executes as; on a hierarchy the
@@ -6757,6 +8124,11 @@ async function tokenCommand(flags) {
6757
8124
  if (data.resolution) {
6758
8125
  output.resolution = data.resolution;
6759
8126
  }
8127
+ if (Array.isArray(data.stagedMaskedByVariant) && data.stagedMaskedByVariant.length) {
8128
+ output.stagedMaskedByVariant = data.stagedMaskedByVariant;
8129
+ // stderr: stdout is the JSON document. Said before the URL is opened, not discovered from what renders.
8130
+ stagedMaskedByVariantLines(data.stagedMaskedByVariant, data.accountId).forEach((line) => console.error(line.replace(/^ /, '')));
8131
+ }
6760
8132
 
6761
8133
  // Present only when the path resolved to an Embeddable. `embedTokenKey` is what a host site pastes
6762
8134
  // into the loader snippet; the render shape is echoed because it decides what that host receives.
@@ -6770,10 +8142,14 @@ async function tokenCommand(flags) {
6770
8142
  output.embedSnippet = data.embedSnippet;
6771
8143
  }
6772
8144
 
6773
- printSessionResolutionWarning(sessionContext);
8145
+ if (!flagEnabled(flags.json)) {
8146
+ printSessionResolutionWarning(sessionContext);
8147
+ printLocalCommandContext(cwd, flags, { accountId: output.accountId || accountId, branchName, workspace: output.workspace, dataMode: output.dataMode || dataMode });
8148
+ printLocalStateWarnings(cwd, flags);
8149
+ }
6774
8150
  console.log(JSON.stringify(output, null, 2));
6775
8151
 
6776
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
8152
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
6777
8153
  type: 'token_inspect',
6778
8154
  success: true,
6779
8155
  claim: 'Verification token minted',
@@ -6786,7 +8162,7 @@ async function tokenCommand(flags) {
6786
8162
  },
6787
8163
  evidenceCategories: ['token_inspect'],
6788
8164
  token: Object.assign({}, output, { tokenKey: output.tokenKey ? '[redacted]' : undefined, embedTokenKey: output.embedTokenKey ? '[redacted]' : undefined })
6789
- });
8165
+ }, { quiet: flagEnabled(flags.json) });
6790
8166
  }
6791
8167
 
6792
8168
  async function tokenInspectCommand(flags) {
@@ -6988,13 +8364,19 @@ async function toolCommand(flags) {
6988
8364
 
6989
8365
  data.responseFile = statusResponse.responseFile;
6990
8366
  data.sessionLog = sessionJsonlFile(cwd);
8367
+ data.localState = localStateSummary(cwd, flags);
6991
8368
  // A completed run can still carry a tool-level refusal — see toolResultFailed.
6992
8369
  const polledFailed = data.toolSuccess === false || toolResultFailed(data.result);
6993
8370
  if (polledFailed) {
6994
8371
  data.toolMessage = data.toolMessage || toolResultMessage(data.result);
6995
8372
  }
6996
8373
  if (data.status === 'failed' || polledFailed) process.exitCode = 1;
6997
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
8374
+ // Same question as the call that started it: a poll must not attach where the call itself would not.
8375
+ const statusVerification = await verificationPreflight(api, cwd, session, accountId, flags, {
8376
+ packetType: 'tool_call',
8377
+ world: { accountId: statusAccountId, dataMode: data.dataMode || dataMode, branchName: flags.branch || (stored && stored.branchName) || branchName, workspace: resolveWorkspace(cwd, flags) }
8378
+ }, { command: 'tool status', neverRefuse: true, quiet: jsonOutput });
8379
+ await appendVerificationPacket(api, cwd, session, accountId, statusVerification.evidenceFlags, {
6998
8380
  type: 'tool_call',
6999
8381
  success: data.status !== 'failed' && !polledFailed,
7000
8382
  claim: 'Tool status ' + requestedCallId,
@@ -7010,6 +8392,8 @@ async function toolCommand(flags) {
7010
8392
  }
7011
8393
  printSessionResolutionWarning(sessionContext);
7012
8394
  printResolvedBaseUrl(baseUrl);
8395
+ printLocalCommandContext(cwd, flags, { accountId: statusAccountId, branchName: flags.branch || (stored && stored.branchName) || branchName, dataMode: data.dataMode || dataMode });
8396
+ printLocalStateWarnings(cwd, flags);
7013
8397
  console.log('Tool call status:', data.status);
7014
8398
  console.log('Call ID:', requestedCallId);
7015
8399
  console.log('Data mode:', data.dataMode || dataMode);
@@ -7032,6 +8416,12 @@ async function toolCommand(flags) {
7032
8416
  stderr: jsonOutput
7033
8417
  });
7034
8418
 
8419
+ const toolWorkspace = resolveWorkspace(cwd, flags);
8420
+ const verification = await verificationPreflight(api, cwd, session, accountId, flags, {
8421
+ packetType: 'tool_call',
8422
+ world: { accountId, dataMode, branchName, workspace: toolWorkspace }
8423
+ }, { command: 'tool --name ' + String(toolName) });
8424
+
7035
8425
  const response = await loggedPost(api, cwd, '/cli/tool', {
7036
8426
  token: session.token,
7037
8427
  accountId,
@@ -7041,7 +8431,7 @@ async function toolCommand(flags) {
7041
8431
  // what the call may discover — has to survive that. The server validates it; it is never authorization.
7042
8432
  scopeRootAccountId: accountId,
7043
8433
  branchName,
7044
- workspace: resolveWorkspace(cwd, flags),
8434
+ workspace: toolWorkspace,
7045
8435
  dataMode,
7046
8436
  variantBranch,
7047
8437
  name: String(toolName),
@@ -7064,6 +8454,7 @@ async function toolCommand(flags) {
7064
8454
  const toolFailureMessage = data.toolMessage || toolResultMessage(data.result);
7065
8455
  data.responseFile = response.responseFile;
7066
8456
  data.sessionLog = sessionJsonlFile(cwd);
8457
+ data.localState = localStateSummary(cwd, flags);
7067
8458
  if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
7068
8459
 
7069
8460
  let evidenceData = data;
@@ -7072,6 +8463,8 @@ async function toolCommand(flags) {
7072
8463
  if (!jsonOutput) {
7073
8464
  printSessionResolutionWarning(sessionContext);
7074
8465
  printResolvedBaseUrl(baseUrl);
8466
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace: resolveWorkspace(cwd, flags), dataMode: data.dataMode || dataMode });
8467
+ printLocalStateWarnings(cwd, flags);
7075
8468
  console.log(asyncMode ? 'Tool call started.'
7076
8469
  : (toolFailed ? 'Tool call FAILED — the tool ran and returned an error.' : 'Tool call succeeded.'));
7077
8470
  if (toolFailed && toolFailureMessage) console.log('Tool error:', toolFailureMessage);
@@ -7118,7 +8511,7 @@ async function toolCommand(flags) {
7118
8511
  if (jsonOutput) Object.assign(data, { finalStatus: finalStatus.data });
7119
8512
  }
7120
8513
 
7121
- await appendVerificationPacket(api, cwd, session, accountId, flags, {
8514
+ await appendVerificationPacket(api, cwd, session, accountId, verification.evidenceFlags, {
7122
8515
  type: 'tool_call',
7123
8516
  success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
7124
8517
  pending: asyncMode && !waitForAsync,
@@ -7140,6 +8533,225 @@ async function toolCommand(flags) {
7140
8533
  }
7141
8534
  }
7142
8535
 
8536
+ // Human output for the read-only corpus commands, from the platform's response alone.
8537
+ // How a run or a case actually exercised AI. A replayed suite and a measured one produce identical outcomes,
8538
+ // scores and metrics, so every scorecard line has to say which it was or it is not evidence of anything.
8539
+ function corpusAiLabel(row = {}) {
8540
+ // When the corpus cases themselves recorded no AI, the platform falls back to the whole suite run's counters
8541
+ // and says so with aiModeSource. Report that as "run AI ...", never as the corpus cases' own measurement.
8542
+ const fromRun = row.aiModeSource === 'run';
8543
+ const live = Number((fromRun ? row.runLiveAiCalls : row.liveAiCalls) || 0);
8544
+ const mocked = Number((fromRun ? row.runMockedAiCalls : row.mockedAiCalls) || 0);
8545
+ const calls = Number((fromRun ? row.runAiCalls : row.aiCalls) || 0);
8546
+ const prefix = fromRun ? 'run AI ' : 'AI ';
8547
+ if (live > 0) return prefix + 'live ' + formatUsd(row.liveAiCost) + ' (' + live + '/' + (calls || live) + ' live)';
8548
+ if (mocked > 0 || calls > 0) return prefix + 'MOCKED (' + (mocked || calls) + ' replayed, $0.00)';
8549
+ return 'no AI';
8550
+ }
8551
+
8552
+ function corpusLines(sub, data = {}) {
8553
+ const lines = ['Corpus: ' + (data.corpus || '?') + ' (owner account ' + (data.ownerAccountId || '?') + ')'];
8554
+ if (sub === 'cases') {
8555
+ const cases = data.cases || [];
8556
+ lines.push('Cases: ' + cases.length);
8557
+ cases.forEach((c) => lines.push('- ' + c.caseKey + (c.split ? ' [' + c.split + ']' : '') + (c.tags && c.tags.length ? ' tags ' + c.tags.join(',') : '') +
8558
+ ' expected ' + (c.hasExpected ? (c.expectedStatus || 'present') : 'none') + ' artifacts ' + (c.artifacts || []).map((a) => a.name + ':' + a.bytes + 'b').join(' ')));
8559
+ } else if (sub === 'results') {
8560
+ (data.results || []).forEach((r) => lines.push('- ' + r.caseKey + ' ' + (r.outcome || '?') + (r.score !== undefined ? ' score ' + r.score : '') +
8561
+ ' run ' + r.taskId + ' ' + corpusAiLabel(r) + (r.durationMs ? ' ' + formatDurationMs(r.durationMs) : '') +
8562
+ ' metrics ' + String(r.metricsHash || '').slice(0, 10) + ' ' + formatTime(r.recordedAt)));
8563
+ } else if (sub === 'runs') {
8564
+ (data.runs || []).forEach((r) => lines.push('- ' + r.taskId + ' ' + r.cases + ' case(s) ' + Object.keys(r.outcomes || {}).map((k) => k + ' ' + r.outcomes[k]).join(', ') +
8565
+ ' ' + corpusAiLabel(r) + ' ' + (r.dataMode || '?') + ' ' + (r.branchName || '?') + (r.workspace ? ' [ws:' + r.workspace + ']' : '') +
8566
+ (r.laneContentHash ? ' lane ' + String(r.laneContentHash).slice(0, 12) : '') + ' ' + formatTime(r.recordedAt)));
8567
+ if ((data.runs || []).some((r) => r.aiMode === 'mocked')) {
8568
+ lines.push('Runs marked AI MOCKED replayed every AI turn from aiMock - they measure the harness, not the model.');
8569
+ }
8570
+ } else if (sub === 'compare') {
8571
+ const cmp = data.comparison || {};
8572
+ lines.push('Base ' + cmp.baseTaskId + ' -> head ' + cmp.headTaskId + ': ' + Object.keys(cmp.counts || {}).map((k) => k + ' ' + cmp.counts[k]).join(', '));
8573
+ lines.push('AI mode: base ' + (cmp.baseAiMode || '?') + ', head ' + (cmp.headAiMode || '?'));
8574
+ if (cmp.aiModeChanged) lines.push('WARNING: ' + cmp.aiModeChanged);
8575
+ (cmp.cases || []).filter((c) => c.direction !== 'same').forEach((c) => {
8576
+ lines.push('- ' + c.caseKey + ' ' + c.direction + (c.base ? ' ' + c.base.outcome : '') + (c.head ? ' -> ' + c.head.outcome : '') +
8577
+ (c.expectedChanged ? ' (EXPECTED ANSWER CHANGED between runs)' : ''));
8578
+ (c.changedMetrics || []).slice(0, 8).forEach((m) => lines.push(' ' + m.metric + ': ' + JSON.stringify(m.base) + ' -> ' + JSON.stringify(m.head)));
8579
+ if (c.head && c.head.outcomeReason && c.head.outcome !== 'passed') lines.push(' reason: ' + String(c.head.outcomeReason).slice(0, 300));
8580
+ });
8581
+ } else if (sub === 'retire') {
8582
+ lines.push('Result: ' + Object.keys(data.counts || {}).map((k) => k + ' ' + data.counts[k]).join(', '));
8583
+ (data.cases || []).forEach((c) => lines.push('- ' + c.caseKey + ' ' + c.status + (c.message ? ' ' + c.message : (c.changed === false ? ' (already in that state)' : ''))));
8584
+ // Only explain what retirement means when something was actually retired; after a wholly refused call the
8585
+ // reader needs the refusal, not a description of a state nothing reached.
8586
+ if ((data.cases || []).some((c) => c.status === 'retired' || c.status === 'restored')) {
8587
+ lines.push('Retired cases stay readable in results and compare; they are just no longer handed to runs. `corpus cases --include-retired` lists them, `corpus retire --restore` puts them back.');
8588
+ }
8589
+ } else if (sub === 'consistency') {
8590
+ const con = data.consistency || {};
8591
+ lines.push('Case ' + con.caseKey + ': ' + con.runs + ' run(s), ' + (con.stable ? 'STABLE (identical metrics every run)' : (con.variants || []).length + ' distinct metric results') +
8592
+ (con.aiModes && con.aiModes.length ? ' AI ' + con.aiModes.join('+') : ''));
8593
+ if (con.note) lines.push('WARNING: ' + con.note);
8594
+ (con.variants || []).forEach((v) => lines.push('- metrics ' + String(v.metricsHash).slice(0, 10) + ' runs ' + (v.runs || []).map((r) => r.taskId + ':' + r.outcome + (r.aiMode && r.aiMode !== 'none' ? '/' + r.aiMode : '')).join(', ')));
8595
+ }
8596
+ return lines;
8597
+ }
8598
+
8599
+ // `corpus import|cases|results|runs|compare|consistency`: test corpora through the platform's own corpus(name).
8600
+ async function corpusCommand(flags, subcommand) {
8601
+ const cwd = process.cwd();
8602
+ ensureLocalState(cwd);
8603
+ const sessionContext = resolveSessionContext(cwd, flags);
8604
+ const { session, accountId } = sessionContext;
8605
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
8606
+ const branchName = flags.branch || currentBranch(cwd);
8607
+ const workspace = resolveWorkspace(cwd, flags);
8608
+ const dataMode = hasExplicitDataModeFlag(flags) ? resolveDataMode(flags, null) : DEFAULT_DATA_MODE;
8609
+ const jsonOutput = flagEnabled(flags.json);
8610
+ const sub = String(subcommand || 'cases').toLowerCase();
8611
+ const emit = jsonOutput ? console.error : console.log;
8612
+ const api = buildAxios(baseUrl, session.token, parsePositiveInt(flags['timeout-ms'], 300000));
8613
+ const base = {
8614
+ token: session.token,
8615
+ accountId,
8616
+ asAccountId: flags['as-account'] || flags['as-account-id'],
8617
+ variantBranch: flags['variant-branch'],
8618
+ branchName,
8619
+ workspace,
8620
+ dataMode
8621
+ };
8622
+ const post = async (payload) => {
8623
+ try {
8624
+ return await loggedPost(api, cwd, '/cli/corpus', Object.assign({}, base, payload)).then((r) => r.data);
8625
+ } catch (err) {
8626
+ const data = err && err.response && err.response.data;
8627
+ if (data && data.message) throw new Error(data.message);
8628
+ throw err;
8629
+ }
8630
+ };
8631
+
8632
+ if (sub === 'import') {
8633
+ const manifestFile = flags.manifest || flags.file;
8634
+ if (!manifestFile) throw new Error('corpus import requires --manifest <manifest.json>');
8635
+ const resolved = path.resolve(cwd, String(manifestFile));
8636
+ const manifest = JSON.parse(fs.readFileSync(resolved, 'utf8'));
8637
+ const corpusName = flags.corpus || manifest.corpus;
8638
+ if (!corpusName) throw new Error('corpus import needs a corpus name: "corpus" in the manifest or --corpus');
8639
+ const entries = corpusManifestEntries(manifest, path.dirname(resolved));
8640
+ if (!entries.length) throw new Error('The manifest has no cases');
8641
+
8642
+ withStdoutRoutedToStderr(jsonOutput, () => {
8643
+ printSessionResolutionWarning(sessionContext);
8644
+ printResolvedBaseUrl(baseUrl);
8645
+ printLocalCommandContext(cwd, flags, { accountId: flags['as-account'] || accountId, branchName, workspace, dataMode });
8646
+ printProdDataBanner({ dataMode, accountId: flags['as-account'] || accountId, baseUrl, operation: 'corpus import', mutating: true });
8647
+ });
8648
+ const totalBytes = entries.reduce((sum, e) => sum + e.artifacts.reduce((n, a) => n + a.bytes, 0), 0);
8649
+ emit('Importing ' + entries.length + ' case(s) into corpus "' + corpusName + '", ' +
8650
+ entries.reduce((n, e) => n + e.artifacts.length, 0) + ' artifact(s), ' + totalBytes + ' bytes (artifacts in the ' + dataMode + ' lane)');
8651
+
8652
+ const responses = [];
8653
+ for (const batch of batchCorpusEntries(entries)) {
8654
+ const cases = batch.map((entry) => Object.assign({}, entry, {
8655
+ artifacts: entry.artifacts.map((a) => ({
8656
+ name: a.name,
8657
+ fileName: a.fileName,
8658
+ contentType: a.contentType,
8659
+ // Read and encoded only here, per batch. The session log records sizes and hashes, never this value.
8660
+ contentBase64: fs.readFileSync(a.path).toString('base64')
8661
+ }))
8662
+ }));
8663
+ responses.push(await post({ command: 'import', corpus: corpusName, confirmProd: flagEnabled(flags['confirm-prod']), cases }));
8664
+ }
8665
+ const body = corpusImportPacketBody(responses);
8666
+ if (!body.success) process.exitCode = 1;
8667
+ if (jsonOutput) {
8668
+ console.log(JSON.stringify(body, null, 2));
8669
+ } else {
8670
+ console.log('Corpus: ' + body.corpus + ' (owner account ' + body.ownerAccountId + ')');
8671
+ console.log('Result: ' + Object.keys(body.counts).map((k) => k + ' ' + body.counts[k]).join(', '));
8672
+ body.cases.forEach((c) => {
8673
+ console.log('- ' + c.caseKey + ' ' + c.status + (c.changedKeys && c.changedKeys.length && c.status === 'updated' ? ' changed: ' + c.changedKeys.join(', ') : '') + (c.message ? ' ' + c.message : ''));
8674
+ (c.artifacts || []).forEach((a) => console.log(' ' + a.name + ' object ' + a.objectId + ' ' + a.bytes + ' bytes sha256 ' + String(a.sha256).slice(0, 12) + ' ' + (a.created ? 'stored' : 'already stored')));
8675
+ });
8676
+ // Only point at the next step when something actually landed. Printing it after a wholly failed import
8677
+ // told the reader to go use a corpus that has no cases in it.
8678
+ if (body.success) {
8679
+ console.log('Use it in a Test with corpus(\'' + body.corpus + '\').cases(); compare runs with remits-cli corpus compare.');
8680
+ } else {
8681
+ console.log('Import did not fully succeed - fix the case(s) above and re-run; import is idempotent, so unchanged cases stay unchanged.');
8682
+ }
8683
+ }
8684
+ await appendVerificationPacket(api, cwd, session, accountId, flags, {
8685
+ type: 'corpus_import',
8686
+ success: body.success,
8687
+ claim: 'Corpus ' + body.corpus + ' import: ' + Object.keys(body.counts).map((k) => k + ' ' + body.counts[k]).join(', '),
8688
+ world: Object.assign({}, body.world || {}, { host: normalizeBaseUrl(baseUrl), branchName, workspace, dataMode }),
8689
+ revision: collectVerificationSource(cwd, flags),
8690
+ evidenceCategories: ['corpus_import'],
8691
+ corpusImport: { corpus: body.corpus, ownerAccountId: body.ownerAccountId, counts: body.counts, cases: body.cases }
8692
+ }, { quiet: jsonOutput });
8693
+ return body;
8694
+ }
8695
+
8696
+ const corpusName = flags.corpus || (flags._ && flags._[2]);
8697
+ if (!corpusName) throw new Error('corpus ' + sub + ' requires --corpus <name>');
8698
+ let data;
8699
+ if (sub === 'cases') {
8700
+ const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
8701
+ const keys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
8702
+ data = await post({ command: 'cases', corpus: corpusName, split: flags.split, tags: tags.length ? tags : undefined, keys: keys.length ? keys : undefined, includeValues: flagEnabled(flags['include-values']), includeRetired: flagEnabled(flags['include-retired']), limit: flags.limit });
8703
+ } else if (sub === 'results') {
8704
+ data = await post({ command: 'results', corpus: corpusName, caseKey: flags.case || flags['case-key'], taskId: flags['task-id'], limit: flags.limit || 50 });
8705
+ } else if (sub === 'runs') {
8706
+ data = await post({ command: 'runs', corpus: corpusName, limit: flags.limit || 20 });
8707
+ } else if (sub === 'compare') {
8708
+ if (!flags.base || !flags.head) throw new Error('corpus compare requires --base <taskId> --head <taskId>');
8709
+ data = await post({ command: 'compare', corpus: corpusName, base: flags.base, head: flags.head });
8710
+ } else if (sub === 'consistency') {
8711
+ const caseKey = flags.case || flags['case-key'];
8712
+ if (!caseKey) throw new Error('corpus consistency requires --case <caseKey>');
8713
+ data = await post({ command: 'consistency', corpus: corpusName, caseKey, limit: flags.limit || 20 });
8714
+ } else if (sub === 'retire') {
8715
+ const retireKeys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
8716
+ if (!retireKeys.length) throw new Error('corpus retire requires --case <caseKey> (repeatable) or --keys a,b,c');
8717
+ data = await post({ command: 'retire', corpus: corpusName, keys: retireKeys, restore: flagEnabled(flags.restore) });
8718
+ if (data && data.success === false) process.exitCode = 1;
8719
+ } else {
8720
+ throw new Error('Unknown corpus subcommand: ' + sub + ' (use import, cases, results, runs, compare, consistency or retire)');
8721
+ }
8722
+ if (jsonOutput) {
8723
+ console.log(JSON.stringify(data, null, 2));
8724
+ return data;
8725
+ }
8726
+ printSessionResolutionWarning(sessionContext);
8727
+ printResolvedBaseUrl(baseUrl);
8728
+ corpusLines(sub, data).forEach((line) => console.log(line));
8729
+ return data;
8730
+ }
8731
+
8732
+ // `verify token` / `verify tool` say "prove this for the envelope", so without an explicit --data-mode they run
8733
+ // in the envelope's lane when that lane is TEST. A plain `token`/`tool` follows the session, and a session parked
8734
+ // on prod otherwise refuses (or silently detaches) against the test envelope the agent just started. Never
8735
+ // adopts PROD: production proof keeps requiring an explicit --data-mode prod, which the refusal names.
8736
+ async function verifyWrapperFlags(api, cwd, session, accountId, flags, envelopeId) {
8737
+ const pinned = Object.assign({}, flags, { 'verify-envelope': envelopeId });
8738
+ if (hasExplicitDataModeFlag(flags)) return pinned;
8739
+ try {
8740
+ const result = await postVerificationCommand(api, cwd, session, accountId, 'compatibility', {
8741
+ envelopeId,
8742
+ probe: { packetType: 'manual_observation', world: {} }
8743
+ });
8744
+ const lane = result.comparisonWorld && result.comparisonWorld.dataMode;
8745
+ if (lane && normalizeDataMode(lane) === 'test' && resolveDataMode(flags, session) !== 'test') {
8746
+ console.error('Data lane: test (the lane envelope ' + envelopeId + ' proves; your session is on ' + resolveDataMode(flags, session) + ')');
8747
+ return Object.assign(pinned, { 'data-mode': 'test' });
8748
+ }
8749
+ } catch (_) {
8750
+ // The command's own preflight still answers; this only saves a turn.
8751
+ }
8752
+ return pinned;
8753
+ }
8754
+
7143
8755
  async function verifyCommand(flags, subcommand) {
7144
8756
  const cwd = process.cwd();
7145
8757
  ensureLocalState(cwd);
@@ -7148,28 +8760,57 @@ async function verifyCommand(flags, subcommand) {
7148
8760
  const sessionContext = resolveSessionContext(cwd, flags);
7149
8761
  const { session, accountId } = sessionContext;
7150
8762
  const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
7151
- const dataMode = resolveDataMode(flags, session);
8763
+ // `verify start` proves in the TEST lane unless told otherwise, for the reason `test run` does: production
8764
+ // proof needs explicit provenance. It used to inherit the session's lane, so a session parked on prod started
8765
+ // prod envelopes that every default `test run` then contradicted.
8766
+ const dataMode = sub === 'start' && !hasExplicitDataModeFlag(flags)
8767
+ ? DEFAULT_DATA_MODE
8768
+ : resolveDataMode(flags, session);
7152
8769
  const branchName = flags.branch || currentBranch(cwd);
7153
8770
  const workspace = resolveWorkspace(cwd, flags);
7154
8771
  const api = buildAxios(baseUrl, session.token);
8772
+ // Selection is by checkout world only (no data lane) - see verificationContextKey.
7155
8773
  const activeContext = activeVerificationContext(cwd, flags, session, accountId);
8774
+ const checkoutWorldLine = 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared');
8775
+ // What a plain `test run` from here would record, for the "would it count" probe.
8776
+ const testRunWorld = {
8777
+ accountId,
8778
+ executionAccountId: Number(accountId),
8779
+ dataMode: hasExplicitDataModeFlag(flags) ? resolveDataMode(flags, null) : DEFAULT_DATA_MODE,
8780
+ branchName,
8781
+ workspace
8782
+ };
7156
8783
 
7157
8784
  if (sub === 'current') {
7158
8785
  const active = readActiveVerificationEnvelope(cwd, activeContext);
7159
8786
  printSessionResolutionWarning(sessionContext);
7160
8787
  printResolvedBaseUrl(baseUrl);
7161
- console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
8788
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
8789
+ printLocalStateWarnings(cwd, flags);
8790
+ console.log('Checkout world:', checkoutWorldLine);
7162
8791
  if (!active) {
7163
- const legacy = readActiveVerificationEnvelope(cwd);
7164
- console.log('No active verification envelope for this context.');
7165
- if (legacy) {
7166
- console.log('Legacy active envelope ignored for this context:', legacy);
8792
+ console.log('No active verification envelope for this checkout world.');
8793
+ // Name the envelopes selected in SIBLING worlds (another workspace/branch of this checkout): the usual
8794
+ // cause is a workspace or branch switch, and "start a new one" is the wrong next step for that.
8795
+ const siblings = Object.values(readActiveVerificationContexts(cwd))
8796
+ .filter((entry) => entry && typeof entry === 'object' && entry.envelopeId && entry.context &&
8797
+ String(entry.context.accountId) === String(accountId))
8798
+ .sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || '')))
8799
+ .filter((entry, index, all) => all.findIndex((other) => other.envelopeId === entry.envelopeId) === index)
8800
+ .slice(0, 3);
8801
+ siblings.forEach((entry) => {
8802
+ console.log(' selected in another world: ' + entry.envelopeId + ' (branch=' + (entry.context.branchName || '?') +
8803
+ ', workspace=' + (entry.context.workspace || 'shared') + ')');
8804
+ });
8805
+ if (siblings.length) {
8806
+ console.log(' Switch back to that workspace/branch, or `remits-cli verify use <id>` to select one here.');
7167
8807
  }
7168
8808
  return;
7169
8809
  }
7170
8810
  console.log('Active verification envelope:', active);
7171
8811
  const local = readLocalVerificationEnvelope(cwd, active);
7172
8812
  if (local && local.summary) console.log('Summary:', local.summary);
8813
+ await printVerificationRunProbe(api, cwd, session, accountId, active, testRunWorld);
7173
8814
  return;
7174
8815
  }
7175
8816
 
@@ -7178,7 +8819,10 @@ async function verifyCommand(flags, subcommand) {
7178
8819
  if (!envelopeId) throw new Error('Usage: remits-cli verify use <envelopeId>');
7179
8820
  writeActiveVerificationEnvelope(cwd, envelopeId, activeContext);
7180
8821
  console.log('Active verification envelope:', envelopeId);
7181
- console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
8822
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
8823
+ printLocalStateWarnings(cwd, flags);
8824
+ console.log('Checkout world:', checkoutWorldLine);
8825
+ await printVerificationRunProbe(api, cwd, session, accountId, String(envelopeId), testRunWorld);
7182
8826
  return;
7183
8827
  }
7184
8828
 
@@ -7189,8 +8833,9 @@ async function verifyCommand(flags, subcommand) {
7189
8833
  console.log('All active verification envelope pointers cleared for this checkout.');
7190
8834
  } else {
7191
8835
  writeActiveVerificationEnvelope(cwd, null, activeContext);
7192
- console.log('Verification envelope cleared for this context.');
7193
- console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
8836
+ console.log('Verification envelope cleared for this checkout world.');
8837
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
8838
+ console.log('Checkout world:', checkoutWorldLine);
7194
8839
  }
7195
8840
  return;
7196
8841
  }
@@ -7241,7 +8886,15 @@ async function verifyCommand(flags, subcommand) {
7241
8886
  writeLocalVerificationEnvelope(cwd, envelope);
7242
8887
  writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
7243
8888
  console.log('Verification envelope started:', envelope.envelopeId);
7244
- console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
8889
+ printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
8890
+ printLocalStateWarnings(cwd, flags);
8891
+ console.log('Target:', checkoutWorldLine + ', dataMode=' + dataMode +
8892
+ (hasExplicitDataModeFlag(flags) ? '' : ' (verify start default; pass --data-mode prod to prove against production data)'));
8893
+ // Say NOW whether a plain `test run` from here will count - a prod lane or a manifest world elsewhere both
8894
+ // mean it will not - rather than after the first refusal. A PLAIN run: `--data-mode prod` on `verify start`
8895
+ // names the envelope's lane, not the run's. Silent when it attaches.
8896
+ await printVerificationRunProbe(api, cwd, session, accountId, envelope.envelopeId,
8897
+ Object.assign({}, testRunWorld, { dataMode: DEFAULT_DATA_MODE }), { onlyMismatch: true });
7245
8898
  printAcceptanceWarnings(envelope);
7246
8899
  printEnvelopeWarnings(envelope);
7247
8900
  console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
@@ -7370,7 +9023,7 @@ async function verifyCommand(flags, subcommand) {
7370
9023
  return await testCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
7371
9024
  }
7372
9025
  if (sub === 'token') {
7373
- return await tokenCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
9026
+ return await tokenCommand(await verifyWrapperFlags(api, cwd, session, accountId, flags, envelopeId));
7374
9027
  }
7375
9028
  if (sub === 'stage') {
7376
9029
  return await pushComponentsCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
@@ -7379,7 +9032,7 @@ async function verifyCommand(flags, subcommand) {
7379
9032
  return await syncComponentsCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
7380
9033
  }
7381
9034
  if (sub === 'tool') {
7382
- return await toolCommand(Object.assign({}, flags, { 'verify-envelope': envelopeId }));
9035
+ return await toolCommand(await verifyWrapperFlags(api, cwd, session, accountId, flags, envelopeId));
7383
9036
  }
7384
9037
 
7385
9038
  if (sub === 'invalidate') {
@@ -7411,6 +9064,12 @@ async function verifyCommand(flags, subcommand) {
7411
9064
  if (sub === 'status' || sub === 'report') {
7412
9065
  let statusPacket = null;
7413
9066
  try {
9067
+ // Only attach the status observation when it is from the envelope's world; otherwise it is noise the
9068
+ // report would list as wrong-world evidence on every status check.
9069
+ const statusVerification = await verificationPreflight(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, {
9070
+ packetType: 'component_status',
9071
+ world: { accountId, dataMode, branchName, workspace }
9072
+ }, { command: 'verify status', quiet: true, neverRefuse: true });
7414
9073
  const componentStatus = await loggedPost(api, cwd, '/cli/components', {
7415
9074
  token: session.token,
7416
9075
  accountId,
@@ -7429,22 +9088,16 @@ async function verifyCommand(flags, subcommand) {
7429
9088
  rawRefs: { command: 'components status' },
7430
9089
  componentStatus
7431
9090
  };
7432
- await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, statusPacket, { quiet: true });
9091
+ await appendVerificationPacket(api, cwd, session, accountId, statusVerification.evidenceFlags, statusPacket, { quiet: true });
7433
9092
  } catch (err) {
7434
9093
  if (!flagEnabled(flags.json)) {
7435
9094
  console.error('Warning: could not attach component_status packet before rendering report:', describeError(err));
7436
9095
  }
7437
9096
  }
7438
- const response = await postVerificationCommand(api, cwd, session, accountId, sub, {
7439
- envelopeId,
7440
- currentWorld: buildCommandWorld(statusPacket && statusPacket.componentStatus, {
7441
- accountId,
7442
- dataMode,
7443
- branchName,
7444
- workspace,
7445
- host: normalizeBaseUrl(baseUrl)
7446
- })
7447
- });
9097
+ // No `currentWorld`: status and report judge the SAME world (manifest, else the envelope's start world), so
9098
+ // they cannot give two verdicts on one envelope. Sending the session's world made every --as-account packet
9099
+ // wrong-world under `status` while `report` verified it, and made the verdict depend on the session lane.
9100
+ const response = await postVerificationCommand(api, cwd, session, accountId, sub, { envelopeId });
7448
9101
  if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
7449
9102
  if (response.report) {
7450
9103
  console.log(response.report);
@@ -8104,7 +9757,10 @@ function detectIssueSignals(repoEntries, globalFiles) {
8104
9757
  }
8105
9758
 
8106
9759
  for (const repo of repoEntries || []) {
8107
- const sessionLogFile = (repo.files || []).find((file) => file.label === '.remits-cli/session log (tail)');
9760
+ const sessionLogFile = (repo.files || []).find((file) =>
9761
+ file.label === '.remits-cli/active-actor/session log (tail)' ||
9762
+ file.label === '.remits-cli/session log (tail)' ||
9763
+ file.label === '.remits-cli/legacy/session log (tail)');
8108
9764
  if (!sessionLogFile || !sessionLogFile.path || !fs.existsSync(sessionLogFile.path)) {
8109
9765
  continue;
8110
9766
  }
@@ -8300,23 +9956,41 @@ function buildGlobalFileSnapshot() {
8300
9956
  function buildRepoSnapshot(entry) {
8301
9957
  const directory = entry.directory;
8302
9958
  const localPaths = localStatePaths(directory);
9959
+ const actors = listActorDirectories(directory);
8303
9960
  const currentSessionName = fs.existsSync(localPaths.currentSessionFile)
8304
9961
  ? fs.readFileSync(localPaths.currentSessionFile, 'utf8').trim()
8305
9962
  : '';
8306
9963
  const currentSessionLog = currentSessionName
8307
9964
  ? path.join(localPaths.sessionsDir, currentSessionName + '.jsonl')
8308
9965
  : null;
9966
+ const legacySessionName = fs.existsSync(localPaths.legacy.currentSessionFile)
9967
+ ? fs.readFileSync(localPaths.legacy.currentSessionFile, 'utf8').trim()
9968
+ : '';
9969
+ const legacySessionLog = legacySessionName
9970
+ ? path.join(localPaths.legacy.sessionsDir, legacySessionName + '.jsonl')
9971
+ : null;
8309
9972
  const repoFiles = [
8310
9973
  { label: 'account-info.json', path: entry.accountInfoPath || path.join(directory, 'account-info.json'), mode: 'json' },
8311
9974
  { label: 'account-hierarchy.json', path: path.join(directory, 'account-hierarchy.json'), mode: 'json' },
8312
- { label: '.remits-cli/current-session.txt', path: localPaths.currentSessionFile, mode: 'text' },
8313
- { label: '.remits-cli/tools/tools.json', path: path.join(localPaths.toolsDir, 'tools.json'), mode: 'json' },
8314
- { label: '.remits-cli/session log (tail)', path: currentSessionLog, mode: 'tail' }
9975
+ { label: '.remits-cli/active-actor/current-session.txt', path: localPaths.currentSessionFile, mode: 'text' },
9976
+ { label: '.remits-cli/shared/tools/tools.json', path: path.join(localPaths.toolsDir, 'tools.json'), mode: 'json' },
9977
+ { label: '.remits-cli/active-actor/session log (tail)', path: currentSessionLog, mode: 'tail' },
9978
+ { label: '.remits-cli/legacy/current-session.txt', path: localPaths.legacy.currentSessionFile, mode: 'text' },
9979
+ { label: '.remits-cli/legacy/session log (tail)', path: legacySessionLog, mode: 'tail' }
8315
9980
  ];
8316
9981
 
8317
9982
  return {
8318
9983
  ...entry,
8319
9984
  exists: fs.existsSync(directory),
9985
+ localState: {
9986
+ activeActor: localPaths.actor,
9987
+ activeActorDir: localPaths.actorDir,
9988
+ actors,
9989
+ foreignActors: actors.filter((actor) => !actor.current),
9990
+ legacyStatePresent: fs.existsSync(localPaths.legacy.currentSessionFile) ||
9991
+ fs.existsSync(localPaths.legacy.sessionsDir) ||
9992
+ fs.existsSync(localPaths.legacy.toolResponsesDir)
9993
+ },
8320
9994
  files: repoFiles.map((file) => {
8321
9995
  const stat = file.path ? fileStat(file.path) : null;
8322
9996
  let content = null;
@@ -12176,6 +13850,137 @@ function describeRunLocation(record) {
12176
13850
  return parts.join(' ');
12177
13851
  }
12178
13852
 
13853
+ async function activityInspectCommand(flags) {
13854
+ const cwd = process.cwd();
13855
+ ensureLocalState(cwd);
13856
+ const sessionContext = resolveSessionContext(cwd, flags);
13857
+ const { session, accountId } = sessionContext;
13858
+ const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
13859
+ const api = buildAxios(baseUrl, session.token, 30000);
13860
+ const response = await loggedPost(api, cwd, '/cli/activityInspect', {
13861
+ token: session.token,
13862
+ accountId,
13863
+ scope: flags.scope || 'self',
13864
+ dataMode: resolveDataMode(flags, session),
13865
+ userId: flags['user-id'] || flags.userId || flags['cli-user-id'],
13866
+ userEmail: flags['user-email'] || flags.email,
13867
+ limit: flags.limit || flags.max || 20,
13868
+ envelopeLimit: flags['envelope-limit'] || flags.envelopeLimit
13869
+ }).then((r) => r.data);
13870
+
13871
+ if (!response.success) throw new Error(response.message || 'Activity inspection failed');
13872
+ if (flagEnabled(flags.json)) {
13873
+ console.log(JSON.stringify(response, null, 2));
13874
+ return response;
13875
+ }
13876
+
13877
+ printSessionResolutionWarning(sessionContext);
13878
+ printResolvedBaseUrl(baseUrl);
13879
+ printActivityInspectSummary(response);
13880
+ return response;
13881
+ }
13882
+
13883
+ function printActivityInspectSummary(response) {
13884
+ const subject = response.subjectUser
13885
+ ? ' for ' + (response.subjectUser.email || response.subjectUser.name || ('user ' + response.subjectUser.id))
13886
+ : '';
13887
+ console.log('Activity inspection' + subject + ' — ' + response.dataMode + ' lane');
13888
+ console.log('Scope: ' + (response.accountName || ('account ' + response.accountId)) +
13889
+ ' (' + response.accountId + '), ' + (response.scope || 'self') +
13890
+ ' · accounts ' + ((response.accountIds || []).join(', ') || response.accountId));
13891
+ const counts = response.counts || {};
13892
+ console.log('Counts: ' +
13893
+ (counts.agents || 0) + ' agent(s), ' +
13894
+ (counts.lanes || 0) + ' lane(s), ' +
13895
+ (counts.verificationEnvelopes || 0) + ' envelope(s), ' +
13896
+ (counts.testRuns || 0) + ' test run(s), ' +
13897
+ (counts.corpusResults || 0) + ' corpus result(s)');
13898
+
13899
+ const smells = response.smells || [];
13900
+ console.log('');
13901
+ if (!smells.length) {
13902
+ console.log('Smells: none detected in this slice.');
13903
+ } else {
13904
+ console.log('Smells (' + smells.length + '):');
13905
+ smells.slice(0, 20).forEach((smell) => {
13906
+ console.log(' [' + (smell.severity || 'info') + '] ' + (smell.type || 'signal') + ': ' + smell.summary);
13907
+ const pivots = [
13908
+ smell.accountId ? 'account ' + smell.accountId : null,
13909
+ smell.laneId ? 'lane ' + smell.laneId : null,
13910
+ smell.envelopeId ? 'envelope ' + smell.envelopeId : null,
13911
+ smell.taskId ? 'task ' + smell.taskId : null,
13912
+ smell.testName || null,
13913
+ smell.branchName ? 'branch ' + smell.branchName : null,
13914
+ smell.workspace ? 'ws:' + smell.workspace : null
13915
+ ].filter(Boolean);
13916
+ if (pivots.length) console.log(' ' + pivots.join(' · '));
13917
+ });
13918
+ if (smells.length > 20) console.log(' ...' + (smells.length - 20) + ' more; rerun with --json');
13919
+ }
13920
+
13921
+ const lanes = response.lanes || [];
13922
+ if (lanes.length) {
13923
+ console.log('');
13924
+ console.log('Current staging lanes:');
13925
+ lanes.slice(0, 12).forEach((lane) => {
13926
+ const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : ' [shared]';
13927
+ const workset = lane.worksetKnown && lane.worksetCountAsOf != null
13928
+ ? ' workset ' + lane.worksetCountAsOf
13929
+ : ' workset unknown';
13930
+ console.log(' - account ' + lane.accountId + ' ' + (lane.branchName || '?') + ws +
13931
+ ' overlay ' + (lane.stagedCountAsOf || 0) + workset +
13932
+ (lane.userEmail ? ' · ' + lane.userEmail : '') +
13933
+ (lane.expiresInSeconds != null ? ' · expires ~' + Math.round(lane.expiresInSeconds / 60) + 'm' : ''));
13934
+ });
13935
+ if (lanes.length > 12) console.log(' ...' + (lanes.length - 12) + ' more lane(s)');
13936
+ }
13937
+
13938
+ const envelopes = response.envelopes || [];
13939
+ if (envelopes.length) {
13940
+ console.log('');
13941
+ console.log('Recent verification envelopes:');
13942
+ envelopes.slice(0, 8).forEach((env) => {
13943
+ console.log(' - ' + (env.status || 'unknown') + ' · ' + (env.packetCount || 0) + ' packet(s) · ' +
13944
+ (env.summary || env.envelopeId) + ' · ' + formatTime(env.updatedAtMs || env.lastPacketAtMs || env.createdAt));
13945
+ if (env.currentFailureCount) console.log(' current failures: ' + env.currentFailureCount);
13946
+ });
13947
+ }
13948
+
13949
+ const runs = response.testRuns || [];
13950
+ if (runs.length) {
13951
+ console.log('');
13952
+ console.log('Recent test runs:');
13953
+ runs.slice(0, 8).forEach((run) => {
13954
+ console.log(' - ' + (run.testName || run.taskId) + ' · ' + (run.passed || 0) + '/' + (run.total || 0) +
13955
+ ' passed · ' + (run.branchName || '?') + (run.workspace ? ' [ws:' + run.workspace + ']' : '') +
13956
+ ' · ' + corpusAiLabel(run) + ' · ' + formatTime(run.completedAt || run.startedAt));
13957
+ });
13958
+ }
13959
+
13960
+ const corpus = response.corpusResults || [];
13961
+ if (corpus.length) {
13962
+ console.log('');
13963
+ console.log('Recent corpus measurements:');
13964
+ corpus.slice(0, 6).forEach((row) => {
13965
+ console.log(' - ' + (row.corpus || '?') + '/' + (row.caseKey || '?') + ' · ' +
13966
+ (row.outcome || 'unknown') + ' · ' + (row.testName || row.taskId) +
13967
+ ' · ' + formatTime(row.recordedAt));
13968
+ });
13969
+ }
13970
+
13971
+ const timeline = response.timeline || [];
13972
+ if (timeline.length) {
13973
+ console.log('');
13974
+ console.log('Timeline:');
13975
+ timeline.slice(0, 12).forEach((item) => {
13976
+ console.log(' - ' + formatTime(item.at) + ' · ' + item.type + ' · ' +
13977
+ (item.label || '') + (item.detail ? ' · ' + item.detail : ''));
13978
+ });
13979
+ }
13980
+ console.log('');
13981
+ console.log('Full structured payload: remits-cli activity inspect --json');
13982
+ }
13983
+
12179
13984
  /**
12180
13985
  * The live work map: which repositories are being edited, by whom, and who else is around.
12181
13986
  *
@@ -12449,6 +14254,7 @@ async function agentCommand(flags, subcommand) {
12449
14254
  case 'work': return agentWorkCommand(flags);
12450
14255
  case 'list': case 'ls': return agentListCommand(flags);
12451
14256
  case 'map': case 'work-map': case 'workmap': return agentWorkMapCommand(flags);
14257
+ case 'inspect': case 'activity': return activityInspectCommand(flags);
12452
14258
  case 'repos': case 'repo': return agentReposCommand(flags);
12453
14259
  case 'release': case 'deregister': case 'stop': return agentReleaseCommand(flags);
12454
14260
  case 'heartbeat-daemon': return agentHeartbeatDaemonCommand(flags);
@@ -12501,6 +14307,8 @@ function printAgentHelp() {
12501
14307
  console.log('');
12502
14308
  console.log(' list [--account-id ID] [--json] Who else is available for an account.');
12503
14309
  console.log(' map [--account-ids 1,4] [--json] Who is EDITING which repository right now.');
14310
+ console.log(' inspect [--account-id ID] [--scope self|children] [--user-email E] [--json]');
14311
+ console.log(' Read-only workstream story + smell signals for another CLI user or agent fleet.');
12504
14312
  console.log(' One agent at a time may edit an account\'s repo (the edit lease); everyone else');
12505
14313
  console.log(' investigates read-only. Check this before starting work that changes files.');
12506
14314
  console.log(' repos [--rescan] [--json] WHERE each account is checked out on this machine —');
@@ -13048,6 +14856,8 @@ function printComponentsHelp(subcommand) {
13048
14856
  function printTestHelp() {
13049
14857
  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]');
13050
14858
  console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
14859
+ console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
14860
+ console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
13051
14861
  console.log('');
13052
14862
  console.log('Runs a Test component against the staged/variant world for this checkout.');
13053
14863
  console.log('Examples:');
@@ -13065,6 +14875,38 @@ function printTestHelp() {
13065
14875
  console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
13066
14876
  console.log(' --json prints only the final status JSON to stdout; banners and progress go to stderr.');
13067
14877
  console.log(' --data-mode prod intentionally targets live production data.');
14878
+ console.log(' Every finished run is recorded durably: test status keeps working after the live status');
14879
+ console.log(' expires, and test runs / test compare read those records.');
14880
+ }
14881
+
14882
+
14883
+ function printCorpusHelp() {
14884
+ console.log('Usage: remits-cli corpus import --manifest corpus-manifest.json [--corpus <name>] [--as-account ID] [--data-mode test|prod --confirm-prod] [--json]');
14885
+ console.log(' remits-cli corpus cases --corpus <name> [--split dev] [--tag name] [--key caseKey] [--include-values] [--include-retired] [--json]');
14886
+ console.log(' remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--limit 50] [--json]');
14887
+ console.log(' remits-cli corpus runs --corpus <name> [--limit 20] [--json]');
14888
+ console.log(' remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--json]');
14889
+ console.log(' remits-cli corpus consistency --corpus <name> --case <caseKey> [--limit 20] [--json]');
14890
+ console.log(' remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json]');
14891
+ console.log('');
14892
+ console.log('A corpus is a named, durable set of cases an evaluation-style Test runs again and again (front stage:');
14893
+ console.log('corpus(name)). Cases live in a platform table owned by one account and visible to the accounts beneath');
14894
+ console.log('it, whatever the data lane or branch. Case files are stored once as immutable FILE source Objects.');
14895
+ console.log('Measurements are recorded by Test runs and read back with results / runs / compare / consistency.');
14896
+ console.log('');
14897
+ console.log(' - import is idempotent by caseKey: created | updated (with changed keys) | unchanged; identical artifact');
14898
+ console.log(' bytes are not stored twice. Artifacts are created in the --data-mode lane (default test).');
14899
+ console.log(' - Output and session logs carry keys, statuses, hashes, sizes and object ids, never artifact bytes;');
14900
+ console.log(' cases hides expected/source values unless --include-values.');
14901
+ console.log(' - cases filters by --split, repeated/comma-separated --tag/--tags, and --key/--keys before applying --limit.');
14902
+ console.log(' - runs / results / compare label each run AI live, AI MOCKED or no AI. A suite whose AI turns were all');
14903
+ console.log(' replayed from aiMock produces the same outcomes and metrics as a measured one, so read that label');
14904
+ console.log(' before treating a green run as evidence; compare warns when base and head used different AI modes.');
14905
+ console.log(' - retire drops a case from future runs without deleting it: existing measurements stay readable.');
14906
+ console.log('');
14907
+ console.log('Manifest (paths relative to the manifest):');
14908
+ console.log(' {"corpus":"invoice-totals","cases":[{"caseKey":"inv-001","split":"dev","tags":["multi-page"],');
14909
+ console.log(' "expected":{"total":1234.5},"source":{"note":"copied from ..."},"artifacts":[{"name":"source","path":"inv-001.pdf"}]}]}');
13068
14910
  }
13069
14911
 
13070
14912
  function printToolHelp() {
@@ -13105,7 +14947,7 @@ function printVerifyHelp() {
13105
14947
  console.log('Usage: remits-cli verify <subcommand>');
13106
14948
  console.log('');
13107
14949
  console.log('Envelope lifecycle:');
13108
- console.log(' remits-cli verify start --summary "..." [--manifest file.json] [--ticket ID]');
14950
+ console.log(' remits-cli verify start --summary "..." [--manifest file.json] [--ticket ID] [--data-mode test|prod]');
13109
14951
  console.log(' remits-cli verify list [--max 50] [--json]');
13110
14952
  console.log(' remits-cli verify use <envelopeId>');
13111
14953
  console.log(' remits-cli verify current');
@@ -13115,7 +14957,8 @@ function printVerifyHelp() {
13115
14957
  console.log(' remits-cli verify report [--envelope ID]');
13116
14958
  console.log(' remits-cli verify abandon --envelope ID --reason "..."');
13117
14959
  console.log(' remits-cli verify supersede --envelope ID --superseded-by ID [--reason "..."]');
13118
- console.log(' Active envelopes are scoped by host, account, branch, workspace, and data mode.');
14960
+ console.log(' One active envelope per checkout world: host, account, git branch, workspace (not data lane).');
14961
+ console.log(' verify start proves in the test lane unless you pass --data-mode prod.');
13119
14962
  console.log('');
13120
14963
  console.log('Manifest and manual evidence:');
13121
14964
  console.log(' remits-cli verify manifest --file file.json [--envelope ID]');
@@ -13131,6 +14974,14 @@ function printVerifyHelp() {
13131
14974
  console.log(' remits-cli verify sync --safe');
13132
14975
  console.log(' remits-cli verify tool --name mcp_tool --input \'{...}\'');
13133
14976
  console.log('');
14977
+ console.log('World check (asked of the platform before stage, test run, token, tool, sync, and commit run):');
14978
+ console.log(' attach same world as the envelope requires - evidence attaches.');
14979
+ console.log(' refuse different world AND a requirement could use this evidence - nothing runs; the');
14980
+ console.log(' refusal names the flag that fixes it (e.g. --data-mode prod, --as-account 21).');
14981
+ console.log(' detach different world, nothing in the manifest could use it - runs, attaches nothing, says so.');
14982
+ console.log(' --allow-wrong-world-evidence attaches anyway as context (the report lists it wrong-world).');
14983
+ console.log(' verify token / verify tool run in the envelope\'s lane when it is test.');
14984
+ console.log('');
13134
14985
  console.log('Existing commands also accept --verify-envelope <id> and --no-verify-envelope.');
13135
14986
  }
13136
14987
 
@@ -13138,6 +14989,7 @@ async function main() {
13138
14989
  migrateSessionIfNeeded();
13139
14990
  const originalArgv = process.argv.slice(2);
13140
14991
  const args = parseArgs(originalArgv);
14992
+ configureLocalActor(args);
13141
14993
  const [command, subcommand] = args._;
13142
14994
  const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
13143
14995
  const wantsJson = flagEnabled(args.json);
@@ -13168,7 +15020,7 @@ async function main() {
13168
15020
  try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
13169
15021
  // Auto-start the background service if not already running.
13170
15022
  // Skip for lifecycle subcommands and help.
13171
- const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status' || command === 'whoami' || command === 'agent' || command === 'ticket';
15023
+ const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status' || command === 'whoami' || command === 'doctor' || command === 'agent' || command === 'ticket';
13172
15024
  const isHelpCmd = !command || command === 'help' || command === '--help' || wantsHelp;
13173
15025
  if (!wantsJson && !isHelpCmd && !isLifecycleCmd && !isListenerRunning()) {
13174
15026
  try {
@@ -13191,7 +15043,7 @@ async function main() {
13191
15043
 
13192
15044
  if (!command || command === 'help' || command === '--help') {
13193
15045
  console.log('Usage: remits-cli <command>');
13194
- console.log('Global options: --no-auto-update');
15046
+ console.log('Global options: --no-auto-update, --local-agent NAME');
13195
15047
  console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
13196
15048
  console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
13197
15049
  console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
@@ -13200,10 +15052,12 @@ async function main() {
13200
15052
  console.log(' remits-cli agent status [--state idle|working|paused] [--ticket ID] [--activity "..."]');
13201
15053
  console.log(' remits-cli agent list [--account-id ID] # who else is available');
13202
15054
  console.log(' remits-cli agent map [--account-ids 1,4] [--json] # who is EDITING what, where, right now');
15055
+ console.log(' remits-cli activity inspect [--account-id ID] [--scope self|children] [--user-email E] [--json]');
13203
15056
  console.log(' remits-cli agent release # stop receiving tickets now');
13204
15057
  console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
13205
15058
  console.log(' remits-cli stop');
13206
15059
  console.log(' remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
15060
+ console.log(' remits-cli doctor local-state [--json] # actor/workspace/local state hygiene');
13207
15061
  console.log(' remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
13208
15062
  console.log(' remits-cli listen [stop|status] [--foreground true] # compatibility alias');
13209
15063
  console.log(' remits-cli data-mode [set test|prod]');
@@ -13228,6 +15082,10 @@ async function main() {
13228
15082
  console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
13229
15083
  console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
13230
15084
  console.log(' 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]');
15085
+ console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
15086
+ console.log(' remits-cli test compare --base <taskId> --head <taskId>');
15087
+ console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
15088
+ console.log(' remits-cli corpus runs|results|compare|consistency --corpus <name> # measurements across Test runs');
13231
15089
  console.log(' remits-cli token [--base-url URL] [--branch BRANCH] [--path embeddable/path] [--data-mode test|prod] [--as-account ID] [--variant-branch NAME|none]');
13232
15090
  console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
13233
15091
  console.log('');
@@ -13398,6 +15256,11 @@ async function main() {
13398
15256
  return;
13399
15257
  }
13400
15258
 
15259
+ if (command === 'doctor') {
15260
+ await doctorCommand(args, subcommand);
15261
+ return;
15262
+ }
15263
+
13401
15264
  if (command === 'whoami') {
13402
15265
  await whoamiCommand(args);
13403
15266
  return;
@@ -13412,6 +15275,22 @@ async function main() {
13412
15275
  return;
13413
15276
  }
13414
15277
 
15278
+ if (command === 'activity') {
15279
+ if (wantsHelp || !subcommand) {
15280
+ console.log('Usage: remits-cli activity inspect [--account-id ID] [--scope self|children] [--user-id ID|--user-email EMAIL] [--limit N] [--json]');
15281
+ console.log('');
15282
+ console.log('Read-only workstream inspection for agent/operator review. Joins current staging lanes,');
15283
+ console.log('verification envelopes, durable test runs, corpus measurements, live agent presence,');
15284
+ console.log('and heuristic smell signals without changing any lane or checkout.');
15285
+ return;
15286
+ }
15287
+ if (subcommand === 'inspect') {
15288
+ await activityInspectCommand(args);
15289
+ return;
15290
+ }
15291
+ throw new Error('Unknown activity subcommand: ' + subcommand);
15292
+ }
15293
+
13415
15294
  if (command === 'ticket') {
13416
15295
  if (wantsHelp || !subcommand) {
13417
15296
  printTicketHelp();
@@ -13444,6 +15323,23 @@ async function main() {
13444
15323
  await testStatusCommand(args);
13445
15324
  return;
13446
15325
  }
15326
+ if (command === 'test' && (subcommand === 'runs' || subcommand === 'history')) {
15327
+ await testRunsCommand(args);
15328
+ return;
15329
+ }
15330
+ if (command === 'test' && subcommand === 'compare') {
15331
+ await testCompareCommand(args);
15332
+ return;
15333
+ }
15334
+
15335
+ if (command === 'corpus') {
15336
+ if (wantsHelp || !subcommand) {
15337
+ printCorpusHelp();
15338
+ process.exit(0);
15339
+ }
15340
+ await corpusCommand(args, subcommand);
15341
+ return;
15342
+ }
13447
15343
 
13448
15344
  if (command === 'token') {
13449
15345
  await tokenCommand(args);