@remits/remits-cli 0.1.129 → 0.1.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1500 -72
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +15 -5
- package/skills/remits-cli/references/cli-state.md +49 -20
- package/skills/remits-cli/references/command-reference.md +31 -9
- 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 +25 -6
- package/skills/remits-cli/references/support-tickets.md +3 -1
- package/skills/remits-cli/references/tool-reference.md +23 -2
- 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
|
+
- L2018 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L2676 Component Discovery And HTTP Logging
|
|
11
|
+
- L3252 Skill Delivery And TOC Resolution
|
|
12
|
+
- L3562 Auth And Component Staging
|
|
13
|
+
- L4171 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L6204 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L7813 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L8943 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L11059 Agent And Ticket Workflows
|
|
18
|
+
- L14117 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) {
|
|
@@ -898,14 +1421,18 @@ function verificationContextKey(context = {}) {
|
|
|
898
1421
|
}
|
|
899
1422
|
|
|
900
1423
|
function readActiveVerificationContexts(cwd) {
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1424
|
+
const paths = localStatePaths(cwd);
|
|
1425
|
+
const candidates = [paths.activeVerificationContextsFile, paths.legacy.activeVerificationContextsFile];
|
|
1426
|
+
for (const file of candidates) {
|
|
1427
|
+
if (!fs.existsSync(file)) continue;
|
|
1428
|
+
try {
|
|
1429
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1430
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
1431
|
+
} catch (_) {
|
|
1432
|
+
return {};
|
|
1433
|
+
}
|
|
908
1434
|
}
|
|
1435
|
+
return {};
|
|
909
1436
|
}
|
|
910
1437
|
|
|
911
1438
|
function writeActiveVerificationContexts(cwd, contexts) {
|
|
@@ -931,14 +1458,18 @@ function readActiveVerificationEnvelope(cwd, context = null) {
|
|
|
931
1458
|
if (typeof entry === 'string') return entry || null;
|
|
932
1459
|
return null;
|
|
933
1460
|
}
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1461
|
+
const paths = verificationPaths(cwd);
|
|
1462
|
+
const candidates = [paths.activeFile, localStatePaths(cwd).legacy.activeVerificationFile];
|
|
1463
|
+
for (const file of candidates) {
|
|
1464
|
+
try {
|
|
1465
|
+
if (!fs.existsSync(file)) continue;
|
|
1466
|
+
const id = fs.readFileSync(file, 'utf8').trim();
|
|
1467
|
+
return id || null;
|
|
1468
|
+
} catch (_) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
941
1471
|
}
|
|
1472
|
+
return null;
|
|
942
1473
|
}
|
|
943
1474
|
|
|
944
1475
|
function writeActiveVerificationEnvelope(cwd, envelopeId, context = null) {
|
|
@@ -990,29 +1521,36 @@ function writeLocalVerificationEnvelope(cwd, envelope) {
|
|
|
990
1521
|
|
|
991
1522
|
function readLocalVerificationEnvelope(cwd, envelopeId) {
|
|
992
1523
|
const paths = verificationPaths(cwd, envelopeId);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1524
|
+
const legacyBase = path.join(localStatePaths(cwd).legacy.verificationDir, String(envelopeId));
|
|
1525
|
+
const candidates = [paths.envelopeFile, path.join(legacyBase, 'envelope.json')];
|
|
1526
|
+
for (const file of candidates) {
|
|
1527
|
+
if (!fs.existsSync(file)) continue;
|
|
1528
|
+
try {
|
|
1529
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1530
|
+
} catch (_) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
998
1533
|
}
|
|
1534
|
+
return null;
|
|
999
1535
|
}
|
|
1000
1536
|
|
|
1001
1537
|
function readLocalVerificationPackets(cwd, envelopeId, filters = {}) {
|
|
1002
1538
|
const paths = verificationPaths(cwd, envelopeId);
|
|
1539
|
+
const legacyPacketsDir = path.join(localStatePaths(cwd).legacy.verificationDir, String(envelopeId), 'packets');
|
|
1003
1540
|
const packets = [];
|
|
1004
|
-
|
|
1005
|
-
fs.
|
|
1541
|
+
[paths.packetsDir, legacyPacketsDir].forEach((packetsDir) => {
|
|
1542
|
+
if (!fs.existsSync(packetsDir)) return;
|
|
1543
|
+
fs.readdirSync(packetsDir)
|
|
1006
1544
|
.filter((name) => name.endsWith('.json'))
|
|
1007
1545
|
.forEach((name) => {
|
|
1008
1546
|
try {
|
|
1009
|
-
const parsed = JSON.parse(fs.readFileSync(path.join(
|
|
1547
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(packetsDir, name), 'utf8'));
|
|
1010
1548
|
if (parsed && typeof parsed === 'object') packets.push(parsed);
|
|
1011
1549
|
} catch (_) {
|
|
1012
1550
|
// Ignore one malformed local packet; the rest of the envelope remains useful.
|
|
1013
1551
|
}
|
|
1014
1552
|
});
|
|
1015
|
-
}
|
|
1553
|
+
});
|
|
1016
1554
|
if (!packets.length) {
|
|
1017
1555
|
const envelope = readLocalVerificationEnvelope(cwd, envelopeId);
|
|
1018
1556
|
if (envelope && Array.isArray(envelope.packets)) packets.push(...envelope.packets);
|
|
@@ -1144,6 +1682,29 @@ function syncEvidencePayload(sync) {
|
|
|
1144
1682
|
return Object.assign({}, sync, { syncResults: trimmedResults });
|
|
1145
1683
|
}
|
|
1146
1684
|
|
|
1685
|
+
// The world an envelope was STARTED in, read the same way the platform reads it
|
|
1686
|
+
// (VerificationEnvelopeService.envelopeStartWorld / ENVELOPE_START_WORLD_KEYS). Kept field-for-field identical so
|
|
1687
|
+
// the attach-time warning and the verify-time wrong-world verdict can never disagree.
|
|
1688
|
+
function envelopeWorldDrift(envelope, context = {}) {
|
|
1689
|
+
if (!envelope || typeof envelope !== 'object') return [];
|
|
1690
|
+
const source = envelope.source && typeof envelope.source === 'object' ? envelope.source : {};
|
|
1691
|
+
const account = envelope.account && typeof envelope.account === 'object' ? envelope.account : {};
|
|
1692
|
+
const started = {
|
|
1693
|
+
'repo account': account.repoAccountId != null ? String(account.repoAccountId) : (envelope.accountId != null ? String(envelope.accountId) : null),
|
|
1694
|
+
'data lane': normalizeDataMode(account.dataMode || source.dataMode) || null,
|
|
1695
|
+
'git branch': source.gitBranch || source.branchName || null,
|
|
1696
|
+
workspace: normalizeWorkspace(source.workspace) || null
|
|
1697
|
+
};
|
|
1698
|
+
const now = {
|
|
1699
|
+
'repo account': context.accountId != null ? String(context.accountId) : null,
|
|
1700
|
+
'data lane': normalizeDataMode(context.dataMode) || null,
|
|
1701
|
+
'git branch': context.branchName || null,
|
|
1702
|
+
workspace: normalizeWorkspace(context.workspace) || null
|
|
1703
|
+
};
|
|
1704
|
+
return Object.keys(started).filter((key) => started[key] && now[key] && started[key] !== now[key])
|
|
1705
|
+
.map((key) => key + ' ' + started[key] + ' -> ' + now[key]);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1147
1708
|
async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
|
|
1148
1709
|
const context = activeVerificationContext(cwd, flags, session, accountId);
|
|
1149
1710
|
const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
|
|
@@ -1163,7 +1724,16 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
|
|
|
1163
1724
|
});
|
|
1164
1725
|
if (response.packet) writeLocalVerificationPacket(cwd, envelopeId, response.packet);
|
|
1165
1726
|
if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
1166
|
-
if (!options.quiet)
|
|
1727
|
+
if (!options.quiet) {
|
|
1728
|
+
console.log('Verification envelope:', envelopeId, '(packet ' + finalPacket.type + ' attached)');
|
|
1729
|
+
// Say it HERE, not only at `verify` time. Evidence attached to an envelope started in a different world
|
|
1730
|
+
// is evidence about a different world, and the reader has to learn that while they can still redo the run.
|
|
1731
|
+
const drift = envelopeWorldDrift(response.envelope, context);
|
|
1732
|
+
if (drift.length) {
|
|
1733
|
+
console.log(' WARNING: this envelope was started in a different world - ' + drift.join('; ') +
|
|
1734
|
+
'. The packet is attached, but it will be reported wrong-world at verify time.');
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1167
1737
|
return response;
|
|
1168
1738
|
} catch (err) {
|
|
1169
1739
|
if (!options.quiet) {
|
|
@@ -2114,11 +2684,75 @@ function resolveSessionContext(cwd, flags) {
|
|
|
2114
2684
|
* file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
|
|
2115
2685
|
* identity is the only thing both the payload and the git changed set agree on.
|
|
2116
2686
|
*/
|
|
2117
|
-
function withFileKey(target, key) {
|
|
2687
|
+
function withFileKey(target, key, filePaths) {
|
|
2118
2688
|
Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
|
|
2689
|
+
if (filePaths) {
|
|
2690
|
+
addHiddenFilePaths(target, filePaths);
|
|
2691
|
+
}
|
|
2692
|
+
return target;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
function addHiddenFilePaths(target, filePaths) {
|
|
2696
|
+
if (!target || !filePaths) return target;
|
|
2697
|
+
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
2698
|
+
if (!Object.prototype.hasOwnProperty.call(target, 'filePaths')) {
|
|
2699
|
+
Object.defineProperty(target, 'filePaths', { value: [], enumerable: false, configurable: true });
|
|
2700
|
+
}
|
|
2701
|
+
for (const filePath of paths) {
|
|
2702
|
+
const normalized = String(filePath || '').replace(/\\/g, '/');
|
|
2703
|
+
if (normalized && !target.filePaths.includes(normalized)) target.filePaths.push(normalized);
|
|
2704
|
+
}
|
|
2119
2705
|
return target;
|
|
2120
2706
|
}
|
|
2121
2707
|
|
|
2708
|
+
function isRootReadmeComponent(component) {
|
|
2709
|
+
return component &&
|
|
2710
|
+
component.type === 'prompt' &&
|
|
2711
|
+
component.fileKey === 'prompt:name:readme' &&
|
|
2712
|
+
normalizeName(String(component.name || '')).toLowerCase() === 'readme' &&
|
|
2713
|
+
String(component.purpose || '').toUpperCase() === 'README' &&
|
|
2714
|
+
(component.path === 'README.md' || (component.filePaths || []).includes('README.md'));
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function isLegacyReadmePromptComponent(component) {
|
|
2718
|
+
if (!component || component.type !== 'prompt' ||
|
|
2719
|
+
normalizeName(String(component.name || '')).toLowerCase() !== 'readme') {
|
|
2720
|
+
return false;
|
|
2721
|
+
}
|
|
2722
|
+
return [component.path].concat(component.filePaths || []).some((filePath) =>
|
|
2723
|
+
/^components\/prompts\/[^/]+_README\.md$/i.test(String(filePath || '')));
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function canonicalizeReadmePromptComponents(components) {
|
|
2727
|
+
const list = Array.isArray(components) ? components : [];
|
|
2728
|
+
const rootReadme = list.find(isRootReadmeComponent);
|
|
2729
|
+
if (!rootReadme) {
|
|
2730
|
+
return { components: list, warnings: [] };
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const warnings = [];
|
|
2734
|
+
const canonical = [];
|
|
2735
|
+
for (const component of list) {
|
|
2736
|
+
if (component !== rootReadme && isLegacyReadmePromptComponent(component)) {
|
|
2737
|
+
const legacyPaths = [component.path].concat(component.filePaths || [])
|
|
2738
|
+
.filter((filePath) => /^components\/prompts\/[^/]+_README\.md$/i.test(String(filePath || '')))
|
|
2739
|
+
.sort();
|
|
2740
|
+
warnings.push({
|
|
2741
|
+
type: 'legacy-readme-prompt-suppressed',
|
|
2742
|
+
canonicalPath: 'README.md',
|
|
2743
|
+
legacyPaths,
|
|
2744
|
+
id: component.id == null ? null : component.id,
|
|
2745
|
+
name: component.name || 'README',
|
|
2746
|
+
message: 'Skipped legacy README prompt file ' + (legacyPaths.join(', ') || 'components/prompts/*_README.md') +
|
|
2747
|
+
' because root README.md owns the reserved README prompt identity.'
|
|
2748
|
+
});
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
canonical.push(component);
|
|
2752
|
+
}
|
|
2753
|
+
return { components: canonical, warnings };
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2122
2756
|
function collectComponents(cwd) {
|
|
2123
2757
|
const mapping = {
|
|
2124
2758
|
schemas: 'schema',
|
|
@@ -2170,6 +2804,7 @@ function collectComponents(cwd) {
|
|
|
2170
2804
|
}
|
|
2171
2805
|
const component = byKey.get(key);
|
|
2172
2806
|
const filePath = path.join(dir, fileName);
|
|
2807
|
+
addHiddenFilePaths(component, path.relative(cwd, filePath).replace(/\\/g, '/'));
|
|
2173
2808
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
2174
2809
|
|
|
2175
2810
|
if (ext === 'meta') {
|
|
@@ -2233,7 +2868,7 @@ function collectComponents(cwd) {
|
|
|
2233
2868
|
category: 'default',
|
|
2234
2869
|
prompt: fs.readFileSync(rootReadme, 'utf8'),
|
|
2235
2870
|
metadataAuthoritative: true
|
|
2236
|
-
}, 'prompt:name:readme');
|
|
2871
|
+
}, 'prompt:name:readme', 'README.md');
|
|
2237
2872
|
const fingerprint = {};
|
|
2238
2873
|
Object.keys(component).sort().forEach((key) => {
|
|
2239
2874
|
if (key !== 'hash') {
|
|
@@ -2244,7 +2879,13 @@ function collectComponents(cwd) {
|
|
|
2244
2879
|
components.push(component);
|
|
2245
2880
|
}
|
|
2246
2881
|
|
|
2247
|
-
|
|
2882
|
+
const canonicalized = canonicalizeReadmePromptComponents(components);
|
|
2883
|
+
Object.defineProperty(canonicalized.components, 'discoveryWarnings', {
|
|
2884
|
+
value: canonicalized.warnings,
|
|
2885
|
+
enumerable: false,
|
|
2886
|
+
configurable: true
|
|
2887
|
+
});
|
|
2888
|
+
return canonicalized.components;
|
|
2248
2889
|
}
|
|
2249
2890
|
|
|
2250
2891
|
function componentPathInfo(cwd, filePath) {
|
|
@@ -2534,6 +3175,11 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2534
3175
|
let responseData = null;
|
|
2535
3176
|
let error = null;
|
|
2536
3177
|
let responseFile = null;
|
|
3178
|
+
const actor = resolveLocalActor();
|
|
3179
|
+
const unsafePayloadLog = unsafePayloadLoggingEnabled();
|
|
3180
|
+
if (unsafePayloadLog && !options.skipSessionLog) {
|
|
3181
|
+
console.error('WARNING: REMITS_CLI_UNSAFE_LOG_PAYLOADS=1 is enabled; session logs may contain request payload values.');
|
|
3182
|
+
}
|
|
2537
3183
|
|
|
2538
3184
|
try {
|
|
2539
3185
|
responseData = await api.post(endpoint, payload).then((r) => r.data);
|
|
@@ -2551,15 +3197,19 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2551
3197
|
const status = error ? 'error' : 'success';
|
|
2552
3198
|
const responseForLog = responseFile
|
|
2553
3199
|
? { responseFile, note: 'Tool response stored externally' }
|
|
2554
|
-
:
|
|
3200
|
+
: summarizeResponseForLog(responseData);
|
|
2555
3201
|
appendSessionLog(cwd, {
|
|
2556
3202
|
ts: new Date().toISOString(),
|
|
2557
3203
|
requestId,
|
|
2558
3204
|
endpoint,
|
|
2559
3205
|
method: 'POST',
|
|
3206
|
+
actor: actor.id,
|
|
3207
|
+
actorSource: actor.source,
|
|
2560
3208
|
status,
|
|
2561
3209
|
durationMs: Date.now() - started,
|
|
2562
|
-
|
|
3210
|
+
requestSummary: summarizeRequestForLog(payload),
|
|
3211
|
+
unsafePayloadLogging: unsafePayloadLog,
|
|
3212
|
+
request: requestPayloadForSessionLog(payload),
|
|
2563
3213
|
response: responseForLog,
|
|
2564
3214
|
error: error ? describeError(error) : null
|
|
2565
3215
|
});
|
|
@@ -2572,6 +3222,7 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2572
3222
|
const started = Date.now();
|
|
2573
3223
|
let responseData = null;
|
|
2574
3224
|
let error = null;
|
|
3225
|
+
const actor = resolveLocalActor();
|
|
2575
3226
|
try {
|
|
2576
3227
|
responseData = await api.get(endpoint, { params }).then((r) => r.data);
|
|
2577
3228
|
return { data: responseData, requestId };
|
|
@@ -2585,10 +3236,13 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2585
3236
|
requestId,
|
|
2586
3237
|
endpoint,
|
|
2587
3238
|
method: 'GET',
|
|
3239
|
+
actor: actor.id,
|
|
3240
|
+
actorSource: actor.source,
|
|
2588
3241
|
status: error ? 'error' : 'success',
|
|
2589
3242
|
durationMs: Date.now() - started,
|
|
2590
|
-
|
|
2591
|
-
|
|
3243
|
+
requestSummary: summarizeRequestForLog(params),
|
|
3244
|
+
request: requestPayloadForSessionLog(params),
|
|
3245
|
+
response: summarizeResponseForLog(responseData),
|
|
2592
3246
|
error: error ? describeError(error) : null
|
|
2593
3247
|
});
|
|
2594
3248
|
}
|
|
@@ -3128,6 +3782,7 @@ async function pushComponentsCommand(flags) {
|
|
|
3128
3782
|
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
3129
3783
|
|
|
3130
3784
|
let components = collectComponents(cwd);
|
|
3785
|
+
const componentDiscoveryWarnings = Array.isArray(components.discoveryWarnings) ? components.discoveryWarnings : [];
|
|
3131
3786
|
// Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
|
|
3132
3787
|
const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
|
|
3133
3788
|
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
@@ -3187,7 +3842,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3187
3842
|
: null,
|
|
3188
3843
|
changedFromWorkingTree: [],
|
|
3189
3844
|
changedFromWorkingTreeAvailable: true,
|
|
3190
|
-
unrepresentableChanges: unstageable
|
|
3845
|
+
unrepresentableChanges: unstageable,
|
|
3846
|
+
componentDiscoveryWarnings,
|
|
3847
|
+
localState: localStateSummary(cwd, flags)
|
|
3191
3848
|
};
|
|
3192
3849
|
if (flagEnabled(flags.json)) {
|
|
3193
3850
|
console.log(JSON.stringify(emptyResponse, null, 2));
|
|
@@ -3199,6 +3856,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3199
3856
|
console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
|
|
3200
3857
|
}
|
|
3201
3858
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
3859
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
3860
|
+
printLocalStateWarnings(cwd, flags);
|
|
3861
|
+
printComponentDiscoveryWarnings(componentDiscoveryWarnings);
|
|
3202
3862
|
printUnrepresentableChanges(unstageable);
|
|
3203
3863
|
}
|
|
3204
3864
|
return emptyResponse;
|
|
@@ -3287,6 +3947,8 @@ async function pushComponentsCommand(flags) {
|
|
|
3287
3947
|
response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
|
|
3288
3948
|
response.requestedStageMode = stageMode;
|
|
3289
3949
|
response.unrepresentableChanges = unstageable;
|
|
3950
|
+
response.componentDiscoveryWarnings = componentDiscoveryWarnings;
|
|
3951
|
+
response.localState = localStateSummary(cwd, flags);
|
|
3290
3952
|
if (response.success === false) {
|
|
3291
3953
|
// A 200 that says `success:false`. The 422 path throws out of stageOrRefuse above and never lands
|
|
3292
3954
|
// here, so this stays as the belt-and-braces case — print whatever detail came with it first.
|
|
@@ -3320,6 +3982,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3320
3982
|
|
|
3321
3983
|
printSessionResolutionWarning(sessionContext);
|
|
3322
3984
|
printResolvedBaseUrl(baseUrl);
|
|
3985
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
|
|
3986
|
+
printLocalStateWarnings(cwd, flags);
|
|
3987
|
+
printComponentDiscoveryWarnings(componentDiscoveryWarnings);
|
|
3323
3988
|
printStagingLane(response.branchName || branchName, response.workspace || workspace, workspaceSource(cwd, flags));
|
|
3324
3989
|
console.log('Data mode:', response.dataMode || dataMode);
|
|
3325
3990
|
printComponentPolicy(response);
|
|
@@ -3522,6 +4187,15 @@ function printComponentPolicy(response) {
|
|
|
3522
4187
|
(policy.overrides || []).forEach((id) => console.log('Policy override recorded for rule: ' + id));
|
|
3523
4188
|
}
|
|
3524
4189
|
|
|
4190
|
+
function printComponentDiscoveryWarnings(warnings) {
|
|
4191
|
+
const list = Array.isArray(warnings) ? warnings : [];
|
|
4192
|
+
if (!list.length) return;
|
|
4193
|
+
console.log('Component discovery warnings:');
|
|
4194
|
+
list.forEach((warning) => {
|
|
4195
|
+
console.log(' - ' + (warning.message || JSON.stringify(warning)));
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
|
|
3525
4199
|
/**
|
|
3526
4200
|
* What the platform compiled, and what it could not.
|
|
3527
4201
|
*
|
|
@@ -4091,6 +4765,8 @@ async function workspaceCommand(flags) {
|
|
|
4091
4765
|
const branchName = flags.branch || currentBranch(cwd);
|
|
4092
4766
|
const workspace = resolveWorkspace(cwd, flags);
|
|
4093
4767
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
4768
|
+
printLocalActor(cwd, flags);
|
|
4769
|
+
printLocalStateWarnings(cwd, flags);
|
|
4094
4770
|
console.log('');
|
|
4095
4771
|
console.log('Precedence: --workspace > REMITS_WORKSPACE > .remits-cli/workspace > shared default lane');
|
|
4096
4772
|
console.log('');
|
|
@@ -4100,6 +4776,74 @@ async function workspaceCommand(flags) {
|
|
|
4100
4776
|
console.log(' remits-cli components status see every lane staged on this branch');
|
|
4101
4777
|
}
|
|
4102
4778
|
|
|
4779
|
+
function printFileRows(label, rows, max = 5) {
|
|
4780
|
+
console.log(label + ':', rows.length);
|
|
4781
|
+
rows.slice(0, max).forEach((row) => {
|
|
4782
|
+
console.log(' - ' + row.path + ' (' + row.size + ' bytes' + (row.updatedAt ? ', updated ' + row.updatedAt : '') + ')');
|
|
4783
|
+
});
|
|
4784
|
+
if (rows.length > max) {
|
|
4785
|
+
console.log(' ...' + (rows.length - max) + ' more');
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
async function doctorCommand(flags, subcommand) {
|
|
4790
|
+
const sub = String(subcommand || (flags._ && flags._[1]) || 'local-state').toLowerCase();
|
|
4791
|
+
if (sub !== 'local-state' && sub !== 'state') {
|
|
4792
|
+
throw new Error('Unknown doctor subcommand "' + sub + '". Expected local-state.');
|
|
4793
|
+
}
|
|
4794
|
+
const cwd = process.cwd();
|
|
4795
|
+
if (flagEnabled(flags['prune-actors'])) {
|
|
4796
|
+
const pruned = pruneStaleActorDirectories(cwd);
|
|
4797
|
+
console.log('Pruned ' + pruned.removed.length + ' idle actor dir(s)' +
|
|
4798
|
+
(pruned.kept ? ', kept ' + pruned.kept + ' (current or active in the last 7 days)' : '') + '.');
|
|
4799
|
+
pruned.removed.slice(0, 20).forEach((name) => console.log(' - ' + name));
|
|
4800
|
+
if (pruned.removed.length > 20) console.log(' ...' + (pruned.removed.length - 20) + ' more');
|
|
4801
|
+
if (pruned.failed.length) console.log(' Could not remove: ' + pruned.failed.join(', '));
|
|
4802
|
+
}
|
|
4803
|
+
const { report, warnings } = localStateWarnings(cwd, flags);
|
|
4804
|
+
if (flagEnabled(flags.json)) {
|
|
4805
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4806
|
+
return report;
|
|
4807
|
+
}
|
|
4808
|
+
console.log('Local state doctor');
|
|
4809
|
+
console.log('Working directory:', cwd);
|
|
4810
|
+
console.log('Local actor:', report.actor.id + ' (from ' + report.actor.source + ')');
|
|
4811
|
+
console.log('Active actor state:', report.activeActorStateDir);
|
|
4812
|
+
console.log('Workspace:', report.workspace || 'shared default lane', ' [' + report.workspaceSource + ']');
|
|
4813
|
+
console.log('Workspace file:', report.workspaceFile);
|
|
4814
|
+
if (warnings.length) {
|
|
4815
|
+
console.log('');
|
|
4816
|
+
console.log('Warnings:');
|
|
4817
|
+
warnings.forEach((warning) => console.log(' - ' + warning));
|
|
4818
|
+
}
|
|
4819
|
+
console.log('');
|
|
4820
|
+
console.log('Actor directories:', report.actors.length);
|
|
4821
|
+
const listedActors = report.actors
|
|
4822
|
+
.slice()
|
|
4823
|
+
.sort((a, b) => (a.current ? -1 : b.current ? 1 : Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0)));
|
|
4824
|
+
listedActors.slice(0, 15).forEach((actor) => {
|
|
4825
|
+
console.log(' ' + (actor.current ? '* ' : ' ') + actor.actor + ' ' + actor.path + (actor.updatedAt ? ' updated ' + actor.updatedAt : ''));
|
|
4826
|
+
});
|
|
4827
|
+
if (listedActors.length > 15) console.log(' ...' + (listedActors.length - 15) + ' more (newest first; --prune-actors clears the idle ones)');
|
|
4828
|
+
console.log(' (* = active actor)');
|
|
4829
|
+
console.log('');
|
|
4830
|
+
console.log('.remits-cli tracked by git:', report.trackedFiles.length ? 'YES' : 'no');
|
|
4831
|
+
report.trackedFiles.slice(0, 20).forEach((file) => console.log(' - ' + file));
|
|
4832
|
+
if (report.trackedFiles.length > 20) console.log(' ...' + (report.trackedFiles.length - 20) + ' more');
|
|
4833
|
+
console.log('');
|
|
4834
|
+
printFileRows('Current actor session logs', report.current.sessionLogs);
|
|
4835
|
+
printFileRows('Current actor tool responses', report.current.toolResponses);
|
|
4836
|
+
printFileRows('Legacy session logs', report.legacy.sessionLogs);
|
|
4837
|
+
printFileRows('Legacy tool responses', report.legacy.toolResponses);
|
|
4838
|
+
printFileRows('Largest local-state files', report.largestFiles, 10);
|
|
4839
|
+
const suspicious = report.current.likelyRawPayloadLogs.concat(report.legacy.likelyRawPayloadLogs);
|
|
4840
|
+
if (suspicious.length) {
|
|
4841
|
+
console.log('');
|
|
4842
|
+
printFileRows('Logs with raw/unsafe payload-shaped fields', suspicious, 10);
|
|
4843
|
+
}
|
|
4844
|
+
return report;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4103
4847
|
async function statusComponentsCommand(flags) {
|
|
4104
4848
|
const cwd = process.cwd();
|
|
4105
4849
|
ensureLocalState(cwd);
|
|
@@ -4140,6 +4884,7 @@ async function statusComponentsCommand(flags) {
|
|
|
4140
4884
|
isAncestor: (a, b) => gitIsAncestor(cwd, a, b)
|
|
4141
4885
|
});
|
|
4142
4886
|
response.repositoryCheck = repositoryCheck(cwd, response.branchContext);
|
|
4887
|
+
response.localState = localStateSummary(cwd, flags);
|
|
4143
4888
|
|
|
4144
4889
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4145
4890
|
type: 'component_status',
|
|
@@ -4159,6 +4904,8 @@ async function statusComponentsCommand(flags) {
|
|
|
4159
4904
|
|
|
4160
4905
|
printSessionResolutionWarning(sessionContext);
|
|
4161
4906
|
printResolvedBaseUrl(baseUrl);
|
|
4907
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
|
|
4908
|
+
printLocalStateWarnings(cwd, flags);
|
|
4162
4909
|
console.log('Data mode:', response.dataMode || dataMode);
|
|
4163
4910
|
console.log('Mode:', response.mode || 'status');
|
|
4164
4911
|
printStatusSummary(response, flags);
|
|
@@ -5812,6 +6559,17 @@ function short(sha) {
|
|
|
5812
6559
|
return sha ? String(sha).slice(0, 8) : 'unknown';
|
|
5813
6560
|
}
|
|
5814
6561
|
|
|
6562
|
+
// A component signature is structured, not a bare hash: 'version:12:ab34cd', 'content:<sha>', 'cli:<hash>'.
|
|
6563
|
+
// Truncating the whole thing to 8 characters rendered the commonest case as the literal string 'version:' —
|
|
6564
|
+
// the prefix exactly fills the budget and the part that identifies the code is what got cut.
|
|
6565
|
+
function shortSignature(signature) {
|
|
6566
|
+
if (!signature) return 'unknown';
|
|
6567
|
+
const parts = String(signature).split(':');
|
|
6568
|
+
if (parts.length < 2) return short(signature);
|
|
6569
|
+
const last = parts.pop();
|
|
6570
|
+
return parts.concat(short(last)).join(':');
|
|
6571
|
+
}
|
|
6572
|
+
|
|
5815
6573
|
async function commitComponentsCommand(flags) {
|
|
5816
6574
|
const cwd = process.cwd();
|
|
5817
6575
|
ensureLocalState(cwd);
|
|
@@ -6363,7 +7121,12 @@ function printTestRunPivots(status = {}) {
|
|
|
6363
7121
|
}
|
|
6364
7122
|
tests.filter((test) => test && test.passed === false).forEach((test) => {
|
|
6365
7123
|
console.log('Pivots for failed case:', test.name || '(unnamed)');
|
|
7124
|
+
if (test.outcome) console.log(' Outcome:', test.outcome + (test.outcomeReason && test.outcomeReason !== test.error ? ' - ' + String(test.outcomeReason).slice(0, 300) : ''));
|
|
6366
7125
|
if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
|
|
7126
|
+
if (test.ai && (test.ai.calls || test.ai.liveCalls)) {
|
|
7127
|
+
console.log(' AI:', (test.ai.liveCalls || 0) + ' live ' + formatUsd(test.ai.cost) + ', ' + (test.ai.mockedCalls || 0) + ' mocked' +
|
|
7128
|
+
((test.ai.errors || []).length ? ', errors: ' + test.ai.errors.map((e) => (e.classification || 'error') + ' ' + (e.model || '')).join('; ') : ''));
|
|
7129
|
+
}
|
|
6367
7130
|
if (test.threadGroupingId || test.threadGroupId || test.traceId) {
|
|
6368
7131
|
console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
|
|
6369
7132
|
}
|
|
@@ -6379,7 +7142,8 @@ function printTestRunPivots(status = {}) {
|
|
|
6379
7142
|
(entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
|
|
6380
7143
|
}
|
|
6381
7144
|
if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
|
|
6382
|
-
|
|
7145
|
+
const intentional = test.liveHttpCalls.filter((call) => call && call.intentional === true).length;
|
|
7146
|
+
console.log(' Live HTTP calls:', test.liveHttpCalls.length + (intentional ? ' (' + intentional + ' intentional, inside withAiBudget)' : ''));
|
|
6383
7147
|
}
|
|
6384
7148
|
});
|
|
6385
7149
|
}
|
|
@@ -6391,6 +7155,249 @@ function formatDurationMs(value) {
|
|
|
6391
7155
|
return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
|
|
6392
7156
|
}
|
|
6393
7157
|
|
|
7158
|
+
// A local corpus manifest -> case entries, reading expected/source JSON files and artifact paths RELATIVE to the
|
|
7159
|
+
// manifest. Artifacts are described (path, size) and only base64-encoded at send time, per batch.
|
|
7160
|
+
function corpusManifestEntries(manifest, manifestDir) {
|
|
7161
|
+
const root = manifest && typeof manifest === 'object' ? manifest : {};
|
|
7162
|
+
const cases = Array.isArray(root.cases) ? root.cases : [];
|
|
7163
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(path.resolve(manifestDir, file), 'utf8'));
|
|
7164
|
+
return cases.map((raw, index) => {
|
|
7165
|
+
const entry = raw && typeof raw === 'object' ? raw : {};
|
|
7166
|
+
if (!entry.caseKey) throw new Error('cases[' + index + '] has no caseKey');
|
|
7167
|
+
const out = { caseKey: String(entry.caseKey) };
|
|
7168
|
+
['split', 'tags', 'expected', 'expectedStatus', 'source', 'notes', 'retired'].forEach((key) => {
|
|
7169
|
+
if (entry[key] !== undefined) out[key] = entry[key];
|
|
7170
|
+
});
|
|
7171
|
+
if (typeof entry.expectedFile === 'string') out.expected = readJson(entry.expectedFile);
|
|
7172
|
+
if (typeof entry.sourceFile === 'string') out.source = readJson(entry.sourceFile);
|
|
7173
|
+
out.artifacts = (Array.isArray(entry.artifacts) ? entry.artifacts : []).map((artifact, artifactIndex) => {
|
|
7174
|
+
const spec = typeof artifact === 'string' ? { path: artifact } : (artifact || {});
|
|
7175
|
+
if (!spec.path) throw new Error('cases[' + index + '].artifacts[' + artifactIndex + '] has no path');
|
|
7176
|
+
const resolved = path.resolve(manifestDir, spec.path);
|
|
7177
|
+
if (!fs.existsSync(resolved)) throw new Error('cases[' + index + '] artifact not found: ' + resolved);
|
|
7178
|
+
return {
|
|
7179
|
+
name: spec.name || (artifactIndex === 0 ? 'source' : path.basename(resolved).replace(/[^A-Za-z0-9._-]+/g, '_')),
|
|
7180
|
+
path: resolved,
|
|
7181
|
+
fileName: spec.fileName || path.basename(resolved),
|
|
7182
|
+
contentType: spec.contentType || undefined,
|
|
7183
|
+
bytes: fs.statSync(resolved).size
|
|
7184
|
+
};
|
|
7185
|
+
});
|
|
7186
|
+
return out;
|
|
7187
|
+
});
|
|
7188
|
+
}
|
|
7189
|
+
|
|
7190
|
+
// Split corpus case entries into requests under the server's per-request limits (artifact bytes and count).
|
|
7191
|
+
function batchCorpusEntries(entries, maxBytes = 40 * 1024 * 1024, maxCount = 100) {
|
|
7192
|
+
const batches = [];
|
|
7193
|
+
let current = [];
|
|
7194
|
+
let currentBytes = 0;
|
|
7195
|
+
(entries || []).forEach((entry) => {
|
|
7196
|
+
const bytes = (entry.artifacts || []).reduce((sum, a) => sum + Number(a.bytes || 0), 0);
|
|
7197
|
+
if (current.length && (current.length >= maxCount || currentBytes + bytes > maxBytes)) {
|
|
7198
|
+
batches.push(current);
|
|
7199
|
+
current = [];
|
|
7200
|
+
currentBytes = 0;
|
|
7201
|
+
}
|
|
7202
|
+
current.push(entry);
|
|
7203
|
+
currentBytes += bytes;
|
|
7204
|
+
});
|
|
7205
|
+
if (current.length) batches.push(current);
|
|
7206
|
+
return batches;
|
|
7207
|
+
}
|
|
7208
|
+
|
|
7209
|
+
// The corpus_import verification packet: case keys, statuses, changed KEYS, artifact ids/hashes/sizes. Never an
|
|
7210
|
+
// expected answer, a provenance value or a byte of artifact content.
|
|
7211
|
+
function corpusImportPacketBody(responses) {
|
|
7212
|
+
const cases = [];
|
|
7213
|
+
let world = null;
|
|
7214
|
+
let corpus = null;
|
|
7215
|
+
let ownerAccountId = null;
|
|
7216
|
+
(responses || []).forEach((response) => {
|
|
7217
|
+
if (!response) return;
|
|
7218
|
+
if (response.world && !world) world = response.world;
|
|
7219
|
+
corpus = corpus || response.corpus;
|
|
7220
|
+
ownerAccountId = ownerAccountId || response.ownerAccountId;
|
|
7221
|
+
(Array.isArray(response.cases) ? response.cases : []).forEach((c) => {
|
|
7222
|
+
cases.push({
|
|
7223
|
+
caseKey: c.caseKey,
|
|
7224
|
+
status: c.status,
|
|
7225
|
+
changedKeys: c.changedKeys,
|
|
7226
|
+
artifacts: (c.artifacts || []).map((a) => ({ name: a.name, objectId: a.objectId, sha256: a.sha256, bytes: a.bytes, created: a.created })),
|
|
7227
|
+
message: c.status === 'failed' ? c.message : undefined
|
|
7228
|
+
});
|
|
7229
|
+
});
|
|
7230
|
+
});
|
|
7231
|
+
const counts = cases.reduce((acc, c) => { acc[c.status] = (acc[c.status] || 0) + 1; return acc; }, {});
|
|
7232
|
+
return { world, corpus, ownerAccountId, counts, cases, success: cases.length > 0 && !cases.some((c) => c.status === 'failed') };
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7235
|
+
function parseCorpusFilterValues(flags, names) {
|
|
7236
|
+
const values = [];
|
|
7237
|
+
(names || []).forEach((name) => {
|
|
7238
|
+
const raw = flags[name];
|
|
7239
|
+
if (raw === undefined || raw === null || raw === true) return;
|
|
7240
|
+
(Array.isArray(raw) ? raw : [raw]).forEach((value) => {
|
|
7241
|
+
String(value).split(',').forEach((part) => {
|
|
7242
|
+
const trimmed = part.trim();
|
|
7243
|
+
if (trimmed) values.push(trimmed);
|
|
7244
|
+
});
|
|
7245
|
+
});
|
|
7246
|
+
});
|
|
7247
|
+
return Array.from(new Set(values));
|
|
7248
|
+
}
|
|
7249
|
+
|
|
7250
|
+
function testRunDependencies(status = {}) {
|
|
7251
|
+
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
7252
|
+
const liveHttpCalls = [];
|
|
7253
|
+
tests.forEach((test) => (Array.isArray(test.liveHttpCalls) ? test.liveHttpCalls : []).forEach((call) => {
|
|
7254
|
+
liveHttpCalls.push(Object.assign({ caseName: test.name }, call));
|
|
7255
|
+
}));
|
|
7256
|
+
const ai = status.result && status.result.summary && status.result.summary.ai;
|
|
7257
|
+
return {
|
|
7258
|
+
liveHttpCalls: liveHttpCalls.slice(0, 50),
|
|
7259
|
+
unintendedLiveHttpCalls: liveHttpCalls.filter((call) => call.intentional !== true).length,
|
|
7260
|
+
liveAi: ai ? {
|
|
7261
|
+
liveCalls: ai.liveCalls || 0,
|
|
7262
|
+
mockedCalls: ai.mockedCalls || 0,
|
|
7263
|
+
cost: ai.cost,
|
|
7264
|
+
intentional: (ai.intentionalLiveCalls || 0) > 0 && (ai.intentionalLiveCalls || 0) >= (ai.liveCalls || 0),
|
|
7265
|
+
intentionalLiveCalls: ai.intentionalLiveCalls || 0,
|
|
7266
|
+
budgets: ai.budgets || []
|
|
7267
|
+
} : null
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
|
|
7271
|
+
function formatUsd(value) {
|
|
7272
|
+
const n = Number(value);
|
|
7273
|
+
if (!Number.isFinite(n)) return 'unknown';
|
|
7274
|
+
// Real spend must never render as $0.00. Per-call costs of a few microdollars are normal for small models,
|
|
7275
|
+
// and a live run printing "$0.0000" reads as "this cost nothing", which is how a live run gets mistaken for
|
|
7276
|
+
// a replay — the exact confusion the AI-mode labelling exists to prevent.
|
|
7277
|
+
if (n !== 0 && Math.abs(n) < 0.0001) return (n < 0 ? '-' : '') + '<$0.0001';
|
|
7278
|
+
return '$' + n.toFixed(n !== 0 && Math.abs(n) < 0.01 ? 4 : 2);
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
// The world a run executes in, as the lines printed BEFORE the result: host, who it runs as, the data lane,
|
|
7282
|
+
// the component world, the staging lane with its content hash, and the platform sync against the local HEAD.
|
|
7283
|
+
// One block, stated once, so an agent never has to assemble "which world was that?" from five other outputs —
|
|
7284
|
+
// the localhost-vs-prod mismatch that sent account 21 in circles was two of these lines disagreeing.
|
|
7285
|
+
function runWorldLines(world, context = {}) {
|
|
7286
|
+
if (!world || typeof world !== 'object') return [];
|
|
7287
|
+
const lines = ['World:'];
|
|
7288
|
+
if (context.host) lines.push(' host: ' + context.host);
|
|
7289
|
+
const accounts = ['account ' + (world.executionAccountId || context.accountId || 'unknown')];
|
|
7290
|
+
if (world.checkoutAccountId && world.checkoutAccountId !== world.executionAccountId) accounts.push('checkout ' + world.checkoutAccountId);
|
|
7291
|
+
if (world.componentOwnerAccountId && world.componentOwnerAccountId !== world.executionAccountId) accounts.push('component owner ' + world.componentOwnerAccountId);
|
|
7292
|
+
lines.push(' runs as: ' + accounts.join(', '));
|
|
7293
|
+
lines.push(' data lane: ' + (world.dataMode || context.dataMode || 'unknown'));
|
|
7294
|
+
const branchSource = world.variantBranchSource ? ' [' + world.variantBranchSource + ']' : '';
|
|
7295
|
+
lines.push(' component world: ' + (world.resolves || (world.componentBranch ? "trunk + the '" + world.componentBranch + "' variant overlays" : 'trunk')) + branchSource);
|
|
7296
|
+
const laneParts = [world.stagingLane || (world.branchName ? world.branchName + (world.workspace ? ' [ws:' + world.workspace + ']' : '') : 'none')];
|
|
7297
|
+
if (world.sharedLane) laneParts.push('(shared lane)');
|
|
7298
|
+
if (world.laneContentHash) laneParts.push('content ' + String(world.laneContentHash).slice(0, 12));
|
|
7299
|
+
if (world.stagedCount !== undefined && world.stagedCount !== null) laneParts.push(world.stagedCount + ' staged');
|
|
7300
|
+
lines.push(' staging lane: ' + laneParts.join(' '));
|
|
7301
|
+
if (world.laneId) lines.push(' lane id: ' + world.laneId);
|
|
7302
|
+
const localHead = context.gitHead ? String(context.gitHead).slice(0, 8) : null;
|
|
7303
|
+
const synced = world.platformSyncSha ? String(world.platformSyncSha).slice(0, 8) : null;
|
|
7304
|
+
if (synced || localHead) {
|
|
7305
|
+
let syncLine = ' platform sync: ' + (synced || 'unknown') + (localHead ? ' local HEAD ' + localHead : '');
|
|
7306
|
+
if (synced && localHead && !String(context.gitHead).startsWith(String(world.platformSyncSha).slice(0, 8))) {
|
|
7307
|
+
syncLine += ' (differ: committed components run as last synced; only STAGED components reflect this checkout)';
|
|
7308
|
+
}
|
|
7309
|
+
lines.push(syncLine);
|
|
7310
|
+
}
|
|
7311
|
+
if (world.testComponentSource) {
|
|
7312
|
+
lines.push(' test component: ' + world.testComponentSource + (world.testComponentSignature ? ' ' + shortSignature(world.testComponentSignature) : '') +
|
|
7313
|
+
(world.testComponentBranch ? " variant '" + world.testComponentBranch + "'" : ''));
|
|
7314
|
+
}
|
|
7315
|
+
return lines;
|
|
7316
|
+
}
|
|
7317
|
+
|
|
7318
|
+
// The corpus-style roll-up printed after a run: outcome counts, case duration percentiles, AI usage split
|
|
7319
|
+
// live/mocked, and the durable record. Built only from the result the platform returned.
|
|
7320
|
+
function testRunSummaryLines(status = {}) {
|
|
7321
|
+
const result = status.result || {};
|
|
7322
|
+
const summary = result.summary || {};
|
|
7323
|
+
const lines = [];
|
|
7324
|
+
const outcomes = summary.outcomes && typeof summary.outcomes === 'object' ? summary.outcomes : null;
|
|
7325
|
+
if (outcomes && Object.keys(outcomes).length) {
|
|
7326
|
+
lines.push('Outcomes: ' + Object.keys(outcomes).map((key) => key + ' ' + outcomes[key]).join(', '));
|
|
7327
|
+
}
|
|
7328
|
+
if (summary.durationMs) {
|
|
7329
|
+
lines.push('Case duration: median ' + formatDurationMs(summary.durationMs.median) + ', p95 ' +
|
|
7330
|
+
formatDurationMs(summary.durationMs.p95) + ', max ' + formatDurationMs(summary.durationMs.max));
|
|
7331
|
+
}
|
|
7332
|
+
const ai = summary.ai;
|
|
7333
|
+
if (ai && (ai.calls || ai.liveCalls || ai.mockedCalls)) {
|
|
7334
|
+
let aiLine = 'AI: ' + (ai.liveCalls || 0) + ' live call(s) ' + formatUsd(ai.cost) + ', ' + (ai.mockedCalls || 0) + ' mocked';
|
|
7335
|
+
if (ai.uncostedLiveCalls) aiLine += ', ' + ai.uncostedLiveCalls + ' live call(s) unpriced (cost is a floor)';
|
|
7336
|
+
if (ai.intentionalLiveCalls) aiLine += ', ' + ai.intentionalLiveCalls + ' intentional (withAiBudget)';
|
|
7337
|
+
lines.push(aiLine);
|
|
7338
|
+
const models = Array.isArray(ai.models) ? ai.models : [];
|
|
7339
|
+
if (models.length) {
|
|
7340
|
+
lines.push('AI models: ' + models.slice(0, 6).map((m) => (m.model || 'unknown') + ' ' + (m.liveCalls || 0) + ' live/' + (m.mockedCalls || 0) + ' mocked').join('; '));
|
|
7341
|
+
}
|
|
7342
|
+
if (Array.isArray(ai.budgets) && ai.budgets.length) lines.push('AI budgets: ' + ai.budgets.join(', '));
|
|
7343
|
+
}
|
|
7344
|
+
const unintended = (Array.isArray(result.tests) ? result.tests : [])
|
|
7345
|
+
.reduce((sum, test) => sum + (Array.isArray(test.liveHttpCalls) ? test.liveHttpCalls.filter((c) => c && c.intentional !== true).length : 0), 0);
|
|
7346
|
+
if (unintended) {
|
|
7347
|
+
lines.push('Unmocked live HTTP calls: ' + unintended + ' (missing mocks, or live AI outside withAiBudget)');
|
|
7348
|
+
}
|
|
7349
|
+
if (result.durableRecord && result.durableRecord.stored) {
|
|
7350
|
+
lines.push('Durable record: kept after the live status expires — remits-cli test status --task-id ' + result.durableRecord.taskId);
|
|
7351
|
+
} else if (status.durable) {
|
|
7352
|
+
lines.push('Durable record: ' + (status.durableNote || 'read from the durable test run record'));
|
|
7353
|
+
}
|
|
7354
|
+
return lines;
|
|
7355
|
+
}
|
|
7356
|
+
|
|
7357
|
+
// Per-case comparison of two runs of the same suite: which cases changed outcome, and how cost and duration
|
|
7358
|
+
// moved. The answer to "did this revision do better than the last one" without reconstructing either run.
|
|
7359
|
+
function compareTestRunResults(baseResult = {}, headResult = {}) {
|
|
7360
|
+
const outcomeOf = (test) => test.outcome || (test.passed === true ? 'passed' : (test.passed === false ? 'failed' : 'unknown'));
|
|
7361
|
+
const byName = (result) => {
|
|
7362
|
+
const map = new Map();
|
|
7363
|
+
(Array.isArray(result.tests) ? result.tests : []).forEach((test) => { if (test && test.name) map.set(test.name, test); });
|
|
7364
|
+
return map;
|
|
7365
|
+
};
|
|
7366
|
+
const base = byName(baseResult);
|
|
7367
|
+
const head = byName(headResult);
|
|
7368
|
+
const names = Array.from(new Set([...base.keys(), ...head.keys()]));
|
|
7369
|
+
const cases = names.map((name) => {
|
|
7370
|
+
const b = base.get(name);
|
|
7371
|
+
const h = head.get(name);
|
|
7372
|
+
const entry = {
|
|
7373
|
+
name,
|
|
7374
|
+
base: b ? outcomeOf(b) : 'absent',
|
|
7375
|
+
head: h ? outcomeOf(h) : 'absent',
|
|
7376
|
+
baseCost: b && b.ai ? Number(b.ai.cost || 0) : null,
|
|
7377
|
+
headCost: h && h.ai ? Number(h.ai.cost || 0) : null,
|
|
7378
|
+
baseDurationMs: b ? b.duration : null,
|
|
7379
|
+
headDurationMs: h ? h.duration : null,
|
|
7380
|
+
headReason: h && h.outcome !== 'passed' ? (h.outcomeReason || h.error || null) : null
|
|
7381
|
+
};
|
|
7382
|
+
entry.changed = entry.base !== entry.head;
|
|
7383
|
+
entry.direction = !entry.changed ? 'same' : (entry.head === 'passed' ? 'improved' : (entry.base === 'passed' ? 'regressed' : 'changed'));
|
|
7384
|
+
return entry;
|
|
7385
|
+
});
|
|
7386
|
+
const count = (result, outcome) => (Array.isArray(result.tests) ? result.tests : []).filter((t) => outcomeOf(t) === outcome).length;
|
|
7387
|
+
const aiCost = (result) => (result.summary && result.summary.ai && Number(result.summary.ai.cost)) || 0;
|
|
7388
|
+
return {
|
|
7389
|
+
base: { taskId: baseResult.taskId, passed: count(baseResult, 'passed'), total: (baseResult.tests || []).length, cost: aiCost(baseResult) },
|
|
7390
|
+
head: { taskId: headResult.taskId, passed: count(headResult, 'passed'), total: (headResult.tests || []).length, cost: aiCost(headResult) },
|
|
7391
|
+
improved: cases.filter((c) => c.direction === 'improved').map((c) => c.name),
|
|
7392
|
+
regressed: cases.filter((c) => c.direction === 'regressed').map((c) => c.name),
|
|
7393
|
+
changed: cases.filter((c) => c.changed),
|
|
7394
|
+
cases
|
|
7395
|
+
};
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7398
|
+
|
|
7399
|
+
|
|
7400
|
+
|
|
6394
7401
|
async function waitForToolStatus(api, cwd, options) {
|
|
6395
7402
|
const started = Date.now();
|
|
6396
7403
|
let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
|
|
@@ -6422,6 +7429,19 @@ async function waitForToolStatus(api, cwd, options) {
|
|
|
6422
7429
|
}
|
|
6423
7430
|
}
|
|
6424
7431
|
|
|
7432
|
+
// One compact progress line for a streamed TestSuite frame: a finished case, or the suite roll-up.
|
|
7433
|
+
function testSuiteProgressLine(payload = {}) {
|
|
7434
|
+
if (payload.total != null || Array.isArray(payload.tests)) {
|
|
7435
|
+
const total = payload.total != null ? payload.total : (payload.tests || []).length;
|
|
7436
|
+
const passed = payload.passed != null ? payload.passed : (payload.tests || []).filter((t) => t && t.passed).length;
|
|
7437
|
+
return '[ws] ' + (payload.testName || 'suite') + ': ' + passed + '/' + total + ' passed';
|
|
7438
|
+
}
|
|
7439
|
+
const mark = payload.passed === false ? 'FAIL' : 'pass';
|
|
7440
|
+
const detail = payload.outcome && payload.outcome !== 'passed' ? ' [' + payload.outcome + ']' : '';
|
|
7441
|
+
return '[ws] ' + mark + ' ' + (payload.name || 'case') + detail +
|
|
7442
|
+
(payload.duration != null ? ' ' + payload.duration + 'ms' : '');
|
|
7443
|
+
}
|
|
7444
|
+
|
|
6425
7445
|
function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
6426
7446
|
const emit = options.stderr ? console.error : console.log;
|
|
6427
7447
|
const client = new Client({
|
|
@@ -6442,7 +7462,11 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
|
6442
7462
|
return;
|
|
6443
7463
|
}
|
|
6444
7464
|
if (payload.tests || payload.name || payload.total != null) {
|
|
6445
|
-
|
|
7465
|
+
// Progress, not the payload. This used to print the whole suite result — every case, every
|
|
7466
|
+
// stacktrace — as one enormous line immediately before the readable summary said the same thing,
|
|
7467
|
+
// which buried the run world, the outcomes and the pivots an agent actually reads. --verbose still
|
|
7468
|
+
// gets the raw frame.
|
|
7469
|
+
emit(options.verbose ? '[ws][TestSuite] ' + JSON.stringify(payload) : testSuiteProgressLine(payload));
|
|
6446
7470
|
}
|
|
6447
7471
|
}
|
|
6448
7472
|
} catch (err) {
|
|
@@ -6525,12 +7549,20 @@ async function testCommand(flags) {
|
|
|
6525
7549
|
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
6526
7550
|
printSessionResolutionWarning(sessionContext);
|
|
6527
7551
|
printResolvedBaseUrl(baseUrl);
|
|
7552
|
+
printLocalCommandContext(cwd, flags, { accountId: asAccountId || accountId, branchName, workspace, dataMode });
|
|
7553
|
+
printLocalStateWarnings(cwd, flags);
|
|
6528
7554
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
6529
7555
|
printStagingLaneOwnerNotice(start.staging || {});
|
|
6530
7556
|
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
6531
7557
|
printOrphanedWorkspaceWarning(start.staging);
|
|
6532
7558
|
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
6533
7559
|
}
|
|
7560
|
+
runWorldLines(start.world, {
|
|
7561
|
+
host: normalizeBaseUrl(baseUrl),
|
|
7562
|
+
accountId: asAccountId || accountId,
|
|
7563
|
+
dataMode,
|
|
7564
|
+
gitHead: safeGitValue(cwd, 'git rev-parse HEAD')
|
|
7565
|
+
}).forEach((line) => console.log(line));
|
|
6534
7566
|
console.log('Test run started:', start.taskId);
|
|
6535
7567
|
});
|
|
6536
7568
|
runtimeState.currentTestTaskId = start.taskId;
|
|
@@ -6539,11 +7571,12 @@ async function testCommand(flags) {
|
|
|
6539
7571
|
if (flags.watch !== 'false') {
|
|
6540
7572
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
6541
7573
|
if (topic) {
|
|
6542
|
-
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput });
|
|
7574
|
+
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
|
|
6543
7575
|
}
|
|
6544
7576
|
}
|
|
6545
7577
|
|
|
6546
7578
|
const status = await waitForStatus(api, cwd, accountId, branchName, start.taskId, session.token, dataMode);
|
|
7579
|
+
status.localState = localStateSummary(cwd, flags);
|
|
6547
7580
|
if (stopWs) {
|
|
6548
7581
|
stopWs();
|
|
6549
7582
|
}
|
|
@@ -6556,6 +7589,7 @@ async function testCommand(flags) {
|
|
|
6556
7589
|
} else {
|
|
6557
7590
|
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
6558
7591
|
printStagingLaneOwnerNotice(status.staging || {});
|
|
7592
|
+
testRunSummaryLines(status).forEach((line) => console.log(line));
|
|
6559
7593
|
printTestRunPivots(status);
|
|
6560
7594
|
}
|
|
6561
7595
|
|
|
@@ -6625,10 +7659,14 @@ async function testCommand(flags) {
|
|
|
6625
7659
|
unmatchedTestNames: unmatched
|
|
6626
7660
|
},
|
|
6627
7661
|
evidenceCategories: testEvidenceCategories(status, names),
|
|
7662
|
+
// What the run spent and whether that live AI was declared (withAiBudget), so an envelope can tell a
|
|
7663
|
+
// measurement run from a suite with missing mocks without re-reading every case.
|
|
7664
|
+
dependencies: testRunDependencies(status),
|
|
7665
|
+
summary: status.result && status.result.summary,
|
|
6628
7666
|
limitations: []
|
|
6629
7667
|
.concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
|
|
6630
7668
|
.concat(nondeterminism ? [nondeterminism.message] : []),
|
|
6631
|
-
rawRefs: { testStatusKey: start.taskId },
|
|
7669
|
+
rawRefs: { testStatusKey: start.taskId, durableTestRun: status.result && status.result.durableRecord ? start.taskId : undefined },
|
|
6632
7670
|
status
|
|
6633
7671
|
}, { quiet: jsonOutput });
|
|
6634
7672
|
}
|
|
@@ -6665,6 +7703,11 @@ async function testStatusCommand(flags) {
|
|
|
6665
7703
|
printResolvedBaseUrl(baseUrl);
|
|
6666
7704
|
console.log('Test run status:', status.status || 'unknown');
|
|
6667
7705
|
console.log('Task ID:', taskId);
|
|
7706
|
+
// Say where this answer came from. A durable record is a finished snapshot rebuilt from the database after the
|
|
7707
|
+
// live status was gone; without this line a reconstructed run is indistinguishable from one still being watched.
|
|
7708
|
+
console.log('Source:', status.durable
|
|
7709
|
+
? 'durable run record (the live status has expired; this run is final)'
|
|
7710
|
+
: 'live run status');
|
|
6668
7711
|
console.log('Data mode:', status.dataMode || dataMode);
|
|
6669
7712
|
if (status.result) {
|
|
6670
7713
|
console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
|
|
@@ -6672,12 +7715,100 @@ async function testStatusCommand(flags) {
|
|
|
6672
7715
|
if (status.error || status.message) {
|
|
6673
7716
|
console.log('Message:', status.error || status.message);
|
|
6674
7717
|
}
|
|
7718
|
+
if (status.world) runWorldLines(status.world, { host: normalizeBaseUrl(baseUrl) }).forEach((line) => console.log(line));
|
|
7719
|
+
testRunSummaryLines(status).forEach((line) => console.log(line));
|
|
7720
|
+
printTestRunPivots(status);
|
|
6675
7721
|
if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
|
|
6676
7722
|
process.exitCode = 1;
|
|
6677
7723
|
}
|
|
6678
7724
|
return status;
|
|
6679
7725
|
}
|
|
6680
7726
|
|
|
7727
|
+
// `test runs --test X`: the durable history of a suite, newest first. With --compare, the latest run is
|
|
7728
|
+
// compared case-by-case with the one before it.
|
|
7729
|
+
async function testRunsCommand(flags) {
|
|
7730
|
+
const cwd = process.cwd();
|
|
7731
|
+
ensureLocalState(cwd);
|
|
7732
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
7733
|
+
const { session, accountId } = sessionContext;
|
|
7734
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
7735
|
+
const api = buildAxios(baseUrl, session.token);
|
|
7736
|
+
const testRef = flags.test || flags['test-id'] || flags.name;
|
|
7737
|
+
const data = await loggedPost(api, cwd, '/cli/testRuns', {
|
|
7738
|
+
token: session.token,
|
|
7739
|
+
accountId,
|
|
7740
|
+
asAccountId: flags['as-account'] || flags['as-account-id'],
|
|
7741
|
+
testId: testRef && /^\d+$/.test(String(testRef)) ? Number(testRef) : undefined,
|
|
7742
|
+
testName: testRef && !/^\d+$/.test(String(testRef)) ? String(testRef) : undefined,
|
|
7743
|
+
max: flags.limit || flags.max || 20
|
|
7744
|
+
}).then((r) => r.data);
|
|
7745
|
+
if (!data.success) throw new Error(data.message || 'Could not list test runs');
|
|
7746
|
+
|
|
7747
|
+
if (flagEnabled(flags.compare)) {
|
|
7748
|
+
const runs = data.runs || [];
|
|
7749
|
+
if (runs.length < 2) throw new Error('--compare needs at least two recorded runs' + (testRef ? ' of ' + testRef : '') + '; found ' + runs.length);
|
|
7750
|
+
return testCompareCommand(Object.assign({}, flags, { base: runs[1].taskId, head: runs[0].taskId }));
|
|
7751
|
+
}
|
|
7752
|
+
if (flagEnabled(flags.json)) {
|
|
7753
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7754
|
+
return data;
|
|
7755
|
+
}
|
|
7756
|
+
printSessionResolutionWarning(sessionContext);
|
|
7757
|
+
printResolvedBaseUrl(baseUrl);
|
|
7758
|
+
console.log('Recorded test runs' + (testRef ? ' for ' + testRef : '') + ' (account ' + data.accountId + '), newest first:');
|
|
7759
|
+
(data.runs || []).forEach((run) => {
|
|
7760
|
+
console.log('- ' + run.taskId + ' ' + (run.status || 'unknown') + ' ' + (run.passed || 0) + '/' + (run.total || 0) + ' passed' +
|
|
7761
|
+
' ' + corpusAiLabel(run) +
|
|
7762
|
+
(run.durationMs ? ' ' + formatDurationMs(run.durationMs) : '') +
|
|
7763
|
+
' ' + (run.dataMode || '?') + ' ' + (run.branchName || '?') + (run.workspace ? ' [ws:' + run.workspace + ']' : '') +
|
|
7764
|
+
(run.laneContentHash ? ' lane ' + String(run.laneContentHash).slice(0, 12) : '') +
|
|
7765
|
+
' ' + formatTime(run.completedAt) + ' ' + (run.testName || ''));
|
|
7766
|
+
});
|
|
7767
|
+
if (!(data.runs || []).length) console.log('(none recorded)');
|
|
7768
|
+
return data;
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
// `test compare --base <taskId> --head <taskId>`: per-case outcome, cost and duration deltas between two runs,
|
|
7772
|
+
// read from the durable records.
|
|
7773
|
+
async function testCompareCommand(flags) {
|
|
7774
|
+
const cwd = process.cwd();
|
|
7775
|
+
ensureLocalState(cwd);
|
|
7776
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
7777
|
+
const { session, accountId } = sessionContext;
|
|
7778
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
7779
|
+
const api = buildAxios(baseUrl, session.token);
|
|
7780
|
+
const baseTask = flags.base;
|
|
7781
|
+
const headTask = flags.head;
|
|
7782
|
+
if (!baseTask || !headTask) throw new Error('test compare needs --base <taskId> and --head <taskId> (or: test runs --test X --compare)');
|
|
7783
|
+
const fetchStatus = (taskId) => loggedPost(api, cwd, '/cli/test', {
|
|
7784
|
+
token: session.token, command: 'status', accountId, branchName: flags.branch || currentBranch(cwd), taskId
|
|
7785
|
+
}).then((r) => r.data);
|
|
7786
|
+
const [baseStatus, headStatus] = await Promise.all([fetchStatus(baseTask), fetchStatus(headTask)]);
|
|
7787
|
+
const comparison = compareTestRunResults(Object.assign({ taskId: baseTask }, baseStatus.result || {}), Object.assign({ taskId: headTask }, headStatus.result || {}));
|
|
7788
|
+
comparison.baseWorld = baseStatus.world || (baseStatus.result && baseStatus.result.world) || null;
|
|
7789
|
+
comparison.headWorld = headStatus.world || (headStatus.result && headStatus.result.world) || null;
|
|
7790
|
+
if (flagEnabled(flags.json)) {
|
|
7791
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
7792
|
+
return comparison;
|
|
7793
|
+
}
|
|
7794
|
+
printResolvedBaseUrl(baseUrl);
|
|
7795
|
+
console.log('Base ' + baseTask + ': ' + comparison.base.passed + '/' + comparison.base.total + ' passed, AI ' + formatUsd(comparison.base.cost));
|
|
7796
|
+
console.log('Head ' + headTask + ': ' + comparison.head.passed + '/' + comparison.head.total + ' passed, AI ' + formatUsd(comparison.head.cost));
|
|
7797
|
+
const worldDiff = ['laneContentHash', 'componentBranch', 'dataMode', 'workspace', 'platformSyncSha']
|
|
7798
|
+
.filter((key) => comparison.baseWorld && comparison.headWorld && String(comparison.baseWorld[key] || '') !== String(comparison.headWorld[key] || ''))
|
|
7799
|
+
.map((key) => key + ' ' + short(String(comparison.baseWorld[key] || 'none')) + ' -> ' + short(String(comparison.headWorld[key] || 'none')));
|
|
7800
|
+
if (worldDiff.length) console.log('World changed: ' + worldDiff.join('; '));
|
|
7801
|
+
if (comparison.improved.length) console.log('Improved (' + comparison.improved.length + '): ' + comparison.improved.join(', '));
|
|
7802
|
+
if (comparison.regressed.length) console.log('Regressed (' + comparison.regressed.length + '): ' + comparison.regressed.join(', '));
|
|
7803
|
+
comparison.changed.filter((c) => c.direction === 'changed').forEach((c) => console.log('Changed: ' + c.name + ' ' + c.base + ' -> ' + c.head));
|
|
7804
|
+
comparison.cases.filter((c) => c.head !== 'passed' && c.headReason).slice(0, 20).forEach((c) => {
|
|
7805
|
+
console.log(' ' + c.name + ' [' + c.head + ']: ' + String(c.headReason).slice(0, 300));
|
|
7806
|
+
});
|
|
7807
|
+
if (!comparison.changed.length) console.log('No case changed outcome.');
|
|
7808
|
+
return comparison;
|
|
7809
|
+
}
|
|
7810
|
+
|
|
7811
|
+
|
|
6681
7812
|
/*
|
|
6682
7813
|
## Tokens, Tools, Verification, And Config
|
|
6683
7814
|
*/
|
|
@@ -6747,7 +7878,8 @@ async function tokenCommand(flags) {
|
|
|
6747
7878
|
accountLanes: data.accountLanes,
|
|
6748
7879
|
dataMode: data.dataMode || dataMode,
|
|
6749
7880
|
shortBasePath: data.shortBasePath,
|
|
6750
|
-
embeddableUrl
|
|
7881
|
+
embeddableUrl,
|
|
7882
|
+
localState: localStateSummary(cwd, flags)
|
|
6751
7883
|
};
|
|
6752
7884
|
|
|
6753
7885
|
// What the token will RESOLVE AS. `accountId` says which account it executes as; on a hierarchy the
|
|
@@ -6770,7 +7902,11 @@ async function tokenCommand(flags) {
|
|
|
6770
7902
|
output.embedSnippet = data.embedSnippet;
|
|
6771
7903
|
}
|
|
6772
7904
|
|
|
6773
|
-
|
|
7905
|
+
if (!flagEnabled(flags.json)) {
|
|
7906
|
+
printSessionResolutionWarning(sessionContext);
|
|
7907
|
+
printLocalCommandContext(cwd, flags, { accountId: output.accountId || accountId, branchName, workspace: output.workspace, dataMode: output.dataMode || dataMode });
|
|
7908
|
+
printLocalStateWarnings(cwd, flags);
|
|
7909
|
+
}
|
|
6774
7910
|
console.log(JSON.stringify(output, null, 2));
|
|
6775
7911
|
|
|
6776
7912
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -6786,7 +7922,7 @@ async function tokenCommand(flags) {
|
|
|
6786
7922
|
},
|
|
6787
7923
|
evidenceCategories: ['token_inspect'],
|
|
6788
7924
|
token: Object.assign({}, output, { tokenKey: output.tokenKey ? '[redacted]' : undefined, embedTokenKey: output.embedTokenKey ? '[redacted]' : undefined })
|
|
6789
|
-
});
|
|
7925
|
+
}, { quiet: flagEnabled(flags.json) });
|
|
6790
7926
|
}
|
|
6791
7927
|
|
|
6792
7928
|
async function tokenInspectCommand(flags) {
|
|
@@ -6988,6 +8124,7 @@ async function toolCommand(flags) {
|
|
|
6988
8124
|
|
|
6989
8125
|
data.responseFile = statusResponse.responseFile;
|
|
6990
8126
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
8127
|
+
data.localState = localStateSummary(cwd, flags);
|
|
6991
8128
|
// A completed run can still carry a tool-level refusal — see toolResultFailed.
|
|
6992
8129
|
const polledFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
6993
8130
|
if (polledFailed) {
|
|
@@ -7010,6 +8147,8 @@ async function toolCommand(flags) {
|
|
|
7010
8147
|
}
|
|
7011
8148
|
printSessionResolutionWarning(sessionContext);
|
|
7012
8149
|
printResolvedBaseUrl(baseUrl);
|
|
8150
|
+
printLocalCommandContext(cwd, flags, { accountId: statusAccountId, branchName: flags.branch || (stored && stored.branchName) || branchName, dataMode: data.dataMode || dataMode });
|
|
8151
|
+
printLocalStateWarnings(cwd, flags);
|
|
7013
8152
|
console.log('Tool call status:', data.status);
|
|
7014
8153
|
console.log('Call ID:', requestedCallId);
|
|
7015
8154
|
console.log('Data mode:', data.dataMode || dataMode);
|
|
@@ -7064,6 +8203,7 @@ async function toolCommand(flags) {
|
|
|
7064
8203
|
const toolFailureMessage = data.toolMessage || toolResultMessage(data.result);
|
|
7065
8204
|
data.responseFile = response.responseFile;
|
|
7066
8205
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
8206
|
+
data.localState = localStateSummary(cwd, flags);
|
|
7067
8207
|
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
7068
8208
|
|
|
7069
8209
|
let evidenceData = data;
|
|
@@ -7072,6 +8212,8 @@ async function toolCommand(flags) {
|
|
|
7072
8212
|
if (!jsonOutput) {
|
|
7073
8213
|
printSessionResolutionWarning(sessionContext);
|
|
7074
8214
|
printResolvedBaseUrl(baseUrl);
|
|
8215
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace: resolveWorkspace(cwd, flags), dataMode: data.dataMode || dataMode });
|
|
8216
|
+
printLocalStateWarnings(cwd, flags);
|
|
7075
8217
|
console.log(asyncMode ? 'Tool call started.'
|
|
7076
8218
|
: (toolFailed ? 'Tool call FAILED — the tool ran and returned an error.' : 'Tool call succeeded.'));
|
|
7077
8219
|
if (toolFailed && toolFailureMessage) console.log('Tool error:', toolFailureMessage);
|
|
@@ -7140,6 +8282,202 @@ async function toolCommand(flags) {
|
|
|
7140
8282
|
}
|
|
7141
8283
|
}
|
|
7142
8284
|
|
|
8285
|
+
// Human output for the read-only corpus commands, from the platform's response alone.
|
|
8286
|
+
// How a run or a case actually exercised AI. A replayed suite and a measured one produce identical outcomes,
|
|
8287
|
+
// scores and metrics, so every scorecard line has to say which it was or it is not evidence of anything.
|
|
8288
|
+
function corpusAiLabel(row = {}) {
|
|
8289
|
+
// When the corpus cases themselves recorded no AI, the platform falls back to the whole suite run's counters
|
|
8290
|
+
// and says so with aiModeSource. Report that as "run AI ...", never as the corpus cases' own measurement.
|
|
8291
|
+
const fromRun = row.aiModeSource === 'run';
|
|
8292
|
+
const live = Number((fromRun ? row.runLiveAiCalls : row.liveAiCalls) || 0);
|
|
8293
|
+
const mocked = Number((fromRun ? row.runMockedAiCalls : row.mockedAiCalls) || 0);
|
|
8294
|
+
const calls = Number((fromRun ? row.runAiCalls : row.aiCalls) || 0);
|
|
8295
|
+
const prefix = fromRun ? 'run AI ' : 'AI ';
|
|
8296
|
+
if (live > 0) return prefix + 'live ' + formatUsd(row.liveAiCost) + ' (' + live + '/' + (calls || live) + ' live)';
|
|
8297
|
+
if (mocked > 0 || calls > 0) return prefix + 'MOCKED (' + (mocked || calls) + ' replayed, $0.00)';
|
|
8298
|
+
return 'no AI';
|
|
8299
|
+
}
|
|
8300
|
+
|
|
8301
|
+
function corpusLines(sub, data = {}) {
|
|
8302
|
+
const lines = ['Corpus: ' + (data.corpus || '?') + ' (owner account ' + (data.ownerAccountId || '?') + ')'];
|
|
8303
|
+
if (sub === 'cases') {
|
|
8304
|
+
const cases = data.cases || [];
|
|
8305
|
+
lines.push('Cases: ' + cases.length);
|
|
8306
|
+
cases.forEach((c) => lines.push('- ' + c.caseKey + (c.split ? ' [' + c.split + ']' : '') + (c.tags && c.tags.length ? ' tags ' + c.tags.join(',') : '') +
|
|
8307
|
+
' expected ' + (c.hasExpected ? (c.expectedStatus || 'present') : 'none') + ' artifacts ' + (c.artifacts || []).map((a) => a.name + ':' + a.bytes + 'b').join(' ')));
|
|
8308
|
+
} else if (sub === 'results') {
|
|
8309
|
+
(data.results || []).forEach((r) => lines.push('- ' + r.caseKey + ' ' + (r.outcome || '?') + (r.score !== undefined ? ' score ' + r.score : '') +
|
|
8310
|
+
' run ' + r.taskId + ' ' + corpusAiLabel(r) + (r.durationMs ? ' ' + formatDurationMs(r.durationMs) : '') +
|
|
8311
|
+
' metrics ' + String(r.metricsHash || '').slice(0, 10) + ' ' + formatTime(r.recordedAt)));
|
|
8312
|
+
} else if (sub === 'runs') {
|
|
8313
|
+
(data.runs || []).forEach((r) => lines.push('- ' + r.taskId + ' ' + r.cases + ' case(s) ' + Object.keys(r.outcomes || {}).map((k) => k + ' ' + r.outcomes[k]).join(', ') +
|
|
8314
|
+
' ' + corpusAiLabel(r) + ' ' + (r.dataMode || '?') + ' ' + (r.branchName || '?') + (r.workspace ? ' [ws:' + r.workspace + ']' : '') +
|
|
8315
|
+
(r.laneContentHash ? ' lane ' + String(r.laneContentHash).slice(0, 12) : '') + ' ' + formatTime(r.recordedAt)));
|
|
8316
|
+
if ((data.runs || []).some((r) => r.aiMode === 'mocked')) {
|
|
8317
|
+
lines.push('Runs marked AI MOCKED replayed every AI turn from aiMock - they measure the harness, not the model.');
|
|
8318
|
+
}
|
|
8319
|
+
} else if (sub === 'compare') {
|
|
8320
|
+
const cmp = data.comparison || {};
|
|
8321
|
+
lines.push('Base ' + cmp.baseTaskId + ' -> head ' + cmp.headTaskId + ': ' + Object.keys(cmp.counts || {}).map((k) => k + ' ' + cmp.counts[k]).join(', '));
|
|
8322
|
+
lines.push('AI mode: base ' + (cmp.baseAiMode || '?') + ', head ' + (cmp.headAiMode || '?'));
|
|
8323
|
+
if (cmp.aiModeChanged) lines.push('WARNING: ' + cmp.aiModeChanged);
|
|
8324
|
+
(cmp.cases || []).filter((c) => c.direction !== 'same').forEach((c) => {
|
|
8325
|
+
lines.push('- ' + c.caseKey + ' ' + c.direction + (c.base ? ' ' + c.base.outcome : '') + (c.head ? ' -> ' + c.head.outcome : '') +
|
|
8326
|
+
(c.expectedChanged ? ' (EXPECTED ANSWER CHANGED between runs)' : ''));
|
|
8327
|
+
(c.changedMetrics || []).slice(0, 8).forEach((m) => lines.push(' ' + m.metric + ': ' + JSON.stringify(m.base) + ' -> ' + JSON.stringify(m.head)));
|
|
8328
|
+
if (c.head && c.head.outcomeReason && c.head.outcome !== 'passed') lines.push(' reason: ' + String(c.head.outcomeReason).slice(0, 300));
|
|
8329
|
+
});
|
|
8330
|
+
} else if (sub === 'retire') {
|
|
8331
|
+
lines.push('Result: ' + Object.keys(data.counts || {}).map((k) => k + ' ' + data.counts[k]).join(', '));
|
|
8332
|
+
(data.cases || []).forEach((c) => lines.push('- ' + c.caseKey + ' ' + c.status + (c.message ? ' ' + c.message : (c.changed === false ? ' (already in that state)' : ''))));
|
|
8333
|
+
// Only explain what retirement means when something was actually retired; after a wholly refused call the
|
|
8334
|
+
// reader needs the refusal, not a description of a state nothing reached.
|
|
8335
|
+
if ((data.cases || []).some((c) => c.status === 'retired' || c.status === 'restored')) {
|
|
8336
|
+
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.');
|
|
8337
|
+
}
|
|
8338
|
+
} else if (sub === 'consistency') {
|
|
8339
|
+
const con = data.consistency || {};
|
|
8340
|
+
lines.push('Case ' + con.caseKey + ': ' + con.runs + ' run(s), ' + (con.stable ? 'STABLE (identical metrics every run)' : (con.variants || []).length + ' distinct metric results') +
|
|
8341
|
+
(con.aiModes && con.aiModes.length ? ' AI ' + con.aiModes.join('+') : ''));
|
|
8342
|
+
if (con.note) lines.push('WARNING: ' + con.note);
|
|
8343
|
+
(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(', ')));
|
|
8344
|
+
}
|
|
8345
|
+
return lines;
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
// `corpus import|cases|results|runs|compare|consistency`: test corpora through the platform's own corpus(name).
|
|
8349
|
+
async function corpusCommand(flags, subcommand) {
|
|
8350
|
+
const cwd = process.cwd();
|
|
8351
|
+
ensureLocalState(cwd);
|
|
8352
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
8353
|
+
const { session, accountId } = sessionContext;
|
|
8354
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
8355
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
8356
|
+
const workspace = resolveWorkspace(cwd, flags);
|
|
8357
|
+
const dataMode = hasExplicitDataModeFlag(flags) ? resolveDataMode(flags, null) : DEFAULT_DATA_MODE;
|
|
8358
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
8359
|
+
const sub = String(subcommand || 'cases').toLowerCase();
|
|
8360
|
+
const emit = jsonOutput ? console.error : console.log;
|
|
8361
|
+
const api = buildAxios(baseUrl, session.token, parsePositiveInt(flags['timeout-ms'], 300000));
|
|
8362
|
+
const base = {
|
|
8363
|
+
token: session.token,
|
|
8364
|
+
accountId,
|
|
8365
|
+
asAccountId: flags['as-account'] || flags['as-account-id'],
|
|
8366
|
+
variantBranch: flags['variant-branch'],
|
|
8367
|
+
branchName,
|
|
8368
|
+
workspace,
|
|
8369
|
+
dataMode
|
|
8370
|
+
};
|
|
8371
|
+
const post = async (payload) => {
|
|
8372
|
+
try {
|
|
8373
|
+
return await loggedPost(api, cwd, '/cli/corpus', Object.assign({}, base, payload)).then((r) => r.data);
|
|
8374
|
+
} catch (err) {
|
|
8375
|
+
const data = err && err.response && err.response.data;
|
|
8376
|
+
if (data && data.message) throw new Error(data.message);
|
|
8377
|
+
throw err;
|
|
8378
|
+
}
|
|
8379
|
+
};
|
|
8380
|
+
|
|
8381
|
+
if (sub === 'import') {
|
|
8382
|
+
const manifestFile = flags.manifest || flags.file;
|
|
8383
|
+
if (!manifestFile) throw new Error('corpus import requires --manifest <manifest.json>');
|
|
8384
|
+
const resolved = path.resolve(cwd, String(manifestFile));
|
|
8385
|
+
const manifest = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
8386
|
+
const corpusName = flags.corpus || manifest.corpus;
|
|
8387
|
+
if (!corpusName) throw new Error('corpus import needs a corpus name: "corpus" in the manifest or --corpus');
|
|
8388
|
+
const entries = corpusManifestEntries(manifest, path.dirname(resolved));
|
|
8389
|
+
if (!entries.length) throw new Error('The manifest has no cases');
|
|
8390
|
+
|
|
8391
|
+
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
8392
|
+
printSessionResolutionWarning(sessionContext);
|
|
8393
|
+
printResolvedBaseUrl(baseUrl);
|
|
8394
|
+
printLocalCommandContext(cwd, flags, { accountId: flags['as-account'] || accountId, branchName, workspace, dataMode });
|
|
8395
|
+
printProdDataBanner({ dataMode, accountId: flags['as-account'] || accountId, baseUrl, operation: 'corpus import', mutating: true });
|
|
8396
|
+
});
|
|
8397
|
+
const totalBytes = entries.reduce((sum, e) => sum + e.artifacts.reduce((n, a) => n + a.bytes, 0), 0);
|
|
8398
|
+
emit('Importing ' + entries.length + ' case(s) into corpus "' + corpusName + '", ' +
|
|
8399
|
+
entries.reduce((n, e) => n + e.artifacts.length, 0) + ' artifact(s), ' + totalBytes + ' bytes (artifacts in the ' + dataMode + ' lane)');
|
|
8400
|
+
|
|
8401
|
+
const responses = [];
|
|
8402
|
+
for (const batch of batchCorpusEntries(entries)) {
|
|
8403
|
+
const cases = batch.map((entry) => Object.assign({}, entry, {
|
|
8404
|
+
artifacts: entry.artifacts.map((a) => ({
|
|
8405
|
+
name: a.name,
|
|
8406
|
+
fileName: a.fileName,
|
|
8407
|
+
contentType: a.contentType,
|
|
8408
|
+
// Read and encoded only here, per batch. The session log records sizes and hashes, never this value.
|
|
8409
|
+
contentBase64: fs.readFileSync(a.path).toString('base64')
|
|
8410
|
+
}))
|
|
8411
|
+
}));
|
|
8412
|
+
responses.push(await post({ command: 'import', corpus: corpusName, confirmProd: flagEnabled(flags['confirm-prod']), cases }));
|
|
8413
|
+
}
|
|
8414
|
+
const body = corpusImportPacketBody(responses);
|
|
8415
|
+
if (!body.success) process.exitCode = 1;
|
|
8416
|
+
if (jsonOutput) {
|
|
8417
|
+
console.log(JSON.stringify(body, null, 2));
|
|
8418
|
+
} else {
|
|
8419
|
+
console.log('Corpus: ' + body.corpus + ' (owner account ' + body.ownerAccountId + ')');
|
|
8420
|
+
console.log('Result: ' + Object.keys(body.counts).map((k) => k + ' ' + body.counts[k]).join(', '));
|
|
8421
|
+
body.cases.forEach((c) => {
|
|
8422
|
+
console.log('- ' + c.caseKey + ' ' + c.status + (c.changedKeys && c.changedKeys.length && c.status === 'updated' ? ' changed: ' + c.changedKeys.join(', ') : '') + (c.message ? ' ' + c.message : ''));
|
|
8423
|
+
(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')));
|
|
8424
|
+
});
|
|
8425
|
+
// Only point at the next step when something actually landed. Printing it after a wholly failed import
|
|
8426
|
+
// told the reader to go use a corpus that has no cases in it.
|
|
8427
|
+
if (body.success) {
|
|
8428
|
+
console.log('Use it in a Test with corpus(\'' + body.corpus + '\').cases(); compare runs with remits-cli corpus compare.');
|
|
8429
|
+
} else {
|
|
8430
|
+
console.log('Import did not fully succeed - fix the case(s) above and re-run; import is idempotent, so unchanged cases stay unchanged.');
|
|
8431
|
+
}
|
|
8432
|
+
}
|
|
8433
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
8434
|
+
type: 'corpus_import',
|
|
8435
|
+
success: body.success,
|
|
8436
|
+
claim: 'Corpus ' + body.corpus + ' import: ' + Object.keys(body.counts).map((k) => k + ' ' + body.counts[k]).join(', '),
|
|
8437
|
+
world: Object.assign({}, body.world || {}, { host: normalizeBaseUrl(baseUrl), branchName, workspace, dataMode }),
|
|
8438
|
+
revision: collectVerificationSource(cwd, flags),
|
|
8439
|
+
evidenceCategories: ['corpus_import'],
|
|
8440
|
+
corpusImport: { corpus: body.corpus, ownerAccountId: body.ownerAccountId, counts: body.counts, cases: body.cases }
|
|
8441
|
+
}, { quiet: jsonOutput });
|
|
8442
|
+
return body;
|
|
8443
|
+
}
|
|
8444
|
+
|
|
8445
|
+
const corpusName = flags.corpus || (flags._ && flags._[2]);
|
|
8446
|
+
if (!corpusName) throw new Error('corpus ' + sub + ' requires --corpus <name>');
|
|
8447
|
+
let data;
|
|
8448
|
+
if (sub === 'cases') {
|
|
8449
|
+
const tags = parseCorpusFilterValues(flags, ['tag', 'tags']);
|
|
8450
|
+
const keys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
|
|
8451
|
+
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 });
|
|
8452
|
+
} else if (sub === 'results') {
|
|
8453
|
+
data = await post({ command: 'results', corpus: corpusName, caseKey: flags.case || flags['case-key'], taskId: flags['task-id'], limit: flags.limit || 50 });
|
|
8454
|
+
} else if (sub === 'runs') {
|
|
8455
|
+
data = await post({ command: 'runs', corpus: corpusName, limit: flags.limit || 20 });
|
|
8456
|
+
} else if (sub === 'compare') {
|
|
8457
|
+
if (!flags.base || !flags.head) throw new Error('corpus compare requires --base <taskId> --head <taskId>');
|
|
8458
|
+
data = await post({ command: 'compare', corpus: corpusName, base: flags.base, head: flags.head });
|
|
8459
|
+
} else if (sub === 'consistency') {
|
|
8460
|
+
const caseKey = flags.case || flags['case-key'];
|
|
8461
|
+
if (!caseKey) throw new Error('corpus consistency requires --case <caseKey>');
|
|
8462
|
+
data = await post({ command: 'consistency', corpus: corpusName, caseKey, limit: flags.limit || 20 });
|
|
8463
|
+
} else if (sub === 'retire') {
|
|
8464
|
+
const retireKeys = parseCorpusFilterValues(flags, ['key', 'keys', 'case', 'case-key']);
|
|
8465
|
+
if (!retireKeys.length) throw new Error('corpus retire requires --case <caseKey> (repeatable) or --keys a,b,c');
|
|
8466
|
+
data = await post({ command: 'retire', corpus: corpusName, keys: retireKeys, restore: flagEnabled(flags.restore) });
|
|
8467
|
+
if (data && data.success === false) process.exitCode = 1;
|
|
8468
|
+
} else {
|
|
8469
|
+
throw new Error('Unknown corpus subcommand: ' + sub + ' (use import, cases, results, runs, compare, consistency or retire)');
|
|
8470
|
+
}
|
|
8471
|
+
if (jsonOutput) {
|
|
8472
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8473
|
+
return data;
|
|
8474
|
+
}
|
|
8475
|
+
printSessionResolutionWarning(sessionContext);
|
|
8476
|
+
printResolvedBaseUrl(baseUrl);
|
|
8477
|
+
corpusLines(sub, data).forEach((line) => console.log(line));
|
|
8478
|
+
return data;
|
|
8479
|
+
}
|
|
8480
|
+
|
|
7143
8481
|
async function verifyCommand(flags, subcommand) {
|
|
7144
8482
|
const cwd = process.cwd();
|
|
7145
8483
|
ensureLocalState(cwd);
|
|
@@ -7158,6 +8496,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7158
8496
|
const active = readActiveVerificationEnvelope(cwd, activeContext);
|
|
7159
8497
|
printSessionResolutionWarning(sessionContext);
|
|
7160
8498
|
printResolvedBaseUrl(baseUrl);
|
|
8499
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8500
|
+
printLocalStateWarnings(cwd, flags);
|
|
7161
8501
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7162
8502
|
if (!active) {
|
|
7163
8503
|
const legacy = readActiveVerificationEnvelope(cwd);
|
|
@@ -7178,6 +8518,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7178
8518
|
if (!envelopeId) throw new Error('Usage: remits-cli verify use <envelopeId>');
|
|
7179
8519
|
writeActiveVerificationEnvelope(cwd, envelopeId, activeContext);
|
|
7180
8520
|
console.log('Active verification envelope:', envelopeId);
|
|
8521
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8522
|
+
printLocalStateWarnings(cwd, flags);
|
|
7181
8523
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7182
8524
|
return;
|
|
7183
8525
|
}
|
|
@@ -7190,6 +8532,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7190
8532
|
} else {
|
|
7191
8533
|
writeActiveVerificationEnvelope(cwd, null, activeContext);
|
|
7192
8534
|
console.log('Verification envelope cleared for this context.');
|
|
8535
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
7193
8536
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7194
8537
|
}
|
|
7195
8538
|
return;
|
|
@@ -7241,6 +8584,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7241
8584
|
writeLocalVerificationEnvelope(cwd, envelope);
|
|
7242
8585
|
writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
|
|
7243
8586
|
console.log('Verification envelope started:', envelope.envelopeId);
|
|
8587
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8588
|
+
printLocalStateWarnings(cwd, flags);
|
|
7244
8589
|
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7245
8590
|
printAcceptanceWarnings(envelope);
|
|
7246
8591
|
printEnvelopeWarnings(envelope);
|
|
@@ -8104,7 +9449,10 @@ function detectIssueSignals(repoEntries, globalFiles) {
|
|
|
8104
9449
|
}
|
|
8105
9450
|
|
|
8106
9451
|
for (const repo of repoEntries || []) {
|
|
8107
|
-
const sessionLogFile = (repo.files || []).find((file) =>
|
|
9452
|
+
const sessionLogFile = (repo.files || []).find((file) =>
|
|
9453
|
+
file.label === '.remits-cli/active-actor/session log (tail)' ||
|
|
9454
|
+
file.label === '.remits-cli/session log (tail)' ||
|
|
9455
|
+
file.label === '.remits-cli/legacy/session log (tail)');
|
|
8108
9456
|
if (!sessionLogFile || !sessionLogFile.path || !fs.existsSync(sessionLogFile.path)) {
|
|
8109
9457
|
continue;
|
|
8110
9458
|
}
|
|
@@ -8300,23 +9648,41 @@ function buildGlobalFileSnapshot() {
|
|
|
8300
9648
|
function buildRepoSnapshot(entry) {
|
|
8301
9649
|
const directory = entry.directory;
|
|
8302
9650
|
const localPaths = localStatePaths(directory);
|
|
9651
|
+
const actors = listActorDirectories(directory);
|
|
8303
9652
|
const currentSessionName = fs.existsSync(localPaths.currentSessionFile)
|
|
8304
9653
|
? fs.readFileSync(localPaths.currentSessionFile, 'utf8').trim()
|
|
8305
9654
|
: '';
|
|
8306
9655
|
const currentSessionLog = currentSessionName
|
|
8307
9656
|
? path.join(localPaths.sessionsDir, currentSessionName + '.jsonl')
|
|
8308
9657
|
: null;
|
|
9658
|
+
const legacySessionName = fs.existsSync(localPaths.legacy.currentSessionFile)
|
|
9659
|
+
? fs.readFileSync(localPaths.legacy.currentSessionFile, 'utf8').trim()
|
|
9660
|
+
: '';
|
|
9661
|
+
const legacySessionLog = legacySessionName
|
|
9662
|
+
? path.join(localPaths.legacy.sessionsDir, legacySessionName + '.jsonl')
|
|
9663
|
+
: null;
|
|
8309
9664
|
const repoFiles = [
|
|
8310
9665
|
{ label: 'account-info.json', path: entry.accountInfoPath || path.join(directory, 'account-info.json'), mode: 'json' },
|
|
8311
9666
|
{ 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' }
|
|
9667
|
+
{ label: '.remits-cli/active-actor/current-session.txt', path: localPaths.currentSessionFile, mode: 'text' },
|
|
9668
|
+
{ label: '.remits-cli/shared/tools/tools.json', path: path.join(localPaths.toolsDir, 'tools.json'), mode: 'json' },
|
|
9669
|
+
{ label: '.remits-cli/active-actor/session log (tail)', path: currentSessionLog, mode: 'tail' },
|
|
9670
|
+
{ label: '.remits-cli/legacy/current-session.txt', path: localPaths.legacy.currentSessionFile, mode: 'text' },
|
|
9671
|
+
{ label: '.remits-cli/legacy/session log (tail)', path: legacySessionLog, mode: 'tail' }
|
|
8315
9672
|
];
|
|
8316
9673
|
|
|
8317
9674
|
return {
|
|
8318
9675
|
...entry,
|
|
8319
9676
|
exists: fs.existsSync(directory),
|
|
9677
|
+
localState: {
|
|
9678
|
+
activeActor: localPaths.actor,
|
|
9679
|
+
activeActorDir: localPaths.actorDir,
|
|
9680
|
+
actors,
|
|
9681
|
+
foreignActors: actors.filter((actor) => !actor.current),
|
|
9682
|
+
legacyStatePresent: fs.existsSync(localPaths.legacy.currentSessionFile) ||
|
|
9683
|
+
fs.existsSync(localPaths.legacy.sessionsDir) ||
|
|
9684
|
+
fs.existsSync(localPaths.legacy.toolResponsesDir)
|
|
9685
|
+
},
|
|
8320
9686
|
files: repoFiles.map((file) => {
|
|
8321
9687
|
const stat = file.path ? fileStat(file.path) : null;
|
|
8322
9688
|
let content = null;
|
|
@@ -13048,6 +14414,8 @@ function printComponentsHelp(subcommand) {
|
|
|
13048
14414
|
function printTestHelp() {
|
|
13049
14415
|
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
14416
|
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
14417
|
+
console.log(' remits-cli test runs [--test <id|name>] [--limit 20] [--compare] [--json] # durable run history');
|
|
14418
|
+
console.log(' remits-cli test compare --base <taskId> --head <taskId> [--json] # per-case outcome/cost deltas');
|
|
13051
14419
|
console.log('');
|
|
13052
14420
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
13053
14421
|
console.log('Examples:');
|
|
@@ -13065,6 +14433,38 @@ function printTestHelp() {
|
|
|
13065
14433
|
console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
|
|
13066
14434
|
console.log(' --json prints only the final status JSON to stdout; banners and progress go to stderr.');
|
|
13067
14435
|
console.log(' --data-mode prod intentionally targets live production data.');
|
|
14436
|
+
console.log(' Every finished run is recorded durably: test status keeps working after the live status');
|
|
14437
|
+
console.log(' expires, and test runs / test compare read those records.');
|
|
14438
|
+
}
|
|
14439
|
+
|
|
14440
|
+
|
|
14441
|
+
function printCorpusHelp() {
|
|
14442
|
+
console.log('Usage: remits-cli corpus import --manifest corpus-manifest.json [--corpus <name>] [--as-account ID] [--data-mode test|prod --confirm-prod] [--json]');
|
|
14443
|
+
console.log(' remits-cli corpus cases --corpus <name> [--split dev] [--tag name] [--key caseKey] [--include-values] [--include-retired] [--json]');
|
|
14444
|
+
console.log(' remits-cli corpus results --corpus <name> [--case <caseKey>] [--task-id <taskId>] [--limit 50] [--json]');
|
|
14445
|
+
console.log(' remits-cli corpus runs --corpus <name> [--limit 20] [--json]');
|
|
14446
|
+
console.log(' remits-cli corpus compare --corpus <name> --base <taskId> --head <taskId> [--json]');
|
|
14447
|
+
console.log(' remits-cli corpus consistency --corpus <name> --case <caseKey> [--limit 20] [--json]');
|
|
14448
|
+
console.log(' remits-cli corpus retire --corpus <name> --case <caseKey> [--case ...] [--restore] [--json]');
|
|
14449
|
+
console.log('');
|
|
14450
|
+
console.log('A corpus is a named, durable set of cases an evaluation-style Test runs again and again (front stage:');
|
|
14451
|
+
console.log('corpus(name)). Cases live in a platform table owned by one account and visible to the accounts beneath');
|
|
14452
|
+
console.log('it, whatever the data lane or branch. Case files are stored once as immutable FILE source Objects.');
|
|
14453
|
+
console.log('Measurements are recorded by Test runs and read back with results / runs / compare / consistency.');
|
|
14454
|
+
console.log('');
|
|
14455
|
+
console.log(' - import is idempotent by caseKey: created | updated (with changed keys) | unchanged; identical artifact');
|
|
14456
|
+
console.log(' bytes are not stored twice. Artifacts are created in the --data-mode lane (default test).');
|
|
14457
|
+
console.log(' - Output and session logs carry keys, statuses, hashes, sizes and object ids, never artifact bytes;');
|
|
14458
|
+
console.log(' cases hides expected/source values unless --include-values.');
|
|
14459
|
+
console.log(' - cases filters by --split, repeated/comma-separated --tag/--tags, and --key/--keys before applying --limit.');
|
|
14460
|
+
console.log(' - runs / results / compare label each run AI live, AI MOCKED or no AI. A suite whose AI turns were all');
|
|
14461
|
+
console.log(' replayed from aiMock produces the same outcomes and metrics as a measured one, so read that label');
|
|
14462
|
+
console.log(' before treating a green run as evidence; compare warns when base and head used different AI modes.');
|
|
14463
|
+
console.log(' - retire drops a case from future runs without deleting it: existing measurements stay readable.');
|
|
14464
|
+
console.log('');
|
|
14465
|
+
console.log('Manifest (paths relative to the manifest):');
|
|
14466
|
+
console.log(' {"corpus":"invoice-totals","cases":[{"caseKey":"inv-001","split":"dev","tags":["multi-page"],');
|
|
14467
|
+
console.log(' "expected":{"total":1234.5},"source":{"note":"copied from ..."},"artifacts":[{"name":"source","path":"inv-001.pdf"}]}]}');
|
|
13068
14468
|
}
|
|
13069
14469
|
|
|
13070
14470
|
function printToolHelp() {
|
|
@@ -13138,6 +14538,7 @@ async function main() {
|
|
|
13138
14538
|
migrateSessionIfNeeded();
|
|
13139
14539
|
const originalArgv = process.argv.slice(2);
|
|
13140
14540
|
const args = parseArgs(originalArgv);
|
|
14541
|
+
configureLocalActor(args);
|
|
13141
14542
|
const [command, subcommand] = args._;
|
|
13142
14543
|
const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
|
|
13143
14544
|
const wantsJson = flagEnabled(args.json);
|
|
@@ -13168,7 +14569,7 @@ async function main() {
|
|
|
13168
14569
|
try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
|
|
13169
14570
|
// Auto-start the background service if not already running.
|
|
13170
14571
|
// Skip for lifecycle subcommands and help.
|
|
13171
|
-
const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status' || command === 'whoami' || command === 'agent' || command === 'ticket';
|
|
14572
|
+
const isLifecycleCmd = command === 'listen' || command === 'start' || command === 'stop' || command === 'status' || command === 'whoami' || command === 'doctor' || command === 'agent' || command === 'ticket';
|
|
13172
14573
|
const isHelpCmd = !command || command === 'help' || command === '--help' || wantsHelp;
|
|
13173
14574
|
if (!wantsJson && !isHelpCmd && !isLifecycleCmd && !isListenerRunning()) {
|
|
13174
14575
|
try {
|
|
@@ -13191,7 +14592,7 @@ async function main() {
|
|
|
13191
14592
|
|
|
13192
14593
|
if (!command || command === 'help' || command === '--help') {
|
|
13193
14594
|
console.log('Usage: remits-cli <command>');
|
|
13194
|
-
console.log('Global options: --no-auto-update');
|
|
14595
|
+
console.log('Global options: --no-auto-update, --local-agent NAME');
|
|
13195
14596
|
console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
|
|
13196
14597
|
console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
|
|
13197
14598
|
console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
|
|
@@ -13204,6 +14605,7 @@ async function main() {
|
|
|
13204
14605
|
console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
|
|
13205
14606
|
console.log(' remits-cli stop');
|
|
13206
14607
|
console.log(' remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
|
|
14608
|
+
console.log(' remits-cli doctor local-state [--json] # actor/workspace/local state hygiene');
|
|
13207
14609
|
console.log(' remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
|
|
13208
14610
|
console.log(' remits-cli listen [stop|status] [--foreground true] # compatibility alias');
|
|
13209
14611
|
console.log(' remits-cli data-mode [set test|prod]');
|
|
@@ -13228,6 +14630,10 @@ async function main() {
|
|
|
13228
14630
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
13229
14631
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
13230
14632
|
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]');
|
|
14633
|
+
console.log(' remits-cli test runs [--test <id|name>] [--compare] # durable run history; compare latest two');
|
|
14634
|
+
console.log(' remits-cli test compare --base <taskId> --head <taskId>');
|
|
14635
|
+
console.log(' remits-cli corpus import --manifest corpus-manifest.json # seed a test corpus: cases + artifacts');
|
|
14636
|
+
console.log(' remits-cli corpus runs|results|compare|consistency --corpus <name> # measurements across Test runs');
|
|
13231
14637
|
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
14638
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
13233
14639
|
console.log('');
|
|
@@ -13398,6 +14804,11 @@ async function main() {
|
|
|
13398
14804
|
return;
|
|
13399
14805
|
}
|
|
13400
14806
|
|
|
14807
|
+
if (command === 'doctor') {
|
|
14808
|
+
await doctorCommand(args, subcommand);
|
|
14809
|
+
return;
|
|
14810
|
+
}
|
|
14811
|
+
|
|
13401
14812
|
if (command === 'whoami') {
|
|
13402
14813
|
await whoamiCommand(args);
|
|
13403
14814
|
return;
|
|
@@ -13444,6 +14855,23 @@ async function main() {
|
|
|
13444
14855
|
await testStatusCommand(args);
|
|
13445
14856
|
return;
|
|
13446
14857
|
}
|
|
14858
|
+
if (command === 'test' && (subcommand === 'runs' || subcommand === 'history')) {
|
|
14859
|
+
await testRunsCommand(args);
|
|
14860
|
+
return;
|
|
14861
|
+
}
|
|
14862
|
+
if (command === 'test' && subcommand === 'compare') {
|
|
14863
|
+
await testCompareCommand(args);
|
|
14864
|
+
return;
|
|
14865
|
+
}
|
|
14866
|
+
|
|
14867
|
+
if (command === 'corpus') {
|
|
14868
|
+
if (wantsHelp || !subcommand) {
|
|
14869
|
+
printCorpusHelp();
|
|
14870
|
+
process.exit(0);
|
|
14871
|
+
}
|
|
14872
|
+
await corpusCommand(args, subcommand);
|
|
14873
|
+
return;
|
|
14874
|
+
}
|
|
13447
14875
|
|
|
13448
14876
|
if (command === 'token') {
|
|
13449
14877
|
await tokenCommand(args);
|