@remits/remits-cli 0.1.127 → 0.1.129
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 +524 -66
- package/package.json +1 -1
- package/skills/remits-cli/references/branch-variants.md +3 -1
- package/skills/remits-cli/references/cli-state.md +5 -0
- package/skills/remits-cli/references/command-reference.md +34 -16
- package/skills/remits-cli/references/component-resolution.md +7 -2
- package/skills/remits-cli/references/development-loop.md +7 -3
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
|
+
- L1448 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L2106 Component Discovery And HTTP Logging
|
|
11
|
+
- L2598 Skill Delivery And TOC Resolution
|
|
12
|
+
- L2908 Auth And Component Staging
|
|
13
|
+
- L3506 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L5457 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L6682 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L7598 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L9693 Agent And Ticket Workflows
|
|
18
|
+
- L12751 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -821,6 +821,22 @@ function fileEvidence(pathname) {
|
|
|
821
821
|
};
|
|
822
822
|
}
|
|
823
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
|
+
|
|
824
840
|
function boundedToolExcerpt(data = {}) {
|
|
825
841
|
const result = data.result && typeof data.result === 'object' ? data.result : {};
|
|
826
842
|
const candidates = [
|
|
@@ -982,6 +998,35 @@ function readLocalVerificationEnvelope(cwd, envelopeId) {
|
|
|
982
998
|
}
|
|
983
999
|
}
|
|
984
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
|
+
|
|
985
1030
|
function writeLocalVerificationPacket(cwd, envelopeId, packet) {
|
|
986
1031
|
if (!envelopeId || !packet) return null;
|
|
987
1032
|
const paths = verificationPaths(cwd, envelopeId);
|
|
@@ -1062,6 +1107,43 @@ async function postVerificationCommand(api, cwd, session, accountId, command, pa
|
|
|
1062
1107
|
return response;
|
|
1063
1108
|
}
|
|
1064
1109
|
|
|
1110
|
+
// The repo/session account a packet was recorded from, stated explicitly in its world. `world.accountId` is the
|
|
1111
|
+
// account the command EXECUTED as, so an --as-account run carried the subscriber there and a manifest's
|
|
1112
|
+
// repoAccountId could never match it. Only fills a missing value; a packet that already knows keeps its own.
|
|
1113
|
+
function stampPacketRepoAccount(packet, accountId) {
|
|
1114
|
+
if (!packet || accountId === undefined || accountId === null || accountId === '') return packet;
|
|
1115
|
+
const repoAccountId = Number(accountId);
|
|
1116
|
+
if (!Number.isFinite(repoAccountId)) return packet;
|
|
1117
|
+
if (!packet.world || typeof packet.world !== 'object') packet.world = {};
|
|
1118
|
+
if (packet.world.repoAccountId === undefined || packet.world.repoAccountId === null) {
|
|
1119
|
+
packet.world.repoAccountId = repoAccountId;
|
|
1120
|
+
}
|
|
1121
|
+
return packet;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// A sync packet is evidence of WHAT a sync planned or wrote, not a second copy of the preview. The per-field
|
|
1125
|
+
// trunk/branch text a dry run attaches for the admin diff panel is up to 400 KB per sync, and storing it on
|
|
1126
|
+
// every packet made one busy envelope 9.4 MB — slow enough that `verify show` timed out. Field NAMES
|
|
1127
|
+
// (`changedFields`) stay; the full plan is always one `components sync --dry-run` away.
|
|
1128
|
+
function syncEvidencePayload(sync) {
|
|
1129
|
+
if (!sync || typeof sync !== 'object') return sync;
|
|
1130
|
+
const results = sync.syncResults;
|
|
1131
|
+
if (!results || typeof results !== 'object') return sync;
|
|
1132
|
+
const trimmedResults = {};
|
|
1133
|
+
Object.keys(results).forEach((bucket) => {
|
|
1134
|
+
const entries = results[bucket];
|
|
1135
|
+
trimmedResults[bucket] = Array.isArray(entries)
|
|
1136
|
+
? entries.map((entry) => {
|
|
1137
|
+
if (!entry || typeof entry !== 'object' || !('fields' in entry)) return entry;
|
|
1138
|
+
const copy = Object.assign({}, entry);
|
|
1139
|
+
delete copy.fields;
|
|
1140
|
+
return copy;
|
|
1141
|
+
})
|
|
1142
|
+
: entries;
|
|
1143
|
+
});
|
|
1144
|
+
return Object.assign({}, sync, { syncResults: trimmedResults });
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1065
1147
|
async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
|
|
1066
1148
|
const context = activeVerificationContext(cwd, flags, session, accountId);
|
|
1067
1149
|
const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
|
|
@@ -1072,6 +1154,7 @@ async function appendVerificationPacket(api, cwd, session, accountId, flags, pac
|
|
|
1072
1154
|
success: true
|
|
1073
1155
|
}, packet || {});
|
|
1074
1156
|
finalPacket.envelopeId = envelopeId;
|
|
1157
|
+
stampPacketRepoAccount(finalPacket, accountId);
|
|
1075
1158
|
writeLocalVerificationPacket(cwd, envelopeId, finalPacket);
|
|
1076
1159
|
try {
|
|
1077
1160
|
const response = await postVerificationCommand(api, cwd, session, accountId, 'packet', {
|
|
@@ -1103,11 +1186,13 @@ function printVerificationSummaryLine(env) {
|
|
|
1103
1186
|
const failures = env.currentFailureCount ? ' currentFailures=' + env.currentFailureCount : (env.failedCount ? ' failed=' + env.failedCount : '');
|
|
1104
1187
|
const required = ' required=' + (env.satisfiedCount || 0) + '/' + (env.requiredCount || 0);
|
|
1105
1188
|
const noManifest = env.noRequiredEvidence ? ' no-manifest' : '';
|
|
1189
|
+
const stale = env.staleCount ? ' stale=' + env.staleCount : '';
|
|
1190
|
+
const changed = env.requirementsChangedAfterEvidence ? ' requirements-changed' : '';
|
|
1106
1191
|
const lane = [env.branchName || 'unknown-branch', env.workspace ? 'ws:' + env.workspace : 'shared', env.dataMode || null, env.sourceLayer || null]
|
|
1107
1192
|
.filter(Boolean).join(' | ');
|
|
1108
|
-
console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + noManifest);
|
|
1193
|
+
console.log('- ' + (env.envelopeId || '(no id)') + ' ' + (env.status || 'unknown') + health + failures + required + stale + noManifest + changed);
|
|
1109
1194
|
if (env.summary) console.log(' ' + env.summary);
|
|
1110
|
-
console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
|
|
1195
|
+
console.log(' ' + lane + ' packets=' + (env.packetCount || 0) + ' lastPacket=' + formatTime(env.lastPacketAtMs || env.latestPacketAtMs) + ' updated=' + formatTime(env.updatedAtMs || env.latestPacketAtMs));
|
|
1111
1196
|
}
|
|
1112
1197
|
|
|
1113
1198
|
function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
@@ -1124,6 +1209,12 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
|
1124
1209
|
if (evaluation.failedCount) {
|
|
1125
1210
|
console.log('Failed packets:', evaluation.failedCount, 'current failures:', evaluation.currentFailureCount || 0);
|
|
1126
1211
|
}
|
|
1212
|
+
if (evaluation.requirementsChangedAfterEvidence) {
|
|
1213
|
+
console.log('Requirements changed after evidence was collected.');
|
|
1214
|
+
}
|
|
1215
|
+
if (Array.isArray(evaluation.stale) && evaluation.stale.length) {
|
|
1216
|
+
console.log('Stale packets:', evaluation.stale.length);
|
|
1217
|
+
}
|
|
1127
1218
|
const packets = Array.isArray(envelope.packets) ? envelope.packets : [];
|
|
1128
1219
|
console.log('Packets:', packets.length);
|
|
1129
1220
|
packets.slice(-20).forEach((packet) => {
|
|
@@ -1147,6 +1238,56 @@ function printAcceptanceWarnings(envelope) {
|
|
|
1147
1238
|
});
|
|
1148
1239
|
}
|
|
1149
1240
|
|
|
1241
|
+
function printEnvelopeWarnings(envelope) {
|
|
1242
|
+
const warnings = envelope && Array.isArray(envelope.warnings) ? envelope.warnings : [];
|
|
1243
|
+
if (!warnings.length) return;
|
|
1244
|
+
console.log('Envelope warnings:');
|
|
1245
|
+
warnings.forEach((warning) => {
|
|
1246
|
+
console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
|
|
1247
|
+
if (Array.isArray(warning.envelopes) && warning.envelopes.length) {
|
|
1248
|
+
warning.envelopes.forEach((env) => {
|
|
1249
|
+
console.log(' ' + (env.envelopeId || '(no id)') + (env.status ? ' ' + env.status : ''));
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function manifestRequiredEvidence(manifest = {}) {
|
|
1256
|
+
const required = [];
|
|
1257
|
+
if (Array.isArray(manifest.requiredEvidence)) required.push(...manifest.requiredEvidence);
|
|
1258
|
+
const journeys = Array.isArray(manifest.journeys) ? manifest.journeys :
|
|
1259
|
+
(Array.isArray(manifest.requestedJourneys) ? manifest.requestedJourneys : []);
|
|
1260
|
+
journeys.forEach((journey) => {
|
|
1261
|
+
if (journey && Array.isArray(journey.requiredEvidence)) required.push(...journey.requiredEvidence);
|
|
1262
|
+
});
|
|
1263
|
+
return required;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function readVerificationEnvelopeForDiagnostics(api, cwd, session, accountId, envelopeId) {
|
|
1267
|
+
if (!envelopeId) return null;
|
|
1268
|
+
try {
|
|
1269
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'show', { envelopeId });
|
|
1270
|
+
if (response.envelope) {
|
|
1271
|
+
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
1272
|
+
return response.envelope;
|
|
1273
|
+
}
|
|
1274
|
+
} catch (_) {
|
|
1275
|
+
// Local evidence is still useful for diagnostics when the platform read fails.
|
|
1276
|
+
}
|
|
1277
|
+
return readLocalVerificationEnvelope(cwd, envelopeId);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function readVerificationPacketsForDiagnostics(api, cwd, session, accountId, envelopeId, filters = {}) {
|
|
1281
|
+
if (!envelopeId) return [];
|
|
1282
|
+
try {
|
|
1283
|
+
const response = await postVerificationCommand(api, cwd, session, accountId, 'packets', Object.assign({ envelopeId }, filters));
|
|
1284
|
+
if (Array.isArray(response.packets)) return response.packets;
|
|
1285
|
+
} catch (_) {
|
|
1286
|
+
// Local full-fidelity packet files remain the fallback when the platform read is unavailable.
|
|
1287
|
+
}
|
|
1288
|
+
return readLocalVerificationPackets(cwd, envelopeId, filters);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1150
1291
|
function buildCommandWorld(response, fallback = {}) {
|
|
1151
1292
|
const source = (response && response.staging) || {};
|
|
1152
1293
|
const resolution = (response && response.resolution) || {};
|
|
@@ -1178,6 +1319,17 @@ function buildCommandWorld(response, fallback = {}) {
|
|
|
1178
1319
|
};
|
|
1179
1320
|
}
|
|
1180
1321
|
|
|
1322
|
+
// A variant sync writes overlays on exactly one component branch; say so in the packet world, where a manifest's
|
|
1323
|
+
// componentBranch is compared. Without it no sync_mutation could satisfy a manifest that declares one.
|
|
1324
|
+
function syncPacketWorld(response, world, branchName) {
|
|
1325
|
+
const sync = (response && response.sync) || {};
|
|
1326
|
+
const out = Object.assign({}, world || {});
|
|
1327
|
+
if (sync.mode === 'variant' && !out.componentBranch) {
|
|
1328
|
+
out.componentBranch = sync.branchName || branchName || null;
|
|
1329
|
+
}
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1181
1333
|
function laneContentHashFrom(response) {
|
|
1182
1334
|
if (!response) return null;
|
|
1183
1335
|
const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
|
|
@@ -2395,21 +2547,23 @@ async function loggedPost(api, cwd, endpoint, payload, options = {}) {
|
|
|
2395
2547
|
error = err;
|
|
2396
2548
|
throw err;
|
|
2397
2549
|
} finally {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2550
|
+
if (!options.skipSessionLog) {
|
|
2551
|
+
const status = error ? 'error' : 'success';
|
|
2552
|
+
const responseForLog = responseFile
|
|
2553
|
+
? { responseFile, note: 'Tool response stored externally' }
|
|
2554
|
+
: sanitizeForLog(responseData);
|
|
2555
|
+
appendSessionLog(cwd, {
|
|
2556
|
+
ts: new Date().toISOString(),
|
|
2557
|
+
requestId,
|
|
2558
|
+
endpoint,
|
|
2559
|
+
method: 'POST',
|
|
2560
|
+
status,
|
|
2561
|
+
durationMs: Date.now() - started,
|
|
2562
|
+
request: sanitizeForLog(payload),
|
|
2563
|
+
response: responseForLog,
|
|
2564
|
+
error: error ? describeError(error) : null
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2413
2567
|
}
|
|
2414
2568
|
}
|
|
2415
2569
|
|
|
@@ -3598,6 +3752,7 @@ function describeStageMode(mode) {
|
|
|
3598
3752
|
function printStatusSummary(response, flags) {
|
|
3599
3753
|
printBranchContext(response);
|
|
3600
3754
|
printStagingLane(response.branchName, response.workspace, null);
|
|
3755
|
+
printStagingLaneOwnerNotice(response);
|
|
3601
3756
|
printLaneSummary(response);
|
|
3602
3757
|
printOrphanedWorkspaceWarning(response);
|
|
3603
3758
|
printStagingFreshness(response.freshness);
|
|
@@ -3625,6 +3780,28 @@ function printStatusSummary(response, flags) {
|
|
|
3625
3780
|
printComponentCommandResponse('Components staging', response, flags);
|
|
3626
3781
|
}
|
|
3627
3782
|
|
|
3783
|
+
function stagingLaneOwnerNotice(response = {}) {
|
|
3784
|
+
const owner = response.laneOwnerAccountId != null ? response.laneOwnerAccountId :
|
|
3785
|
+
(response.staging && response.staging.laneOwnerAccountId);
|
|
3786
|
+
const checkout = response.checkoutAccountId != null ? response.checkoutAccountId :
|
|
3787
|
+
(response.accountId != null ? response.accountId : (response.staging && response.staging.checkoutAccountId));
|
|
3788
|
+
const stagedCount = Number(response.stagedCount != null ? response.stagedCount :
|
|
3789
|
+
(response.laneSummary && response.laneSummary.stagedCount != null ? response.laneSummary.stagedCount :
|
|
3790
|
+
(response.staging && response.staging.stagedCount != null ? response.staging.stagedCount :
|
|
3791
|
+
(response.staging && response.staging.laneSummary && response.staging.laneSummary.stagedCount))));
|
|
3792
|
+
if (owner == null || checkout == null || String(owner) === String(checkout) || !(stagedCount > 0)) {
|
|
3793
|
+
return [];
|
|
3794
|
+
}
|
|
3795
|
+
return [
|
|
3796
|
+
'WARNING: staged entries shown here are under account ' + owner + ', not checkout account ' + checkout + '.',
|
|
3797
|
+
' clear with: remits-cli components clear --all --account-id ' + owner
|
|
3798
|
+
];
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
function printStagingLaneOwnerNotice(response = {}) {
|
|
3802
|
+
stagingLaneOwnerNotice(response).forEach((line) => console.log(line));
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3628
3805
|
function printLanesSummary(response, flags) {
|
|
3629
3806
|
printBranchContext(response);
|
|
3630
3807
|
const lanes = Array.isArray(response.lanes) ? response.lanes : (Array.isArray(response.accountLanes) ? response.accountLanes : []);
|
|
@@ -3999,19 +4176,25 @@ async function lanesComponentsCommand(flags) {
|
|
|
3999
4176
|
const dataMode = resolveDataMode(flags, session);
|
|
4000
4177
|
const api = buildAxios(baseUrl, session.token);
|
|
4001
4178
|
|
|
4002
|
-
const
|
|
4179
|
+
const requestPayload = {
|
|
4003
4180
|
token: session.token,
|
|
4004
4181
|
accountId,
|
|
4005
4182
|
branchName,
|
|
4006
4183
|
workspace,
|
|
4007
4184
|
dataMode,
|
|
4008
4185
|
mode: 'lanes'
|
|
4009
|
-
}
|
|
4186
|
+
};
|
|
4187
|
+
|
|
4188
|
+
let response = await loggedPost(api, cwd, '/cli/components', requestPayload).then((r) => r.data);
|
|
4010
4189
|
|
|
4011
4190
|
if (!response.success) {
|
|
4012
4191
|
throw new Error(response.message || 'Staging lanes failed');
|
|
4013
4192
|
}
|
|
4014
4193
|
|
|
4194
|
+
if (flagEnabled(flags['wait-change']) || flagEnabled(flags.waitChange)) {
|
|
4195
|
+
response = await waitForLaneChange(api, cwd, requestPayload, response, flags);
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4015
4198
|
if (flagEnabled(flags.json)) {
|
|
4016
4199
|
console.log(JSON.stringify(response, null, 2));
|
|
4017
4200
|
return response;
|
|
@@ -4024,6 +4207,55 @@ async function lanesComponentsCommand(flags) {
|
|
|
4024
4207
|
return response;
|
|
4025
4208
|
}
|
|
4026
4209
|
|
|
4210
|
+
function laneWaitSignature(response, laneId) {
|
|
4211
|
+
const lanes = Array.isArray(response && response.lanes)
|
|
4212
|
+
? response.lanes
|
|
4213
|
+
: (Array.isArray(response && response.accountLanes) ? response.accountLanes : []);
|
|
4214
|
+
const selected = laneId ? lanes.filter((lane) => String(lane.laneId || '') === String(laneId)) : lanes;
|
|
4215
|
+
return stableStringify(selected.map((lane) => ({
|
|
4216
|
+
laneId: lane.laneId,
|
|
4217
|
+
accountId: lane.accountId,
|
|
4218
|
+
branchName: lane.branchName,
|
|
4219
|
+
workspace: lane.workspace || null,
|
|
4220
|
+
stagedCount: lane.stagedCountAsOf || lane.stagedCount || 0,
|
|
4221
|
+
contentHash: lane.contentHash || (lane.laneSummary && lane.laneSummary.contentHash) || null,
|
|
4222
|
+
updatedAtMs: lane.updatedAtMs || null
|
|
4223
|
+
})));
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
async function waitForLaneChange(api, cwd, requestPayload, initialResponse, flags) {
|
|
4227
|
+
const laneId = flags['lane-id'] || flags.laneId || flags.lane;
|
|
4228
|
+
const timeoutSeconds = parsePositiveInt(flags.timeout || flags['timeout-seconds'] || flags.timeoutSeconds, 600);
|
|
4229
|
+
const pollMs = parsePositiveInt(flags['poll-ms'] || flags.pollMs, 2000);
|
|
4230
|
+
const started = Date.now();
|
|
4231
|
+
const initialSignature = laneWaitSignature(initialResponse, laneId);
|
|
4232
|
+
if (!flagEnabled(flags.json)) {
|
|
4233
|
+
console.log('Waiting for staging lane change' + (laneId ? ' on ' + laneId : '') + ' for up to ' + timeoutSeconds + 's...');
|
|
4234
|
+
}
|
|
4235
|
+
while (Date.now() - started < timeoutSeconds * 1000) {
|
|
4236
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
4237
|
+
const next = await loggedPost(api, cwd, '/cli/components', requestPayload, { skipSessionLog: true }).then((r) => r.data);
|
|
4238
|
+
if (!next.success) {
|
|
4239
|
+
throw new Error(next.message || 'Staging lanes failed while waiting for a change');
|
|
4240
|
+
}
|
|
4241
|
+
if (laneWaitSignature(next, laneId) !== initialSignature) {
|
|
4242
|
+
next.waitChange = {
|
|
4243
|
+
changed: true,
|
|
4244
|
+
laneId: laneId || null,
|
|
4245
|
+
waitedMs: Date.now() - started
|
|
4246
|
+
};
|
|
4247
|
+
return next;
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
initialResponse.waitChange = {
|
|
4251
|
+
changed: false,
|
|
4252
|
+
laneId: laneId || null,
|
|
4253
|
+
waitedMs: Date.now() - started,
|
|
4254
|
+
timeoutSeconds
|
|
4255
|
+
};
|
|
4256
|
+
return initialResponse;
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4027
4259
|
async function entriesComponentsCommand(flags) {
|
|
4028
4260
|
const cwd = process.cwd();
|
|
4029
4261
|
ensureLocalState(cwd);
|
|
@@ -4219,6 +4451,30 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4219
4451
|
}
|
|
4220
4452
|
throw new Error(mismatch + ' Nothing was synced.');
|
|
4221
4453
|
}
|
|
4454
|
+
const featureRefusal = !namesOnly ? featureBranchSyncRefusal(branchContext, branchName, flags) : null;
|
|
4455
|
+
if (featureRefusal) {
|
|
4456
|
+
if (flagEnabled(flags.json)) {
|
|
4457
|
+
const refusal = {
|
|
4458
|
+
success: false,
|
|
4459
|
+
mode: 'sync',
|
|
4460
|
+
dataMode,
|
|
4461
|
+
accountId,
|
|
4462
|
+
branchName,
|
|
4463
|
+
workspace,
|
|
4464
|
+
dryRun,
|
|
4465
|
+
safe,
|
|
4466
|
+
branchContext,
|
|
4467
|
+
repositoryCheck: preflightRepoCheck,
|
|
4468
|
+
refusal: 'feature_branch_landing',
|
|
4469
|
+
gateViolations: [featureRefusal],
|
|
4470
|
+
message: featureRefusal
|
|
4471
|
+
};
|
|
4472
|
+
console.log(JSON.stringify(refusal, null, 2));
|
|
4473
|
+
process.exitCode = 1;
|
|
4474
|
+
return refusal;
|
|
4475
|
+
}
|
|
4476
|
+
throw new Error(featureRefusal + ' Nothing was synced.');
|
|
4477
|
+
}
|
|
4222
4478
|
if (branchContextIsTrunk(branchContext)) {
|
|
4223
4479
|
// Trunk has no dry-run plan to gate on, and pretending otherwise would be worse than refusing:
|
|
4224
4480
|
// an agent would read "safe" and get an ungated authoritative reconcile. Say what trunk sync is
|
|
@@ -4335,7 +4591,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4335
4591
|
type: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation',
|
|
4336
4592
|
success: response.success !== false,
|
|
4337
4593
|
claim: response.sync && response.sync.dryRun ? 'Component sync dry-run observed' : 'Component sync completed',
|
|
4338
|
-
world: buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }),
|
|
4594
|
+
world: syncPacketWorld(response, buildCommandWorld(response, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: response.sync && response.sync.mode === 'variant' ? 'variant' : 'trunk' }), branchName),
|
|
4339
4595
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
4340
4596
|
platformSyncedSha: response.sync && (response.sync.postSyncSha || response.sync.branchHeadSha)
|
|
4341
4597
|
}),
|
|
@@ -4344,7 +4600,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4344
4600
|
{ category: response.sync && response.sync.dryRun ? 'sync_dry_run' : 'sync_mutation', summary }
|
|
4345
4601
|
],
|
|
4346
4602
|
rawRefs: { command: 'components sync' },
|
|
4347
|
-
sync: response.sync,
|
|
4603
|
+
sync: syncEvidencePayload(response.sync),
|
|
4348
4604
|
summary
|
|
4349
4605
|
}, { quiet: flagEnabled(flags.json) });
|
|
4350
4606
|
|
|
@@ -4375,11 +4631,31 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4375
4631
|
return response;
|
|
4376
4632
|
}
|
|
4377
4633
|
console.log('Components sync:', JSON.stringify(response, null, 2));
|
|
4634
|
+
printVariantSyncWrites(summary);
|
|
4378
4635
|
printPromotionSignals(response);
|
|
4379
4636
|
failOnSyncGate(gate);
|
|
4380
4637
|
return response;
|
|
4381
4638
|
}
|
|
4382
4639
|
|
|
4640
|
+
// The one line a person needs from a variant sync of a long-lived branch: what THIS sync changes, apart from
|
|
4641
|
+
// the overlays it merely re-confirms (the totals alone read the same on every sync).
|
|
4642
|
+
function printVariantSyncWrites(summary) {
|
|
4643
|
+
const writes = summary && summary.writes;
|
|
4644
|
+
if (!writes || summary.mode !== 'variant') return;
|
|
4645
|
+
console.log('');
|
|
4646
|
+
if (!writes.known) {
|
|
4647
|
+
console.log('Overlay changes: unknown (this platform does not report which overlays were already stored).');
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
const verb = summary.dryRun ? 'would change' : 'changed';
|
|
4651
|
+
console.log('Overlay changes: ' + writes.changed + ' ' + verb + ', ' + writes.alreadyCurrent + ' already current, ' +
|
|
4652
|
+
writes.removed + ' removed, ' + writes.pruned + ' pruned (converged back to trunk)');
|
|
4653
|
+
(writes.changedComponents || []).forEach((entry) => {
|
|
4654
|
+
console.log(' ' + String(entry.type || 'component').toLowerCase() + ':' + (entry.id != null ? entry.id : entry.name) +
|
|
4655
|
+
(entry.name && entry.id != null ? ' ' + entry.name : ''));
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4383
4659
|
// A sync is a step in a longer loop, and the steps after it are the ones that get skipped — so print what
|
|
4384
4660
|
// the server just observed about this branch's relationship to trunk, not only what it wrote.
|
|
4385
4661
|
function printPromotionSignals(response) {
|
|
@@ -4548,7 +4824,9 @@ function syncPlanEntries(response) {
|
|
|
4548
4824
|
type: String(entry.type || entry.kind || 'component').toLowerCase(),
|
|
4549
4825
|
id: entry.id == null ? null : String(entry.id),
|
|
4550
4826
|
name: entry.name || null,
|
|
4551
|
-
path: entry.path || null
|
|
4827
|
+
path: entry.path || null,
|
|
4828
|
+
// true when the overlay already stored for this component matches the branch: re-confirmed, not changed.
|
|
4829
|
+
storedCurrent: entry.storedCurrent === true
|
|
4552
4830
|
});
|
|
4553
4831
|
});
|
|
4554
4832
|
});
|
|
@@ -4594,6 +4872,8 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4594
4872
|
const expectedRemoved = parseExpectedRemoved(flags);
|
|
4595
4873
|
const violations = [];
|
|
4596
4874
|
const checks = {};
|
|
4875
|
+
// Overlays the --changed-only gate did not count because the platform already stores them with this content.
|
|
4876
|
+
let alreadyCurrentCount = null;
|
|
4597
4877
|
|
|
4598
4878
|
if (flagEnabled(flags['fail-on-errors']) || flagEnabled(flags.failOnErrors)) {
|
|
4599
4879
|
checks.failOnErrors = errors.length === 0;
|
|
@@ -4633,13 +4913,20 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4633
4913
|
const allowedIds = new Set(changedSet.filter((c) => c.id != null).map((c) => c.type + ':' + c.id));
|
|
4634
4914
|
const allowedNames = new Set(changedSet.filter((c) => c.name).map((c) => c.type + ':' + String(c.name).toLowerCase()));
|
|
4635
4915
|
const allowedPaths = new Set([].concat(...changedSet.map((c) => (Array.isArray(c.paths) ? c.paths : []))).map(String));
|
|
4916
|
+
// An overlay the platform ALREADY stores with this exact content is not something this sync changes. A
|
|
4917
|
+
// long-lived branch (a permanent customer release train) re-reports every one of its overlays on every sync,
|
|
4918
|
+
// so counting those made the gate refuse ordinary work with "plan touches 80 components" and blame a stale
|
|
4919
|
+
// branch that `components promotion` reported as ready. Removals are never exempt.
|
|
4920
|
+
const alreadyCurrent = entries.filter((entry) => entry.storedCurrent && entry.bucket !== 'removed');
|
|
4636
4921
|
const unexpected = entries.filter((entry) => {
|
|
4922
|
+
if (entry.storedCurrent && entry.bucket !== 'removed') return false;
|
|
4637
4923
|
if (entry.path && allowedPaths.has(String(entry.path))) return false;
|
|
4638
4924
|
if (entry.id != null && allowedIds.has(entry.type + ':' + entry.id)) return false;
|
|
4639
4925
|
if (entry.name && allowedNames.has(entry.type + ':' + String(entry.name).toLowerCase())) return false;
|
|
4640
4926
|
return true;
|
|
4641
4927
|
});
|
|
4642
4928
|
checks.changedOnly = unexpected.length === 0;
|
|
4929
|
+
alreadyCurrentCount = alreadyCurrent.length;
|
|
4643
4930
|
if (unexpected.length) {
|
|
4644
4931
|
// Name the most likely cause instead of only the symptom. An empty changed set with a non-empty
|
|
4645
4932
|
// plan is the ordinary post-commit state, not evidence that the plan is dangerous.
|
|
@@ -4651,11 +4938,10 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4651
4938
|
// physically carries old copies of files nobody on it touched, and a variant sync turns each of
|
|
4652
4939
|
// those into an unrelated override. Naming the cause is the difference between a gate that
|
|
4653
4940
|
// stops the damage and a gate that also tells you how to clear it.
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
'\n git merge <trunk> && git push origin <branch>'
|
|
4941
|
+
// With --changed-since the empty-set hint above does not apply, so a ref that shows no committed change
|
|
4942
|
+
// still needs the cause named — that is exactly the behind-trunk branch whose plan is all stale copies.
|
|
4943
|
+
: ((changedSet.length || since) && unexpected.length > changedSet.length)
|
|
4944
|
+
? staleBranchGateHint(response)
|
|
4659
4945
|
: '';
|
|
4660
4946
|
violations.push('--changed-only: plan touches ' + unexpected.length + ' component(s) this checkout did not change: ' +
|
|
4661
4947
|
unexpected.slice(0, 20).map(syncEntryToken).join(', ') + (unexpected.length > 20 ? ', ...' : '') + hint);
|
|
@@ -4663,7 +4949,28 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4663
4949
|
}
|
|
4664
4950
|
}
|
|
4665
4951
|
|
|
4666
|
-
return { violations, checks };
|
|
4952
|
+
return { violations, checks, alreadyCurrentCount };
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
// Name the stale-branch cause only when the platform's own git comparison says the branch IS behind trunk. The
|
|
4956
|
+
// hint used to fire on the count alone, and told agents on an up-to-date permanent branch to merge trunk in.
|
|
4957
|
+
function staleBranchGateHint(response) {
|
|
4958
|
+
const sync = (response && response.sync) || {};
|
|
4959
|
+
const comparison = sync.branchComparison || (sync.promotion && sync.promotion.git) || null;
|
|
4960
|
+
const behindBy = comparison && Number.isFinite(Number(comparison.behindBy)) ? Number(comparison.behindBy) : null;
|
|
4961
|
+
if (behindBy !== null && behindBy > 0) {
|
|
4962
|
+
return '\n Most likely cause: this branch is BEHIND trunk by ' + behindBy + ' commit(s) and still carries old copies ' +
|
|
4963
|
+
'of files it never changed. A variant sync turns each of those into an unrelated override.' +
|
|
4964
|
+
'\n Next step: merge trunk into this branch, push, then re-run the same command:' +
|
|
4965
|
+
'\n git merge <trunk> && git push origin <branch>';
|
|
4966
|
+
}
|
|
4967
|
+
if (behindBy === 0) {
|
|
4968
|
+
return '\n The branch is current with trunk, so these are not stale copies: they are overlays whose stored content ' +
|
|
4969
|
+
'differs from this branch but that this checkout\'s changed set does not name. Widen --changed-since to the ' +
|
|
4970
|
+
'commit the platform last synced (components promotion shows it), or review the plan with --dry-run.';
|
|
4971
|
+
}
|
|
4972
|
+
return '\n The platform could not compare this branch with trunk. Run `remits-cli components promotion` to see ' +
|
|
4973
|
+
'whether it is behind before merging anything.';
|
|
4667
4974
|
}
|
|
4668
4975
|
|
|
4669
4976
|
function failOnSyncGate(gate) {
|
|
@@ -4781,6 +5088,15 @@ function featureBranchCommitRefusal(branchContext, branchName, flags) {
|
|
|
4781
5088
|
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
4782
5089
|
}
|
|
4783
5090
|
|
|
5091
|
+
function featureBranchSyncRefusal(branchContext, branchName, flags) {
|
|
5092
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
5093
|
+
if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
|
|
5094
|
+
return 'components sync refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
|
|
5095
|
+
'subscribers). ' + featureBranchLandingHint(branchContext) + ' Syncing here would write overlays for \'' +
|
|
5096
|
+
branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
|
|
5097
|
+
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
5098
|
+
}
|
|
5099
|
+
|
|
4784
5100
|
// What a verification envelope records as the world it verified. Taken from the platform's answer
|
|
4785
5101
|
// (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
|
|
4786
5102
|
// variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
|
|
@@ -4947,6 +5263,36 @@ function printTrunkSyncWarning(trunkBranch, options) {
|
|
|
4947
5263
|
emit(' For a gated plan, work on a variant branch instead.');
|
|
4948
5264
|
}
|
|
4949
5265
|
|
|
5266
|
+
function variantSyncWrites(overridden, added, removed, unchanged) {
|
|
5267
|
+
const overlays = overridden.concat(added);
|
|
5268
|
+
const known = overlays.every((entry) => entry && typeof entry.storedCurrent === 'boolean');
|
|
5269
|
+
const pruned = unchanged.filter((entry) => entry && entry.pruned === true).length;
|
|
5270
|
+
if (!known) {
|
|
5271
|
+
return { changed: null, alreadyCurrent: null, removed: removed.length, pruned, known: false };
|
|
5272
|
+
}
|
|
5273
|
+
const changed = overlays.filter((entry) => !entry.storedCurrent);
|
|
5274
|
+
return {
|
|
5275
|
+
changed: changed.length,
|
|
5276
|
+
changedComponents: changed.slice(0, 50).map((entry) => ({ type: entry.type, name: entry.name, id: entry.id })),
|
|
5277
|
+
alreadyCurrent: overlays.length - changed.length,
|
|
5278
|
+
removed: removed.length,
|
|
5279
|
+
pruned,
|
|
5280
|
+
known: true
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// `|` separates selectors and a repeated --names needs no separator. A COMMA is never a separator any more: it
|
|
5285
|
+
// is sent as part of the selector, and the platform matches a comma-bearing selector either as one whole case name
|
|
5286
|
+
// ("… the complete statement, offer, and surcharge matrix") or, for older invocations, as its comma-separated
|
|
5287
|
+
// parts (Test.selectorMatchesCase). Splitting in the CLI took the real name apart before the platform could see it.
|
|
5288
|
+
function parseTestCaseSelectors(rawNames) {
|
|
5289
|
+
return (Array.isArray(rawNames) ? rawNames : [rawNames])
|
|
5290
|
+
.filter((value) => value !== undefined && value !== null)
|
|
5291
|
+
.flatMap((value) => String(value).split('|'))
|
|
5292
|
+
.map((value) => value.trim())
|
|
5293
|
+
.filter(Boolean);
|
|
5294
|
+
}
|
|
5295
|
+
|
|
4950
5296
|
function buildSyncSummary(response) {
|
|
4951
5297
|
const sync = (response && response.sync) || {};
|
|
4952
5298
|
const results = sync.syncResults || {};
|
|
@@ -5076,6 +5422,10 @@ function buildSyncSummary(response) {
|
|
|
5076
5422
|
gateViolations: (response && response.gateViolations) || null,
|
|
5077
5423
|
overridden: overridden.length,
|
|
5078
5424
|
added: added.length,
|
|
5425
|
+
// What THIS sync changes, as opposed to the whole overlay set it re-reports. `overridden`/`added` are totals,
|
|
5426
|
+
// so five consecutive syncs of a long-lived branch all printed `overridden: 48, added: 32` and nobody could see
|
|
5427
|
+
// which overlays their own sync moved. Null counts mean the platform predates the storedCurrent flag.
|
|
5428
|
+
writes: variantSyncWrites(overridden, added, removed, unchanged),
|
|
5079
5429
|
removed: removed.map((entry) => ({
|
|
5080
5430
|
type: entry.type,
|
|
5081
5431
|
kind: entry.kind,
|
|
@@ -5916,6 +6266,94 @@ function testCompileSignatures(status = {}) {
|
|
|
5916
6266
|
.filter(Boolean);
|
|
5917
6267
|
}
|
|
5918
6268
|
|
|
6269
|
+
function testRunSuiteNameFromStatus(status = {}) {
|
|
6270
|
+
return status.test && status.test.name ? String(status.test.name) : null;
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
function testRunSuiteNameFromPacket(packet = {}) {
|
|
6274
|
+
return packet.test && packet.test.testName
|
|
6275
|
+
? String(packet.test.testName)
|
|
6276
|
+
: (packet.status && packet.status.test && packet.status.test.name ? String(packet.status.test.name) : null);
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6279
|
+
function testCaseOutcomesFromStatusLike(status = {}) {
|
|
6280
|
+
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests :
|
|
6281
|
+
(status.test && Array.isArray(status.test.tests) ? status.test.tests : []);
|
|
6282
|
+
return tests
|
|
6283
|
+
.filter((test) => test && test.name && typeof test.passed === 'boolean')
|
|
6284
|
+
.map((test) => ({ name: String(test.name), passed: test.passed === true }));
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
function provenanceSignature(value) {
|
|
6288
|
+
const rows = Array.isArray(value) ? value : [];
|
|
6289
|
+
return sha256(stableStringify(rows.map((entry) => ({
|
|
6290
|
+
type: entry && entry.type || null,
|
|
6291
|
+
id: entry && entry.id || null,
|
|
6292
|
+
name: entry && entry.name || null,
|
|
6293
|
+
signature: entry && entry.signature || null,
|
|
6294
|
+
source: entry && entry.source || null
|
|
6295
|
+
}))));
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
function packetEvidenceIdentity(packet = {}) {
|
|
6299
|
+
const world = packet.world || {};
|
|
6300
|
+
const revision = packet.revision || {};
|
|
6301
|
+
const laneSummary = packet.laneSummary || (packet.componentStatus && packet.componentStatus.laneSummary) || {};
|
|
6302
|
+
return {
|
|
6303
|
+
laneContentHash: world.laneContentHash || world.stagedOverlayHash || revision.laneContentHash ||
|
|
6304
|
+
revision.stagedOverlayHash || laneSummary.contentHash || null,
|
|
6305
|
+
gitHead: revision.gitHead || null
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
function sameNondeterminismIdentity(left = {}, right = {}) {
|
|
6310
|
+
return !!(left.laneContentHash && right.laneContentHash && left.gitHead && right.gitHead &&
|
|
6311
|
+
String(left.laneContentHash) === String(right.laneContentHash) &&
|
|
6312
|
+
String(left.gitHead) === String(right.gitHead));
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
function detectNondeterministicTestRun(priorPackets, currentStatus, currentProvenance, currentIdentity = {}) {
|
|
6316
|
+
const packets = Array.isArray(priorPackets) ? priorPackets : [];
|
|
6317
|
+
const suiteName = testRunSuiteNameFromStatus(currentStatus);
|
|
6318
|
+
const currentCases = testCaseOutcomesFromStatusLike(currentStatus);
|
|
6319
|
+
if (!suiteName || !currentCases.length) return null;
|
|
6320
|
+
if (!Array.isArray(currentProvenance) || !currentProvenance.length) return null;
|
|
6321
|
+
if (!currentIdentity.laneContentHash || !currentIdentity.gitHead) return null;
|
|
6322
|
+
|
|
6323
|
+
const currentSignature = provenanceSignature(currentProvenance);
|
|
6324
|
+
const currentByName = new Map(currentCases.map((outcome) => [outcome.name, outcome]));
|
|
6325
|
+
for (let i = packets.length - 1; i >= 0; i -= 1) {
|
|
6326
|
+
const packet = packets[i];
|
|
6327
|
+
if (!packet || packet.type !== 'test_run') continue;
|
|
6328
|
+
if (testRunSuiteNameFromPacket(packet) !== suiteName) continue;
|
|
6329
|
+
const previousProvenance = packet.revision && packet.revision.componentProvenance;
|
|
6330
|
+
if (!Array.isArray(previousProvenance) || !previousProvenance.length) continue;
|
|
6331
|
+
if (!sameNondeterminismIdentity(currentIdentity, packetEvidenceIdentity(packet))) continue;
|
|
6332
|
+
const previousSignature = provenanceSignature(previousProvenance);
|
|
6333
|
+
if (previousSignature !== currentSignature) continue;
|
|
6334
|
+
const previousCases = testCaseOutcomesFromStatusLike(packet.status || packet);
|
|
6335
|
+
const flips = previousCases
|
|
6336
|
+
.map((previous) => {
|
|
6337
|
+
const current = currentByName.get(previous.name);
|
|
6338
|
+
return current && current.passed !== previous.passed
|
|
6339
|
+
? { caseName: previous.name, previousPassed: previous.passed, currentPassed: current.passed, previousPacketId: packet.packetId }
|
|
6340
|
+
: null;
|
|
6341
|
+
})
|
|
6342
|
+
.filter(Boolean);
|
|
6343
|
+
if (flips.length) {
|
|
6344
|
+
return {
|
|
6345
|
+
nondeterministic: true,
|
|
6346
|
+
message: 'Outcome changed with no source change - likely nondeterministic (AI/live data).',
|
|
6347
|
+
suite: suiteName,
|
|
6348
|
+
provenanceSignature: currentSignature,
|
|
6349
|
+
previousPacketId: packet.packetId,
|
|
6350
|
+
flips
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
return null;
|
|
6355
|
+
}
|
|
6356
|
+
|
|
5919
6357
|
function printTestRunPivots(status = {}) {
|
|
5920
6358
|
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
5921
6359
|
if (!tests.length) return;
|
|
@@ -6041,26 +6479,12 @@ async function testCommand(flags) {
|
|
|
6041
6479
|
throw new Error('Missing --test <id-or-name>');
|
|
6042
6480
|
}
|
|
6043
6481
|
|
|
6044
|
-
// Test case names are English prose, so COMMAS occur in them naturally.
|
|
6045
|
-
//
|
|
6046
|
-
// report unmatched selectors) reported a clean run of zero cases. `|` is the delimiter now; the
|
|
6047
|
-
// comma still works when no `|` is present, so existing invocations keep behaving as before.
|
|
6048
|
-
// `--names` may also be repeated, which needs no delimiter at all.
|
|
6482
|
+
// Test case names are English prose, so COMMAS occur in them naturally. `|` (or repeating --names) is the
|
|
6483
|
+
// only delimiter; see parseTestCaseSelectors for how a comma-bearing selector is matched.
|
|
6049
6484
|
const rawNames = flags.names === undefined || flags.names === null
|
|
6050
6485
|
? []
|
|
6051
6486
|
: (Array.isArray(flags.names) ? flags.names : [flags.names]);
|
|
6052
|
-
const
|
|
6053
|
-
const names = rawNames
|
|
6054
|
-
.flatMap((value) => {
|
|
6055
|
-
const text = String(value);
|
|
6056
|
-
if (text.includes('|')) return text.split('|');
|
|
6057
|
-
// Comma-splitting is the legacy behaviour, kept so existing invocations still work. It is applied
|
|
6058
|
-
// ONLY to a single --names value: repeating the flag is already unambiguous, so splitting there
|
|
6059
|
-
// would take a deliberate literal name back apart.
|
|
6060
|
-
return repeated ? [text] : text.split(',');
|
|
6061
|
-
})
|
|
6062
|
-
.map((s) => s.trim())
|
|
6063
|
-
.filter(Boolean);
|
|
6487
|
+
const names = parseTestCaseSelectors(rawNames);
|
|
6064
6488
|
|
|
6065
6489
|
const api = buildAxios(baseUrl, session.token);
|
|
6066
6490
|
// --as-account runs the Test AS a (descendant) subscriber account, so that account's relationship
|
|
@@ -6102,6 +6526,7 @@ async function testCommand(flags) {
|
|
|
6102
6526
|
printSessionResolutionWarning(sessionContext);
|
|
6103
6527
|
printResolvedBaseUrl(baseUrl);
|
|
6104
6528
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
6529
|
+
printStagingLaneOwnerNotice(start.staging || {});
|
|
6105
6530
|
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
6106
6531
|
printOrphanedWorkspaceWarning(start.staging);
|
|
6107
6532
|
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
@@ -6130,6 +6555,7 @@ async function testCommand(flags) {
|
|
|
6130
6555
|
console.log(JSON.stringify(status, null, 2));
|
|
6131
6556
|
} else {
|
|
6132
6557
|
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
6558
|
+
printStagingLaneOwnerNotice(status.staging || {});
|
|
6133
6559
|
printTestRunPivots(status);
|
|
6134
6560
|
}
|
|
6135
6561
|
|
|
@@ -6155,15 +6581,38 @@ async function testCommand(flags) {
|
|
|
6155
6581
|
process.exitCode = 1;
|
|
6156
6582
|
}
|
|
6157
6583
|
|
|
6584
|
+
const verificationContext = activeVerificationContext(cwd, flags, session, accountId);
|
|
6585
|
+
const activeEnvelopeId = verificationEnvelopeIdForCommand(cwd, flags, verificationContext);
|
|
6586
|
+
const componentProvenance = testComponentProvenance(status);
|
|
6587
|
+
const sourceRevision = collectVerificationSource(cwd, flags);
|
|
6588
|
+
const priorPackets = await readVerificationPacketsForDiagnostics(api, cwd, session, accountId, activeEnvelopeId, {
|
|
6589
|
+
packetType: 'test_run',
|
|
6590
|
+
suite: status.test && status.test.name,
|
|
6591
|
+
max: 50
|
|
6592
|
+
});
|
|
6593
|
+
const nondeterminism = detectNondeterministicTestRun(priorPackets, status, componentProvenance, {
|
|
6594
|
+
laneContentHash: status.staging && status.staging.laneSummary && status.staging.laneSummary.contentHash,
|
|
6595
|
+
gitHead: sourceRevision.gitHead
|
|
6596
|
+
});
|
|
6597
|
+
if (nondeterminism && !jsonOutput) {
|
|
6598
|
+
console.log('Nondeterministic signal:', nondeterminism.message);
|
|
6599
|
+
nondeterminism.flips.slice(0, 6).forEach((flip) => {
|
|
6600
|
+
console.log(' - ' + flip.caseName + ': ' + (flip.previousPassed ? 'passed' : 'failed') + ' -> ' + (flip.currentPassed ? 'passed' : 'failed') +
|
|
6601
|
+
' (previous packet ' + (flip.previousPacketId || 'unknown') + ')');
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
|
|
6158
6605
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
6159
6606
|
type: 'test_run',
|
|
6160
6607
|
success: status.status === 'completed' && !(status.result && status.result.failed > 0) && !unmatched.length,
|
|
6161
6608
|
claim: 'Test run ' + String(testRef),
|
|
6162
6609
|
world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
|
|
6163
|
-
revision: Object.assign(
|
|
6610
|
+
revision: Object.assign(sourceRevision, {
|
|
6164
6611
|
compileSignatures: testCompileSignatures(status),
|
|
6165
|
-
componentProvenance
|
|
6612
|
+
componentProvenance
|
|
6166
6613
|
}),
|
|
6614
|
+
nondeterministic: nondeterminism ? true : undefined,
|
|
6615
|
+
nondeterminism: nondeterminism || undefined,
|
|
6167
6616
|
test: {
|
|
6168
6617
|
taskId: start.taskId,
|
|
6169
6618
|
testId: status.test && status.test.id,
|
|
@@ -6176,7 +6625,9 @@ async function testCommand(flags) {
|
|
|
6176
6625
|
unmatchedTestNames: unmatched
|
|
6177
6626
|
},
|
|
6178
6627
|
evidenceCategories: testEvidenceCategories(status, names),
|
|
6179
|
-
limitations:
|
|
6628
|
+
limitations: []
|
|
6629
|
+
.concat(unmatched.length ? ['One or more requested test case selectors matched no case.'] : [])
|
|
6630
|
+
.concat(nondeterminism ? [nondeterminism.message] : []),
|
|
6180
6631
|
rawRefs: { testStatusKey: start.taskId },
|
|
6181
6632
|
status
|
|
6182
6633
|
}, { quiet: jsonOutput });
|
|
@@ -6564,8 +7015,7 @@ async function toolCommand(flags) {
|
|
|
6564
7015
|
console.log('Data mode:', data.dataMode || dataMode);
|
|
6565
7016
|
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6566
7017
|
console.log('Session log:', data.sessionLog);
|
|
6567
|
-
|
|
6568
|
-
console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
7018
|
+
printToolResponseFileLine('Tool response file', data.responseFile);
|
|
6569
7019
|
if (polledFailed) {
|
|
6570
7020
|
console.log('Tool error:', data.toolMessage);
|
|
6571
7021
|
}
|
|
@@ -6636,8 +7086,7 @@ async function toolCommand(flags) {
|
|
|
6636
7086
|
console.log('Component:', `${data.componentSource}${sig}`);
|
|
6637
7087
|
}
|
|
6638
7088
|
console.log('Session log:', data.sessionLog);
|
|
6639
|
-
|
|
6640
|
-
console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
7089
|
+
printToolResponseFileLine('Tool response file', data.responseFile);
|
|
6641
7090
|
}
|
|
6642
7091
|
|
|
6643
7092
|
// Non-zero exit so scripted/agent callers that check status notice the refusal too.
|
|
@@ -6661,8 +7110,7 @@ async function toolCommand(flags) {
|
|
|
6661
7110
|
evidenceResponseFile = finalStatus.responseFile;
|
|
6662
7111
|
if (!jsonOutput) {
|
|
6663
7112
|
console.log('Final status:', finalStatus.data.status);
|
|
6664
|
-
|
|
6665
|
-
console.log('Tool response file:', finalStatus.responseFile + (finalEvidence.bytes ? ' (' + finalEvidence.bytes + ' bytes, sha256 ' + finalEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
7113
|
+
printToolResponseFileLine('Tool response file', finalStatus.responseFile);
|
|
6666
7114
|
}
|
|
6667
7115
|
if (finalStatus.data.status !== 'completed') {
|
|
6668
7116
|
process.exitCode = 1;
|
|
@@ -6673,6 +7121,7 @@ async function toolCommand(flags) {
|
|
|
6673
7121
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
6674
7122
|
type: 'tool_call',
|
|
6675
7123
|
success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
|
|
7124
|
+
pending: asyncMode && !waitForAsync,
|
|
6676
7125
|
claim: 'Tool call ' + String(toolName),
|
|
6677
7126
|
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 }),
|
|
6678
7127
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
@@ -6753,6 +7202,12 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6753
7202
|
if (flags.manifest || flags.file) {
|
|
6754
7203
|
manifest = parseManifestFile(flags.manifest || flags.file);
|
|
6755
7204
|
}
|
|
7205
|
+
if (dataMode === 'prod' && !manifestRequiredEvidence(manifest).length && !flagEnabled(flags['no-contract'])) {
|
|
7206
|
+
console.error('');
|
|
7207
|
+
console.error('Warning: starting a prod-data verification envelope with no required evidence contract.');
|
|
7208
|
+
console.error('Attach a manifest with requiredEvidence before destructive work, or pass --no-contract when this is intentionally evidence-only.');
|
|
7209
|
+
console.error('');
|
|
7210
|
+
}
|
|
6756
7211
|
let statusResponse = null;
|
|
6757
7212
|
try {
|
|
6758
7213
|
statusResponse = await loggedPost(api, cwd, '/cli/components', {
|
|
@@ -6788,6 +7243,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6788
7243
|
console.log('Verification envelope started:', envelope.envelopeId);
|
|
6789
7244
|
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
6790
7245
|
printAcceptanceWarnings(envelope);
|
|
7246
|
+
printEnvelopeWarnings(envelope);
|
|
6791
7247
|
console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
|
|
6792
7248
|
return envelope;
|
|
6793
7249
|
}
|
|
@@ -6829,6 +7285,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6829
7285
|
console.log('Manifest attached to envelope:', envelopeId);
|
|
6830
7286
|
console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
|
|
6831
7287
|
printAcceptanceWarnings(response.envelope);
|
|
7288
|
+
printEnvelopeWarnings(response.envelope);
|
|
6832
7289
|
return response.envelope;
|
|
6833
7290
|
}
|
|
6834
7291
|
|
|
@@ -12521,11 +12978,12 @@ function printComponentsHelp(subcommand) {
|
|
|
12521
12978
|
return;
|
|
12522
12979
|
}
|
|
12523
12980
|
if (subcommand === 'lanes' || subcommand === 'lane') {
|
|
12524
|
-
console.log('Usage: remits-cli components lanes [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--json]');
|
|
12981
|
+
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]');
|
|
12525
12982
|
console.log('');
|
|
12526
12983
|
console.log('Lists every indexed staging lane on the account, across users, branches, and workspaces.');
|
|
12527
12984
|
console.log('This is read-only and uses the same lane registry that powers the admin Platforms view.');
|
|
12528
12985
|
console.log('Use `components entries --lane-id <id>` to inspect the actual staged files in a lane.');
|
|
12986
|
+
console.log('Use --wait-change to wait until the lane registry changes instead of polling in a loop.');
|
|
12529
12987
|
return;
|
|
12530
12988
|
}
|
|
12531
12989
|
if (subcommand === 'entries' || subcommand === 'entry' || subcommand === 'staged') {
|
|
@@ -12564,7 +13022,7 @@ function printComponentsHelp(subcommand) {
|
|
|
12564
13022
|
console.log('so several agents can iterate at once. See: remits-cli workspace --help');
|
|
12565
13023
|
console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--json|--verbose]');
|
|
12566
13024
|
console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE --component-id ID] [--json|--verbose]');
|
|
12567
|
-
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--json]');
|
|
13025
|
+
console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--wait-change [--lane-id ID] [--timeout 600]] [--json]');
|
|
12568
13026
|
console.log(' remits-cli components entries --lane-id ID [--base-url URL] [--account-id ID] [--json|--verbose]');
|
|
12569
13027
|
console.log(' remits-cli components clear [--base-url URL] [--account-id ID] [--branch BRANCH] [--component-type TYPE] [--component-id ID] [--all] [--json|--verbose]');
|
|
12570
13028
|
console.log(' remits-cli components sync [--safe] [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary]');
|
package/package.json
CHANGED
|
@@ -363,7 +363,9 @@ The analogous hazard is different, and you must still respect it:
|
|
|
363
363
|
explicitly.
|
|
364
364
|
- **Use `components sync --dry-run` before risky variant syncs.** It reports `overridden`, `added`,
|
|
365
365
|
`removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
|
|
366
|
-
staging.
|
|
366
|
+
staging. `overridden`/`added` are the branch's WHOLE overlay set; read the `Overlay changes:` line (or
|
|
367
|
+
`writes` in `--summary`) for what this sync actually changes, and `storedCurrent: true` on an entry for
|
|
368
|
+
an overlay that is already stored as-is. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
|
|
367
369
|
branch has converged back to trunk and the overlay would be removed. It is rejected on trunk, and
|
|
368
370
|
`components commit --dry-run` is unsupported because `commit` performs compile validation and local git
|
|
369
371
|
writes before syncing.
|
|
@@ -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
|
|
|
@@ -272,7 +275,10 @@ Existing commands also accept `--verify-envelope <id>` to attach to a specific e
|
|
|
272
275
|
Failed `remits-cli test run` output includes compact pivots per failed case: duration, trace id,
|
|
273
276
|
bounded `report(...)` diagnostics, live HTTP signals, and resolved component provenance when the
|
|
274
277
|
platform returns it. Re-read an existing run with `remits-cli test status --task-id <taskId>` before
|
|
275
|
-
rerunning a long suite.
|
|
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.
|
|
276
282
|
|
|
277
283
|
Test-run requirements must name the suite/cases they mean:
|
|
278
284
|
|
|
@@ -281,23 +287,28 @@ Test-run requirements must name the suite/cases they mean:
|
|
|
281
287
|
{"id":"one_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
|
|
282
288
|
```
|
|
283
289
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
tool
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
292
304
|
|
|
293
305
|
The manifest is the proof contract. It should name the world being exercised: repo account, host,
|
|
294
306
|
git/component branch, workspace, data mode, source layer, user journeys, artifacts and hashes, and the
|
|
295
307
|
evidence categories required before the final response may claim the work is done. If the manifest
|
|
296
308
|
cannot be written because the workflow is ambiguous, ask before implementation. For machine-gated report
|
|
297
309
|
requirements, prefer structured entries like `{"id":"actual_upload","packetType":"browser_step",
|
|
298
|
-
"category":"browser_session.actual_upload"}`. Plain strings work
|
|
299
|
-
|
|
300
|
-
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.
|
|
301
312
|
|
|
302
313
|
Common packet meanings:
|
|
303
314
|
|
|
@@ -318,14 +329,21 @@ Final claims should come from `remits-cli verify report`. Treat `Verified` as th
|
|
|
318
329
|
the report lists it under `Verified`. If the report says `partially_verified`, stale, or missing evidence,
|
|
319
330
|
say that plainly instead of widening the claim. In particular, staged proof is not committed variant/trunk
|
|
320
331
|
proof, a token is not browser proof, and a direct DOM or Alpine state mutation is not the same as a user
|
|
321
|
-
|
|
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.
|
|
322
339
|
|
|
323
340
|
For tests specifically:
|
|
324
341
|
- If `--data-mode` is omitted, `remits-cli test run` uses `test` and sends `dataModeSource:"cliDefault"`.
|
|
325
342
|
An explicit `--data-mode prod` sends `dataModeSource:"explicitFlag"` so production test runs are
|
|
326
343
|
auditable from the server status payload even when the original terminal history is gone.
|
|
327
|
-
- `--names` is `|`-delimited
|
|
328
|
-
selector fails the run instead of
|
|
344
|
+
- `--names` is `|`-delimited and may be repeated. A selector containing commas matches a case with that
|
|
345
|
+
exact name first, else its comma-separated parts; an unmatched selector fails the run instead of
|
|
346
|
+
reporting zero cases as success.
|
|
329
347
|
- `--as-account <ID>` runs AS a descendant subscriber so its edge selects the component branch
|
|
330
348
|
(*"what does customer X get?"*); `--variant-branch <name>` probes a branch from any checkout
|
|
331
349
|
(*"what does branch Y look like?"*), and `--variant-branch none` forces production/subscription semantics.
|
|
@@ -160,7 +160,10 @@ TestMode still uses the committed DB source. Staging remains a dev/verification
|
|
|
160
160
|
- **Inspect another lane without impersonating it:** `remits-cli components lanes` lists every indexed
|
|
161
161
|
lane on the account; `remits-cli components entries --lane-id <id>` reads the authoritative staged
|
|
162
162
|
files for one lane. These are read-only review surfaces. They do not switch your workspace, clear
|
|
163
|
-
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.
|
|
164
167
|
- **Clear only your own lane:** `remits-cli components clear` removes entries when you intentionally want
|
|
165
168
|
to fall back to DB source. `--all` is scoped to the command's account/user/branch/workspace lane, not
|
|
166
169
|
every lane another agent may be using.
|
|
@@ -209,7 +212,9 @@ commit write `ComponentVariant` overlays for a branch nobody subscribes to).
|
|
|
209
212
|
account's lane context in their responses. A shared current lane is printed loudly; sibling lanes are
|
|
210
213
|
listed when they matter.
|
|
211
214
|
- `remits-cli components lanes` is the review view across users/branches/workspaces, and
|
|
212
|
-
`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.
|
|
213
218
|
- `remits-cli components clear --all` is scoped to YOUR lane and never touches another agent's.
|
|
214
219
|
- In lane rows, `currentLane` (also `mine`) marks THIS command's lane; `ownedByCaller` marks every lane
|
|
215
220
|
staged by your CLI user — your other clones' agents included.
|
|
@@ -104,9 +104,13 @@ For Test requirements, be specific enough for the evaluator to know what a pass
|
|
|
104
104
|
{"id":"duplicate_fee_case", "packetType":"test_run", "suite":"Statement API Flow", "cases":["rejects duplicated fee evidence"]}
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
A requirement
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
110
114
|
|
|
111
115
|
## Development Workflow
|
|
112
116
|
|