@remits/remits-cli 0.1.128 → 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 +1646 -98
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +15 -5
- package/skills/remits-cli/references/branch-variants.md +3 -1
- package/skills/remits-cli/references/cli-state.md +49 -20
- package/skills/remits-cli/references/command-reference.md +34 -11
- 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);
|
|
@@ -1107,6 +1645,66 @@ async function postVerificationCommand(api, cwd, session, accountId, command, pa
|
|
|
1107
1645
|
return response;
|
|
1108
1646
|
}
|
|
1109
1647
|
|
|
1648
|
+
// The repo/session account a packet was recorded from, stated explicitly in its world. `world.accountId` is the
|
|
1649
|
+
// account the command EXECUTED as, so an --as-account run carried the subscriber there and a manifest's
|
|
1650
|
+
// repoAccountId could never match it. Only fills a missing value; a packet that already knows keeps its own.
|
|
1651
|
+
function stampPacketRepoAccount(packet, accountId) {
|
|
1652
|
+
if (!packet || accountId === undefined || accountId === null || accountId === '') return packet;
|
|
1653
|
+
const repoAccountId = Number(accountId);
|
|
1654
|
+
if (!Number.isFinite(repoAccountId)) return packet;
|
|
1655
|
+
if (!packet.world || typeof packet.world !== 'object') packet.world = {};
|
|
1656
|
+
if (packet.world.repoAccountId === undefined || packet.world.repoAccountId === null) {
|
|
1657
|
+
packet.world.repoAccountId = repoAccountId;
|
|
1658
|
+
}
|
|
1659
|
+
return packet;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// A sync packet is evidence of WHAT a sync planned or wrote, not a second copy of the preview. The per-field
|
|
1663
|
+
// trunk/branch text a dry run attaches for the admin diff panel is up to 400 KB per sync, and storing it on
|
|
1664
|
+
// every packet made one busy envelope 9.4 MB — slow enough that `verify show` timed out. Field NAMES
|
|
1665
|
+
// (`changedFields`) stay; the full plan is always one `components sync --dry-run` away.
|
|
1666
|
+
function syncEvidencePayload(sync) {
|
|
1667
|
+
if (!sync || typeof sync !== 'object') return sync;
|
|
1668
|
+
const results = sync.syncResults;
|
|
1669
|
+
if (!results || typeof results !== 'object') return sync;
|
|
1670
|
+
const trimmedResults = {};
|
|
1671
|
+
Object.keys(results).forEach((bucket) => {
|
|
1672
|
+
const entries = results[bucket];
|
|
1673
|
+
trimmedResults[bucket] = Array.isArray(entries)
|
|
1674
|
+
? entries.map((entry) => {
|
|
1675
|
+
if (!entry || typeof entry !== 'object' || !('fields' in entry)) return entry;
|
|
1676
|
+
const copy = Object.assign({}, entry);
|
|
1677
|
+
delete copy.fields;
|
|
1678
|
+
return copy;
|
|
1679
|
+
})
|
|
1680
|
+
: entries;
|
|
1681
|
+
});
|
|
1682
|
+
return Object.assign({}, sync, { syncResults: trimmedResults });
|
|
1683
|
+
}
|
|
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
|
+
|
|
1110
1708
|
async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
|
|
1111
1709
|
const context = activeVerificationContext(cwd, flags, session, accountId);
|
|
1112
1710
|
const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
|
|
@@ -1117,6 +1715,7 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
|
|
|
1117
1715
|
success: true
|
|
1118
1716
|
}, packet || {});
|
|
1119
1717
|
finalPacket.envelopeId = envelopeId;
|
|
1718
|
+
stampPacketRepoAccount(finalPacket, accountId);
|
|
1120
1719
|
writeLocalVerificationPacket(cwd, envelopeId, finalPacket);
|
|
1121
1720
|
try {
|
|
1122
1721
|
const response = await postVerificationCommand(api, cwd, session, accountId, 'packet', {
|
|
@@ -1125,7 +1724,16 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
|
|
|
1125
1724
|
});
|
|
1126
1725
|
if (response.packet) writeLocalVerificationPacket(cwd, envelopeId, response.packet);
|
|
1127
1726
|
if (response.envelope) writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
1128
|
-
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
|
+
}
|
|
1129
1737
|
return response;
|
|
1130
1738
|
} catch (err) {
|
|
1131
1739
|
if (!options.quiet) {
|
|
@@ -1281,6 +1889,17 @@ function buildCommandWorld(response, fallback = {}) {
|
|
|
1281
1889
|
};
|
|
1282
1890
|
}
|
|
1283
1891
|
|
|
1892
|
+
// A variant sync writes overlays on exactly one component branch; say so in the packet world, where a manifest's
|
|
1893
|
+
// componentBranch is compared. Without it no sync_mutation could satisfy a manifest that declares one.
|
|
1894
|
+
function syncPacketWorld(response, world, branchName) {
|
|
1895
|
+
const sync = (response && response.sync) || {};
|
|
1896
|
+
const out = Object.assign({}, world || {});
|
|
1897
|
+
if (sync.mode === 'variant' && !out.componentBranch) {
|
|
1898
|
+
out.componentBranch = sync.branchName || branchName || null;
|
|
1899
|
+
}
|
|
1900
|
+
return out;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1284
1903
|
function laneContentHashFrom(response) {
|
|
1285
1904
|
if (!response) return null;
|
|
1286
1905
|
const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
|
|
@@ -2065,11 +2684,75 @@ function resolveSessionContext(cwd, flags) {
|
|
|
2065
2684
|
* file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
|
|
2066
2685
|
* identity is the only thing both the payload and the git changed set agree on.
|
|
2067
2686
|
*/
|
|
2068
|
-
function withFileKey(target, key) {
|
|
2687
|
+
function withFileKey(target, key, filePaths) {
|
|
2069
2688
|
Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
|
|
2689
|
+
if (filePaths) {
|
|
2690
|
+
addHiddenFilePaths(target, filePaths);
|
|
2691
|
+
}
|
|
2070
2692
|
return target;
|
|
2071
2693
|
}
|
|
2072
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
|
+
}
|
|
2705
|
+
return target;
|
|
2706
|
+
}
|
|
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
|
+
|
|
2073
2756
|
function collectComponents(cwd) {
|
|
2074
2757
|
const mapping = {
|
|
2075
2758
|
schemas: 'schema',
|
|
@@ -2121,6 +2804,7 @@ function collectComponents(cwd) {
|
|
|
2121
2804
|
}
|
|
2122
2805
|
const component = byKey.get(key);
|
|
2123
2806
|
const filePath = path.join(dir, fileName);
|
|
2807
|
+
addHiddenFilePaths(component, path.relative(cwd, filePath).replace(/\\/g, '/'));
|
|
2124
2808
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
2125
2809
|
|
|
2126
2810
|
if (ext === 'meta') {
|
|
@@ -2184,7 +2868,7 @@ function collectComponents(cwd) {
|
|
|
2184
2868
|
category: 'default',
|
|
2185
2869
|
prompt: fs.readFileSync(rootReadme, 'utf8'),
|
|
2186
2870
|
metadataAuthoritative: true
|
|
2187
|
-
}, 'prompt:name:readme');
|
|
2871
|
+
}, 'prompt:name:readme', 'README.md');
|
|
2188
2872
|
const fingerprint = {};
|
|
2189
2873
|
Object.keys(component).sort().forEach((key) => {
|
|
2190
2874
|
if (key !== 'hash') {
|
|
@@ -2195,7 +2879,13 @@ function collectComponents(cwd) {
|
|
|
2195
2879
|
components.push(component);
|
|
2196
2880
|
}
|
|
2197
2881
|
|
|
2198
|
-
|
|
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;
|
|
2199
2889
|
}
|
|
2200
2890
|
|
|
2201
2891
|
function componentPathInfo(cwd, filePath) {
|
|
@@ -2485,6 +3175,11 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2485
3175
|
let responseData = null;
|
|
2486
3176
|
let error = null;
|
|
2487
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
|
+
}
|
|
2488
3183
|
|
|
2489
3184
|
try {
|
|
2490
3185
|
responseData = await api.post(endpoint, payload).then((r) => r.data);
|
|
@@ -2502,15 +3197,19 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2502
3197
|
const status = error ? 'error' : 'success';
|
|
2503
3198
|
const responseForLog = responseFile
|
|
2504
3199
|
? { responseFile, note: 'Tool response stored externally' }
|
|
2505
|
-
:
|
|
3200
|
+
: summarizeResponseForLog(responseData);
|
|
2506
3201
|
appendSessionLog(cwd, {
|
|
2507
3202
|
ts: new Date().toISOString(),
|
|
2508
3203
|
requestId,
|
|
2509
3204
|
endpoint,
|
|
2510
3205
|
method: 'POST',
|
|
3206
|
+
actor: actor.id,
|
|
3207
|
+
actorSource: actor.source,
|
|
2511
3208
|
status,
|
|
2512
3209
|
durationMs: Date.now() - started,
|
|
2513
|
-
|
|
3210
|
+
requestSummary: summarizeRequestForLog(payload),
|
|
3211
|
+
unsafePayloadLogging: unsafePayloadLog,
|
|
3212
|
+
request: requestPayloadForSessionLog(payload),
|
|
2514
3213
|
response: responseForLog,
|
|
2515
3214
|
error: error ? describeError(error) : null
|
|
2516
3215
|
});
|
|
@@ -2523,6 +3222,7 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2523
3222
|
const started = Date.now();
|
|
2524
3223
|
let responseData = null;
|
|
2525
3224
|
let error = null;
|
|
3225
|
+
const actor = resolveLocalActor();
|
|
2526
3226
|
try {
|
|
2527
3227
|
responseData = await api.get(endpoint, { params }).then((r) => r.data);
|
|
2528
3228
|
return { data: responseData, requestId };
|
|
@@ -2536,10 +3236,13 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2536
3236
|
requestId,
|
|
2537
3237
|
endpoint,
|
|
2538
3238
|
method: 'GET',
|
|
3239
|
+
actor: actor.id,
|
|
3240
|
+
actorSource: actor.source,
|
|
2539
3241
|
status: error ? 'error' : 'success',
|
|
2540
3242
|
durationMs: Date.now() - started,
|
|
2541
|
-
|
|
2542
|
-
|
|
3243
|
+
requestSummary: summarizeRequestForLog(params),
|
|
3244
|
+
request: requestPayloadForSessionLog(params),
|
|
3245
|
+
response: summarizeResponseForLog(responseData),
|
|
2543
3246
|
error: error ? describeError(error) : null
|
|
2544
3247
|
});
|
|
2545
3248
|
}
|
|
@@ -3079,6 +3782,7 @@ async function pushComponentsCommand(flags) {
|
|
|
3079
3782
|
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
3080
3783
|
|
|
3081
3784
|
let components = collectComponents(cwd);
|
|
3785
|
+
const componentDiscoveryWarnings = Array.isArray(components.discoveryWarnings) ? components.discoveryWarnings : [];
|
|
3082
3786
|
// Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
|
|
3083
3787
|
const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
|
|
3084
3788
|
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
@@ -3138,7 +3842,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3138
3842
|
: null,
|
|
3139
3843
|
changedFromWorkingTree: [],
|
|
3140
3844
|
changedFromWorkingTreeAvailable: true,
|
|
3141
|
-
unrepresentableChanges: unstageable
|
|
3845
|
+
unrepresentableChanges: unstageable,
|
|
3846
|
+
componentDiscoveryWarnings,
|
|
3847
|
+
localState: localStateSummary(cwd, flags)
|
|
3142
3848
|
};
|
|
3143
3849
|
if (flagEnabled(flags.json)) {
|
|
3144
3850
|
console.log(JSON.stringify(emptyResponse, null, 2));
|
|
@@ -3150,6 +3856,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3150
3856
|
console.log(' or re-run with `--empty-workset clear` if an empty lane is what you meant.');
|
|
3151
3857
|
}
|
|
3152
3858
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
3859
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
3860
|
+
printLocalStateWarnings(cwd, flags);
|
|
3861
|
+
printComponentDiscoveryWarnings(componentDiscoveryWarnings);
|
|
3153
3862
|
printUnrepresentableChanges(unstageable);
|
|
3154
3863
|
}
|
|
3155
3864
|
return emptyResponse;
|
|
@@ -3238,6 +3947,8 @@ async function pushComponentsCommand(flags) {
|
|
|
3238
3947
|
response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
|
|
3239
3948
|
response.requestedStageMode = stageMode;
|
|
3240
3949
|
response.unrepresentableChanges = unstageable;
|
|
3950
|
+
response.componentDiscoveryWarnings = componentDiscoveryWarnings;
|
|
3951
|
+
response.localState = localStateSummary(cwd, flags);
|
|
3241
3952
|
if (response.success === false) {
|
|
3242
3953
|
// A 200 that says `success:false`. The 422 path throws out of stageOrRefuse above and never lands
|
|
3243
3954
|
// here, so this stays as the belt-and-braces case — print whatever detail came with it first.
|
|
@@ -3271,6 +3982,9 @@ async function pushComponentsCommand(flags) {
|
|
|
3271
3982
|
|
|
3272
3983
|
printSessionResolutionWarning(sessionContext);
|
|
3273
3984
|
printResolvedBaseUrl(baseUrl);
|
|
3985
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
|
|
3986
|
+
printLocalStateWarnings(cwd, flags);
|
|
3987
|
+
printComponentDiscoveryWarnings(componentDiscoveryWarnings);
|
|
3274
3988
|
printStagingLane(response.branchName || branchName, response.workspace || workspace, workspaceSource(cwd, flags));
|
|
3275
3989
|
console.log('Data mode:', response.dataMode || dataMode);
|
|
3276
3990
|
printComponentPolicy(response);
|
|
@@ -3473,6 +4187,15 @@ function printComponentPolicy(response) {
|
|
|
3473
4187
|
(policy.overrides || []).forEach((id) => console.log('Policy override recorded for rule: ' + id));
|
|
3474
4188
|
}
|
|
3475
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
|
+
|
|
3476
4199
|
/**
|
|
3477
4200
|
* What the platform compiled, and what it could not.
|
|
3478
4201
|
*
|
|
@@ -4042,6 +4765,8 @@ async function workspaceCommand(flags) {
|
|
|
4042
4765
|
const branchName = flags.branch || currentBranch(cwd);
|
|
4043
4766
|
const workspace = resolveWorkspace(cwd, flags);
|
|
4044
4767
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
4768
|
+
printLocalActor(cwd, flags);
|
|
4769
|
+
printLocalStateWarnings(cwd, flags);
|
|
4045
4770
|
console.log('');
|
|
4046
4771
|
console.log('Precedence: --workspace > REMITS_WORKSPACE > .remits-cli/workspace > shared default lane');
|
|
4047
4772
|
console.log('');
|
|
@@ -4051,6 +4776,74 @@ async function workspaceCommand(flags) {
|
|
|
4051
4776
|
console.log(' remits-cli components status see every lane staged on this branch');
|
|
4052
4777
|
}
|
|
4053
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
|
+
|
|
4054
4847
|
async function statusComponentsCommand(flags) {
|
|
4055
4848
|
const cwd = process.cwd();
|
|
4056
4849
|
ensureLocalState(cwd);
|
|
@@ -4091,6 +4884,7 @@ async function statusComponentsCommand(flags) {
|
|
|
4091
4884
|
isAncestor: (a, b) => gitIsAncestor(cwd, a, b)
|
|
4092
4885
|
});
|
|
4093
4886
|
response.repositoryCheck = repositoryCheck(cwd, response.branchContext);
|
|
4887
|
+
response.localState = localStateSummary(cwd, flags);
|
|
4094
4888
|
|
|
4095
4889
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
4096
4890
|
type: 'component_status',
|
|
@@ -4110,6 +4904,8 @@ async function statusComponentsCommand(flags) {
|
|
|
4110
4904
|
|
|
4111
4905
|
printSessionResolutionWarning(sessionContext);
|
|
4112
4906
|
printResolvedBaseUrl(baseUrl);
|
|
4907
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode: response.dataMode || dataMode });
|
|
4908
|
+
printLocalStateWarnings(cwd, flags);
|
|
4113
4909
|
console.log('Data mode:', response.dataMode || dataMode);
|
|
4114
4910
|
console.log('Mode:', response.mode || 'status');
|
|
4115
4911
|
printStatusSummary(response, flags);
|
|
@@ -4542,7 +5338,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4542
5338
|
type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
|
|
4543
5339
|
success: response.success !== false,
|
|
4544
5340
|
claim: response.sync && response.sync.dryRun ? 'Component sync dry-run observed' : 'Component sync completed',
|
|
4545
|
-
world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }),
|
|
5341
|
+
world: syncPacketWorld(response, buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }), branchName),
|
|
4546
5342
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
4547
5343
|
platformSyncedSha: response.sync && (response.sync.postSyncSha || response.sync.branchHeadSha)
|
|
4548
5344
|
}),
|
|
@@ -4551,7 +5347,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4551
5347
|
{ category: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation', summary }
|
|
4552
5348
|
],
|
|
4553
5349
|
rawRefs: { command: 'components sync' },
|
|
4554
|
-
sync: response.sync,
|
|
5350
|
+
sync: syncEvidencePayload(response.sync),
|
|
4555
5351
|
summary
|
|
4556
5352
|
}, { quiet: flagEnabled(flags.json) });
|
|
4557
5353
|
|
|
@@ -4582,11 +5378,31 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4582
5378
|
return response;
|
|
4583
5379
|
}
|
|
4584
5380
|
console.log('Components sync:', JSON.stringify(response, null, 2));
|
|
5381
|
+
printVariantSyncWrites(summary);
|
|
4585
5382
|
printPromotionSignals(response);
|
|
4586
5383
|
failOnSyncGate(gate);
|
|
4587
5384
|
return response;
|
|
4588
5385
|
}
|
|
4589
5386
|
|
|
5387
|
+
// The one line a person needs from a variant sync of a long-lived branch: what THIS sync changes, apart from
|
|
5388
|
+
// the overlays it merely re-confirms (the totals alone read the same on every sync).
|
|
5389
|
+
function printVariantSyncWrites(summary) {
|
|
5390
|
+
const writes = summary && summary.writes;
|
|
5391
|
+
if (!writes || summary.mode !== 'variant') return;
|
|
5392
|
+
console.log('');
|
|
5393
|
+
if (!writes.known) {
|
|
5394
|
+
console.log('Overlay changes: unknown (this platform does not report which overlays were already stored).');
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
const verb = summary.dryRun ? 'would change' : 'changed';
|
|
5398
|
+
console.log('Overlay changes: ' + writes.changed + ' ' + verb + ', ' + writes.alreadyCurrent + ' already current, ' +
|
|
5399
|
+
writes.removed + ' removed, ' + writes.pruned + ' pruned (converged back to trunk)');
|
|
5400
|
+
(writes.changedComponents || []).forEach((entry) => {
|
|
5401
|
+
console.log(' ' + String(entry.type || 'component').toLowerCase() + ':' + (entry.id != null ? entry.id : entry.name) +
|
|
5402
|
+
(entry.name && entry.id != null ? ' ' + entry.name : ''));
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
|
|
4590
5406
|
// A sync is a step in a longer loop, and the steps after it are the ones that get skipped — so print what
|
|
4591
5407
|
// the server just observed about this branch's relationship to trunk, not only what it wrote.
|
|
4592
5408
|
function printPromotionSignals(response) {
|
|
@@ -4755,7 +5571,9 @@ function syncPlanEntries(response) {
|
|
|
4755
5571
|
type: String(entry.type || entry.kind || 'component').toLowerCase(),
|
|
4756
5572
|
id: entry.id == null ? null : String(entry.id),
|
|
4757
5573
|
name: entry.name || null,
|
|
4758
|
-
path: entry.path || null
|
|
5574
|
+
path: entry.path || null,
|
|
5575
|
+
// true when the overlay already stored for this component matches the branch: re-confirmed, not changed.
|
|
5576
|
+
storedCurrent: entry.storedCurrent === true
|
|
4759
5577
|
});
|
|
4760
5578
|
});
|
|
4761
5579
|
});
|
|
@@ -4801,6 +5619,8 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4801
5619
|
const expectedRemoved = parseExpectedRemoved(flags);
|
|
4802
5620
|
const violations = [];
|
|
4803
5621
|
const checks = {};
|
|
5622
|
+
// Overlays the --changed-only gate did not count because the platform already stores them with this content.
|
|
5623
|
+
let alreadyCurrentCount = null;
|
|
4804
5624
|
|
|
4805
5625
|
if (flagEnabled(flags['fail-on-errors']) || flagEnabled(flags.failOnErrors)) {
|
|
4806
5626
|
checks.failOnErrors = errors.length === 0;
|
|
@@ -4840,13 +5660,20 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4840
5660
|
const allowedIds = new Set(changedSet.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
|
|
4841
5661
|
const allowedNames = new Set(changedSet.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
|
|
4842
5662
|
const allowedPaths = new Set([].concat(...changedSet.map((c) => (Array.isArray(c.paths) ? c.paths : []))).map(String));
|
|
5663
|
+
// An overlay the platform ALREADY stores with this exact content is not something this sync changes. A
|
|
5664
|
+
// long-lived branch (a permanent customer release train) re-reports every one of its overlays on every sync,
|
|
5665
|
+
// so counting those made the gate refuse ordinary work with "plan touches 80 components" and blame a stale
|
|
5666
|
+
// branch that `components promotion` reported as ready. Removals are never exempt.
|
|
5667
|
+
const alreadyCurrent = entries.filter((entry) => entry.storedCurrent && entry.bucket !== 'removed');
|
|
4843
5668
|
const unexpected = entries.filter((entry) => {
|
|
5669
|
+
if (entry.storedCurrent && entry.bucket !== 'removed') return false;
|
|
4844
5670
|
if (entry.path && allowedPaths.has(String(entry.path))) return false;
|
|
4845
5671
|
if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
|
|
4846
5672
|
if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
|
|
4847
5673
|
return true;
|
|
4848
5674
|
});
|
|
4849
5675
|
checks.changedOnly = unexpected.length === 0;
|
|
5676
|
+
alreadyCurrentCount = alreadyCurrent.length;
|
|
4850
5677
|
if (unexpected.length) {
|
|
4851
5678
|
// Name the most likely cause instead of only the symptom. An empty changed set with a non-empty
|
|
4852
5679
|
// plan is the ordinary post-commit state, not evidence that the plan is dangerous.
|
|
@@ -4858,11 +5685,10 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4858
5685
|
// physically carries old copies of files nobody on it touched, and a variant sync turns each of
|
|
4859
5686
|
// those into an unrelated override. Naming the cause is the difference between a gate that
|
|
4860
5687
|
// stops the damage and a gate that also tells you how to clear it.
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
'\n git merge <trunk> && git push origin <branch>'
|
|
5688
|
+
// With --changed-since the empty-set hint above does not apply, so a ref that shows no committed change
|
|
5689
|
+
// still needs the cause named — that is exactly the behind-trunk branch whose plan is all stale copies.
|
|
5690
|
+
: ((changedSet.length || since) && unexpected.length > changedSet.length)
|
|
5691
|
+
? staleBranchGateHint(response)
|
|
4866
5692
|
: '';
|
|
4867
5693
|
violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this checkout did not change: ' +
|
|
4868
5694
|
unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : '') + hint);
|
|
@@ -4870,7 +5696,28 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4870
5696
|
}
|
|
4871
5697
|
}
|
|
4872
5698
|
|
|
4873
|
-
return { violations, checks };
|
|
5699
|
+
return { violations, checks, alreadyCurrentCount };
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
// Name the stale-branch cause only when the platform's own git comparison says the branch IS behind trunk. The
|
|
5703
|
+
// hint used to fire on the count alone, and told agents on an up-to-date permanent branch to merge trunk in.
|
|
5704
|
+
function staleBranchGateHint(response) {
|
|
5705
|
+
const sync = (response && response.sync) || {};
|
|
5706
|
+
const comparison = sync.branchComparison || (sync.promotion && sync.promotion.git) || null;
|
|
5707
|
+
const behindBy = comparison && Number.isFinite(Number(comparison.behindBy)) ? Number(comparison.behindBy) : null;
|
|
5708
|
+
if (behindBy !== null && behindBy > 0) {
|
|
5709
|
+
return '\n Most likely cause: this branch is BEHIND trunk by ' + behindBy + ' commit(s) and still carries old copies ' +
|
|
5710
|
+
'of files it never changed. A variant sync turns each of those into an unrelated override.' +
|
|
5711
|
+
'\n Next step: merge trunk into this branch, push, then re-run the same command:' +
|
|
5712
|
+
'\n git merge <trunk> && git push origin <branch>';
|
|
5713
|
+
}
|
|
5714
|
+
if (behindBy === 0) {
|
|
5715
|
+
return '\n The branch is current with trunk, so these are not stale copies: they are overlays whose stored content ' +
|
|
5716
|
+
'differs from this branch but that this checkout\'s changed set does not name. Widen --changed-since to the ' +
|
|
5717
|
+
'commit the platform last synced (components promotion shows it), or review the plan with --dry-run.';
|
|
5718
|
+
}
|
|
5719
|
+
return '\n The platform could not compare this branch with trunk. Run `remits-cli components promotion` to see ' +
|
|
5720
|
+
'whether it is behind before merging anything.';
|
|
4874
5721
|
}
|
|
4875
5722
|
|
|
4876
5723
|
function failOnSyncGate(gate) {
|
|
@@ -5163,6 +6010,36 @@ function printTrunkSyncWarning(trunkBranch, options) {
|
|
|
5163
6010
|
emit(' For a gated plan, work on a variant branch instead.');
|
|
5164
6011
|
}
|
|
5165
6012
|
|
|
6013
|
+
function variantSyncWrites(overridden, added, removed, unchanged) {
|
|
6014
|
+
const overlays = overridden.concat(added);
|
|
6015
|
+
const known = overlays.every((entry) => entry && typeof entry.storedCurrent === 'boolean');
|
|
6016
|
+
const pruned = unchanged.filter((entry) => entry && entry.pruned === true).length;
|
|
6017
|
+
if (!known) {
|
|
6018
|
+
return { changed: null, alreadyCurrent: null, removed: removed.length, pruned, known: false };
|
|
6019
|
+
}
|
|
6020
|
+
const changed = overlays.filter((entry) => !entry.storedCurrent);
|
|
6021
|
+
return {
|
|
6022
|
+
changed: changed.length,
|
|
6023
|
+
changedComponents: changed.slice(0, 50).map((entry) => ({ type: entry.type, name: entry.name, id: entry.id })),
|
|
6024
|
+
alreadyCurrent: overlays.length - changed.length,
|
|
6025
|
+
removed: removed.length,
|
|
6026
|
+
pruned,
|
|
6027
|
+
known: true
|
|
6028
|
+
};
|
|
6029
|
+
}
|
|
6030
|
+
|
|
6031
|
+
// `|` separates selectors and a repeated --names needs no separator. A COMMA is never a separator any more: it
|
|
6032
|
+
// is sent as part of the selector, and the platform matches a comma-bearing selector either as one whole case name
|
|
6033
|
+
// ("… the complete statement, offer, and surcharge matrix") or, for older invocations, as its comma-separated
|
|
6034
|
+
// parts (Test.selectorMatchesCase). Splitting in the CLI took the real name apart before the platform could see it.
|
|
6035
|
+
function parseTestCaseSelectors(rawNames) {
|
|
6036
|
+
return (Array.isArray(rawNames) ? rawNames : [rawNames])
|
|
6037
|
+
.filter((value) => value !== undefined && value !== null)
|
|
6038
|
+
.flatMap((value) => String(value).split('|'))
|
|
6039
|
+
.map((value) => value.trim())
|
|
6040
|
+
.filter(Boolean);
|
|
6041
|
+
}
|
|
6042
|
+
|
|
5166
6043
|
function buildSyncSummary(response) {
|
|
5167
6044
|
const sync = (response && response.sync) || {};
|
|
5168
6045
|
const results = sync.syncResults || {};
|
|
@@ -5292,6 +6169,10 @@ function buildSyncSummary(response) {
|
|
|
5292
6169
|
gateViolations: (response && response.gateViolations) || null,
|
|
5293
6170
|
overridden: overridden.length,
|
|
5294
6171
|
added: added.length,
|
|
6172
|
+
// What THIS sync changes, as opposed to the whole overlay set it re-reports. `overridden`/`added` are totals,
|
|
6173
|
+
// so five consecutive syncs of a long-lived branch all printed `overridden: 48, added: 32` and nobody could see
|
|
6174
|
+
// which overlays their own sync moved. Null counts mean the platform predates the storedCurrent flag.
|
|
6175
|
+
writes: variantSyncWrites(overridden, added, removed, unchanged),
|
|
5295
6176
|
removed: removed.map((entry) => ({
|
|
5296
6177
|
type: entry.type,
|
|
5297
6178
|
kind: entry.kind,
|
|
@@ -5678,6 +6559,17 @@ function short(sha) {
|
|
|
5678
6559
|
return sha ? String(sha).slice(0, 8) : 'unknown';
|
|
5679
6560
|
}
|
|
5680
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
|
+
|
|
5681
6573
|
async function commitComponentsCommand(flags) {
|
|
5682
6574
|
const cwd = process.cwd();
|
|
5683
6575
|
ensureLocalState(cwd);
|
|
@@ -6229,7 +7121,12 @@ function printTestRunPivots(status = {}) {
|
|
|
6229
7121
|
}
|
|
6230
7122
|
tests.filter((test) => test && test.passed === false).forEach((test) => {
|
|
6231
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) : ''));
|
|
6232
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
|
+
}
|
|
6233
7130
|
if (test.threadGroupingId || test.threadGroupId || test.traceId) {
|
|
6234
7131
|
console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
|
|
6235
7132
|
}
|
|
@@ -6245,7 +7142,8 @@ function printTestRunPivots(status = {}) {
|
|
|
6245
7142
|
(entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
|
|
6246
7143
|
}
|
|
6247
7144
|
if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
|
|
6248
|
-
|
|
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)' : ''));
|
|
6249
7147
|
}
|
|
6250
7148
|
});
|
|
6251
7149
|
}
|
|
@@ -6257,6 +7155,249 @@ function formatDurationMs(value) {
|
|
|
6257
7155
|
return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
|
|
6258
7156
|
}
|
|
6259
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
|
+
|
|
6260
7401
|
async function waitForToolStatus(api, cwd, options) {
|
|
6261
7402
|
const started = Date.now();
|
|
6262
7403
|
let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
|
|
@@ -6288,6 +7429,19 @@ async function waitForToolStatus(api, cwd, options) {
|
|
|
6288
7429
|
}
|
|
6289
7430
|
}
|
|
6290
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
|
+
|
|
6291
7445
|
function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
6292
7446
|
const emit = options.stderr ? console.error : console.log;
|
|
6293
7447
|
const client = new Client({
|
|
@@ -6308,7 +7462,11 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
|
6308
7462
|
return;
|
|
6309
7463
|
}
|
|
6310
7464
|
if (payload.tests || payload.name || payload.total != null) {
|
|
6311
|
-
|
|
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));
|
|
6312
7470
|
}
|
|
6313
7471
|
}
|
|
6314
7472
|
} catch (err) {
|
|
@@ -6345,26 +7503,12 @@ async function testCommand(flags) {
|
|
|
6345
7503
|
throw new Error('Missing --test <id-or-name>');
|
|
6346
7504
|
}
|
|
6347
7505
|
|
|
6348
|
-
// Test case names are English prose, so COMMAS occur in them naturally.
|
|
6349
|
-
//
|
|
6350
|
-
// report unmatched selectors) reported a clean run of zero cases. `|` is the delimiter now; the
|
|
6351
|
-
// comma still works when no `|` is present, so existing invocations keep behaving as before.
|
|
6352
|
-
// `--names` may also be repeated, which needs no delimiter at all.
|
|
7506
|
+
// Test case names are English prose, so COMMAS occur in them naturally. `|` (or repeating --names) is the
|
|
7507
|
+
// only delimiter; see parseTestCaseSelectors for how a comma-bearing selector is matched.
|
|
6353
7508
|
const rawNames = flags.names === undefined || flags.names === null
|
|
6354
7509
|
? []
|
|
6355
7510
|
: (Array.isArray(flags.names) ? flags.names : [flags.names]);
|
|
6356
|
-
const
|
|
6357
|
-
const names = rawNames
|
|
6358
|
-
.flatMap((value) => {
|
|
6359
|
-
const text = String(value);
|
|
6360
|
-
if (text.includes('|')) return text.split('|');
|
|
6361
|
-
// Comma-splitting is the legacy behaviour, kept so existing invocations still work. It is applied
|
|
6362
|
-
// ONLY to a single --names value: repeating the flag is already unambiguous, so splitting there
|
|
6363
|
-
// would take a deliberate literal name back apart.
|
|
6364
|
-
return repeated ? [text] : text.split(',');
|
|
6365
|
-
})
|
|
6366
|
-
.map((s) => s.trim())
|
|
6367
|
-
.filter(Boolean);
|
|
7511
|
+
const names = parseTestCaseSelectors(rawNames);
|
|
6368
7512
|
|
|
6369
7513
|
const api = buildAxios(baseUrl, session.token);
|
|
6370
7514
|
// --as-account runs the Test AS a (descendant) subscriber account, so that account's relationship
|
|
@@ -6405,12 +7549,20 @@ async function testCommand(flags) {
|
|
|
6405
7549
|
withStdoutRoutedToStderr(jsonOutput, () => {
|
|
6406
7550
|
printSessionResolutionWarning(sessionContext);
|
|
6407
7551
|
printResolvedBaseUrl(baseUrl);
|
|
7552
|
+
printLocalCommandContext(cwd, flags, { accountId: asAccountId || accountId, branchName, workspace, dataMode });
|
|
7553
|
+
printLocalStateWarnings(cwd, flags);
|
|
6408
7554
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
6409
7555
|
printStagingLaneOwnerNotice(start.staging || {});
|
|
6410
7556
|
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
6411
7557
|
printOrphanedWorkspaceWarning(start.staging);
|
|
6412
7558
|
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
6413
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));
|
|
6414
7566
|
console.log('Test run started:', start.taskId);
|
|
6415
7567
|
});
|
|
6416
7568
|
runtimeState.currentTestTaskId = start.taskId;
|
|
@@ -6419,11 +7571,12 @@ async function testCommand(flags) {
|
|
|
6419
7571
|
if (flags.watch !== 'false') {
|
|
6420
7572
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
6421
7573
|
if (topic) {
|
|
6422
|
-
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput });
|
|
7574
|
+
stopWs = watchWebsocket(baseUrl, topic, start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
|
|
6423
7575
|
}
|
|
6424
7576
|
}
|
|
6425
7577
|
|
|
6426
7578
|
const status = await waitForStatus(api, cwd, accountId, branchName, start.taskId, session.token, dataMode);
|
|
7579
|
+
status.localState = localStateSummary(cwd, flags);
|
|
6427
7580
|
if (stopWs) {
|
|
6428
7581
|
stopWs();
|
|
6429
7582
|
}
|
|
@@ -6436,6 +7589,7 @@ async function testCommand(flags) {
|
|
|
6436
7589
|
} else {
|
|
6437
7590
|
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
6438
7591
|
printStagingLaneOwnerNotice(status.staging || {});
|
|
7592
|
+
testRunSummaryLines(status).forEach((line) => console.log(line));
|
|
6439
7593
|
printTestRunPivots(status);
|
|
6440
7594
|
}
|
|
6441
7595
|
|
|
@@ -6505,10 +7659,14 @@ async function testCommand(flags) {
|
|
|
6505
7659
|
unmatchedTestNames: unmatched
|
|
6506
7660
|
},
|
|
6507
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,
|
|
6508
7666
|
limitations: []
|
|
6509
7667
|
.concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
|
|
6510
7668
|
.concat(nondeterminism ? [nondeterminism.message] : []),
|
|
6511
|
-
rawRefs: { testStatusKey: start.taskId },
|
|
7669
|
+
rawRefs: { testStatusKey: start.taskId, durableTestRun: status.result && status.result.durableRecord ? start.taskId : undefined },
|
|
6512
7670
|
status
|
|
6513
7671
|
}, { quiet: jsonOutput });
|
|
6514
7672
|
}
|
|
@@ -6545,6 +7703,11 @@ async function testStatusCommand(flags) {
|
|
|
6545
7703
|
printResolvedBaseUrl(baseUrl);
|
|
6546
7704
|
console.log('Test run status:', status.status || 'unknown');
|
|
6547
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');
|
|
6548
7711
|
console.log('Data mode:', status.dataMode || dataMode);
|
|
6549
7712
|
if (status.result) {
|
|
6550
7713
|
console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
|
|
@@ -6552,12 +7715,100 @@ async function testStatusCommand(flags) {
|
|
|
6552
7715
|
if (status.error || status.message) {
|
|
6553
7716
|
console.log('Message:', status.error || status.message);
|
|
6554
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);
|
|
6555
7721
|
if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
|
|
6556
7722
|
process.exitCode = 1;
|
|
6557
7723
|
}
|
|
6558
7724
|
return status;
|
|
6559
7725
|
}
|
|
6560
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
|
+
|
|
6561
7812
|
/*
|
|
6562
7813
|
## Tokens, Tools, Verification, And Config
|
|
6563
7814
|
*/
|
|
@@ -6627,7 +7878,8 @@ async function tokenCommand(flags) {
|
|
|
6627
7878
|
accountLanes: data.accountLanes,
|
|
6628
7879
|
dataMode: data.dataMode || dataMode,
|
|
6629
7880
|
shortBasePath: data.shortBasePath,
|
|
6630
|
-
embeddableUrl
|
|
7881
|
+
embeddableUrl,
|
|
7882
|
+
localState: localStateSummary(cwd, flags)
|
|
6631
7883
|
};
|
|
6632
7884
|
|
|
6633
7885
|
// What the token will RESOLVE AS. `accountId` says which account it executes as; on a hierarchy the
|
|
@@ -6650,7 +7902,11 @@ async function tokenCommand(flags) {
|
|
|
6650
7902
|
output.embedSnippet = data.embedSnippet;
|
|
6651
7903
|
}
|
|
6652
7904
|
|
|
6653
|
-
|
|
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
|
+
}
|
|
6654
7910
|
console.log(JSON.stringify(output, null, 2));
|
|
6655
7911
|
|
|
6656
7912
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
@@ -6666,7 +7922,7 @@ async function tokenCommand(flags) {
|
|
|
6666
7922
|
},
|
|
6667
7923
|
evidenceCategories: ['token_inspect'],
|
|
6668
7924
|
token: Object.assign({}, output, { tokenKey: output.tokenKey ? '[redacted]' : undefined, embedTokenKey: output.embedTokenKey ? '[redacted]' : undefined })
|
|
6669
|
-
});
|
|
7925
|
+
}, { quiet: flagEnabled(flags.json) });
|
|
6670
7926
|
}
|
|
6671
7927
|
|
|
6672
7928
|
async function tokenInspectCommand(flags) {
|
|
@@ -6868,6 +8124,7 @@ async function toolCommand(flags) {
|
|
|
6868
8124
|
|
|
6869
8125
|
data.responseFile = statusResponse.responseFile;
|
|
6870
8126
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
8127
|
+
data.localState = localStateSummary(cwd, flags);
|
|
6871
8128
|
// A completed run can still carry a tool-level refusal — see toolResultFailed.
|
|
6872
8129
|
const polledFailed = data.toolSuccess === false || toolResultFailed(data.result);
|
|
6873
8130
|
if (polledFailed) {
|
|
@@ -6890,6 +8147,8 @@ async function toolCommand(flags) {
|
|
|
6890
8147
|
}
|
|
6891
8148
|
printSessionResolutionWarning(sessionContext);
|
|
6892
8149
|
printResolvedBaseUrl(baseUrl);
|
|
8150
|
+
printLocalCommandContext(cwd, flags, { accountId: statusAccountId, branchName: flags.branch || (stored && stored.branchName) || branchName, dataMode: data.dataMode || dataMode });
|
|
8151
|
+
printLocalStateWarnings(cwd, flags);
|
|
6893
8152
|
console.log('Tool call status:', data.status);
|
|
6894
8153
|
console.log('Call ID:', requestedCallId);
|
|
6895
8154
|
console.log('Data mode:', data.dataMode || dataMode);
|
|
@@ -6944,6 +8203,7 @@ async function toolCommand(flags) {
|
|
|
6944
8203
|
const toolFailureMessage = data.toolMessage || toolResultMessage(data.result);
|
|
6945
8204
|
data.responseFile = response.responseFile;
|
|
6946
8205
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
8206
|
+
data.localState = localStateSummary(cwd, flags);
|
|
6947
8207
|
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
6948
8208
|
|
|
6949
8209
|
let evidenceData = data;
|
|
@@ -6952,6 +8212,8 @@ async function toolCommand(flags) {
|
|
|
6952
8212
|
if (!jsonOutput) {
|
|
6953
8213
|
printSessionResolutionWarning(sessionContext);
|
|
6954
8214
|
printResolvedBaseUrl(baseUrl);
|
|
8215
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace: resolveWorkspace(cwd, flags), dataMode: data.dataMode || dataMode });
|
|
8216
|
+
printLocalStateWarnings(cwd, flags);
|
|
6955
8217
|
console.log(asyncMode ? 'Tool call started.'
|
|
6956
8218
|
: (toolFailed ? 'Tool call FAILED — the tool ran and returned an error.' : 'Tool call succeeded.'));
|
|
6957
8219
|
if (toolFailed && toolFailureMessage) console.log('Tool error:', toolFailureMessage);
|
|
@@ -7020,6 +8282,202 @@ async function toolCommand(flags) {
|
|
|
7020
8282
|
}
|
|
7021
8283
|
}
|
|
7022
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
|
+
|
|
7023
8481
|
async function verifyCommand(flags, subcommand) {
|
|
7024
8482
|
const cwd = process.cwd();
|
|
7025
8483
|
ensureLocalState(cwd);
|
|
@@ -7038,6 +8496,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7038
8496
|
const active = readActiveVerificationEnvelope(cwd, activeContext);
|
|
7039
8497
|
printSessionResolutionWarning(sessionContext);
|
|
7040
8498
|
printResolvedBaseUrl(baseUrl);
|
|
8499
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8500
|
+
printLocalStateWarnings(cwd, flags);
|
|
7041
8501
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7042
8502
|
if (!active) {
|
|
7043
8503
|
const legacy = readActiveVerificationEnvelope(cwd);
|
|
@@ -7058,6 +8518,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7058
8518
|
if (!envelopeId) throw new Error('Usage: remits-cli verify use <envelopeId>');
|
|
7059
8519
|
writeActiveVerificationEnvelope(cwd, envelopeId, activeContext);
|
|
7060
8520
|
console.log('Active verification envelope:', envelopeId);
|
|
8521
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8522
|
+
printLocalStateWarnings(cwd, flags);
|
|
7061
8523
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7062
8524
|
return;
|
|
7063
8525
|
}
|
|
@@ -7070,6 +8532,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7070
8532
|
} else {
|
|
7071
8533
|
writeActiveVerificationEnvelope(cwd, null, activeContext);
|
|
7072
8534
|
console.log('Verification envelope cleared for this context.');
|
|
8535
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
7073
8536
|
console.log('Context:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7074
8537
|
}
|
|
7075
8538
|
return;
|
|
@@ -7121,6 +8584,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
7121
8584
|
writeLocalVerificationEnvelope(cwd, envelope);
|
|
7122
8585
|
writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
|
|
7123
8586
|
console.log('Verification envelope started:', envelope.envelopeId);
|
|
8587
|
+
printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
|
|
8588
|
+
printLocalStateWarnings(cwd, flags);
|
|
7124
8589
|
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7125
8590
|
printAcceptanceWarnings(envelope);
|
|
7126
8591
|
printEnvelopeWarnings(envelope);
|
|
@@ -7984,7 +9449,10 @@ function detectIssueSignals(repoEntries, globalFiles) {
|
|
|
7984
9449
|
}
|
|
7985
9450
|
|
|
7986
9451
|
for (const repo of repoEntries || []) {
|
|
7987
|
-
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)');
|
|
7988
9456
|
if (!sessionLogFile || !sessionLogFile.path || !fs.existsSync(sessionLogFile.path)) {
|
|
7989
9457
|
continue;
|
|
7990
9458
|
}
|
|
@@ -8180,23 +9648,41 @@ function buildGlobalFileSnapshot() {
|
|
|
8180
9648
|
function buildRepoSnapshot(entry) {
|
|
8181
9649
|
const directory = entry.directory;
|
|
8182
9650
|
const localPaths = localStatePaths(directory);
|
|
9651
|
+
const actors = listActorDirectories(directory);
|
|
8183
9652
|
const currentSessionName = fs.existsSync(localPaths.currentSessionFile)
|
|
8184
9653
|
? fs.readFileSync(localPaths.currentSessionFile, 'utf8').trim()
|
|
8185
9654
|
: '';
|
|
8186
9655
|
const currentSessionLog = currentSessionName
|
|
8187
9656
|
? path.join(localPaths.sessionsDir, currentSessionName + '.jsonl')
|
|
8188
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;
|
|
8189
9664
|
const repoFiles = [
|
|
8190
9665
|
{ label: 'account-info.json', path: entry.accountInfoPath || path.join(directory, 'account-info.json'), mode: 'json' },
|
|
8191
9666
|
{ label: 'account-hierarchy.json', path: path.join(directory, 'account-hierarchy.json'), mode: 'json' },
|
|
8192
|
-
{ label: '.remits-cli/current-session.txt', path: localPaths.currentSessionFile, mode: 'text' },
|
|
8193
|
-
{ label: '.remits-cli/tools/tools.json', path: path.join(localPaths.toolsDir, 'tools.json'), mode: 'json' },
|
|
8194
|
-
{ 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' }
|
|
8195
9672
|
];
|
|
8196
9673
|
|
|
8197
9674
|
return {
|
|
8198
9675
|
...entry,
|
|
8199
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
|
+
},
|
|
8200
9686
|
files: repoFiles.map((file) => {
|
|
8201
9687
|
const stat = file.path ? fileStat(file.path) : null;
|
|
8202
9688
|
let content = null;
|
|
@@ -12928,6 +14414,8 @@ function printComponentsHelp(subcommand) {
|
|
|
12928
14414
|
function printTestHelp() {
|
|
12929
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]');
|
|
12930
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');
|
|
12931
14419
|
console.log('');
|
|
12932
14420
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
12933
14421
|
console.log('Examples:');
|
|
@@ -12945,6 +14433,38 @@ function printTestHelp() {
|
|
|
12945
14433
|
console.log(' value with --variant-branch none when existing staged entries would shadow DB rows.');
|
|
12946
14434
|
console.log(' --json prints only the final status JSON to stdout; banners and progress go to stderr.');
|
|
12947
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"}]}]}');
|
|
12948
14468
|
}
|
|
12949
14469
|
|
|
12950
14470
|
function printToolHelp() {
|
|
@@ -13018,6 +14538,7 @@ async function main() {
|
|
|
13018
14538
|
migrateSessionIfNeeded();
|
|
13019
14539
|
const originalArgv = process.argv.slice(2);
|
|
13020
14540
|
const args = parseArgs(originalArgv);
|
|
14541
|
+
configureLocalActor(args);
|
|
13021
14542
|
const [command, subcommand] = args._;
|
|
13022
14543
|
const wantsHelp = args.help === true || subcommand === 'help' || subcommand === '--help' || args._.includes('--help');
|
|
13023
14544
|
const wantsJson = flagEnabled(args.json);
|
|
@@ -13048,7 +14569,7 @@ async function main() {
|
|
|
13048
14569
|
try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
|
|
13049
14570
|
// Auto-start the background service if not already running.
|
|
13050
14571
|
// Skip for lifecycle subcommands and help.
|
|
13051
|
-
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';
|
|
13052
14573
|
const isHelpCmd = !command || command === 'help' || command === '--help' || wantsHelp;
|
|
13053
14574
|
if (!wantsJson && !isHelpCmd && !isLifecycleCmd && !isListenerRunning()) {
|
|
13054
14575
|
try {
|
|
@@ -13071,7 +14592,7 @@ async function main() {
|
|
|
13071
14592
|
|
|
13072
14593
|
if (!command || command === 'help' || command === '--help') {
|
|
13073
14594
|
console.log('Usage: remits-cli <command>');
|
|
13074
|
-
console.log('Global options: --no-auto-update');
|
|
14595
|
+
console.log('Global options: --no-auto-update, --local-agent NAME');
|
|
13075
14596
|
console.log(' remits-cli auth [--base-url URL] [--account-id ID] [--port 8765] [--data-mode test|prod]');
|
|
13076
14597
|
console.log(' remits-cli sessions [list|remove] [--account-id ID] [--base-url URL] [--data-mode test|prod]');
|
|
13077
14598
|
console.log(' remits-cli config [set] [--agent claude|codex|gemini]');
|
|
@@ -13084,6 +14605,7 @@ async function main() {
|
|
|
13084
14605
|
console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
|
|
13085
14606
|
console.log(' remits-cli stop');
|
|
13086
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');
|
|
13087
14609
|
console.log(' remits-cli whoami [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]');
|
|
13088
14610
|
console.log(' remits-cli listen [stop|status] [--foreground true] # compatibility alias');
|
|
13089
14611
|
console.log(' remits-cli data-mode [set test|prod]');
|
|
@@ -13108,6 +14630,10 @@ async function main() {
|
|
|
13108
14630
|
console.log(' remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk');
|
|
13109
14631
|
console.log(' remits-cli components branch <name> --retire [--force] # delete the branch\'s overlays');
|
|
13110
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');
|
|
13111
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]');
|
|
13112
14638
|
console.log(' remits-cli token inspect --token <token|tokenKey|URL> [--base-url URL] [--account-id ID]');
|
|
13113
14639
|
console.log('');
|
|
@@ -13278,6 +14804,11 @@ async function main() {
|
|
|
13278
14804
|
return;
|
|
13279
14805
|
}
|
|
13280
14806
|
|
|
14807
|
+
if (command === 'doctor') {
|
|
14808
|
+
await doctorCommand(args, subcommand);
|
|
14809
|
+
return;
|
|
14810
|
+
}
|
|
14811
|
+
|
|
13281
14812
|
if (command === 'whoami') {
|
|
13282
14813
|
await whoamiCommand(args);
|
|
13283
14814
|
return;
|
|
@@ -13324,6 +14855,23 @@ async function main() {
|
|
|
13324
14855
|
await testStatusCommand(args);
|
|
13325
14856
|
return;
|
|
13326
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
|
+
}
|
|
13327
14875
|
|
|
13328
14876
|
if (command === 'token') {
|
|
13329
14877
|
await tokenCommand(args);
|