@remits/remits-cli 0.1.125 → 0.1.127
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 +21 -19
- package/index.js +480 -37
- package/package.json +5 -2
- package/scripts/prepare-index-toc.js +51 -0
- package/skills/remits-cli/SKILL.md +5 -0
- package/skills/remits-cli/references/branch-variants.md +20 -5
- package/skills/remits-cli/references/command-reference.md +26 -2
- package/skills/remits-cli/references/component-resolution.md +7 -3
- package/skills/remits-cli/references/development-loop.md +16 -1
- package/skills/remits-cli/references/tool-reference.md +1 -1
- package/skills/remits-cli/references/troubleshooting.md +2 -1
package/index.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
## Table of Contents
|
|
5
|
+
|
|
6
|
+
- L22 Runtime Bootstrap And Shared State
|
|
7
|
+
- L116 Sessions, Account Resolution, And Production Guards
|
|
8
|
+
- L595 Local State, Workspaces, And Verification Evidence
|
|
9
|
+
- L1296 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L1954 Component Discovery And HTTP Logging
|
|
11
|
+
- L2444 Skill Delivery And TOC Resolution
|
|
12
|
+
- L2754 Auth And Component Staging
|
|
13
|
+
- L3352 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L5107 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L6231 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L7141 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L9236 Agent And Ticket Workflows
|
|
18
|
+
- L12294 Help, Auto Update, And Command Dispatch
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
## Runtime Bootstrap And Shared State
|
|
23
|
+
*/
|
|
24
|
+
|
|
3
25
|
const fs = require('fs');
|
|
4
26
|
const path = require('path');
|
|
5
27
|
const os = require('os');
|
|
@@ -90,6 +112,10 @@ const REPEATABLE_FLAGS = new Set(['expected-removed', 'expectedRemoved', 'names'
|
|
|
90
112
|
// a flag, never a value, and these aliases map it to its long name. Values like `-5` or `-abc` are untouched.
|
|
91
113
|
const SHORT_FLAG_ALIASES = { m: 'message', h: 'help' };
|
|
92
114
|
|
|
115
|
+
/*
|
|
116
|
+
## Sessions, Account Resolution, And Production Guards
|
|
117
|
+
*/
|
|
118
|
+
|
|
93
119
|
function isFlagToken(token) {
|
|
94
120
|
return typeof token === 'string' && (token.startsWith('--') || /^-[a-zA-Z]$/.test(token));
|
|
95
121
|
}
|
|
@@ -565,6 +591,10 @@ function resolveDataMode(flags, session) {
|
|
|
565
591
|
return DEFAULT_DATA_MODE;
|
|
566
592
|
}
|
|
567
593
|
|
|
594
|
+
/*
|
|
595
|
+
## Local State, Workspaces, And Verification Evidence
|
|
596
|
+
*/
|
|
597
|
+
|
|
568
598
|
function ensureDir(dirPath) {
|
|
569
599
|
if (!fs.existsSync(dirPath)) {
|
|
570
600
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -781,6 +811,42 @@ function writeToolResponseFile(cwd, callId, responsePayload) {
|
|
|
781
811
|
return file;
|
|
782
812
|
}
|
|
783
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
|
+
function boundedToolExcerpt(data = {}) {
|
|
825
|
+
const result = data.result && typeof data.result === 'object' ? data.result : {};
|
|
826
|
+
const candidates = [
|
|
827
|
+
data.message,
|
|
828
|
+
data.toolMessage,
|
|
829
|
+
data.error,
|
|
830
|
+
result.message,
|
|
831
|
+
result.error,
|
|
832
|
+
result.reason
|
|
833
|
+
].filter(Boolean).map((value) => String(value));
|
|
834
|
+
const excerpt = candidates.join(' | ').trim();
|
|
835
|
+
return excerpt ? excerpt.slice(0, 1200) : null;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function toolEvidenceSummary(data = {}, responseFile) {
|
|
839
|
+
return Object.assign({
|
|
840
|
+
callId: data.callId,
|
|
841
|
+
name: data.name,
|
|
842
|
+
async: data.async,
|
|
843
|
+
status: data.status,
|
|
844
|
+
toolSuccess: data.toolSuccess,
|
|
845
|
+
threadGroupingId: data.threadGroupingId,
|
|
846
|
+
message: boundedToolExcerpt(data)
|
|
847
|
+
}, fileEvidence(responseFile));
|
|
848
|
+
}
|
|
849
|
+
|
|
784
850
|
function readToolResponseFile(cwd, callId) {
|
|
785
851
|
if (!callId) return null;
|
|
786
852
|
const paths = ensureLocalState(cwd);
|
|
@@ -1070,12 +1136,28 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
|
|
|
1070
1136
|
}
|
|
1071
1137
|
}
|
|
1072
1138
|
|
|
1139
|
+
function printAcceptanceWarnings(envelope) {
|
|
1140
|
+
const warnings = envelope && envelope.acceptance && Array.isArray(envelope.acceptance.warnings)
|
|
1141
|
+
? envelope.acceptance.warnings
|
|
1142
|
+
: [];
|
|
1143
|
+
if (!warnings.length) return;
|
|
1144
|
+
console.log('Manifest warnings:');
|
|
1145
|
+
warnings.forEach((warning) => {
|
|
1146
|
+
console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1073
1150
|
function buildCommandWorld(response, fallback = {}) {
|
|
1074
1151
|
const source = (response && response.staging) || {};
|
|
1075
1152
|
const resolution = (response && response.resolution) || {};
|
|
1153
|
+
const result = (response && response.result) || {};
|
|
1154
|
+
const laneHash = laneContentHashFrom(response) || fallback.laneContentHash || fallback.stagedOverlayHash;
|
|
1155
|
+
const executionAccountId = response && (response.executionAccountId || result.accountId || source.executionAccountId || response.accountId) ||
|
|
1156
|
+
resolution.executionAccountId ||
|
|
1157
|
+
fallback.executionAccountId;
|
|
1076
1158
|
return {
|
|
1077
1159
|
accountId: response && (response.accountId || response.ownerAccountId) || fallback.accountId,
|
|
1078
|
-
executionAccountId
|
|
1160
|
+
executionAccountId,
|
|
1079
1161
|
addressedAccountId: resolution.addressedAccountId || fallback.addressedAccountId,
|
|
1080
1162
|
host: fallback.host,
|
|
1081
1163
|
dataMode: response && response.dataMode || fallback.dataMode,
|
|
@@ -1083,12 +1165,26 @@ function buildCommandWorld(response, fallback = {}) {
|
|
|
1083
1165
|
branchName: response && response.branchName || fallback.branchName,
|
|
1084
1166
|
workspace: response && response.workspace !== undefined ? response.workspace : fallback.workspace,
|
|
1085
1167
|
stagingLane: response && response.stagingLane || source.stagingLane || fallback.stagingLane,
|
|
1086
|
-
|
|
1168
|
+
laneContentHash: laneHash,
|
|
1169
|
+
stagedOverlayHash: laneHash,
|
|
1170
|
+
// `variantWorld.componentBranch` is the platform's answer for the run (the variant branch, else the
|
|
1171
|
+
// subscription); a bare `variantBranch` is null whenever the subscription decided.
|
|
1172
|
+
componentBranch: resolution.componentBranch ||
|
|
1173
|
+
response && ((response.variantWorld && response.variantWorld.componentBranch) || response.variantBranch) ||
|
|
1174
|
+
fallback.componentBranch,
|
|
1175
|
+
variantBranchSource: response && (response.variantBranchSource || (response.variantWorld && response.variantWorld.variantBranchSource)) || fallback.variantBranchSource,
|
|
1087
1176
|
variantId: resolution.variantId || fallback.variantId,
|
|
1088
1177
|
sourceLayer: fallback.sourceLayer || (source.testComponentSource === 'staged' ? 'staged' : undefined)
|
|
1089
1178
|
};
|
|
1090
1179
|
}
|
|
1091
1180
|
|
|
1181
|
+
function laneContentHashFrom(response) {
|
|
1182
|
+
if (!response) return null;
|
|
1183
|
+
const laneSummary = response.laneSummary || (response.staging && response.staging.laneSummary) ||
|
|
1184
|
+
(response.componentStatus && response.componentStatus.laneSummary);
|
|
1185
|
+
return laneSummary && laneSummary.contentHash ? laneSummary.contentHash : null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1092
1188
|
function normalizeName(name) {
|
|
1093
1189
|
return name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim();
|
|
1094
1190
|
}
|
|
@@ -1196,6 +1292,10 @@ function loadAccountInfo(cwd) {
|
|
|
1196
1292
|
}
|
|
1197
1293
|
}
|
|
1198
1294
|
|
|
1295
|
+
/*
|
|
1296
|
+
## Account Repos, Guide Sync, And Platform Repo
|
|
1297
|
+
*/
|
|
1298
|
+
|
|
1199
1299
|
function readAccountRepoIndex() {
|
|
1200
1300
|
if (!fs.existsSync(ACCOUNT_REPO_INDEX_FILE)) {
|
|
1201
1301
|
return {};
|
|
@@ -1850,6 +1950,10 @@ function resolveSessionContext(cwd, flags) {
|
|
|
1850
1950
|
};
|
|
1851
1951
|
}
|
|
1852
1952
|
|
|
1953
|
+
/*
|
|
1954
|
+
## Component Discovery And HTTP Logging
|
|
1955
|
+
*/
|
|
1956
|
+
|
|
1853
1957
|
/**
|
|
1854
1958
|
* Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
|
|
1855
1959
|
* sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
|
|
@@ -2336,6 +2440,10 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
|
|
|
2336
2440
|
}
|
|
2337
2441
|
}
|
|
2338
2442
|
|
|
2443
|
+
/*
|
|
2444
|
+
## Skill Delivery And TOC Resolution
|
|
2445
|
+
*/
|
|
2446
|
+
|
|
2339
2447
|
// ---------------------------------------------------------------------------
|
|
2340
2448
|
// Skill delivery
|
|
2341
2449
|
//
|
|
@@ -2465,6 +2573,14 @@ function resolveTocEntry(line, byAnchor, byTitle) {
|
|
|
2465
2573
|
return formatTocEntry(legacyEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
|
|
2466
2574
|
}
|
|
2467
2575
|
|
|
2576
|
+
// Prepared form. This makes generated TOCs idempotent: a later package/build pass can re-derive the
|
|
2577
|
+
// current line numbers from the emitted heading text instead of preserving stale numbers.
|
|
2578
|
+
const preparedEntry = /^(\s*)[-*]\s+L\d+\s\s(.+?)\s*$/.exec(line);
|
|
2579
|
+
if (preparedEntry) {
|
|
2580
|
+
const title = preparedEntry[2];
|
|
2581
|
+
return formatTocEntry(preparedEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2468
2584
|
return null;
|
|
2469
2585
|
}
|
|
2470
2586
|
|
|
@@ -2634,6 +2750,10 @@ async function installSkillsCommand(flags) {
|
|
|
2634
2750
|
}
|
|
2635
2751
|
}
|
|
2636
2752
|
|
|
2753
|
+
/*
|
|
2754
|
+
## Auth And Component Staging
|
|
2755
|
+
*/
|
|
2756
|
+
|
|
2637
2757
|
async function authCommand(flags) {
|
|
2638
2758
|
const cwd = process.cwd();
|
|
2639
2759
|
ensureLocalState(cwd);
|
|
@@ -2999,6 +3119,7 @@ async function pushComponentsCommand(flags) {
|
|
|
2999
3119
|
rawRefs: { command: 'components stage' },
|
|
3000
3120
|
stage: (err.responseBody || {}).stage,
|
|
3001
3121
|
compileValidation: (err.responseBody || {}).compileValidation,
|
|
3122
|
+
refusalDetails: stageRefusalDetails(err.responseBody || {}),
|
|
3002
3123
|
changedFromWorkingTree: changedFromWorkingTree || [],
|
|
3003
3124
|
unrepresentableChanges: unstageable
|
|
3004
3125
|
}, { quiet: true });
|
|
@@ -3033,6 +3154,7 @@ async function pushComponentsCommand(flags) {
|
|
|
3033
3154
|
rawRefs: { command: 'components stage' },
|
|
3034
3155
|
stage: response.stage,
|
|
3035
3156
|
laneSummary: response.laneSummary,
|
|
3157
|
+
compileValidation: response.compileValidation || null,
|
|
3036
3158
|
changedFromWorkingTree: response.changedFromWorkingTree,
|
|
3037
3159
|
unrepresentableChanges: response.unrepresentableChanges
|
|
3038
3160
|
}, { quiet: flagEnabled(flags.json) });
|
|
@@ -3218,6 +3340,18 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
3218
3340
|
}
|
|
3219
3341
|
}
|
|
3220
3342
|
|
|
3343
|
+
function stageRefusalDetails(body = {}) {
|
|
3344
|
+
const details = {};
|
|
3345
|
+
['duplicates', 'compileValidation', 'policy', 'editLease', 'skipped', 'reconcile', 'laneHoldsRejectedSource'].forEach((key) => {
|
|
3346
|
+
if (body[key] !== undefined) details[key] = sanitizeForLog(body[key]);
|
|
3347
|
+
});
|
|
3348
|
+
return Object.keys(details).length ? details : undefined;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
/*
|
|
3352
|
+
## Component Summaries, Status, And Sync Gates
|
|
3353
|
+
*/
|
|
3354
|
+
|
|
3221
3355
|
/** What the account's rules said about this stage or commit — including "they could not be read". */
|
|
3222
3356
|
function printComponentPolicy(response) {
|
|
3223
3357
|
const policy = response && response.policy;
|
|
@@ -3269,6 +3403,10 @@ function printCompileValidation(validation, options) {
|
|
|
3269
3403
|
// A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
|
|
3270
3404
|
// component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
|
|
3271
3405
|
const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
|
|
3406
|
+
const warnings = Array.isArray(validation.warnings) ? validation.warnings : [];
|
|
3407
|
+
warnings.forEach((warning) => {
|
|
3408
|
+
emit(' Warning: ' + (warning.message || JSON.stringify(warning)));
|
|
3409
|
+
});
|
|
3272
3410
|
if (unchecked.length) {
|
|
3273
3411
|
emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
|
|
3274
3412
|
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
|
|
@@ -3433,6 +3571,9 @@ function printStageSummary(response, flags) {
|
|
|
3433
3571
|
// The CLI's own findings (deleted files) and the platform's (a README withheld on a variant branch) are
|
|
3434
3572
|
// different sources. Preferring the first whenever it existed — even as an empty list — hid the second.
|
|
3435
3573
|
printUnrepresentableChanges(mergeUnrepresentable(response.unrepresentableChanges, stage.unrepresentable));
|
|
3574
|
+
if (typeof printOrphanedWorkspaceWarning === 'function') {
|
|
3575
|
+
printOrphanedWorkspaceWarning(response);
|
|
3576
|
+
}
|
|
3436
3577
|
if (typeof printAccountLanes === 'function') {
|
|
3437
3578
|
printAccountLanes(response);
|
|
3438
3579
|
}
|
|
@@ -3458,6 +3599,7 @@ function printStatusSummary(response, flags) {
|
|
|
3458
3599
|
printBranchContext(response);
|
|
3459
3600
|
printStagingLane(response.branchName, response.workspace, null);
|
|
3460
3601
|
printLaneSummary(response);
|
|
3602
|
+
printOrphanedWorkspaceWarning(response);
|
|
3461
3603
|
printStagingFreshness(response.freshness);
|
|
3462
3604
|
printRepositoryCheck(response.repositoryCheck);
|
|
3463
3605
|
printComponentTypeCounts(response.entries || []);
|
|
@@ -3499,9 +3641,7 @@ function printLanesSummary(response, flags) {
|
|
|
3499
3641
|
}
|
|
3500
3642
|
|
|
3501
3643
|
function printStagingLaneRow(lane) {
|
|
3502
|
-
const world = lane
|
|
3503
|
-
: lane.onTrunk === false ? 'variant'
|
|
3504
|
-
: 'unknown-world';
|
|
3644
|
+
const world = laneWorldLabel(lane);
|
|
3505
3645
|
const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : ' [shared]';
|
|
3506
3646
|
const workset = lane.worksetKnown && lane.worksetCountAsOf != null
|
|
3507
3647
|
? ' workset ' + lane.worksetCountAsOf
|
|
@@ -3598,10 +3738,7 @@ function printAccountLanes(response) {
|
|
|
3598
3738
|
console.log('');
|
|
3599
3739
|
console.log('Other staging lanes on this ACCOUNT (as of the last stage/clear in each):');
|
|
3600
3740
|
lanes.forEach((lane) => {
|
|
3601
|
-
|
|
3602
|
-
const world = lane.onTrunk === true ? 'trunk'
|
|
3603
|
-
: lane.onTrunk === false ? 'variant branch'
|
|
3604
|
-
: 'branch world unknown';
|
|
3741
|
+
const world = laneWorldLabel(lane);
|
|
3605
3742
|
const ws = lane.workspace ? ' [ws:' + lane.workspace + ']' : '';
|
|
3606
3743
|
const who = lane.userEmail ? ' ' + lane.userEmail : '';
|
|
3607
3744
|
const ttl = lane.expiresInSeconds != null
|
|
@@ -3617,7 +3754,51 @@ function printAccountLanes(response) {
|
|
|
3617
3754
|
});
|
|
3618
3755
|
console.log(' (* = this command\'s lane)');
|
|
3619
3756
|
console.log(' A variant-branch lane layers over that branch\'s ComponentVariant overlays, and a commit');
|
|
3620
|
-
console.log(' from it writes overlays — never the trunk rows a trunk lane commits to.');
|
|
3757
|
+
console.log(' from it writes overlays — never the trunk rows a trunk lane commits to. A feature-branch lane');
|
|
3758
|
+
console.log(' resolves the account\'s subscription beneath its staged entries and cannot be landed.');
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function printOrphanedWorkspaceWarning(response) {
|
|
3762
|
+
const currentCount = Number((response && response.stagedCount) ||
|
|
3763
|
+
(response && response.laneSummary && response.laneSummary.stagedCount) || 0);
|
|
3764
|
+
if (currentCount > 0) return;
|
|
3765
|
+
const lanes = Array.isArray(response && response.accountLanes) ? response.accountLanes : [];
|
|
3766
|
+
const currentBranch = response && response.branchName;
|
|
3767
|
+
const currentWorkspace = response && response.workspace;
|
|
3768
|
+
const orphan = lanes.find((lane) => lane && lane.sameWorkspaceOtherBranch === true);
|
|
3769
|
+
if (!orphan) return;
|
|
3770
|
+
const age = orphan.updatedAtMs ? formatAge(Date.now() - Number(orphan.updatedAtMs)) + ' ago' : 'recently';
|
|
3771
|
+
console.log('');
|
|
3772
|
+
console.log('WARNING: Your workspace "' + (currentWorkspace || orphan.workspace || 'shared') + '" has ' +
|
|
3773
|
+
(orphan.stagedCountAsOf || orphan.stagedCount || 0) + ' staged entr' +
|
|
3774
|
+
((orphan.stagedCountAsOf || orphan.stagedCount || 0) === 1 ? 'y' : 'ies') +
|
|
3775
|
+
' on branch "' + orphan.branchName + '" (staged ' + age + '); this checkout is now on "' +
|
|
3776
|
+
(currentBranch || 'unknown') + '". Re-stage here, or switch back.');
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
function formatAge(ms) {
|
|
3780
|
+
const value = Number(ms);
|
|
3781
|
+
if (!Number.isFinite(value) || value < 0) return 'unknown age';
|
|
3782
|
+
const seconds = Math.round(value / 1000);
|
|
3783
|
+
if (seconds < 90) return seconds + 's';
|
|
3784
|
+
const minutes = Math.round(seconds / 60);
|
|
3785
|
+
if (minutes < 90) return minutes + 'm';
|
|
3786
|
+
const hours = Math.round(minutes / 60);
|
|
3787
|
+
if (hours < 48) return hours + 'h';
|
|
3788
|
+
return Math.round(hours / 24) + 'd';
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
// The world a lane row resolves, from the platform's answer. null onTrunk means the owner could not be
|
|
3792
|
+
// resolved, and a non-trunk lane without a reported source (an older platform) is only known to be
|
|
3793
|
+
// non-trunk — never guessed to be a variant branch. Pure.
|
|
3794
|
+
function laneWorldLabel(lane) {
|
|
3795
|
+
if (!lane || lane.onTrunk == null) return 'branch world unknown';
|
|
3796
|
+
if (lane.onTrunk === true) return 'trunk';
|
|
3797
|
+
if (lane.variantBranchSource === 'subscription-fallback') return 'feature branch';
|
|
3798
|
+
if (lane.variantBranchSource === 'branch-has-variants' || lane.variantBranchSource === 'branch-has-subscribers') {
|
|
3799
|
+
return 'variant branch';
|
|
3800
|
+
}
|
|
3801
|
+
return 'non-trunk branch';
|
|
3621
3802
|
}
|
|
3622
3803
|
|
|
3623
3804
|
function isCurrentStagingLane(lane) {
|
|
@@ -3638,10 +3819,15 @@ function printBranchContext(response) {
|
|
|
3638
3819
|
console.log('');
|
|
3639
3820
|
if (ctx.onTrunk) {
|
|
3640
3821
|
console.log('Working tree: TRUNK (' + response.branchName + ')');
|
|
3822
|
+
} else if (ctx.variantBranchSource === 'subscription-fallback') {
|
|
3823
|
+
console.log('Working tree: FEATURE BRANCH "' + response.branchName + '" (not a variant branch; trunk is "' + ctx.trunkBranch + '")');
|
|
3641
3824
|
} else {
|
|
3642
3825
|
console.log('Working tree: VARIANT BRANCH "' + response.branchName + '" (trunk is "' + ctx.trunkBranch + '")');
|
|
3643
3826
|
}
|
|
3644
3827
|
console.log(' runs resolve: ' + ctx.resolves);
|
|
3828
|
+
if (ctx.variantBranchSource) {
|
|
3829
|
+
console.log(' variant world: ' + (ctx.variantBranch || 'none (subscription)') + ' [' + ctx.variantBranchSource + ']');
|
|
3830
|
+
}
|
|
3645
3831
|
console.log(' commit writes: ' + ctx.commitWrites);
|
|
3646
3832
|
if (Object.prototype.hasOwnProperty.call(ctx, 'lastSyncedSha')) {
|
|
3647
3833
|
// null is "unknown" (never synced, expired, or the last sync had errors) — never "nothing landed".
|
|
@@ -3755,6 +3941,9 @@ async function statusComponentsCommand(flags) {
|
|
|
3755
3941
|
workspace,
|
|
3756
3942
|
dataMode,
|
|
3757
3943
|
mode: 'status',
|
|
3944
|
+
// So "runs resolve" describes the run these flags would launch, by the same server-side rule.
|
|
3945
|
+
variantBranch: flags['variant-branch'],
|
|
3946
|
+
asAccountId: flags['as-account'] || flags['as-account-id'],
|
|
3758
3947
|
componentType: flags['component-type'] || flags.type,
|
|
3759
3948
|
componentId: flags['component-id'] || flags.id
|
|
3760
3949
|
}).then((r) => r.data);
|
|
@@ -3767,6 +3956,7 @@ async function statusComponentsCommand(flags) {
|
|
|
3767
3956
|
// supplies the facts (the last SHA it synced, each entry's base) and the comparison happens here.
|
|
3768
3957
|
response.freshness = stagingFreshness({
|
|
3769
3958
|
head: safeGitValue(cwd, 'git rev-parse HEAD'),
|
|
3959
|
+
originHead: safeGitValue(cwd, 'git rev-parse ' + shellQuote('origin/' + branchName)),
|
|
3770
3960
|
lastSyncedSha: response.branchContext && response.branchContext.lastSyncedSha,
|
|
3771
3961
|
entries: response.entries,
|
|
3772
3962
|
laneSummary: response.laneSummary,
|
|
@@ -3983,6 +4173,9 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3983
4173
|
...runContextPayload(flags),
|
|
3984
4174
|
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined,
|
|
3985
4175
|
acknowledge: flags.acknowledge || undefined,
|
|
4176
|
+
// The platform refuses to sync a branch that is not a variant branch (see featureBranchLandingRefusal);
|
|
4177
|
+
// this is the explicit statement that a NEW variant branch is being created on purpose.
|
|
4178
|
+
createVariantBranch: flagEnabled(flags['create-variant-branch']) || undefined,
|
|
3986
4179
|
components: []
|
|
3987
4180
|
};
|
|
3988
4181
|
|
|
@@ -4005,7 +4198,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4005
4198
|
// --safe is fail-closed: a sync that would reconcile a repository other than this checkout's is refused.
|
|
4006
4199
|
const preflightRepoCheck = repositoryCheck(cwd, branchContext);
|
|
4007
4200
|
if (preflightRepoCheck.matches === false) {
|
|
4008
|
-
const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync');
|
|
4201
|
+
const mismatch = repositoryMismatchMessage(preflightRepoCheck, 'components sync', branchContext);
|
|
4009
4202
|
if (flagEnabled(flags.json)) {
|
|
4010
4203
|
const refusal = {
|
|
4011
4204
|
success: false,
|
|
@@ -4132,7 +4325,7 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
4132
4325
|
response.repositoryCheck = repositoryCheck(cwd, { repository: response.sync && response.sync.repository });
|
|
4133
4326
|
if (response.repositoryCheck.matches === false) {
|
|
4134
4327
|
(flagEnabled(flags.json) ? console.error : console.log)('WARNING: ' +
|
|
4135
|
-
repositoryMismatchMessage(response.repositoryCheck, 'components sync'));
|
|
4328
|
+
repositoryMismatchMessage(response.repositoryCheck, 'components sync', response.branchContext));
|
|
4136
4329
|
}
|
|
4137
4330
|
const summary = buildSyncSummary(response);
|
|
4138
4331
|
summary.gates = gate.checks;
|
|
@@ -4550,11 +4743,66 @@ function repositoryCheck(cwd, branchContext) {
|
|
|
4550
4743
|
return { platformRepository, checkoutOrigin, matches: repositoryNamesMatch(platformRepository, checkoutOrigin) };
|
|
4551
4744
|
}
|
|
4552
4745
|
|
|
4553
|
-
function repositoryMismatchMessage(check, commandName) {
|
|
4746
|
+
function repositoryMismatchMessage(check, commandName, branchContext) {
|
|
4554
4747
|
return commandName + ': this checkout\'s origin is ' + check.checkoutOrigin + ', but the platform syncs repository "' +
|
|
4555
4748
|
check.platformRepository + '" for this account. A push from here would never be synced, and the sync would ' +
|
|
4556
|
-
'reconcile a repository you are not editing.
|
|
4557
|
-
'this command resolved
|
|
4749
|
+
'reconcile a repository you are not editing. ' + (featureBranchLandingHint(branchContext) ||
|
|
4750
|
+
('Run it from that repository\'s checkout, or check which account this command resolved ' +
|
|
4751
|
+
'(account-info.json, --account-id).'));
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
// A git branch that is not a variant world (no committed variants, no subscribers) resolves the account's
|
|
4755
|
+
// SUBSCRIBED branch beneath its staged entries — the platform reports that as
|
|
4756
|
+
// `variantBranchSource: 'subscription-fallback'`. Syncing such a branch would write overlays nobody resolves,
|
|
4757
|
+
// so the way to land it is through the branch it resolves. Pure; null when the context says nothing of the kind.
|
|
4758
|
+
function featureBranchLandingHint(branchContext) {
|
|
4759
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
4760
|
+
const subscribed = branchContext.subscribedComponentBranch;
|
|
4761
|
+
const trunk = branchContext.trunkBranch || 'trunk';
|
|
4762
|
+
// Without a subscription the platform cannot know which branch this was cut from; naming trunk would send a
|
|
4763
|
+
// branch cut from a variant branch into trunk.
|
|
4764
|
+
return 'This git branch is not a variant branch: runs from it resolve ' +
|
|
4765
|
+
(subscribed ? 'the \'' + subscribed + '\' variant branch this account subscribes to' : 'trunk') +
|
|
4766
|
+
' plus what you staged. To land the work, ' +
|
|
4767
|
+
(subscribed
|
|
4768
|
+
? 'merge this branch into \'' + subscribed + '\' and commit/sync \'' + subscribed + '\' from that checkout.'
|
|
4769
|
+
: 'merge this branch into the branch you cut it from (trunk \'' + trunk + '\', or the variant branch it came ' +
|
|
4770
|
+
'from) and commit/sync that branch from its checkout.');
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// Refuse a commit from a branch the platform reports as NOT a variant branch, unless the caller says it is
|
|
4774
|
+
// creating one. Mirrors the server-side refusal so nothing is pushed that cannot be synced. Pure.
|
|
4775
|
+
function featureBranchCommitRefusal(branchContext, branchName, flags) {
|
|
4776
|
+
if (!branchContext || branchContext.variantBranchSource !== 'subscription-fallback') return null;
|
|
4777
|
+
if (flags && (flags['create-variant-branch'] === true || flags['create-variant-branch'] === 'true')) return null;
|
|
4778
|
+
return 'components commit refused: \'' + branchName + '\' is not a variant branch (no committed variants, no ' +
|
|
4779
|
+
'subscribers). ' + featureBranchLandingHint(branchContext) + ' Committing here would write overlays for \'' +
|
|
4780
|
+
branchName + '\' that nobody subscribes to, and flip every other lane on that branch to them. If you are ' +
|
|
4781
|
+
'deliberately creating a NEW variant branch, re-run with --create-variant-branch.';
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
// What a verification envelope records as the world it verified. Taken from the platform's answer
|
|
4785
|
+
// (`variantWorld`, the same rule every run uses), never inferred from `onTrunk`: a feature branch cut from a
|
|
4786
|
+
// variant branch is not on trunk, yet resolves the SUBSCRIBED branch, and labelling it by its own name recorded
|
|
4787
|
+
// a world no run executed in. Pure.
|
|
4788
|
+
function verificationWorldFromStatus(statusResponse, branchName) {
|
|
4789
|
+
if (!statusResponse) return {};
|
|
4790
|
+
const ctx = statusResponse.branchContext || {};
|
|
4791
|
+
const world = statusResponse.variantWorld || null;
|
|
4792
|
+
const staged = !!(statusResponse.laneSummary && statusResponse.laneSummary.stagedCount > 0);
|
|
4793
|
+
let componentBranch;
|
|
4794
|
+
if (world) {
|
|
4795
|
+
componentBranch = world.componentBranch || 'trunk';
|
|
4796
|
+
} else {
|
|
4797
|
+
// An older platform that does not report the world: the previous inference, kept for compatibility.
|
|
4798
|
+
componentBranch = ctx.onTrunk ? 'trunk' : branchName;
|
|
4799
|
+
}
|
|
4800
|
+
const sourceLayer = staged ? 'staged' : (componentBranch && componentBranch !== 'trunk' ? 'variant' : 'trunk');
|
|
4801
|
+
return {
|
|
4802
|
+
componentBranch,
|
|
4803
|
+
sourceLayer,
|
|
4804
|
+
variantBranchSource: world ? world.variantBranchSource : undefined
|
|
4805
|
+
};
|
|
4558
4806
|
}
|
|
4559
4807
|
|
|
4560
4808
|
function printRepositoryCheck(check) {
|
|
@@ -4622,7 +4870,7 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
4622
4870
|
* `stageBaseSha`; only the local checkout can order commits, so the comparison happens here. Pure — git is
|
|
4623
4871
|
* injected as `isAncestor(a, b)` — so it can be tested without a repository.
|
|
4624
4872
|
*/
|
|
4625
|
-
function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
4873
|
+
function stagingFreshness({ head, originHead, lastSyncedSha, entries, laneSummary, isAncestor }) {
|
|
4626
4874
|
const rows = Array.isArray(entries) ? entries : [];
|
|
4627
4875
|
const staged = rows.filter((entry) => entry && entry.stageBaseSha);
|
|
4628
4876
|
const memo = new Map();
|
|
@@ -4648,7 +4896,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
|
|
|
4648
4896
|
|
|
4649
4897
|
return {
|
|
4650
4898
|
head: head || null,
|
|
4899
|
+
originHead: originHead || null,
|
|
4651
4900
|
lastSyncedSha: lastSyncedSha || null,
|
|
4901
|
+
freshnessUnknownReason: !originHead ? 'branch has no upstream' : (!lastSyncedSha ? 'platform has no last synced SHA for this branch' : null),
|
|
4652
4902
|
landedSinceHead,
|
|
4653
4903
|
entriesBehindLastSync: behindLastSync.length,
|
|
4654
4904
|
entriesFromOtherCommits: fromOtherCommits.length,
|
|
@@ -4662,6 +4912,9 @@ function stagingFreshness({ head, lastSyncedSha, entries, laneSummary, isAncesto
|
|
|
4662
4912
|
function printStagingFreshness(freshness) {
|
|
4663
4913
|
if (!freshness) return;
|
|
4664
4914
|
const plural = (n) => n + ' staged entr' + (n === 1 ? 'y' : 'ies');
|
|
4915
|
+
if (freshness.freshnessUnknownReason) {
|
|
4916
|
+
console.log('Freshness unknown:', freshness.freshnessUnknownReason + '.');
|
|
4917
|
+
}
|
|
4665
4918
|
if (freshness.landedSinceHead === true) {
|
|
4666
4919
|
console.log('');
|
|
4667
4920
|
console.log('LANDED SINCE YOUR BASE: the platform last synced ' + short(freshness.lastSyncedSha) +
|
|
@@ -4850,6 +5103,10 @@ function buildSyncSummary(response) {
|
|
|
4850
5103
|
};
|
|
4851
5104
|
}
|
|
4852
5105
|
|
|
5106
|
+
/*
|
|
5107
|
+
## Branches, Promotion, Commit, And Test Runs
|
|
5108
|
+
*/
|
|
5109
|
+
|
|
4853
5110
|
// Inspect committed branch variants: durable, branch-scoped overlays of this account's components.
|
|
4854
5111
|
// Unlike `components status` (which shows the ephemeral Redis staging cache), these are what
|
|
4855
5112
|
// subscribing accounts actually resolve in production.
|
|
@@ -5457,7 +5714,7 @@ async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, sk
|
|
|
5457
5714
|
// `--skip-git` pushes nothing, so there is nothing to mis-deliver.
|
|
5458
5715
|
const repoCheck = skipGit ? null : repositoryCheck(cwd, branchContext);
|
|
5459
5716
|
if (repoCheck && repoCheck.matches === false) {
|
|
5460
|
-
const mismatch = repositoryMismatchMessage(repoCheck, 'components commit');
|
|
5717
|
+
const mismatch = repositoryMismatchMessage(repoCheck, 'components commit', branchContext);
|
|
5461
5718
|
if (flagEnabled(flags.json)) {
|
|
5462
5719
|
console.log(JSON.stringify({
|
|
5463
5720
|
success: false,
|
|
@@ -5478,6 +5735,30 @@ async function refuseUnsafeTrunkCommitBeforeGit(flags, accountId, branchName, sk
|
|
|
5478
5735
|
}
|
|
5479
5736
|
throw new Error(mismatch + ' No git commit or push was attempted.');
|
|
5480
5737
|
}
|
|
5738
|
+
// Before ANY git write: is this a feature branch (not a variant branch)? The platform refuses its sync, and
|
|
5739
|
+
// a commit pushed first would leave a pushed-but-unsyncable branch. Same decision, taken before the push.
|
|
5740
|
+
const featureRefusal = featureBranchCommitRefusal(branchContext, branchName, flags);
|
|
5741
|
+
if (featureRefusal) {
|
|
5742
|
+
if (flagEnabled(flags.json)) {
|
|
5743
|
+
console.log(JSON.stringify({
|
|
5744
|
+
success: false,
|
|
5745
|
+
mode: 'commit',
|
|
5746
|
+
dataMode,
|
|
5747
|
+
accountId,
|
|
5748
|
+
branchName,
|
|
5749
|
+
workspace,
|
|
5750
|
+
branchContext,
|
|
5751
|
+
refusal: 'feature_branch_landing',
|
|
5752
|
+
phase: 'pre-git',
|
|
5753
|
+
gitWritten: false,
|
|
5754
|
+
gateViolations: [featureRefusal],
|
|
5755
|
+
message: featureRefusal
|
|
5756
|
+
}, null, 2));
|
|
5757
|
+
process.exitCode = 1;
|
|
5758
|
+
return true;
|
|
5759
|
+
}
|
|
5760
|
+
throw new Error(featureRefusal + ' No git commit or push was attempted.');
|
|
5761
|
+
}
|
|
5481
5762
|
if (!flagEnabled(flags.yes) && branchContextIsTrunk(branchContext)) {
|
|
5482
5763
|
printTrunkSyncWarning(branchContext ? branchContext.trunkBranch || branchName : branchName, {
|
|
5483
5764
|
stderr: flagEnabled(flags.json)
|
|
@@ -5594,6 +5875,84 @@ async function waitForStatus(api, cwd, accountId, branchName, taskId, token, dat
|
|
|
5594
5875
|
}
|
|
5595
5876
|
}
|
|
5596
5877
|
|
|
5878
|
+
function testEvidenceCategories(status = {}, selectedNames = []) {
|
|
5879
|
+
const categories = new Set(['test_run']);
|
|
5880
|
+
const test = status.test || {};
|
|
5881
|
+
if (test.name) categories.add('test_run.suite:' + String(test.name));
|
|
5882
|
+
if (test.id !== undefined && test.id !== null) categories.add('test_run.suite_id:' + String(test.id));
|
|
5883
|
+
(Array.isArray(selectedNames) ? selectedNames : []).forEach((name) => {
|
|
5884
|
+
if (name) categories.add('test_run.' + String(name));
|
|
5885
|
+
});
|
|
5886
|
+
const results = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
5887
|
+
results.forEach((result) => {
|
|
5888
|
+
if (result && result.passed === true && result.name) {
|
|
5889
|
+
categories.add('test_run.case:' + String(result.name));
|
|
5890
|
+
categories.add('test_run.' + String(result.name));
|
|
5891
|
+
}
|
|
5892
|
+
});
|
|
5893
|
+
return Array.from(categories);
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
function testComponentProvenance(status = {}) {
|
|
5897
|
+
const direct = status.resolvedComponentProvenance || (status.result && status.result.resolvedComponentProvenance) ||
|
|
5898
|
+
(status.staging && status.staging.resolvedComponentProvenance);
|
|
5899
|
+
if (Array.isArray(direct)) return direct;
|
|
5900
|
+
const staging = status.staging || {};
|
|
5901
|
+
if (staging.testComponentSignature) {
|
|
5902
|
+
return [{
|
|
5903
|
+
type: 'test',
|
|
5904
|
+
id: status.test && status.test.id,
|
|
5905
|
+
name: status.test && status.test.name,
|
|
5906
|
+
signature: staging.testComponentSignature,
|
|
5907
|
+
source: staging.testComponentSource || null
|
|
5908
|
+
}];
|
|
5909
|
+
}
|
|
5910
|
+
return [];
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
function testCompileSignatures(status = {}) {
|
|
5914
|
+
return testComponentProvenance(status)
|
|
5915
|
+
.map((entry) => entry && entry.signature)
|
|
5916
|
+
.filter(Boolean);
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5919
|
+
function printTestRunPivots(status = {}) {
|
|
5920
|
+
const tests = status.result && Array.isArray(status.result.tests) ? status.result.tests : [];
|
|
5921
|
+
if (!tests.length) return;
|
|
5922
|
+
const slowest = tests.slice().sort((a, b) => Number(b.duration || 0) - Number(a.duration || 0))[0];
|
|
5923
|
+
if (slowest && slowest.duration != null) {
|
|
5924
|
+
console.log('Slowest case:', (slowest.name || '(unnamed)') + ' in ' + formatDurationMs(slowest.duration));
|
|
5925
|
+
}
|
|
5926
|
+
tests.filter((test) => test && test.passed === false).forEach((test) => {
|
|
5927
|
+
console.log('Pivots for failed case:', test.name || '(unnamed)');
|
|
5928
|
+
if (test.duration != null) console.log(' Duration:', formatDurationMs(test.duration));
|
|
5929
|
+
if (test.threadGroupingId || test.threadGroupId || test.traceId) {
|
|
5930
|
+
console.log(' Trace:', test.traceId || test.threadGroupingId || test.threadGroupId);
|
|
5931
|
+
}
|
|
5932
|
+
if (test.error) console.log(' Error:', String(test.error).slice(0, 500));
|
|
5933
|
+
const diagnostics = test.diagnostics && typeof test.diagnostics === 'object' ? test.diagnostics : null;
|
|
5934
|
+
if (diagnostics && Object.keys(diagnostics).length) {
|
|
5935
|
+
console.log(' Diagnostics:', JSON.stringify(diagnostics).slice(0, 1200));
|
|
5936
|
+
}
|
|
5937
|
+
if (Array.isArray(test.resolvedComponentProvenance) && test.resolvedComponentProvenance.length) {
|
|
5938
|
+
console.log(' Components:', test.resolvedComponentProvenance.slice(0, 8).map((entry) =>
|
|
5939
|
+
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') +
|
|
5940
|
+
(entry.source ? ' [' + entry.source + ']' : '') +
|
|
5941
|
+
(entry.signature ? ' ' + short(entry.signature) : '')).join(', '));
|
|
5942
|
+
}
|
|
5943
|
+
if (Array.isArray(test.liveHttpCalls) && test.liveHttpCalls.length) {
|
|
5944
|
+
console.log(' Live HTTP calls:', test.liveHttpCalls.length);
|
|
5945
|
+
}
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5949
|
+
function formatDurationMs(value) {
|
|
5950
|
+
const ms = Number(value);
|
|
5951
|
+
if (!Number.isFinite(ms)) return String(value);
|
|
5952
|
+
if (ms < 1000) return ms + 'ms';
|
|
5953
|
+
return (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + 's';
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5597
5956
|
async function waitForToolStatus(api, cwd, options) {
|
|
5598
5957
|
const started = Date.now();
|
|
5599
5958
|
let pollDelayMs = parsePositiveInt(options.pollIntervalMs, 1000);
|
|
@@ -5744,6 +6103,7 @@ async function testCommand(flags) {
|
|
|
5744
6103
|
printResolvedBaseUrl(baseUrl);
|
|
5745
6104
|
printStagingLane(branchName, workspace, workspaceSource(cwd, flags));
|
|
5746
6105
|
if (start.staging && Array.isArray(start.staging.accountLanes)) {
|
|
6106
|
+
printOrphanedWorkspaceWarning(start.staging);
|
|
5747
6107
|
printAccountLanes({ accountLanes: start.staging.accountLanes, branchName, workspace });
|
|
5748
6108
|
}
|
|
5749
6109
|
console.log('Test run started:', start.taskId);
|
|
@@ -5770,6 +6130,7 @@ async function testCommand(flags) {
|
|
|
5770
6130
|
console.log(JSON.stringify(status, null, 2));
|
|
5771
6131
|
} else {
|
|
5772
6132
|
console.log('Final status:', JSON.stringify(status, null, 2));
|
|
6133
|
+
printTestRunPivots(status);
|
|
5773
6134
|
}
|
|
5774
6135
|
|
|
5775
6136
|
// A selector that matched no case is a mis-specified run, not a passing one. Say so in the terminal
|
|
@@ -5800,7 +6161,8 @@ async function testCommand(flags) {
|
|
|
5800
6161
|
claim: 'Test run ' + String(testRef),
|
|
5801
6162
|
world: buildCommandWorld(status, { accountId, dataMode: status.dataMode || dataMode, dataModeSource, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: status.staging && status.staging.testComponentSource }),
|
|
5802
6163
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
5803
|
-
compileSignatures: status
|
|
6164
|
+
compileSignatures: testCompileSignatures(status),
|
|
6165
|
+
componentProvenance: testComponentProvenance(status)
|
|
5804
6166
|
}),
|
|
5805
6167
|
test: {
|
|
5806
6168
|
taskId: start.taskId,
|
|
@@ -5809,16 +6171,66 @@ async function testCommand(flags) {
|
|
|
5809
6171
|
selectedCases: names,
|
|
5810
6172
|
passed: status.result && status.result.passed,
|
|
5811
6173
|
failed: status.result && status.result.failed,
|
|
6174
|
+
tests: status.result && status.result.tests,
|
|
5812
6175
|
dataModeSource: status.dataModeSource || dataModeSource,
|
|
5813
6176
|
unmatchedTestNames: unmatched
|
|
5814
6177
|
},
|
|
5815
|
-
evidenceCategories:
|
|
6178
|
+
evidenceCategories: testEvidenceCategories(status, names),
|
|
5816
6179
|
limitations: unmatched.length ? ['One or more requested test case selectors matched no case.'] : [],
|
|
5817
6180
|
rawRefs: { testStatusKey: start.taskId },
|
|
5818
6181
|
status
|
|
5819
6182
|
}, { quiet: jsonOutput });
|
|
5820
6183
|
}
|
|
5821
6184
|
|
|
6185
|
+
async function testStatusCommand(flags) {
|
|
6186
|
+
const cwd = process.cwd();
|
|
6187
|
+
ensureLocalState(cwd);
|
|
6188
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
6189
|
+
const { session, accountId } = sessionContext;
|
|
6190
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
6191
|
+
const branchName = flags.branch || currentBranch(cwd);
|
|
6192
|
+
const dataMode = hasExplicitDataModeFlag(flags)
|
|
6193
|
+
? resolveDataMode(flags, null)
|
|
6194
|
+
: DEFAULT_DATA_MODE;
|
|
6195
|
+
const taskId = flags['task-id'] || flags.taskId || flags.id || (flags._ && flags._[2]);
|
|
6196
|
+
const jsonOutput = flagEnabled(flags.json);
|
|
6197
|
+
if (!taskId) throw new Error('Missing --task-id <taskId>');
|
|
6198
|
+
|
|
6199
|
+
const api = buildAxios(baseUrl, session.token);
|
|
6200
|
+
const status = await loggedPost(api, cwd, '/cli/test', {
|
|
6201
|
+
token: session.token,
|
|
6202
|
+
command: 'status',
|
|
6203
|
+
accountId,
|
|
6204
|
+
branchName,
|
|
6205
|
+
taskId,
|
|
6206
|
+
dataMode
|
|
6207
|
+
}).then((r) => r.data);
|
|
6208
|
+
|
|
6209
|
+
if (jsonOutput) {
|
|
6210
|
+
console.log(JSON.stringify(status, null, 2));
|
|
6211
|
+
return status;
|
|
6212
|
+
}
|
|
6213
|
+
printSessionResolutionWarning(sessionContext);
|
|
6214
|
+
printResolvedBaseUrl(baseUrl);
|
|
6215
|
+
console.log('Test run status:', status.status || 'unknown');
|
|
6216
|
+
console.log('Task ID:', taskId);
|
|
6217
|
+
console.log('Data mode:', status.dataMode || dataMode);
|
|
6218
|
+
if (status.result) {
|
|
6219
|
+
console.log('Cases:', (status.result.passed || 0) + ' passed, ' + (status.result.failed || 0) + ' failed, ' + (status.result.total || 0) + ' total');
|
|
6220
|
+
}
|
|
6221
|
+
if (status.error || status.message) {
|
|
6222
|
+
console.log('Message:', status.error || status.message);
|
|
6223
|
+
}
|
|
6224
|
+
if (status.status === 'failed' || (status.result && status.result.failed > 0)) {
|
|
6225
|
+
process.exitCode = 1;
|
|
6226
|
+
}
|
|
6227
|
+
return status;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
/*
|
|
6231
|
+
## Tokens, Tools, Verification, And Config
|
|
6232
|
+
*/
|
|
6233
|
+
|
|
5822
6234
|
async function tokenCommand(flags) {
|
|
5823
6235
|
const subcommand = flags._ && flags._[1];
|
|
5824
6236
|
if (subcommand === 'inspect' || subcommand === 'details' || subcommand === 'decode') {
|
|
@@ -5876,6 +6288,9 @@ async function tokenCommand(flags) {
|
|
|
5876
6288
|
accountId: data.accountId,
|
|
5877
6289
|
branchName: data.branchName,
|
|
5878
6290
|
variantBranch: data.variantBranch,
|
|
6291
|
+
// Why the variant branch is (or is not) set — the platform's one rule, so a null is explainable.
|
|
6292
|
+
variantBranchSource: data.variantBranchSource,
|
|
6293
|
+
variantWorld: data.variantWorld,
|
|
5879
6294
|
workspace: data.workspace !== undefined ? data.workspace : workspace,
|
|
5880
6295
|
stagingLane: data.stagingLane,
|
|
5881
6296
|
accountLanes: data.accountLanes,
|
|
@@ -6136,8 +6551,7 @@ async function toolCommand(flags) {
|
|
|
6136
6551
|
revision: collectVerificationSource(cwd, flags),
|
|
6137
6552
|
evidenceCategories: ['tool_call'],
|
|
6138
6553
|
rawRefs: { toolResponsePath: statusResponse.responseFile, callId: requestedCallId },
|
|
6139
|
-
tool: { callId: requestedCallId,
|
|
6140
|
-
result: data
|
|
6554
|
+
tool: Object.assign(toolEvidenceSummary(data, statusResponse.responseFile), { callId: requestedCallId, name: stored && stored.name })
|
|
6141
6555
|
}, { quiet: jsonOutput });
|
|
6142
6556
|
if (jsonOutput) {
|
|
6143
6557
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -6150,7 +6564,8 @@ async function toolCommand(flags) {
|
|
|
6150
6564
|
console.log('Data mode:', data.dataMode || dataMode);
|
|
6151
6565
|
if (data.threadGroupingId) console.log('Thread grouping ID:', data.threadGroupingId);
|
|
6152
6566
|
console.log('Session log:', data.sessionLog);
|
|
6153
|
-
|
|
6567
|
+
const responseEvidence = fileEvidence(data.responseFile);
|
|
6568
|
+
console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
6154
6569
|
if (polledFailed) {
|
|
6155
6570
|
console.log('Tool error:', data.toolMessage);
|
|
6156
6571
|
}
|
|
@@ -6201,6 +6616,9 @@ async function toolCommand(flags) {
|
|
|
6201
6616
|
data.sessionLog = sessionJsonlFile(cwd);
|
|
6202
6617
|
if (toolFailureMessage && !data.toolMessage) data.toolMessage = toolFailureMessage;
|
|
6203
6618
|
|
|
6619
|
+
let evidenceData = data;
|
|
6620
|
+
let evidenceResponseFile = response.responseFile;
|
|
6621
|
+
|
|
6204
6622
|
if (!jsonOutput) {
|
|
6205
6623
|
printSessionResolutionWarning(sessionContext);
|
|
6206
6624
|
printResolvedBaseUrl(baseUrl);
|
|
@@ -6218,7 +6636,8 @@ async function toolCommand(flags) {
|
|
|
6218
6636
|
console.log('Component:', `${data.componentSource}${sig}`);
|
|
6219
6637
|
}
|
|
6220
6638
|
console.log('Session log:', data.sessionLog);
|
|
6221
|
-
|
|
6639
|
+
const responseEvidence = fileEvidence(data.responseFile);
|
|
6640
|
+
console.log('Tool response file:', data.responseFile + (responseEvidence.bytes ? ' (' + responseEvidence.bytes + ' bytes, sha256 ' + responseEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
6222
6641
|
}
|
|
6223
6642
|
|
|
6224
6643
|
// Non-zero exit so scripted/agent callers that check status notice the refusal too.
|
|
@@ -6238,9 +6657,12 @@ async function toolCommand(flags) {
|
|
|
6238
6657
|
|
|
6239
6658
|
finalStatus.data.responseFile = finalStatus.responseFile;
|
|
6240
6659
|
finalStatus.data.sessionLog = sessionJsonlFile(cwd);
|
|
6660
|
+
evidenceData = finalStatus.data;
|
|
6661
|
+
evidenceResponseFile = finalStatus.responseFile;
|
|
6241
6662
|
if (!jsonOutput) {
|
|
6242
6663
|
console.log('Final status:', finalStatus.data.status);
|
|
6243
|
-
|
|
6664
|
+
const finalEvidence = fileEvidence(finalStatus.responseFile);
|
|
6665
|
+
console.log('Tool response file:', finalStatus.responseFile + (finalEvidence.bytes ? ' (' + finalEvidence.bytes + ' bytes, sha256 ' + finalEvidence.sha256.slice(0, 12) + ')' : ''));
|
|
6244
6666
|
}
|
|
6245
6667
|
if (finalStatus.data.status !== 'completed') {
|
|
6246
6668
|
process.exitCode = 1;
|
|
@@ -6250,20 +6672,19 @@ async function toolCommand(flags) {
|
|
|
6250
6672
|
|
|
6251
6673
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
6252
6674
|
type: 'tool_call',
|
|
6253
|
-
success: !toolFailed && (!asyncMode ||
|
|
6675
|
+
success: asyncMode && !waitForAsync ? null : (!toolFailed && (!asyncMode || process.exitCode !== 1)),
|
|
6254
6676
|
claim: 'Tool call ' + String(toolName),
|
|
6255
|
-
world: buildCommandWorld(
|
|
6677
|
+
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 }),
|
|
6256
6678
|
revision: Object.assign(collectVerificationSource(cwd, flags), {
|
|
6257
|
-
compileSignatures:
|
|
6679
|
+
compileSignatures: evidenceData.componentSignature ? [evidenceData.componentSignature] : []
|
|
6258
6680
|
}),
|
|
6259
6681
|
evidenceCategories: ['tool_call'],
|
|
6260
6682
|
dependencies: {
|
|
6261
6683
|
mocks: [],
|
|
6262
|
-
liveHttpCalls:
|
|
6684
|
+
liveHttpCalls: evidenceData.liveHttpCalls || []
|
|
6263
6685
|
},
|
|
6264
|
-
rawRefs: { toolResponsePath:
|
|
6265
|
-
tool: { callId, name: String(toolName), async: asyncMode
|
|
6266
|
-
result: data
|
|
6686
|
+
rawRefs: { toolResponsePath: evidenceResponseFile, callId },
|
|
6687
|
+
tool: Object.assign(toolEvidenceSummary(evidenceData, evidenceResponseFile), { callId, name: String(toolName), async: asyncMode })
|
|
6267
6688
|
}, { quiet: jsonOutput });
|
|
6268
6689
|
if (jsonOutput) {
|
|
6269
6690
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -6346,10 +6767,12 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6346
6767
|
|
|
6347
6768
|
const source = collectVerificationSource(cwd, flags);
|
|
6348
6769
|
if (statusResponse) {
|
|
6349
|
-
|
|
6350
|
-
source.
|
|
6351
|
-
|
|
6352
|
-
source.
|
|
6770
|
+
const verifiedWorld = verificationWorldFromStatus(statusResponse, branchName);
|
|
6771
|
+
source.componentBranch = verifiedWorld.componentBranch;
|
|
6772
|
+
source.sourceLayer = verifiedWorld.sourceLayer;
|
|
6773
|
+
if (verifiedWorld.variantBranchSource) source.variantBranchSource = verifiedWorld.variantBranchSource;
|
|
6774
|
+
source.laneContentHash = laneContentHashFrom(statusResponse);
|
|
6775
|
+
source.stagedOverlayHash = source.laneContentHash;
|
|
6353
6776
|
}
|
|
6354
6777
|
|
|
6355
6778
|
const response = await postVerificationCommand(api, cwd, session, accountId, 'start', {
|
|
@@ -6364,6 +6787,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6364
6787
|
writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
|
|
6365
6788
|
console.log('Verification envelope started:', envelope.envelopeId);
|
|
6366
6789
|
console.log('Target:', 'account=' + accountId + ', branch=' + branchName + ', workspace=' + (workspace || 'shared') + ', dataMode=' + dataMode);
|
|
6790
|
+
printAcceptanceWarnings(envelope);
|
|
6367
6791
|
console.log('Local mirror:', verificationPaths(cwd, envelope.envelopeId).base);
|
|
6368
6792
|
return envelope;
|
|
6369
6793
|
}
|
|
@@ -6404,6 +6828,7 @@ async function verifyCommand(flags, subcommand) {
|
|
|
6404
6828
|
writeLocalVerificationEnvelope(cwd, response.envelope);
|
|
6405
6829
|
console.log('Manifest attached to envelope:', envelopeId);
|
|
6406
6830
|
console.log('Required evidence:', (((response.envelope || {}).acceptance || {}).requiredEvidence || []).length);
|
|
6831
|
+
printAcceptanceWarnings(response.envelope);
|
|
6407
6832
|
return response.envelope;
|
|
6408
6833
|
}
|
|
6409
6834
|
|
|
@@ -6712,6 +7137,10 @@ async function sessionsCommand(flags, subcommand) {
|
|
|
6712
7137
|
throw new Error('Unknown sessions subcommand: ' + subcommand + '. Use: list, remove');
|
|
6713
7138
|
}
|
|
6714
7139
|
|
|
7140
|
+
/*
|
|
7141
|
+
## Service Dashboard And WebSocket Listener
|
|
7142
|
+
*/
|
|
7143
|
+
|
|
6715
7144
|
// --- Listener PID management ---
|
|
6716
7145
|
function isListenerRunning() {
|
|
6717
7146
|
if (!fs.existsSync(LISTENER_PID_FILE)) {
|
|
@@ -8803,6 +9232,10 @@ async function runManagedWebsocketWatchdog() {
|
|
|
8803
9232
|
|
|
8804
9233
|
// --- Persistent WebSocket listener ---
|
|
8805
9234
|
// ---------------------------------------------------------------------------------------------
|
|
9235
|
+
/*
|
|
9236
|
+
## Agent And Ticket Workflows
|
|
9237
|
+
*/
|
|
9238
|
+
|
|
8806
9239
|
// Local agent sessions
|
|
8807
9240
|
//
|
|
8808
9241
|
// A "CLI agent" is ONE terminal session — one tab running Claude, Codex, or a person — that has
|
|
@@ -11857,6 +12290,10 @@ async function listenStatusCommand(flags = {}) {
|
|
|
11857
12290
|
printSessionIdentity(identity);
|
|
11858
12291
|
}
|
|
11859
12292
|
|
|
12293
|
+
/*
|
|
12294
|
+
## Help, Auto Update, And Command Dispatch
|
|
12295
|
+
*/
|
|
12296
|
+
|
|
11860
12297
|
async function whoamiCommand(flags = {}) {
|
|
11861
12298
|
const identity = resolveSessionIdentity(process.cwd(), flags);
|
|
11862
12299
|
printSessionResolutionWarning(identity);
|
|
@@ -12024,7 +12461,7 @@ function printComponentsHelp(subcommand) {
|
|
|
12024
12461
|
return;
|
|
12025
12462
|
}
|
|
12026
12463
|
if (subcommand === 'sync') {
|
|
12027
|
-
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--json]');
|
|
12464
|
+
console.log('Usage: remits-cli components sync [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--create-variant-branch] [--summary] [--json]');
|
|
12028
12465
|
console.log('');
|
|
12029
12466
|
console.log('On trunk this reconciles the pushed repository into live component rows.');
|
|
12030
12467
|
console.log('On a variant branch this writes ComponentVariant overlays only.');
|
|
@@ -12110,7 +12547,7 @@ function printComponentsHelp(subcommand) {
|
|
|
12110
12547
|
return;
|
|
12111
12548
|
}
|
|
12112
12549
|
if (subcommand === 'commit') {
|
|
12113
|
-
console.log('Usage: remits-cli components commit [--yes] [--safe] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
|
|
12550
|
+
console.log('Usage: remits-cli components commit [--yes] [--safe] [--create-variant-branch] [--message|-m "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones] [--json [--summary]]');
|
|
12114
12551
|
console.log('');
|
|
12115
12552
|
console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
|
|
12116
12553
|
console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
|
|
@@ -12152,11 +12589,13 @@ function printComponentsHelp(subcommand) {
|
|
|
12152
12589
|
|
|
12153
12590
|
function printTestHelp() {
|
|
12154
12591
|
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]');
|
|
12592
|
+
console.log(' remits-cli test status --task-id <taskId> [--base-url URL] [--account-id ID] [--branch stagingScope] [--data-mode test|prod] [--json]');
|
|
12155
12593
|
console.log('');
|
|
12156
12594
|
console.log('Runs a Test component against the staged/variant world for this checkout.');
|
|
12157
12595
|
console.log('Examples:');
|
|
12158
12596
|
console.log(' remits-cli test run --test 11');
|
|
12159
12597
|
console.log(' remits-cli test run --test "Merchant Statements" --names "managed account case"');
|
|
12598
|
+
console.log(' remits-cli test status --task-id 7a1b...');
|
|
12160
12599
|
console.log('');
|
|
12161
12600
|
console.log('Notes:');
|
|
12162
12601
|
console.log(' --names is delimited by "|" (comma also works when no "|" is present), and may be repeated.');
|
|
@@ -12543,6 +12982,10 @@ async function main() {
|
|
|
12543
12982
|
await testCommand(args);
|
|
12544
12983
|
return;
|
|
12545
12984
|
}
|
|
12985
|
+
if (command === 'test' && subcommand === 'status') {
|
|
12986
|
+
await testStatusCommand(args);
|
|
12987
|
+
return;
|
|
12988
|
+
}
|
|
12546
12989
|
|
|
12547
12990
|
if (command === 'token') {
|
|
12548
12991
|
await tokenCommand(args);
|