@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/README.md +2 -0
- package/index.js +2021 -125
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +15 -5
- package/skills/remits-cli/references/branch-variants.md +10 -4
- package/skills/remits-cli/references/cli-state.md +49 -20
- package/skills/remits-cli/references/command-reference.md +58 -10
- package/skills/remits-cli/references/component-integrity.md +2 -1
- package/skills/remits-cli/references/component-resolution.md +7 -0
- package/skills/remits-cli/references/development-loop.md +44 -6
- package/skills/remits-cli/references/support-tickets.md +3 -1
- package/skills/remits-cli/references/tool-reference.md +27 -5
- package/skills/remits-cli/references/troubleshooting.md +10 -8
package/index.js
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
## Table of Contents
|
|
5
5
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
897
|
-
|
|
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
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
|
929
|
-
|
|
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
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
974
|
-
if (explicit
|
|
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
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1005
|
-
fs.
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
2591
|
-
|
|
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,
|
|
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,
|
|
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('
|
|
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
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
8788
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8789
|
+
printLocalStateWarnings(cwd, flags);
|
|
8790
|
+
console.log('Checkout world:', checkoutWorldLine);
|
|
7162
8791
|
if (!active) {
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
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
|
-
|
|
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
|
|
7193
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
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) =>
|
|
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('
|
|
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);
|