@remits/remits-cli 0.1.126 → 0.1.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/index.js +646 -50
- package/package.json +1 -1
- package/skills/remits-cli/references/cli-state.md +5 -0
- package/skills/remits-cli/references/command-reference.md +43 -8
- package/skills/remits-cli/references/component-resolution.md +14 -5
- package/skills/remits-cli/references/development-loop.md +20 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ remits-cli components stage # FULL SNAPSHOT of the repo into t
|
|
|
28
28
|
remits-cli components status
|
|
29
29
|
remits-cli components clear
|
|
30
30
|
remits-cli test run --test 45
|
|
31
|
-
remits-cli test run --test "My New Test" --names "test case 1
|
|
31
|
+
remits-cli test run --test "My New Test" --names "test case 1|test case 2"
|
|
32
32
|
git add -A
|
|
33
33
|
git commit -m "sync passing changes"
|
|
34
34
|
git push
|
|
@@ -98,7 +98,8 @@ remits-cli install --skills --overwrite true
|
|
|
98
98
|
- Branch defaults to the current local git branch.
|
|
99
99
|
- Data mode defaults to `test`. For `remits-cli test run`, that default is now enforced even if the most-recent authenticated session for the account is `prod`; a production test run therefore requires an explicit `--data-mode prod` on the command line. Use `remits-cli data-mode set prod` only for production investigation.
|
|
100
100
|
- If the same account is authenticated against more than one host and you omit `--base-url`, the CLI auto-resolves the best matching session and now prints the resolved host. Pass `--base-url` explicitly whenever the target host matters.
|
|
101
|
-
-
|
|
101
|
+
- `--names` is `|`-delimited and may be repeated. A comma still splits a single value for compatibility,
|
|
102
|
+
so a case name containing a comma should be passed with `|` or by repeating `--names`.
|
|
102
103
|
- Nested help is available before required-argument validation, including `remits-cli test run --help`, `remits-cli components sync --help`, and `remits-cli tool --help`.
|
|
103
104
|
- `components sync --safe` is the recommended agent path on a variant branch. It expands to `--summary --changed-only --fail-on-errors --fail-on-removed`, resolves `--changed-since` from the branch's merge base with trunk when you did not name one (local refs only; it never runs an implicit `git fetch`), and prints the planned writes before mutating unless `--yes` is passed. On trunk there is no plan to gate, so it states what a trunk reconcile does and requires `--yes`.
|
|
104
105
|
- `components sync` has fail-closed safety gates for unattended/agent use. Each exits non-zero instead of printing a wall of JSON: `--changed-only` (fail unless every planned write is a component this checkout edited), `--names-only` (print only `BUCKET type:id name` lines), `--fail-on-removed`, `--fail-on-errors`, and `--expected-removed <type:id>` (repeatable or comma-delimited; implies `--fail-on-removed`, so any removal you did not name fails). `--changed-only` also fails closed when the checkout is not a git working tree, because "git could not answer" must never be read as "nothing changed".
|
package/index.js
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
7
|
- L116 Sessions, Account Resolution, And Production Guards
|
|
8
8
|
- L595 Local State, Workspaces, And Verification Evidence
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
9
|
+
- L1399 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L2057 Component Discovery And HTTP Logging
|
|
11
|
+
- L2549 Skill Delivery And TOC Resolution
|
|
12
|
+
- L2859 Auth And Component Staging
|
|
13
|
+
- L3457 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L5323 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L6562 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L7478 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L9573 Agent And Ticket Workflows
|
|
18
|
+
- L12631 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -811,6 +811,58 @@ function writeToolResponseFile(cwd, callId, responsePayload) {
|
|
|
811
811
|
return file;
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
+
function fileEvidence(pathname) {
|
|
815
|
+
if (!pathname || !fs.existsSync(pathname)) return {};
|
|
816
|
+
const body = fs.readFileSync(pathname);
|
|
817
|
+
return {
|
|
818
|
+
responseFile: pathname,
|
|
819
|
+
bytes: body.length,
|
|
820
|
+
sha256: sha256(body)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const LARGE_TOOL_RESPONSE_BYTES = 200 * 1024;
|
|
825
|
+
|
|
826
|
+
function formatToolResponseFile(pathname) {
|
|
827
|
+
const evidence = fileEvidence(pathname);
|
|
828
|
+
if (!evidence.bytes) return String(pathname || '');
|
|
829
|
+
return pathname + ' (' + evidence.bytes + ' bytes, sha256 ' + evidence.sha256.slice(0, 12) + ')';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function printToolResponseFileLine(label, pathname) {
|
|
833
|
+
console.log(label + ':', formatToolResponseFile(pathname));
|
|
834
|
+
const evidence = fileEvidence(pathname);
|
|
835
|
+
if (evidence.bytes && evidence.bytes >= LARGE_TOOL_RESPONSE_BYTES) {
|
|
836
|
+
console.log('Large response: inspect selectively with jq or targeted offset reads instead of opening the whole file.');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function boundedToolExcerpt(data = {}) {
|
|
841
|
+
const result = data.result && typeof data.result === 'object' ? data.result : {};
|
|
842
|
+
const candidates = [
|
|
843
|
+
data.message,
|
|
844
|
+
data.toolMessage,
|
|
845
|
+
data.error,
|
|
846
|
+
result.message,
|
|
847
|
+
result.error,
|
|
848
|
+
result.reason
|
|
849
|
+
].filter(Boolean).map((value) => String(value));
|
|
850
|
+
const excerpt = candidates.join(' | ').trim();
|
|
851
|
+
return excerpt ? excerpt.slice(0, 1200) : null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function toolEvidenceSummary(data = {}, responseFile) {
|
|
855
|
+
return Object.assign({
|
|
856
|
+
callId: data.callId,
|
|
857
|
+
name: data.name,
|
|
858
|
+
async: data.async,
|
|
859
|
+
status: data.status,
|
|
860
|
+
toolSuccess: data.toolSuccess,
|
|
861
|
+
threadGroupingId: data.threadGroupingId,
|
|
862
|
+
message: boundedToolExcerpt(data)
|
|
863
|
+
}, fileEvidence(responseFile));
|
|
864
|
+
}
|
|
865
|
+
|
|
814
866
|
function readToolResponseFile(cwd, callId) {
|
|
815
867
|
if (!callId) return null;
|
|
816
868
|
const paths = ensureLocalState(cwd);
|
|
@@ -946,6 +998,35 @@ function readLocalVerificationEnvelope(cwd, envelopeId) {
|
|
|
946
998
|
}
|
|
947
999
|
}
|
|
948
1000
|
|
|
1001
|
+
function readLocalVerificationPackets(cwd, envelopeId, filters = {}) {
|
|
1002
|
+
const paths = verificationPaths(cwd, envelopeId);
|
|
1003
|
+
const packets = [];
|
|
1004
|
+
if (fs.existsSync(paths.packetsDir)) {
|
|
1005
|
+
fs.readdirSync(paths.packetsDir)
|
|
1006
|
+
.filter((name) => name.endsWith('.json'))
|
|
1007
|
+
.forEach((name) => {
|
|
1008
|
+
try {
|
|
1009
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(paths.packetsDir, name), 'utf8'));
|
|
1010
|
+
if (parsed && typeof parsed === 'object') packets.push(parsed);
|
|
1011
|
+
} catch (_) {
|
|
1012
|
+
// Ignore one malformed local packet; the rest of the envelope remains useful.
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
if (!packets.length) {
|
|
1017
|
+
const envelope = readLocalVerificationEnvelope(cwd, envelopeId);
|
|
1018
|
+
if (envelope && Array.isArray(envelope.packets)) packets.push(...envelope.packets);
|
|
1019
|
+
}
|
|
1020
|
+
const type = filters.type || filters.packetType;
|
|
1021
|
+
const suite = filters.suite || filters.testName;
|
|
1022
|
+
const max = parsePositiveInt(filters.max, 50);
|
|
1023
|
+
const filtered = packets
|
|
1024
|
+
.filter((packet) => !type || packet.type === type)
|
|
1025
|
+
.filter((packet) => !suite || testRunSuiteNameFromPacket(packet) === String(suite))
|
|
1026
|
+
.sort((a, b) => Number(a.createdAt || 0) - Number(b.createdAt || 0));
|
|
1027
|
+
return filtered.slice(Math.max(0, filtered.length - max));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
949
1030
|
function writeLocalVerificationPacket(cwd, envelopeId, packet) {
|
|
950
1031
|
if (!envelopeId || !packet) return null;
|
|
951
1032
|
const paths = verificationPaths(cwd, envelopeId);
|
|
@@ -1067,11 +1148,13 @@ function printVerificationSummaryLine(env) {
|
|
|
1067
1148
|
const failures = env.currentFailureCount ? ' currentFailures=' + env.currentFailureCount : (env.failedCount ? ' failed=' + env.failedCount : '');
|
|
1068
1149
|
const required = ' required=' + (env.satisfiedCount || 0) + '/' + (env.requiredCount || 0);
|
|
1069
1150
|
const noManifest = env.noRequiredEvidence ? ' no-manifest' : '';
|
|
1151
|
+
const stale = env.staleCount ? ' stale=' + env.staleCount : '';
|
|
1152
|
+
const changed = env.requirementsChangedAfterEvidence ? ' requirements-changed' : '';
|
|
1070
1153
|
const lane = [env.branchName || 'unknown-branch', env.workspace ? 'ws:' + env.workspace : 'shared', env.dataMode || null, env.sourceLayer || null]
|
|
1071
1154
|
.filter(Boolean).join(' | ');
|
|
1072
|
-
console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + noManifest);
|
|
1155
|
+
console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + stale + noManifest + changed);
|
|
1073
1156
|
if (env.summary) console.log(' ' + env.summary);
|
|
1074
|
-
console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
|
|
1157
|
+
console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' lastPacket=' + formatTime(env.lastPacketAtMs || env.latestPacketAtMs) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
|
|
1075
1158
|
}
|
|
1076
1159
|
|
|
1077
1160
|
function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
@@ -1088,6 +1171,12 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
|
1088
1171
|
if (evaluation.failedCount) {
|
|
1089
1172
|
console.log('Failed packets:', evaluation.failedCount, 'current failures:', evaluation.currentFailureCount || 0);
|
|
1090
1173
|
}
|
|
1174
|
+
if (evaluation.requirementsChangedAfterEvidence) {
|
|
1175
|
+
console.log('Requirements changed after evidence was collected.');
|
|
1176
|
+
}
|
|
1177
|
+
if (Array.isArray(evaluation.stale) && evaluation.stale.length) {
|
|
1178
|
+
console.log('Stale packets:', evaluation.stale.length);
|
|
1179
|
+
}
|
|
1091
1180
|
const packets = Array.isArray(envelope.packets) ? envelope.packets : [];
|
|
1092
1181
|
console.log('Packets:', packets.length);
|
|
1093
1182
|
packets.slice(-20).forEach((packet) => {
|
|
@@ -1100,12 +1189,78 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
|
1100
1189
|
}
|
|
1101
1190
|
}
|
|
1102
1191
|
|
|
1192
|
+
function printAcceptanceWarnings(envelope) {
|
|
1193
|
+
const warnings = envelope && envelope.acceptance && Array.isArray(envelope.acceptance.warnings)
|
|
1194
|
+
? envelope.acceptance.warnings
|
|
1195
|
+
: [];
|
|
1196
|
+
if (!warnings.length) return;
|
|
1197
|
+
console.log('Manifest warnings:');
|
|
1198
|
+
warnings.forEach((warning) => {
|
|
1199
|
+
console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function printEnvelopeWarnings(envelope) {
|
|
1204
|
+
const warnings = envelope && Array.isArray(envelope.warnings) ? envelope.warnings : [];
|
|
1205
|
+
if (!warnings.length) return;
|
|
1206
|
+
console.log('Envelope warnings:');
|
|
1207
|
+
warnings.forEach((warning) => {
|
|
1208
|
+
console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
|
|
1209
|
+
if (Array.isArray(warning.envelopes) && warning.envelopes.length) {
|
|
1210
|
+
warning.envelopes.forEach((env) => {
|
|
1211
|
+
console.log(' ' + (env.envelopeId || '(no id)') + (env.status ? ' ' + env.status : ''));
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function manifestRequiredEvidence(manifest = {}) {
|
|
1218
|
+
const required = [];
|
|
1219
|
+
if (Array.isArray(manifest.requiredEvidence)) required.push(...manifest.requiredEvidence);
|
|
1220
|
+
const journeys = Array.isArray(manifest.journeys) ? manifest.journeys :
|
|
1221
|
+
(Array.isArray(manifest.requestedJourneys) ? manifest.requestedJourneys : []);
|
|
1222
|
+
journeys.forEach((journey) => {
|
|
1223
|
+
if (journey && Array.isArray(journey.requiredEvidence)) required.push(...journey.requiredEvidence);
|
|
1224
|
+
});
|
|
1225
|
+
return required;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function readVerificationEnvelopeForDiagnostics(api, cwd, session, accountId, envelopeId) {
|
|
1229
|
+
if (!envelopeId) return null;
|
|
1230
|
+
try {
|
|
1231
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'show', { envelopeId });
|
|
1232
|
+
if (response.envelope) {
|
|
1233
|
+
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
1234
|
+
return response.envelope;
|
|
1235
|
+
}
|
|
1236
|
+
} catch (_) {
|
|
1237
|
+
// Local evidence is still useful for diagnostics when the platform read fails.
|
|
1238
|
+
}
|
|
1239
|
+
return readLocalVerificationEnvelope(cwd, envelopeId);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function readVerificationPacketsForDiagnostics(api, cwd, session, accountId, envelopeId, filters = {}) {
|
|
1243
|
+
if (!envelopeId) return [];
|
|
1244
|
+
try {
|
|
1245
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'packets', Object.assign({ envelopeId }, filters));
|
|
1246
|
+
if (Array.isArray(response.packets)) return response.packets;
|
|
1247
|
+
} catch (_) {
|
|
1248
|
+
// Local full-fidelity packet files remain the fallback when the platform read is unavailable.
|
|
1249
|
+
}
|
|
1250
|
+
return readLocalVerificationPackets(cwd, envelopeId, filters);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1103
1253
|
function buildCommandWorld(response, fallback = {}) {
|
|
1104
1254
|
const source = (response && response.staging) || {};
|
|
1105
1255
|
const resolution = (response && response.resolution) || {};
|
|
1256
|
+
const result = (response && response.result) || {};
|
|
1257
|
+
const laneHash = laneContentHashFrom(response) || fallback.laneContentHash || fallback.stagedOverlayHash;
|
|
1258
|
+
const executionAccountId = response && (response.executionAccountId || result.accountId || source.executionAccountId || response.accountId) ||
|
|
1259
|
+
resolution.executionAccountId ||
|
|
1260
|
+
fallback.executionAccountId;
|
|
1106
1261
|
return {
|
|
1107
1262
|
accountId: response && (response.accountId || response.ownerAccountId) || fallback.accountId,
|
|
1108
|
-
executionAccountId
|
|
1263
|
+
executionAccountId,
|
|
1109
1264
|
addressedAccountId: resolution.addressedAccountId || fallback.addressedAccountId,
|
|
1110
1265
|
host: fallback.host,
|
|
1111
1266
|
dataMode: response && response.dataMode || fallback.dataMode,
|
|
@@ -1113,16 +1268,26 @@ function buildCommandWorld(response, fallback = {}) {
|
|
|
1113
1268
|
branchName: response && response.branchName || fallback.branchName,
|
|
1114
1269
|
workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
|
|
1115
1270
|
stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
|
|
1271
|
+
laneContentHash: laneHash,
|
|
1272
|
+
stagedOverlayHash: laneHash,
|
|
1116
1273
|
// `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
|
|
1117
1274
|
// subscription); a bare `variantBranch` is null whenever the subscription decided.
|
|
1118
1275
|
componentBranch: resolution.componentBranch ||
|
|
1119
1276
|
response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
|
|
1120
1277
|
fallback.componentBranch,
|
|
1278
|
+
variantBranchSource: response && (response.variantBranchSource || (response.variantWorld && response.variantWorld.variantBranchSource)) || fallback.variantBranchSource,
|
|
1121
1279
|
variantId: resolution.variantId || fallback.variantId,
|
|
1122
1280
|
sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
|
|
1123
1281
|
};
|
|
1124
1282
|
}
|
|
1125
1283
|
|
|
1284
|
+
function laneContentHashFrom(response) {
|
|
1285
|
+
if (!response) return null;
|
|
1286
|
+
const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
|
|
1287
|
+
(response.componentStatus && response.componentStatus.laneSummary);
|
|
1288
|
+
return laneSummary && laneSummary.contentHash ? laneSummary.contentHash : null;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1126
1291
|
function normalizeName(name) {
|
|
1127
1292
|
return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
|
|
1128
1293
|
}
|
|
@@ -2333,21 +2498,23 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2333
2498
|
error = err;
|
|
2334
2499
|
throw err;
|
|
2335
2500
|
} finally {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2501
|
+
if (!options.skipSessionLog) {
|
|
2502
|
+
const status = error ? 'error' : 'success';
|
|
2503
|
+
const responseForLog = responseFile
|
|
2504
|
+
? { responseFile, note: 'Tool response stored externally' }
|
|
2505
|
+
: sanitizeForLog(responseData);
|
|
2506
|
+
appendSessionLog(cwd, {
|
|
2507
|
+
ts: new Date().toISOString(),
|
|
2508
|
+
requestId,
|
|
2509
|
+
endpoint,
|
|
2510
|
+
method: 'POST',
|
|
2511
|
+
status,
|
|
2512
|
+
durationMs: Date.now() - started,
|
|
2513
|
+
request: sanitizeForLog(payload),
|
|
2514
|
+
response: responseForLog,
|
|
2515
|
+
error: error ? describeError(error) : null
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2351
2518
|
}
|
|
2352
2519
|
}
|
|
2353
2520
|
|
|
@@ -3057,6 +3224,7 @@ async function pushComponentsCommand(flags) {
|
|
|
3057
3224
|
rawRefs: { command: 'components stage' },
|
|
3058
3225
|
stage: (err.responseBody || {}).stage,
|
|
3059
3226
|
compileValidation: (err.responseBody || {}).compileValidation,
|
|
3227
|
+
refusalDetails: stageRefusalDetails(err.responseBody || {}),
|
|
3060
3228
|
changedFromWorkingTree: changedFromWorkingTree || [],
|
|
3061
3229
|
unrepresentableChanges: unstageable
|
|
3062
3230
|
}, { quiet: true });
|
|
@@ -3091,6 +3259,7 @@ async function pushComponentsCommand(flags) {
|
|
|
3091
3259
|
rawRefs: { command: 'components stage' },
|
|
3092
3260
|
stage: response.stage,
|
|
3093
3261
|
laneSummary: response.laneSummary,
|
|
3262
|
+
compileValidation: response.compileValidation || null,
|
|
3094
3263
|
changedFromWorkingTree: response.changedFromWorkingTree,
|
|
3095
3264
|
unrepresentableChanges: response.unrepresentableChanges
|
|
3096
3265
|
}, { quiet: flagEnabled(flags.json) });
|
|
@@ -3276,6 +3445,14 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
3276
3445
|
}
|
|
3277
3446
|
}
|
|
3278
3447
|
|
|
3448
|
+
function stageRefusalDetails(body = {}) {
|
|
3449
|
+
const details = {};
|
|
3450
|
+
['duplicates', 'compileValidation', 'policy', 'editLease', 'skipped', 'reconcile', 'laneHoldsRejectedSource'].forEach((key) => {
|
|
3451
|
+
if (body[key] !== undefined) details[key] = sanitizeForLog(body[key]);
|
|
3452
|
+
});
|
|
3453
|
+
return Object.keys(details).length ? details : undefined;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3279
3456
|
/*
|
|
3280
3457
|
## Component Summaries, Status, And Sync Gates
|
|
3281
3458
|
*/
|
|
@@ -3331,6 +3508,10 @@ function printCompileValidation(validation, options) {
|
|
|
3331
3508
|
// A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
|
|
3332
3509
|
// component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
|
|
3333
3510
|
const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
|
|
3511
|
+
const warnings = Array.isArray(validation.warnings) ? validation.warnings : [];
|
|
3512
|
+
warnings.forEach((warning) => {
|
|
3513
|
+
emit(' Warning: ' + (warning.message || JSON.stringify(warning)));
|
|
3514
|
+
});
|
|
3334
3515
|
if (unchecked.length) {
|
|
3335
3516
|
emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
|
|
3336
3517
|
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
|
|
@@ -3495,6 +3676,9 @@ function printStageSummary(response, flags) {
|
|
|
3495
3676
|
// The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
|
|
3496
3677
|
// different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
|
|
3497
3678
|
printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
|
|
3679
|
+
if (typeof printOrphanedWorkspaceWarning === 'function') {
|
|
3680
|
+
printOrphanedWorkspaceWarning(response);
|
|
3681
|
+
}
|
|
3498
3682
|
if (typeof printAccountLanes === 'function') {
|
|
3499
3683
|
printAccountLanes(response);
|
|
3500
3684
|
}
|
|
@@ -3519,7 +3703,9 @@ function describeStageMode(mode) {
|
|
|
3519
3703
|
function printStatusSummary(response, flags) {
|
|
3520
3704
|
printBranchContext(response);
|
|
3521
3705
|
printStagingLane(response.branchName, response.workspace, null);
|
|
3706
|
+
printStagingLaneOwnerNotice(response);
|
|
3522
3707
|
printLaneSummary(response);
|
|
3708
|
+
printOrphanedWorkspaceWarning(response);
|
|
3523
3709
|
printStagingFreshness(response.freshness);
|
|
3524
3710
|
printRepositoryCheck(response.repositoryCheck);
|
|
3525
3711
|
printComponentTypeCounts(response.entries || []);
|
|
@@ -3545,6 +3731,28 @@ function printStatusSummary(response, flags) {
|
|
|
3545
3731
|
printComponentCommandResponse('Components staging', response, flags);
|
|
3546
3732
|
}
|
|
3547
3733
|
|
|
3734
|
+
function stagingLaneOwnerNotice(response = {}) {
|
|
3735
|
+
const owner = response.laneOwnerAccountId != null ? response.laneOwnerAccountId :
|
|
3736
|
+
(response.staging && response.staging.laneOwnerAccountId);
|
|
3737
|
+
const checkout = response.checkoutAccountId != null ? response.checkoutAccountId :
|
|
3738
|
+
(response.accountId != null ? response.accountId : (response.staging && response.staging.checkoutAccountId));
|
|
3739
|
+
const stagedCount = Number(response.stagedCount != null ? response.stagedCount :
|
|
3740
|
+
(response.laneSummary && response.laneSummary.stagedCount != null ? response.laneSummary.stagedCount :
|
|
3741
|
+
(response.staging && response.staging.stagedCount != null ? response.staging.stagedCount :
|
|
3742
|
+
(response.staging && response.staging.laneSummary && response.staging.laneSummary.stagedCount))));
|
|
3743
|
+
if (owner == null || checkout == null || String(owner) === String(checkout) || !(stagedCount > 0)) {
|
|
3744
|
+
return [];
|
|
3745
|
+
}
|
|
3746
|
+
return [
|
|
3747
|
+
'WARNING: staged entries shown here are under account ' + owner + ', not checkout account ' + checkout + '.',
|
|
3748
|
+
' clear with: remits-cli components clear --all --account-id ' + owner
|
|
3749
|
+
];
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function printStagingLaneOwnerNotice(response = {}) {
|
|
3753
|
+
stagingLaneOwnerNotice(response).forEach((line) => console.log(line));
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3548
3756
|
function printLanesSummary(response, flags) {
|
|
3549
3757
|
printBranchContext(response);
|
|
3550
3758
|
const lanes = Array.isArray(response.lanes) ? response.lanes : (Array.isArray(response.accountLanes) ? response.accountLanes : []);
|
|
@@ -3678,6 +3886,36 @@ function printAccountLanes(response) {
|
|
|
3678
3886
|
console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
|
|
3679
3887
|
}
|
|
3680
3888
|
|
|
3889
|
+
function printOrphanedWorkspaceWarning(response) {
|
|
3890
|
+
const currentCount = Number((response && response.stagedCount) ||
|
|
3891
|
+
(response && response.laneSummary && response.laneSummary.stagedCount) || 0);
|
|
3892
|
+
if (currentCount > 0) return;
|
|
3893
|
+
const lanes = Array.isArray(response && response.accountLanes) ? response.accountLanes : [];
|
|
3894
|
+
const currentBranch = response && response.branchName;
|
|
3895
|
+
const currentWorkspace = response && response.workspace;
|
|
3896
|
+
const orphan = lanes.find((lane) => lane && lane.sameWorkspaceOtherBranch === true);
|
|
3897
|
+
if (!orphan) return;
|
|
3898
|
+
const age = orphan.updatedAtMs ? formatAge(Date.now() - Number(orphan.updatedAtMs)) + ' ago' : 'recently';
|
|
3899
|
+
console.log('');
|
|
3900
|
+
console.log('WARNING: Your workspace "' + (currentWorkspace || orphan.workspace || 'shared') + '" has ' +
|
|
3901
|
+
(orphan.stagedCountAsOf || orphan.stagedCount || 0) + ' staged entr' +
|
|
3902
|
+
((orphan.stagedCountAsOf || orphan.stagedCount || 0) === 1 ? 'y' : 'ies') +
|
|
3903
|
+
' on branch "' + orphan.branchName + '" (staged ' + age + '); this checkout is now on "' +
|
|
3904
|
+
(currentBranch || 'unknown') + '". Re-stage here, or switch back.');
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
function formatAge(ms) {
|
|
3908
|
+
const value = Number(ms);
|
|
3909
|
+
if (!Number.isFinite(value) || value < 0) return 'unknown age';
|
|
3910
|
+
const seconds = Math.round(value / 1000);
|
|
3911
|
+
if (seconds < 90) return seconds + 's';
|
|
3912
|
+
const minutes = Math.round(seconds / 60);
|
|
3913
|
+
if (minutes < 90) return minutes + 'm';
|
|
3914
|
+
const hours = Math.round(minutes / 60);
|
|
3915
|
+
if (hours < 48) return hours + 'h';
|
|
3916
|
+
return Math.round(hours / 24) + 'd';
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3681
3919
|
// The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
|
|
3682
3920
|
// resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
|
|
3683
3921
|
// non-trunk — never guessed to be a variant branch. Pure.
|
|
@@ -3846,6 +4084,7 @@ async function statusComponentsCommand(flags) {
|
|
|
3846
4084
|
// supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
|
|
3847
4085
|
response.freshness = stagingFreshness({
|
|
3848
4086
|
head: safeGitValue(cwd, 'git rev-parse HEAD'),
|
|
4087
|
+
originHead: safeGitValue(cwd, 'git rev-parse ' + shellQuote('origin/' + branchName)),
|
|
3849
4088
|
lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
|
|
3850
4089
|
entries: response.entries,
|
|
3851
4090
|
laneSummary: response.laneSummary,
|
|
@@ -3888,19 +4127,25 @@ async function lanesComponentsCommand(flags) {
|
|
|
3888
4127
|
const dataMode = resolveDataMode(flags, session);
|
|
3889
4128
|
const api = buildAxios(baseUrl, session.token);
|
|
3890
4129
|
|
|
3891
|
-
const
|
|
4130
|
+
const requestPayload = {
|
|
3892
4131
|
token: session.token,
|
|
3893
4132
|
accountId,
|
|
3894
4133
|
branchName,
|
|
3895
4134
|
workspace,
|
|
3896
4135
|
dataMode,
|
|
3897
4136
|
mode: 'lanes'
|
|
3898
|
-
}
|
|
4137
|
+
};
|
|
4138
|
+
|
|
4139
|
+
let response = await loggedPost(api, cwd, '/cli/components', requestPayload).then((r) => r.data);
|
|
3899
4140
|
|
|
3900
4141
|
if (!response.success) {
|
|
3901
4142
|
throw new Error(response.message || 'Staging lanes failed');
|
|
3902
4143
|
}
|
|
3903
4144
|
|
|
4145
|
+
if (flagEnabled(flags['wait-change']) || flagEnabled(flags.waitChange)) {
|
|
4146
|
+
response = await waitForLaneChange(api, cwd, requestPayload, response, flags);
|
|
4147
|
+
}
|
|
4148
|
+
|
|
3904
4149
|
if (flagEnabled(flags.json)) {
|
|
3905
4150
|
console.log(JSON.stringify(response, null, 2));
|
|
3906
4151
|
return response;
|
|
@@ -3913,6 +4158,55 @@ async function lanesComponentsCommand(flags) {
|
|
|
3913
4158
|
return response;
|
|
3914
4159
|
}
|
|
3915
4160
|
|
|
4161
|
+
function laneWaitSignature(response, laneId) {
|
|
4162
|
+
const lanes = Array.isArray(response && response.lanes)
|
|
4163
|
+
? response.lanes
|
|
4164
|
+
: (Array.isArray(response && response.accountLanes) ? response.accountLanes : []);
|
|
4165
|
+
const selected = laneId ? lanes.filter((lane) => String(lane.laneId || '') === String(laneId)) : lanes;
|
|
4166
|
+
return stableStringify(selected.map((lane) => ({
|
|
4167
|
+
laneId: lane.laneId,
|
|
4168
|
+
accountId: lane.accountId,
|
|
4169
|
+
branchName: lane.branchName,
|
|
4170
|
+
workspace: lane.workspace || null,
|
|
4171
|
+
stagedCount: lane.stagedCountAsOf || lane.stagedCount || 0,
|
|
4172
|
+
contentHash: lane.contentHash || (lane.laneSummary && lane.laneSummary.contentHash) || null,
|
|
4173
|
+
updatedAtMs: lane.updatedAtMs || null
|
|
4174
|
+
})));
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
async function waitForLaneChange(api, cwd, requestPayload, initialResponse, flags) {
|
|
4178
|
+
const laneId = flags['lane-id'] || flags.laneId || flags.lane;
|
|
4179
|
+
const timeoutSeconds = parsePositiveInt(flags.timeout || flags['timeout-seconds'] || flags.timeoutSeconds, 600);
|
|
4180
|
+
const pollMs = parsePositiveInt(flags['poll-ms'] || flags.pollMs, 2000);
|
|
4181
|
+
const started = Date.now();
|
|
4182
|
+
const initialSignature = laneWaitSignature(initialResponse, laneId);
|
|
4183
|
+
if (!flagEnabled(flags.json)) {
|
|
4184
|
+
console.log('Waiting for staging lane change' + (laneId ? ' on ' + laneId : '') + ' for up to ' + timeoutSeconds + 's...');
|
|
4185
|
+
}
|
|
4186
|
+
while (Date.now() - started < timeoutSeconds * 1000) {
|
|
4187
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
4188
|
+
const next = await loggedPost(api, cwd, '/cli/components', requestPayload, { skipSessionLog: true }).then((r) => r.data);
|
|
4189
|
+
if (!next.success) {
|
|
4190
|
+
throw new Error(next.message || 'Staging lanes failed while waiting for a change');
|
|
4191
|
+
}
|
|
4192
|
+
if (laneWaitSignature(next, laneId) !== initialSignature) {
|
|
4193
|
+
next.waitChange = {
|
|
4194
|
+
changed: true,
|
|
4195
|
+
laneId: laneId || null,
|
|
4196
|
+
waitedMs: Date.now() - started
|
|
4197
|
+
};
|
|
4198
|
+
return next;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
initialResponse.waitChange = {
|
|
4202
|
+
changed: false,
|
|
4203
|
+
laneId: laneId || null,
|
|
4204
|
+
waitedMs: Date.now() - started,
|
|
4205
|
+
timeoutSeconds
|
|
4206
|
+
};
|
|
4207
|
+
return initialResponse;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
3916
4210
|
async function entriesComponentsCommand(flags) {
|
|
3917
4211
|
const cwd = process.cwd();
|
|
3918
4212
|
ensureLocalState(cwd);
|
|
@@ -4108,6 +4402,30 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4108
4402
|
}
|
|
4109
4403
|
throw new Error(mismatch + ' Nothing was synced.');
|
|
4110
4404
|
}
|
|
4405
|
+
const featureRefusal = !namesOnly ? featureBranchSyncRefusal(branchContext, branchName, flags) : null;
|
|
4406
|
+
if (featureRefusal) {
|
|
4407
|
+
if (flagEnabled(flags.json)) {
|
|
4408
|
+
const refusal = {
|
|
4409
|
+
success: false,
|
|
4410
|
+
mode: 'sync',
|
|
4411
|
+
dataMode,
|
|
4412
|
+
accountId,
|
|
4413
|
+
branchName,
|
|
4414
|
+
workspace,
|
|
4415
|
+
dryRun,
|
|
4416
|
+
safe,
|
|
4417
|
+
branchContext,
|
|
4418
|
+
repositoryCheck: preflightRepoCheck,
|
|
4419
|
+
refusal: 'feature_branch_landing',
|
|
4420
|
+
gateViolations: [featureRefusal],
|
|
4421
|
+
message: featureRefusal
|
|
4422
|
+
};
|
|
4423
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4424
|
+
process.exitCode = 1;
|
|
4425
|
+
return refusal;
|
|
4426
|
+
}
|
|
4427
|
+
throw new Error(featureRefusal + ' Nothing was synced.');
|
|
4428
|
+
}
|
|
4111
4429
|
if (branchContextIsTrunk(branchContext)) {
|
|
4112
4430
|
// Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
|
|
4113
4431
|
// an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
|
|
@@ -4670,6 +4988,15 @@ function featureBranchCommitRefusal(branchContext, branchName, flags) {
|
|
|
4670
4988
|
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
4671
4989
|
}
|
|
4672
4990
|
|
|
4991
|
+
function featureBranchSyncRefusal(branchContext, branchName, flags) {
|
|
4992
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
4993
|
+
if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
|
|
4994
|
+
return 'components sync refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
|
|
4995
|
+
'subscribers). ' + featureBranchLandingHint(branchContext) + ' Syncing here would write overlays for \'' +
|
|
4996
|
+
branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
|
|
4997
|
+
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
4998
|
+
}
|
|
4999
|
+
|
|
4673
5000
|
// What a verification envelope records as the world it verified. Taken from the platform's answer
|
|
4674
5001
|
// (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
|
|
4675
5002
|
// variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
|
|
@@ -4759,7 +5086,7 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
4759
5086
|
* `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
|
|
4760
5087
|
* injected as `isAncestor(a, b)` — so it can be tested without a repository.
|
|
4761
5088
|
*/
|
|
4762
|
-
function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
5089
|
+
function stagingFreshness({ head, originHead, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
4763
5090
|
const rows = Array.isArray(entries) ? entries : [];
|
|
4764
5091
|
const staged = rows.filter((entry) => entry && entry.stageBaseSha);
|
|
4765
5092
|
const memo = new Map();
|
|
@@ -4785,7 +5112,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
|
|
|
4785
5112
|
|
|
4786
5113
|
return {
|
|
4787
5114
|
head: head || null,
|
|
5115
|
+
originHead: originHead || null,
|
|
4788
5116
|
lastSyncedSha: lastSyncedSha || null,
|
|
5117
|
+
freshnessUnknownReason: !originHead ? 'branch has no upstream' : (!lastSyncedSha ? 'platform has no last synced SHA for this branch' : null),
|
|
4789
5118
|
landedSinceHead,
|
|
4790
5119
|
entriesBehindLastSync: behindLastSync.length,
|
|
4791
5120
|
entriesFromOtherCommits: fromOtherCommits.length,
|
|
@@ -4799,6 +5128,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
|
|
|
4799
5128
|
function printStagingFreshness(freshness) {
|
|
4800
5129
|
if (!freshness) return;
|
|
4801
5130
|
const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
|
|
5131
|
+
if (freshness.freshnessUnknownReason) {
|
|
5132
|
+
console.log('Freshness unknown:', freshness.freshnessUnknownReason + '.');
|
|
5133
|
+
}
|
|
4802
5134
|
if (freshness.landedSinceHead === true) {
|
|
4803
5135
|
console.log('');
|
|
4804
5136
|
console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
|
|
@@ -5759,6 +6091,172 @@ async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dat
|
|
|
5759
6091
|
}
|
|
5760
6092
|
}
|
|
5761
6093
|
|
|
6094
|
+
function testEvidenceCategories(status = {}, selectedNames = []) {
|
|
6095
|
+
const categories = new Set(['test_run']);
|
|
6096
|
+
const test = status.test || {};
|
|
6097
|
+
if (test.name) categories.add('test_run.suite:' + String(test.name));
|
|
6098
|
+
if (test.id !== undefined && test.id !== null) categories.add('test_run.suite_id:' + String(test.id));
|
|
6099
|
+
(Array.isArray(selectedNames) ? selectedNames : []).forEach((name) => {
|
|
6100
|
+
if (name) categories.add('test_run.' + String(name));
|
|
6101
|
+
});
|
|
6102
|
+
const results = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
6103
|
+
results.forEach((result) => {
|
|
6104
|
+
if (result && result.passed === true && result.name) {
|
|
6105
|
+
categories.add('test_run.case:' + String(result.name));
|
|
6106
|
+
categories.add('test_run.' + String(result.name));
|
|
6107
|
+
}
|
|
6108
|
+
});
|
|
6109
|
+
return Array.from(categories);
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6112
|
+
function testComponentProvenance(status = {}) {
|
|
6113
|
+
const direct = status.resolvedComponentProvenance || (status.result && status.result.resolvedComponentProvenance) ||
|
|
6114
|
+
(status.staging && status.staging.resolvedComponentProvenance);
|
|
6115
|
+
if (Array.isArray(direct)) return direct;
|
|
6116
|
+
const staging = status.staging || {};
|
|
6117
|
+
if (staging.testComponentSignature) {
|
|
6118
|
+
return [{
|
|
6119
|
+
type: 'test',
|
|
6120
|
+
id: status.test && status.test.id,
|
|
6121
|
+
name: status.test && status.test.name,
|
|
6122
|
+
signature: staging.testComponentSignature,
|
|
6123
|
+
source: staging.testComponentSource || null
|
|
6124
|
+
}];
|
|
6125
|
+
}
|
|
6126
|
+
return [];
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
function testCompileSignatures(status = {}) {
|
|
6130
|
+
return testComponentProvenance(status)
|
|
6131
|
+
.map((entry) => entry && entry.signature)
|
|
6132
|
+
.filter(Boolean);
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
function testRunSuiteNameFromStatus(status = {}) {
|
|
6136
|
+
return status.test && status.test.name ? String(status.test.name) : null;
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
function testRunSuiteNameFromPacket(packet = {}) {
|
|
6140
|
+
return packet.test && packet.test.testName
|
|
6141
|
+
? String(packet.test.testName)
|
|
6142
|
+
: (packet.status && packet.status.test && packet.status.test.name ? String(packet.status.test.name) : null);
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
function testCaseOutcomesFromStatusLike(status = {}) {
|
|
6146
|
+
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests :
|
|
6147
|
+
(status.test && Array.isArray(status.test.tests) ? status.test.tests : []);
|
|
6148
|
+
return tests
|
|
6149
|
+
.filter((test) => test && test.name && typeof test.passed === 'boolean')
|
|
6150
|
+
.map((test) => ({ name: String(test.name), passed: test.passed === true }));
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6153
|
+
function provenanceSignature(value) {
|
|
6154
|
+
const rows = Array.isArray(value) ? value : [];
|
|
6155
|
+
return sha256(stableStringify(rows.map((entry) => ({
|
|
6156
|
+
type: entry && entry.type || null,
|
|
6157
|
+
id: entry && entry.id || null,
|
|
6158
|
+
name: entry && entry.name || null,
|
|
6159
|
+
signature: entry && entry.signature || null,
|
|
6160
|
+
source: entry && entry.source || null
|
|
6161
|
+
}))));
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
function packetEvidenceIdentity(packet = {}) {
|
|
6165
|
+
const world = packet.world || {};
|
|
6166
|
+
const revision = packet.revision || {};
|
|
6167
|
+
const laneSummary = packet.laneSummary || (packet.componentStatus && packet.componentStatus.laneSummary) || {};
|
|
6168
|
+
return {
|
|
6169
|
+
laneContentHash: world.laneContentHash || world.stagedOverlayHash || revision.laneContentHash ||
|
|
6170
|
+
revision.stagedOverlayHash || laneSummary.contentHash || null,
|
|
6171
|
+
gitHead: revision.gitHead || null
|
|
6172
|
+
};
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
function sameNondeterminismIdentity(left = {}, right = {}) {
|
|
6176
|
+
return !!(left.laneContentHash && right.laneContentHash && left.gitHead && right.gitHead &&
|
|
6177
|
+
String(left.laneContentHash) === String(right.laneContentHash) &&
|
|
6178
|
+
String(left.gitHead) === String(right.gitHead));
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
function detectNondeterministicTestRun(priorPackets, currentStatus, currentProvenance, currentIdentity = {}) {
|
|
6182
|
+
const packets = Array.isArray(priorPackets) ? priorPackets : [];
|
|
6183
|
+
const suiteName = testRunSuiteNameFromStatus(currentStatus);
|
|
6184
|
+
const currentCases = testCaseOutcomesFromStatusLike(currentStatus);
|
|
6185
|
+
if (!suiteName || !currentCases.length) return null;
|
|
6186
|
+
if (!Array.isArray(currentProvenance) || !currentProvenance.length) return null;
|
|
6187
|
+
if (!currentIdentity.laneContentHash || !currentIdentity.gitHead) return null;
|
|
6188
|
+
|
|
6189
|
+
const currentSignature = provenanceSignature(currentProvenance);
|
|
6190
|
+
const currentByName = new Map(currentCases.map((outcome) => [outcome.name, outcome]));
|
|
6191
|
+
for (let i = packets.length - 1; i >= 0; i -= 1) {
|
|
6192
|
+
const packet = packets[i];
|
|
6193
|
+
if (!packet || packet.type !== 'test_run') continue;
|
|
6194
|
+
if (testRunSuiteNameFromPacket(packet) !== suiteName) continue;
|
|
6195
|
+
const previousProvenance = packet.revision && packet.revision.componentProvenance;
|
|
6196
|
+
if (!Array.isArray(previousProvenance) || !previousProvenance.length) continue;
|
|
6197
|
+
if (!sameNondeterminismIdentity(currentIdentity, packetEvidenceIdentity(packet))) continue;
|
|
6198
|
+
const previousSignature = provenanceSignature(previousProvenance);
|
|
6199
|
+
if (previousSignature !== currentSignature) continue;
|
|
6200
|
+
const previousCases = testCaseOutcomesFromStatusLike(packet.status || packet);
|
|
6201
|
+
const flips = previousCases
|
|
6202
|
+
.map((previous) => {
|
|
6203
|
+
const current = currentByName.get(previous.name);
|
|
6204
|
+
return current && current.passed !== previous.passed
|
|
6205
|
+
? { caseName: previous.name, previousPassed: previous.passed, currentPassed: current.passed, previousPacketId: packet.packetId }
|
|
6206
|
+
: null;
|
|
6207
|
+
})
|
|
6208
|
+
.filter(Boolean);
|
|
6209
|
+
if (flips.length) {
|
|
6210
|
+
return {
|
|
6211
|
+
nondeterministic: true,
|
|
6212
|
+
message: 'Outcome changed with no source change - likely nondeterministic (AI/live data).',
|
|
6213
|
+
suite: suiteName,
|
|
6214
|
+
provenanceSignature: currentSignature,
|
|
6215
|
+
previousPacketId: packet.packetId,
|
|
6216
|
+
flips
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
return null;
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
function printTestRunPivots(status = {}) {
|
|
6224
|
+
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
6225
|
+
if (!tests.length) return;
|
|
6226
|
+
const slowest = tests.slice().sort((a, b) => Number(b.duration || 0) - Number(a.duration || 0))[0];
|
|
6227
|
+
if (slowest && slowest.duration != null) {
|
|
6228
|
+
console.log('Slowest case:', (slowest.name || '(unnamed)') + ' in ' + formatDurationMs(slowest.duration));
|
|
6229
|
+
}
|
|
6230
|
+
tests.filter((test) => test && test.passed === false).forEach((test) => {
|
|
6231
|
+
console.log('Pivots for failed case:', test.name || '(unnamed)');
|
|
6232
|
+
if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
|
|
6233
|
+
if (test.threadGroupingId || test.threadGroupId || test.traceId) {
|
|
6234
|
+
console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
|
|
6235
|
+
}
|
|
6236
|
+
if (test.error) console.log(' Error:', String(test.error).slice(0, 500));
|
|
6237
|
+
const diagnostics = test.diagnostics && typeof test.diagnostics === 'object' ? test.diagnostics : null;
|
|
6238
|
+
if (diagnostics && Object.keys(diagnostics).length) {
|
|
6239
|
+
console.log(' Diagnostics:', JSON.stringify(diagnostics).slice(0, 1200));
|
|
6240
|
+
}
|
|
6241
|
+
if (Array.isArray(test.resolvedComponentProvenance) && test.resolvedComponentProvenance.length) {
|
|
6242
|
+
console.log(' Components:', test.resolvedComponentProvenance.slice(0, 8).map((entry) =>
|
|
6243
|
+
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') +
|
|
6244
|
+
(entry.source ? ' [' + entry.source + ']' : '') +
|
|
6245
|
+
(entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
|
|
6246
|
+
}
|
|
6247
|
+
if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
|
|
6248
|
+
console.log(' Live HTTP calls:', test.liveHttpCalls.length);
|
|
6249
|
+
}
|
|
6250
|
+
});
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
function formatDurationMs(value) {
|
|
6254
|
+
const ms = Number(value);
|
|
6255
|
+
if (!Number.isFinite(ms)) return String(value);
|
|
6256
|
+
if (ms < 1000) return ms + 'ms';
|
|
6257
|
+
return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
|
|
6258
|
+
}
|
|
6259
|
+
|
|
5762
6260
|
async function waitForToolStatus(api, cwd, options) {
|
|
5763
6261
|
const started = Date.now();
|
|
5764
6262
|
let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
|
|
@@ -5908,7 +6406,9 @@ async function testCommand(flags) {
|
|
|
5908
6406
|
printSessionResolutionWarning(sessionContext);
|
|
5909
6407
|
printResolvedBaseUrl(baseUrl);
|
|
5910
6408
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
6409
|
+
printStagingLaneOwnerNotice(start.staging || {});
|
|
5911
6410
|
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
6411
|
+
printOrphanedWorkspaceWarning(start.staging);
|
|
5912
6412
|
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
5913
6413
|
}
|
|
5914
6414
|
console.log('Test run started:', start.taskId);
|
|
@@ -5935,6 +6435,8 @@ async function testCommand(flags) {
|
|
|
5935
6435
|
console.log(JSON.stringify(status, null, 2));
|
|
5936
6436
|
} else {
|
|
5937
6437
|
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
6438
|
+
printStagingLaneOwnerNotice(status.staging || {});
|
|
6439
|
+
printTestRunPivots(status);
|
|
5938
6440
|
}
|
|
5939
6441
|
|
|
5940
6442
|
// A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
|
|
@@ -5959,14 +6461,38 @@ async function testCommand(flags) {
|
|
|
5959
6461
|
process.exitCode = 1;
|
|
5960
6462
|
}
|
|
5961
6463
|
|
|
6464
|
+
const verificationContext = activeVerificationContext(cwd, flags, session, accountId);
|
|
6465
|
+
const activeEnvelopeId = verificationEnvelopeIdForCommand(cwd, flags, verificationContext);
|
|
6466
|
+
const componentProvenance = testComponentProvenance(status);
|
|
6467
|
+
const sourceRevision = collectVerificationSource(cwd, flags);
|
|
6468
|
+
const priorPackets = await readVerificationPacketsForDiagnostics(api, cwd, session, accountId, activeEnvelopeId, {
|
|
6469
|
+
packetType: 'test_run',
|
|
6470
|
+
suite: status.test && status.test.name,
|
|
6471
|
+
max: 50
|
|
6472
|
+
});
|
|
6473
|
+
const nondeterminism = detectNondeterministicTestRun(priorPackets, status, componentProvenance, {
|
|
6474
|
+
laneContentHash: status.staging && status.staging.laneSummary && status.staging.laneSummary.contentHash,
|
|
6475
|
+
gitHead: sourceRevision.gitHead
|
|
6476
|
+
});
|
|
6477
|
+
if (nondeterminism && !jsonOutput) {
|
|
6478
|
+
console.log('Nondeterministic signal:', nondeterminism.message);
|
|
6479
|
+
nondeterminism.flips.slice(0, 6).forEach((flip) => {
|
|
6480
|
+
console.log(' - ' + flip.caseName + ': ' + (flip.previousPassed ? 'passed' : 'failed') + ' -> ' + (flip.currentPassed ? 'passed' : 'failed') +
|
|
6481
|
+
' (previous packet ' + (flip.previousPacketId || 'unknown') + ')');
|
|
6482
|
+
});
|
|
6483
|
+
}
|
|
6484
|
+
|
|
5962
6485
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
5963
6486
|
type: 'test_run',
|
|
5964
6487
|
success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
|
|
5965
6488
|
claim: 'Test run ' + String(testRef),
|
|
5966
6489
|
world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
|
|
5967
|
-
revision: Object.assign(
|
|
5968
|
-
compileSignatures: status
|
|
6490
|
+
revision: Object.assign(sourceRevision, {
|
|
6491
|
+
compileSignatures: testCompileSignatures(status),
|
|
6492
|
+
componentProvenance
|
|
5969
6493
|
}),
|
|
6494
|
+
nondeterministic: nondeterminism ? true : undefined,
|
|
6495
|
+
nondeterminism: nondeterminism || undefined,
|
|
5970
6496
|
test: {
|
|
5971
6497
|
taskId: start.taskId,
|
|
5972
6498
|
testId: status.test && status.test.id,
|
|
@@ -5974,16 +6500,64 @@ async function testCommand(flags) {
|
|
|
5974
6500
|
selectedCases: names,
|
|
5975
6501
|
passed: status.result && status.result.passed,
|
|
5976
6502
|
failed: status.result && status.result.failed,
|
|
6503
|
+
tests: status.result && status.result.tests,
|
|
5977
6504
|
dataModeSource: status.dataModeSource || dataModeSource,
|
|
5978
6505
|
unmatchedTestNames: unmatched
|
|
5979
6506
|
},
|
|
5980
|
-
evidenceCategories:
|
|
5981
|
-
limitations:
|
|
6507
|
+
evidenceCategories: testEvidenceCategories(status, names),
|
|
6508
|
+
limitations: []
|
|
6509
|
+
.concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
|
|
6510
|
+
.concat(nondeterminism ? [nondeterminism.message] : []),
|
|
5982
6511
|
rawRefs: { testStatusKey: start.taskId },
|
|
5983
6512
|
status
|
|
5984
6513
|
}, { quiet: jsonOutput });
|
|
5985
6514
|
}
|
|
5986
6515
|
|
|
6516
|
+
async function testStatusCommand(flags) {
|
|
6517
|
+
const cwd = process.cwd();
|
|
6518
|
+
ensureLocalState(cwd);
|
|
6519
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
6520
|
+
const { session, accountId } = sessionContext;
|
|
6521
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
6522
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
6523
|
+
const dataMode = hasExplicitDataModeFlag(flags)
|
|
6524
|
+
? resolveDataMode(flags, null)
|
|
6525
|
+
: DEFAULT_DATA_MODE;
|
|
6526
|
+
const taskId = flags['task-id'] || flags.taskId || flags.id || (flags._ && flags._[2]);
|
|
6527
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
6528
|
+
if (!taskId) throw new Error('Missing --task-id <taskId>');
|
|
6529
|
+
|
|
6530
|
+
const api = buildAxios(baseUrl, session.token);
|
|
6531
|
+
const status = await loggedPost(api, cwd, '/cli/test', {
|
|
6532
|
+
token: session.token,
|
|
6533
|
+
command: 'status',
|
|
6534
|
+
accountId,
|
|
6535
|
+
branchName,
|
|
6536
|
+
taskId,
|
|
6537
|
+
dataMode
|
|
6538
|
+
}).then((r) => r.data);
|
|
6539
|
+
|
|
6540
|
+
if (jsonOutput) {
|
|
6541
|
+
console.log(JSON.stringify(status, null, 2));
|
|
6542
|
+
return status;
|
|
6543
|
+
}
|
|
6544
|
+
printSessionResolutionWarning(sessionContext);
|
|
6545
|
+
printResolvedBaseUrl(baseUrl);
|
|
6546
|
+
console.log('Test run status:', status.status || 'unknown');
|
|
6547
|
+
console.log('Task ID:', taskId);
|
|
6548
|
+
console.log('Data mode:', status.dataMode || dataMode);
|
|
6549
|
+
if (status.result) {
|
|
6550
|
+
console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
|
|
6551
|
+
}
|
|
6552
|
+
if (status.error || status.message) {
|
|
6553
|
+
console.log('Message:', status.error || status.message);
|
|
6554
|
+
}
|
|
6555
|
+
if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
|
|
6556
|
+
process.exitCode = 1;
|
|
6557
|
+
}
|
|
6558
|
+
return status;
|
|
6559
|
+
}
|
|
6560
|
+
|
|
5987
6561
|
/*
|
|
5988
6562
|
## Tokens, Tools, Verification, And Config
|
|
5989
6563
|
*/
|
|
@@ -6308,8 +6882,7 @@ async function toolCommand(flags) {
|
|
|
6308
6882
|
revision: collectVerificationSource(cwd, flags),
|
|
6309
6883
|
evidenceCategories: ['tool_call'],
|
|
6310
6884
|
rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
|
|
6311
|
-
tool: { callId: requestedCallId,
|
|
6312
|
-
result: data
|
|
6885
|
+
tool: Object.assign(toolEvidenceSummary(data, statusResponse.responseFile), { callId: requestedCallId, name: stored && stored.name })
|
|
6313
6886
|
}, { quiet: jsonOutput });
|
|
6314
6887
|
if (jsonOutput) {
|
|
6315
6888
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -6322,7 +6895,7 @@ async function toolCommand(flags) {
|
|
|
6322
6895
|
console.log('Data mode:', data.dataMode || dataMode);
|
|
6323
6896
|
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6324
6897
|
console.log('Session log:', data.sessionLog);
|
|
6325
|
-
|
|
6898
|
+
printToolResponseFileLine('Tool response file', data.responseFile);
|
|
6326
6899
|
if (polledFailed) {
|
|
6327
6900
|
console.log('Tool error:', data.toolMessage);
|
|
6328
6901
|
}
|
|
@@ -6373,6 +6946,9 @@ async function toolCommand(flags) {
|
|
|
6373
6946
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
6374
6947
|
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
6375
6948
|
|
|
6949
|
+
let evidenceData = data;
|
|
6950
|
+
let evidenceResponseFile = response.responseFile;
|
|
6951
|
+
|
|
6376
6952
|
if (!jsonOutput) {
|
|
6377
6953
|
printSessionResolutionWarning(sessionContext);
|
|
6378
6954
|
printResolvedBaseUrl(baseUrl);
|
|
@@ -6390,7 +6966,7 @@ async function toolCommand(flags) {
|
|
|
6390
6966
|
console.log('Component:', `${data.componentSource}${sig}`);
|
|
6391
6967
|
}
|
|
6392
6968
|
console.log('Session log:', data.sessionLog);
|
|
6393
|
-
|
|
6969
|
+
printToolResponseFileLine('Tool response file', data.responseFile);
|
|
6394
6970
|
}
|
|
6395
6971
|
|
|
6396
6972
|
// Non-zero exit so scripted/agent callers that check status notice the refusal too.
|
|
@@ -6410,9 +6986,11 @@ async function toolCommand(flags) {
|
|
|
6410
6986
|
|
|
6411
6987
|
finalStatus.data.responseFile = finalStatus.responseFile;
|
|
6412
6988
|
finalStatus.data.sessionLog = sessionJsonlFile(cwd);
|
|
6989
|
+
evidenceData = finalStatus.data;
|
|
6990
|
+
evidenceResponseFile = finalStatus.responseFile;
|
|
6413
6991
|
if (!jsonOutput) {
|
|
6414
6992
|
console.log('Final status:', finalStatus.data.status);
|
|
6415
|
-
|
|
6993
|
+
printToolResponseFileLine('Tool response file', finalStatus.responseFile);
|
|
6416
6994
|
}
|
|
6417
6995
|
if (finalStatus.data.status !== 'completed') {
|
|
6418
6996
|
process.exitCode = 1;
|
|
@@ -6422,20 +7000,20 @@ async function toolCommand(flags) {
|
|
|
6422
7000
|
|
|
6423
7001
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
6424
7002
|
type: 'tool_call',
|
|
6425
|
-
success: !toolFailed && (!asyncMode ||
|
|
7003
|
+
success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
|
|
7004
|
+
pending: asyncMode && !waitForAsync,
|
|
6426
7005
|
claim: 'Tool call ' + String(toolName),
|
|
6427
|
-
world: buildCommandWorld(
|
|
7006
|
+
world: buildCommandWorld(evidenceData, { accountId: evidenceData.accountId || accountId, dataMode: evidenceData.dataMode || dataMode, branchName: evidenceData.branchName || branchName, workspace: resolveWorkspace(cwd, flags), host: normalizeBaseUrl(baseUrl), sourceLayer: evidenceData.componentSource }),
|
|
6428
7007
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
6429
|
-
compileSignatures:
|
|
7008
|
+
compileSignatures: evidenceData.componentSignature ? [evidenceData.componentSignature] : []
|
|
6430
7009
|
}),
|
|
6431
7010
|
evidenceCategories: ['tool_call'],
|
|
6432
7011
|
dependencies: {
|
|
6433
7012
|
mocks: [],
|
|
6434
|
-
liveHttpCalls:
|
|
7013
|
+
liveHttpCalls: evidenceData.liveHttpCalls || []
|
|
6435
7014
|
},
|
|
6436
|
-
rawRefs: { toolResponsePath:
|
|
6437
|
-
tool: { callId, name: String(toolName), async: asyncMode
|
|
6438
|
-
result: data
|
|
7015
|
+
rawRefs: { toolResponsePath: evidenceResponseFile, callId },
|
|
7016
|
+
tool: Object.assign(toolEvidenceSummary(evidenceData, evidenceResponseFile), { callId, name: String(toolName), async: asyncMode })
|
|
6439
7017
|
}, { quiet: jsonOutput });
|
|
6440
7018
|
if (jsonOutput) {
|
|
6441
7019
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -6504,6 +7082,12 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6504
7082
|
if (flags.manifest || flags.file) {
|
|
6505
7083
|
manifest = parseManifestFile(flags.manifest || flags.file);
|
|
6506
7084
|
}
|
|
7085
|
+
if (dataMode === 'prod' && !manifestRequiredEvidence(manifest).length && !flagEnabled(flags['no-contract'])) {
|
|
7086
|
+
console.error('');
|
|
7087
|
+
console.error('Warning: starting a prod-data verification envelope with no required evidence contract.');
|
|
7088
|
+
console.error('Attach a manifest with requiredEvidence before destructive work, or pass --no-contract when this is intentionally evidence-only.');
|
|
7089
|
+
console.error('');
|
|
7090
|
+
}
|
|
6507
7091
|
let statusResponse = null;
|
|
6508
7092
|
try {
|
|
6509
7093
|
statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
@@ -6522,7 +7106,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6522
7106
|
source.componentBranch = verifiedWorld.componentBranch;
|
|
6523
7107
|
source.sourceLayer = verifiedWorld.sourceLayer;
|
|
6524
7108
|
if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
|
|
6525
|
-
source.
|
|
7109
|
+
source.laneContentHash = laneContentHashFrom(statusResponse);
|
|
7110
|
+
source.stagedOverlayHash = source.laneContentHash;
|
|
6526
7111
|
}
|
|
6527
7112
|
|
|
6528
7113
|
const response = await postVerificationCommand(api, cwd, session, accountId, 'start', {
|
|
@@ -6537,6 +7122,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6537
7122
|
writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
|
|
6538
7123
|
console.log('Verification envelope started:', envelope.envelopeId);
|
|
6539
7124
|
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
7125
|
+
printAcceptanceWarnings(envelope);
|
|
7126
|
+
printEnvelopeWarnings(envelope);
|
|
6540
7127
|
console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
|
|
6541
7128
|
return envelope;
|
|
6542
7129
|
}
|
|
@@ -6577,6 +7164,8 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6577
7164
|
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
6578
7165
|
console.log('Manifest attached to envelope:', envelopeId);
|
|
6579
7166
|
console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
|
|
7167
|
+
printAcceptanceWarnings(response.envelope);
|
|
7168
|
+
printEnvelopeWarnings(response.envelope);
|
|
6580
7169
|
return response.envelope;
|
|
6581
7170
|
}
|
|
6582
7171
|
|
|
@@ -12269,11 +12858,12 @@ function printComponentsHelp(subcommand) {
|
|
|
12269
12858
|
return;
|
|
12270
12859
|
}
|
|
12271
12860
|
if (subcommand === 'lanes' || subcommand === 'lane') {
|
|
12272
|
-
console.log('Usage: remits-cli components lanes [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--json]');
|
|
12861
|
+
console.log('Usage: remits-cli components lanes [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
|
|
12273
12862
|
console.log('');
|
|
12274
12863
|
console.log('Lists every indexed staging lane on the account, across users, branches, and workspaces.');
|
|
12275
12864
|
console.log('This is read-only and uses the same lane registry that powers the admin Platforms view.');
|
|
12276
12865
|
console.log('Use `components entries --lane-id <id>` to inspect the actual staged files in a lane.');
|
|
12866
|
+
console.log('Use --wait-change to wait until the lane registry changes instead of polling in a loop.');
|
|
12277
12867
|
return;
|
|
12278
12868
|
}
|
|
12279
12869
|
if (subcommand === 'entries' || subcommand === 'entry' || subcommand === 'staged') {
|
|
@@ -12312,7 +12902,7 @@ function printComponentsHelp(subcommand) {
|
|
|
12312
12902
|
console.log('so several agents can iterate at once. See: remits-cli workspace --help');
|
|
12313
12903
|
console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
12314
12904
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
12315
|
-
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--json]');
|
|
12905
|
+
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
|
|
12316
12906
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
12317
12907
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
12318
12908
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
|
@@ -12337,11 +12927,13 @@ function printComponentsHelp(subcommand) {
|
|
|
12337
12927
|
|
|
12338
12928
|
function printTestHelp() {
|
|
12339
12929
|
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
|
+
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
12340
12931
|
console.log('');
|
|
12341
12932
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
12342
12933
|
console.log('Examples:');
|
|
12343
12934
|
console.log(' remits-cli test run --test 11');
|
|
12344
12935
|
console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
|
|
12936
|
+
console.log(' remits-cli test status --task-id 7a1b...');
|
|
12345
12937
|
console.log('');
|
|
12346
12938
|
console.log('Notes:');
|
|
12347
12939
|
console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
|
|
@@ -12728,6 +13320,10 @@ async function main() {
|
|
|
12728
13320
|
await testCommand(args);
|
|
12729
13321
|
return;
|
|
12730
13322
|
}
|
|
13323
|
+
if (command === 'test' && subcommand === 'status') {
|
|
13324
|
+
await testStatusCommand(args);
|
|
13325
|
+
return;
|
|
13326
|
+
}
|
|
12731
13327
|
|
|
12732
13328
|
if (command === 'token') {
|
|
12733
13329
|
await tokenCommand(args);
|
package/package.json
CHANGED
|
@@ -72,6 +72,11 @@ Think about remits-cli as two cooperating layers:
|
|
|
72
72
|
- where a large tool response was written
|
|
73
73
|
- which tool schemas were most recently cached here
|
|
74
74
|
|
|
75
|
+
Tool response files under `./.remits-cli/tool-responses/` are the full CLI results. The CLI may print a
|
|
76
|
+
size/hash hint for large files so you can inspect them selectively with `jq`, `rg`, or byte-range reads,
|
|
77
|
+
but it does not truncate the local JSON file. Do not confuse this with the in-platform OpenRouter client,
|
|
78
|
+
which can offload large tool results inside the model conversation and inject a read-back tool for the AI.
|
|
79
|
+
|
|
75
80
|
When a user asks an indirect question, map it to the right layer first:
|
|
76
81
|
|
|
77
82
|
- "Why did this ticket open in the wrong repo?" → start in global state.
|
|
@@ -85,7 +85,10 @@ deterministically:
|
|
|
85
85
|
remits-cli tool --name "mcp_run_action" --input '{"accountId":49,"actionId":200,"executionMode":"async","actionRunId":"my-stable-run-id","actionInput":{"sourceDocumentId":"..."}}' --data-mode prod
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Every tool response is saved to `./.remits-cli/tool-responses/<callId>.json`.
|
|
88
|
+
Every tool response is saved in full to `./.remits-cli/tool-responses/<callId>.json`. Large responses may
|
|
89
|
+
print an early size/hash line and a selective-read hint before any preview text, but the saved CLI file is
|
|
90
|
+
not truncated or offloaded. That is separate from the platform's OpenRouter model loop, where the AI
|
|
91
|
+
client may offload large tool results before feeding context back to the model.
|
|
89
92
|
|
|
90
93
|
### Hierarchy-scoped tool reads
|
|
91
94
|
|
|
@@ -207,6 +210,7 @@ remits-cli components branch <name> --subscribe <accountId> [--parent-account <i
|
|
|
207
210
|
remits-cli components branch <name> --unsubscribe <accountId> # return that account to trunk
|
|
208
211
|
remits-cli components branch <name> --retire [--force] # delete the branch's overlays
|
|
209
212
|
remits-cli test run --test <id|name> [--branch <stagingScope>] [--names "a|b"] [--watch true|false] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>] [--json]
|
|
213
|
+
remits-cli test status --task-id <taskId> [--branch <stagingScope>] [--data-mode test|prod] [--json]
|
|
210
214
|
remits-cli token [--path <embeddablePathOrId>] [--data-mode test|prod] [--as-account <ID>] [--variant-branch <name|none>]
|
|
211
215
|
remits-cli token inspect --token <token|tokenKey|URL> # inspect token metadata, safety/dataMode evidence, and full context
|
|
212
216
|
remits-cli tools [--branch <name>] [--data-mode test|prod] [--variant-branch <name|none>]
|
|
@@ -268,18 +272,43 @@ remits-cli verify report
|
|
|
268
272
|
Existing commands also accept `--verify-envelope <id>` to attach to a specific envelope and
|
|
269
273
|
`--no-verify-envelope` to suppress automatic attachment for one command.
|
|
270
274
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
275
|
+
Failed `remits-cli test run` output includes compact pivots per failed case: duration, trace id,
|
|
276
|
+
bounded `report(...)` diagnostics, live HTTP signals, and resolved component provenance when the
|
|
277
|
+
platform returns it. Re-read an existing run with `remits-cli test status --task-id <taskId>` before
|
|
278
|
+
rerunning a long suite. If the same suite/case flips pass/fail while the resolved component provenance
|
|
279
|
+
signature is unchanged, the CLI prints a nondeterministic signal and records `nondeterministic:true` on
|
|
280
|
+
the `test_run` packet. Treat that as an AI/live-data/flaky-fixture investigation cue, not as proof that
|
|
281
|
+
source changed.
|
|
282
|
+
|
|
283
|
+
Test-run requirements must name the suite/cases they mean:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{"id":"api_flow", "packetType":"test_run", "suite":"Statement API Flow", "allCases":true}
|
|
287
|
+
{"id":"one_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A suite-only Test requirement is treated as `allCases:true`. A category-only Test requirement matches only
|
|
291
|
+
a run carrying that exact evidence category, or the case whose name the category names; unrelated Test
|
|
292
|
+
runs cannot replace it. `verify start` and `verify manifest` warn when a requirement has a `packetType` but
|
|
293
|
+
no discriminator. The evaluator treats pending async tool packets as pending, not passing; a later
|
|
294
|
+
`tool status` packet is the proof. Tool packets store the response file path, byte size and hash rather
|
|
295
|
+
than copying the whole tool payload into the envelope.
|
|
296
|
+
|
|
297
|
+
Use `remits-cli verify list` to review open envelopes for the account without SQL. The list shows stale
|
|
298
|
+
counts, requirements-changed markers, and last packet time. `verify start` warns when another open envelope
|
|
299
|
+
with the same summary/account/world exists; use `verify use <id>` to continue that contract or
|
|
300
|
+
`verify supersede` when a newer envelope replaces it. If a duplicate or abandoned attempt should no longer
|
|
301
|
+
read as live work, use `verify abandon`. Both keep the evidence history. Starting a prod-data envelope
|
|
302
|
+
without required evidence prints a loud no-contract warning; add a manifest unless the envelope is
|
|
303
|
+
intentionally evidence-only.
|
|
274
304
|
|
|
275
305
|
The manifest is the proof contract. It should name the world being exercised: repo account, host,
|
|
276
306
|
git/component branch, workspace, data mode, source layer, user journeys, artifacts and hashes, and the
|
|
277
307
|
evidence categories required before the final response may claim the work is done. If the manifest
|
|
278
308
|
cannot be written because the workflow is ambiguous, ask before implementation. For machine-gated report
|
|
279
309
|
requirements, prefer structured entries like `{"id":"actual_upload","packetType":"browser_step",
|
|
280
|
-
"category":"browser_session.actual_upload"}`. Plain strings work
|
|
281
|
-
|
|
282
|
-
envelope.
|
|
310
|
+
"category":"browser_session.actual_upload"}`. Plain strings work for evidence categories, but do not use
|
|
311
|
+
plain `"test_run"` as acceptance: it cannot say which Test or case proved the behavior.
|
|
283
312
|
|
|
284
313
|
Common packet meanings:
|
|
285
314
|
|
|
@@ -300,7 +329,13 @@ Final claims should come from `remits-cli verify report`. Treat `Verified` as th
|
|
|
300
329
|
the report lists it under `Verified`. If the report says `partially_verified`, stale, or missing evidence,
|
|
301
330
|
say that plainly instead of widening the claim. In particular, staged proof is not committed variant/trunk
|
|
302
331
|
proof, a token is not browser proof, and a direct DOM or Alpine state mutation is not the same as a user
|
|
303
|
-
|
|
332
|
+
action.
|
|
333
|
+
|
|
334
|
+
Evidence from the wrong world is excluded before it can satisfy a requirement. When the manifest declares
|
|
335
|
+
fields such as `repoAccountId`, `gitBranch`, `componentBranch`, `workspace`, or `dataMode`, the report names
|
|
336
|
+
the mismatch (`expected X got Y`) under missing evidence. Later stage/sync/source facts can stale earlier
|
|
337
|
+
packets; explicit invalidations stale only packets collected before the invalidation, so a fresh rerun can
|
|
338
|
+
verify the same requirement again.
|
|
304
339
|
|
|
305
340
|
For tests specifically:
|
|
306
341
|
- If `--data-mode` is omitted, `remits-cli test run` uses `test` and sends `dataModeSource:"cliDefault"`.
|
|
@@ -57,9 +57,10 @@ source-layer facts the command observed. A staged test packet is proof of the st
|
|
|
57
57
|
is proof of a source transition; a token packet is proof of the browser token's resolution tuple. Those
|
|
58
58
|
are not interchangeable.
|
|
59
59
|
|
|
60
|
-
Use `remits-cli verify report` before summarizing the work. It will
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
Use `remits-cli verify report` before summarizing the work. It will say when the evidence only covered
|
|
61
|
+
staged source, when committed variant/trunk proof is missing, when the manifest world does not match the
|
|
62
|
+
packet world, or when later packet facts made an earlier proof stale. Packets carry a lane content hash,
|
|
63
|
+
git head, workspace, data lane, and variant-world facts so a re-stage or branch move is visible.
|
|
63
64
|
|
|
64
65
|
### Staging cache key format
|
|
65
66
|
|
|
@@ -159,7 +160,10 @@ TestMode still uses the committed DB source. Staging remains a dev/verification
|
|
|
159
160
|
- **Inspect another lane without impersonating it:** `remits-cli components lanes` lists every indexed
|
|
160
161
|
lane on the account; `remits-cli components entries --lane-id <id>` reads the authoritative staged
|
|
161
162
|
files for one lane. These are read-only review surfaces. They do not switch your workspace, clear
|
|
162
|
-
anything, or change what your own test/token/tool runs resolve.
|
|
163
|
+
anything, or change what your own test/token/tool runs resolve. Use
|
|
164
|
+
`remits-cli components lanes --wait-change [--lane-id ID] [--timeout 600]` when you need to wait for a
|
|
165
|
+
sibling lane to move; it watches the lane registry's `updatedAtMs`/content hash instead of making you
|
|
166
|
+
poll in a loop.
|
|
163
167
|
- **Clear only your own lane:** `remits-cli components clear` removes entries when you intentionally want
|
|
164
168
|
to fall back to DB source. `--all` is scoped to the command's account/user/branch/workspace lane, not
|
|
165
169
|
every lane another agent may be using.
|
|
@@ -208,10 +212,15 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
|
|
|
208
212
|
account's lane context in their responses. A shared current lane is printed loudly; sibling lanes are
|
|
209
213
|
listed when they matter.
|
|
210
214
|
- `remits-cli components lanes` is the review view across users/branches/workspaces, and
|
|
211
|
-
`remits-cli components entries --lane-id <id>` is the drill-down into actual staged files.
|
|
215
|
+
`remits-cli components entries --lane-id <id>` is the drill-down into actual staged files. When you are
|
|
216
|
+
coordinating with another agent, prefer `components lanes --wait-change --lane-id <id>` to repeated
|
|
217
|
+
status checks.
|
|
212
218
|
- `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
|
|
213
219
|
- In lane rows, `currentLane` (also `mine`) marks THIS command's lane; `ownedByCaller` marks every lane
|
|
214
220
|
staged by your CLI user — your other clones' agents included.
|
|
221
|
+
- If your current lane is empty but the same workspace has staged entries on another branch, the CLI
|
|
222
|
+
prints a warning. That usually means the checkout switched branches after staging; re-stage on this
|
|
223
|
+
branch or switch back.
|
|
215
224
|
- **A landing clears only the lander's lane.** Every stage records the commit it came from
|
|
216
225
|
(`stageBaseSha`), and `components status` compares that with the last commit the platform synced for the
|
|
217
226
|
branch (`branchContext.lastSyncedSha`) using your local git:
|
|
@@ -97,6 +97,21 @@ remits-cli verify report
|
|
|
97
97
|
The report is the final-response source. It separates verified claims, missing evidence, stale packets,
|
|
98
98
|
and the source/account/lane tuple, so do not replace it with a generic "verified" sentence.
|
|
99
99
|
|
|
100
|
+
For Test requirements, be specific enough for the evaluator to know what a pass means:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{"id":"api_flow", "packetType":"test_run", "suite":"Statement API Flow", "allCases":true}
|
|
104
|
+
{"id":"duplicate_fee_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A suite-only Test requirement is treated as `allCases:true`. A category-only Test requirement matches only
|
|
108
|
+
a run carrying that exact evidence category, or the case whose name the category names. A requirement that
|
|
109
|
+
says only `{"packetType":"test_run"}` is intentionally only a warning-worthy sketch: it will not turn a
|
|
110
|
+
random Test packet into acceptance. Full-suite runs emit suite and passed-case categories automatically,
|
|
111
|
+
so case-level requirements can be satisfied by a real full run. The evaluator excludes packets collected
|
|
112
|
+
in the wrong account/data lane/git branch/component branch/workspace before satisfying requirements, and
|
|
113
|
+
pending async packets do not count until a result packet arrives.
|
|
114
|
+
|
|
100
115
|
## Development Workflow
|
|
101
116
|
|
|
102
117
|
### The Golden Rule: Writing Code Is Not Finishing the Job
|
|
@@ -327,9 +342,13 @@ This applies to:
|
|
|
327
342
|
```bash
|
|
328
343
|
remits-cli test run --test <TEST_ID_OR_NAME>
|
|
329
344
|
remits-cli test run --test "Invoice Tests" --names "specific test case"
|
|
345
|
+
remits-cli test status --task-id <TASK_ID>
|
|
330
346
|
```
|
|
331
347
|
|
|
332
|
-
Tests run on the platform against your staged snapshot. They stream results in real-time. If they fail,
|
|
348
|
+
Tests run on the platform against your staged snapshot. They stream results in real-time. If they fail,
|
|
349
|
+
read the printed pivots first: failed cases include timing, trace ids, bounded `report(...)`
|
|
350
|
+
diagnostics, live HTTP signals, and resolved component provenance when available. Fix the code,
|
|
351
|
+
re-stage, and re-run only after those pivots explain the failure.
|
|
333
352
|
|
|
334
353
|
Important test-runner constraints:
|
|
335
354
|
- `remits-cli test run` now defaults to `test` dataMode unless you explicitly pass `--data-mode prod`.
|