@counterfact-labs/cli 0.2.9 → 0.2.11
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 +4 -5
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +10 -5
- package/dist/adapters/completion.js +75 -41
- package/dist/adapters/config-directory.js +5 -0
- package/dist/adapters/files.js +25 -10
- package/dist/adapters/legacy-reports.js +48 -0
- package/dist/adapters/output-paths.js +85 -5
- package/dist/adapters/relay-plan.js +7 -5
- package/dist/adapters/result-receipt.js +78 -0
- package/dist/adapters/results.js +5 -8
- package/dist/adapters/settings.js +10 -4
- package/dist/adapters/storage-identity.js +27 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/file-dependencies.js +1 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -1
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -1
- package/dist/analysis-core/analysis/project-architecture.js +1 -1
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/{5B5UCWD3.js → 2IQN26TC.js} +1 -1
- package/dist/analysis-core/chunks/{MZOKZ3N6.js → 2J3PWUPG.js} +1 -1
- package/dist/analysis-core/chunks/33EUZF25.js +1 -0
- package/dist/analysis-core/chunks/{3GUKUEEJ.js → 5JG3ZRNJ.js} +1 -1
- package/dist/analysis-core/chunks/{FRXOQ2M6.js → 6E4K7JTI.js} +1 -1
- package/dist/analysis-core/chunks/{4RXRAB5S.js → 6JTHFDH3.js} +1 -1
- package/dist/analysis-core/chunks/{4UDQBDDE.js → 6W6B7VSP.js} +1 -1
- package/dist/analysis-core/chunks/{4PT5C4IT.js → AIS3FHQR.js} +7 -7
- package/dist/analysis-core/chunks/CNQZLSAP.js +1 -0
- package/dist/analysis-core/chunks/{YWL52FVW.js → EJ2IZUN2.js} +2 -2
- package/dist/analysis-core/chunks/{LOBIDUCA.js → GOBOE2SC.js} +1 -1
- package/dist/analysis-core/chunks/GYXDN5W4.js +1 -0
- package/dist/analysis-core/chunks/GZSUTDGX.js +1 -0
- package/dist/analysis-core/chunks/HB5BTGTI.js +6 -0
- package/dist/analysis-core/chunks/HH3HDT5K.js +73 -0
- package/dist/analysis-core/chunks/I4F56VMG.js +1 -0
- package/dist/analysis-core/chunks/{RQTNTIR4.js → I5F7S63K.js} +1 -1
- package/dist/analysis-core/chunks/ICJV6KIZ.js +11 -0
- package/dist/analysis-core/chunks/JAPVLU4Z.js +1 -0
- package/dist/analysis-core/chunks/{33IGHOYT.js → JCVB6IC4.js} +1 -1
- package/dist/analysis-core/chunks/{33HRXJ5Q.js → JLW44GIU.js} +1 -1
- package/dist/analysis-core/chunks/JS6HTVYX.js +1 -0
- package/dist/analysis-core/chunks/{JGK37AWN.js → JWQXG5RB.js} +1 -1
- package/dist/analysis-core/chunks/LOODGUMW.js +1 -0
- package/dist/analysis-core/chunks/{HVQJKPQ4.js → NWYNJHY4.js} +1 -1
- package/dist/analysis-core/chunks/{HHFDNY6V.js → OHDC3L6V.js} +1 -1
- package/dist/analysis-core/chunks/OXGKALKM.js +1 -0
- package/dist/analysis-core/chunks/{MJGB25S5.js → P5FCHJ7W.js} +1 -1
- package/dist/analysis-core/chunks/PISOGSJF.js +1 -0
- package/dist/analysis-core/chunks/{CTLHKFPH.js → QENYFKG3.js} +8 -8
- package/dist/analysis-core/chunks/{2YREYGOG.js → QY57UR6E.js} +1 -1
- package/dist/analysis-core/chunks/QYYN4YKB.js +35 -0
- package/dist/analysis-core/chunks/UYG422QR.js +0 -0
- package/dist/analysis-core/chunks/V7GZHSVR.js +1 -0
- package/dist/analysis-core/chunks/{JEWBPIQI.js → VBGQKYIT.js} +1 -1
- package/dist/analysis-core/chunks/{S27Y2TP5.js → VVSHUUWX.js} +1 -1
- package/dist/analysis-core/chunks/VX6LYNXV.js +1 -0
- package/dist/analysis-core/chunks/{JKWTHRUR.js → YDSB7UOR.js} +1 -1
- package/dist/analysis-core/chunks/ZLUI3EHB.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -1
- package/dist/analysis-core/contracts/intake-finding.js +1 -1
- package/dist/analysis-core/contracts/model-errors.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/errors.js +1 -1
- package/dist/analysis-core/import/calls/modules.js +1 -1
- package/dist/analysis-core/import/entry-selection.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +1 -1
- package/dist/analysis-core/llm/cache-policy.js +1 -1
- package/dist/analysis-core/llm/cost.js +1 -1
- package/dist/analysis-core/llm/structured-output-error.js +1 -1
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/frame-budget.js +1 -0
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -1
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/analysis-core/wording.js +1 -0
- package/dist/cli/analysis-preview.js +22 -0
- package/dist/cli/analysis-selection.js +6 -8
- package/dist/cli/commands/analyze.js +29 -26
- package/dist/cli/commands/login.js +5 -0
- package/dist/cli/compatibility.js +39 -0
- package/dist/cli/completion-delivery.js +4 -3
- package/dist/cli/completion-recovery.js +25 -23
- package/dist/cli/credentials.js +3 -2
- package/dist/cli/finding-display.js +3 -2
- package/dist/cli/git.js +5 -2
- package/dist/cli/intake-payload.js +1 -2
- package/dist/cli/main.js +8 -2
- package/dist/cli/mcp.js +35 -10
- package/dist/cli/prior-completion.js +1 -1
- package/dist/cli/relay-check.js +88 -30
- package/dist/cli/relay-failure.js +19 -11
- package/dist/cli/relay-preparation.js +9 -6
- package/dist/cli/scope-choices.js +5 -4
- package/dist/cli/skipped-files.js +2 -1
- package/dist/cli/suggested-email.js +6 -1
- package/dist/cli/summary.js +2 -1
- package/dist/contract/agent-choice.js +2 -0
- package/dist/contract/cli-refusals.js +105 -9
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +2 -1
- package/dist/contract/email-sign-in-messages.js +2 -1
- package/dist/contract/scope-choice.js +4 -1
- package/dist/gatekeeper/client.js +11 -2
- package/dist/gatekeeper/connection-exchange.js +19 -4
- package/dist/gatekeeper/connection.js +15 -6
- package/dist/gatekeeper/console.js +10 -17
- package/dist/gatekeeper/model.js +13 -3
- package/package.json +1 -1
- package/dist/adapters/gitignore-output.js +0 -41
- package/dist/adapters/source-relay-plan.js +0 -7
- package/dist/analysis-core/chunks/2AASSZRO.js +0 -35
- package/dist/analysis-core/chunks/CQQ4TTXE.js +0 -1
- package/dist/analysis-core/chunks/F54DOWKN.js +0 -6
- package/dist/analysis-core/chunks/HCPO6QPU.js +0 -1
- package/dist/analysis-core/chunks/J4AXGE5W.js +0 -1
- package/dist/analysis-core/chunks/LFYTD7JF.js +0 -1
- package/dist/analysis-core/chunks/LMUXOKIM.js +0 -1
- package/dist/analysis-core/chunks/M5YWJY7P.js +0 -1
- package/dist/analysis-core/chunks/MGVIKA74.js +0 -1
- package/dist/analysis-core/chunks/NXHVBJL4.js +0 -1
- package/dist/analysis-core/chunks/OKBK3JL3.js +0 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +0 -10
- package/dist/analysis-core/chunks/W24SFYZM.js +0 -67
- package/dist/analysis-core/chunks/YTHF2E2S.js +0 -1
package/dist/cli/git.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
1
2
|
import { basename, relative, resolve, sep } from "node:path";
|
|
2
3
|
import { gitRepositoryRoot } from "../adapters/repository-root.js";
|
|
3
4
|
import { runGit } from "../adapters/git.js";
|
|
@@ -10,7 +11,9 @@ export async function userEmail(folder, git = async (args) => (await runGit(fold
|
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
export async function repositoryFacts(root, git = async (args) => (await runGit(root, args)).toString("utf8")) {
|
|
13
|
-
|
|
14
|
+
// Git reports the physical checkout root, so resolve aliases before deriving its selected folder.
|
|
15
|
+
const selected = await realpath(root).catch(() => resolve(root));
|
|
16
|
+
const folderName = basename(selected);
|
|
14
17
|
const repoRoot = await gitRepositoryRoot(git);
|
|
15
18
|
if (repoRoot === null)
|
|
16
19
|
return { remoteUrl: null, relPath: "", folderName, branch: null, commit: null, dirty: null };
|
|
@@ -34,6 +37,6 @@ export async function repositoryFacts(root, git = async (args) => (await runGit(
|
|
|
34
37
|
// Include new files even when Git hides them, and exclude sibling folders.
|
|
35
38
|
read(["status", "--porcelain=v2", "--no-renames", "--ignore-submodules=all", "--untracked-files=normal", "--", "."]),
|
|
36
39
|
]);
|
|
37
|
-
return { repoRoot, remoteUrl, relPath: relative(repoRoot,
|
|
40
|
+
return { repoRoot, remoteUrl, relPath: relative(repoRoot, selected).split(sep).join("/"), folderName,
|
|
38
41
|
branch: branch === "HEAD" ? null : branch || null, commit: commit || null, dirty: status === null ? null : status.length > 0 };
|
|
39
42
|
}
|
|
@@ -38,8 +38,7 @@ export function buildIntakePayload(identity, agent, report, options = {}) {
|
|
|
38
38
|
delete legacy.activity;
|
|
39
39
|
return legacy;
|
|
40
40
|
});
|
|
41
|
-
const architecture = options.
|
|
42
|
-
? projectArchitecture(report.graph) : undefined;
|
|
41
|
+
const architecture = options.settings?.CF_LOCAL_ARCHITECTURE === "1" ? projectArchitecture(report.graph) : undefined;
|
|
43
42
|
let result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings, ...metadata, ...(architecture ? { architecture } : {}) });
|
|
44
43
|
// Optional map metadata must not crowd otherwise valid findings out of the total upload allowance.
|
|
45
44
|
if (!result.success && architecture)
|
package/dist/cli/main.js
CHANGED
|
@@ -14,6 +14,8 @@ import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settin
|
|
|
14
14
|
import { loadAnalysisScope } from "./scope-choices.js";
|
|
15
15
|
import { FILE_SCOPE_PUBLIC_USAGE } from "../contract/scope-choice.js";
|
|
16
16
|
export async function run(args, options = {}) {
|
|
17
|
+
const previewEnabled = (options.startup?.transport ?? options.env ?? process.env).CF_FILES_PREVIEW !== "0";
|
|
18
|
+
const previewUsage = previewEnabled ? " counterfact-labs analyze [path] --preview\n" : "";
|
|
17
19
|
const skippedUsage = skippedFilesEnabled(options.startup?.settings ?? options.env ?? process.env)
|
|
18
20
|
? " counterfact-labs analyze [path] [--show-skipped | --skipped-plan | --skipped]\n" : "";
|
|
19
21
|
const usage = `Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n${skippedUsage} counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n`;
|
|
@@ -28,7 +30,7 @@ export async function run(args, options = {}) {
|
|
|
28
30
|
fileMode = fileScopeEnabled(selectionSettings);
|
|
29
31
|
const commandOptions = { ...options, emailSignIn, env: { ...(options.env ?? process.env), ...selectionSettings,
|
|
30
32
|
...(command === "analyze" || command === "mcp" ? {} : {
|
|
31
|
-
CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined, CF_MULTI_ENTRY: undefined,
|
|
33
|
+
CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined, CF_MULTI_ENTRY: undefined, CF_PYTHON_SRC_IMPORTS: undefined,
|
|
32
34
|
}) } };
|
|
33
35
|
const choices = scopeChoicesEnabled(selectionSettings);
|
|
34
36
|
const scopeUsage = choices ? FILE_SCOPE_PUBLIC_USAGE : usage;
|
|
@@ -37,6 +39,10 @@ export async function run(args, options = {}) {
|
|
|
37
39
|
let commandUsage = choices && firstLineEnd > 0
|
|
38
40
|
? `${scopeUsage.slice(0, firstLineEnd)}${skippedUsage}${scopeUsage.slice(firstLineEnd)}`
|
|
39
41
|
: scopeUsage;
|
|
42
|
+
if (previewUsage) {
|
|
43
|
+
const end = commandUsage.indexOf("\n") + 1;
|
|
44
|
+
commandUsage = commandUsage.slice(0, end) + previewUsage + commandUsage.slice(end);
|
|
45
|
+
}
|
|
40
46
|
if (folderPartsEnabled(env))
|
|
41
47
|
commandUsage = commandUsage.replace(choices ? "[--scope <json-file>]" : "[--proposal <json-file>]", choices ? "[--scope <json-file | json-list>]" : "[--proposal <json-file>] [--scope <json-list>]");
|
|
42
48
|
if (["--help", "-h", "help"].includes(command) || !command) {
|
|
@@ -46,7 +52,7 @@ export async function run(args, options = {}) {
|
|
|
46
52
|
if (command === "analyze") {
|
|
47
53
|
let parsed;
|
|
48
54
|
try {
|
|
49
|
-
parsed = parseAnalyzeOptions(rest, choices, multiEntryEnabled(selectionSettings));
|
|
55
|
+
parsed = parseAnalyzeOptions(rest, choices, multiEntryEnabled(selectionSettings), previewEnabled);
|
|
50
56
|
}
|
|
51
57
|
catch {
|
|
52
58
|
throw new Error(commandUsage.trimEnd());
|
package/dist/cli/mcp.js
CHANGED
|
@@ -20,6 +20,7 @@ import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
|
20
20
|
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
21
21
|
import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
|
|
22
22
|
import { suggestedEmail } from "./suggested-email.js";
|
|
23
|
+
import { checkSignInVersion } from "./compatibility.js";
|
|
23
24
|
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
24
25
|
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
25
26
|
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
@@ -95,10 +96,12 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
95
96
|
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
96
97
|
const cwd = options.cwd ?? process.cwd();
|
|
97
98
|
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
99
|
+
const retryAfterFailure = (options.env ?? process.env).CF_MCP_RETRY_AFTER_FAILURE !== "0";
|
|
98
100
|
const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
|
|
99
101
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
100
102
|
const lastAnalysis = new Map();
|
|
101
103
|
const startupSettings = options.startup?.settings ?? options.env ?? process.env;
|
|
104
|
+
const scanPriority = startupSettings.CF_CODE_SCAN_PRIORITY;
|
|
102
105
|
const skippedSettings = Object.freeze({ CF_SKIPPED_FILES: readSkippedFilesSetting(startupSettings.CF_SKIPPED_FILES) });
|
|
103
106
|
const skippedEnabled = skippedFilesEnabled(skippedSettings);
|
|
104
107
|
const partSettings = Object.freeze({ CF_FOLDER_PARTS: readFolderPartsSetting(startupSettings.CF_FOLDER_PARTS) });
|
|
@@ -108,10 +111,12 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
108
111
|
const analysisFindings = new Map();
|
|
109
112
|
const { numbersEnabled, fixesEnabled, agentReadableResults } = findingSettings;
|
|
110
113
|
const analysisSwitches = readSelectionSettings(startupSettings);
|
|
114
|
+
const signInSettings = Object.freeze({ CF_CLI_SIGN_IN_VERSION_CHECK: options.startup
|
|
115
|
+
? options.startup.signInVersionCheck ? "1" : "0" : (options.env ?? process.env).CF_CLI_SIGN_IN_VERSION_CHECK });
|
|
111
116
|
const choicesEnabled = scopeChoicesEnabled(analysisSwitches);
|
|
112
117
|
const selectionEnabled = selectionModeEnabled(analysisSwitches);
|
|
113
118
|
const multipleEnabled = multiEntryEnabled(analysisSwitches);
|
|
114
|
-
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches } : undefined;
|
|
119
|
+
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches, ...signInSettings } : undefined;
|
|
115
120
|
let analysisStartup = options.startup;
|
|
116
121
|
const references = new FindingReferences();
|
|
117
122
|
const emailEnv = analysisEnv && Object.hasOwn(analysisEnv, "CF_EMAIL_SIGN_IN") ? analysisEnv : options.env;
|
|
@@ -218,19 +223,27 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
218
223
|
if (selectionEnabled)
|
|
219
224
|
analysisStartup ??= readCliSettings(analysisEnv, emailSignIn);
|
|
220
225
|
const analysisSettings = analysisStartup?.settings ?? {};
|
|
221
|
-
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, ...skippedSettings, ...partSettings, CF_FINDING_AUDIENCE: audienceChoice },
|
|
222
|
-
onCheckFailure: failure => {
|
|
226
|
+
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, ...skippedSettings, ...partSettings, ...signInSettings, CF_CODE_SCAN_PRIORITY: scanPriority, CF_FINDING_AUDIENCE: audienceChoice },
|
|
227
|
+
onCheckFailure: failure => {
|
|
228
|
+
reason = failure.reason;
|
|
229
|
+
if (guarded && retryAfterFailure && analysisFolder && failure.charged && !url) {
|
|
230
|
+
lastAnalysis.set(analysisFolder, { at: ctx.now(), charged: true });
|
|
231
|
+
}
|
|
232
|
+
options.onCheckFailure?.(failure);
|
|
233
|
+
},
|
|
223
234
|
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
224
235
|
started = true;
|
|
225
|
-
if (guarded && analysisFolder)
|
|
226
|
-
lastAnalysis.set(analysisFolder, ctx.now());
|
|
236
|
+
if (guarded && !retryAfterFailure && analysisFolder)
|
|
237
|
+
lastAnalysis.set(analysisFolder, { at: ctx.now() });
|
|
227
238
|
options.onCheckStart?.();
|
|
228
239
|
}, onScopeChoice: choice => {
|
|
229
240
|
scopeChoice = choice;
|
|
230
|
-
if (guarded && analysisFolder)
|
|
241
|
+
if (guarded && !retryAfterFailure && analysisFolder)
|
|
231
242
|
lastAnalysis.delete(analysisFolder);
|
|
232
243
|
options.onScopeChoice?.(choice);
|
|
233
244
|
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
245
|
+
if (guarded && analysisFolder && retryAfterFailure)
|
|
246
|
+
lastAnalysis.set(analysisFolder, { at: ctx.now(), url: result.url });
|
|
234
247
|
findingIds = result.findingIds ?? [];
|
|
235
248
|
topFindingIds = result.topFindingIds;
|
|
236
249
|
if (skippedEnabled && result.skippedFiles)
|
|
@@ -266,6 +279,8 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
266
279
|
if (!chunk.startsWith("\r"))
|
|
267
280
|
text += chunk;
|
|
268
281
|
} });
|
|
282
|
+
if (name === "sign_in_status" || name === "sign_in" || name === "sign_in_with_code")
|
|
283
|
+
await checkSignInVersion(ctx);
|
|
269
284
|
if (name === "waiting_fixes") {
|
|
270
285
|
const waiting = await waitingFixes(ctx.api);
|
|
271
286
|
fixRequests = waiting;
|
|
@@ -303,9 +318,11 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
303
318
|
const pending = pendingSignIn;
|
|
304
319
|
const allowed = await analyzeToolAllowed(ctx.homeDir, cwd);
|
|
305
320
|
if (args.wait === true && pending) {
|
|
321
|
+
let completed = false;
|
|
306
322
|
if (pending.flow.retryEnabled) {
|
|
307
323
|
try {
|
|
308
324
|
await waitForSignIn(pending.flow, signal);
|
|
325
|
+
completed = true;
|
|
309
326
|
}
|
|
310
327
|
catch (error) {
|
|
311
328
|
if (signal.aborted)
|
|
@@ -320,13 +337,19 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
320
337
|
if (signal.aborted)
|
|
321
338
|
cancel();
|
|
322
339
|
await pending.flow.done;
|
|
340
|
+
completed = true;
|
|
323
341
|
}
|
|
324
342
|
finally {
|
|
325
343
|
signal.removeEventListener("abort", cancel);
|
|
326
344
|
}
|
|
327
345
|
}
|
|
346
|
+
if (completed)
|
|
347
|
+
await checkSignInVersion(ctx);
|
|
328
348
|
}
|
|
329
349
|
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
350
|
+
// The browser flow may finish and clear itself while the initial unsigned probe is in flight.
|
|
351
|
+
if (args.wait === true && !pending && signIn.signedIn)
|
|
352
|
+
await checkSignInVersion(ctx);
|
|
330
353
|
if (fixesEnabled)
|
|
331
354
|
waitingCount = signIn.signedIn ? await waitingFixCount(ctx.api) : 0;
|
|
332
355
|
text = signIn.signedIn ? "Signed in." : "Not signed in.";
|
|
@@ -359,6 +382,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
359
382
|
}
|
|
360
383
|
else {
|
|
361
384
|
await signInWithCode(ctx, email, args.code);
|
|
385
|
+
await checkSignInVersion(ctx);
|
|
362
386
|
signInFailure = undefined;
|
|
363
387
|
signIn = { signedIn: true };
|
|
364
388
|
text = "Signed in.";
|
|
@@ -421,8 +445,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
421
445
|
const key = selection ? includedEntries
|
|
422
446
|
? JSON.stringify([selection.launchRoot, [...includedEntries].sort()]) : JSON.stringify([selection.launchRoot, selection.path]) : folder;
|
|
423
447
|
const last = lastAnalysis.get(key);
|
|
424
|
-
if (name === ANALYZE_TOOL_NAME && guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
425
|
-
throw new CliRefusalError("repeat_too_soon"
|
|
448
|
+
if (name === ANALYZE_TOOL_NAME && guarded && last !== undefined && ctx.now() - last.at < 10 * 60 * 1000) {
|
|
449
|
+
throw new CliRefusalError("repeat_too_soon", retryAfterFailure
|
|
450
|
+
? { retryAt: last.at + 10 * 60 * 1000, url: last.url, charged: last.charged } : undefined);
|
|
426
451
|
}
|
|
427
452
|
analysisFolder = key;
|
|
428
453
|
try {
|
|
@@ -432,8 +457,8 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
432
457
|
finally {
|
|
433
458
|
if (reason === "results_pending")
|
|
434
459
|
lastAnalysis.delete(key);
|
|
435
|
-
else if (guarded && started && !scopeChoice)
|
|
436
|
-
lastAnalysis.set(key, ctx.now());
|
|
460
|
+
else if (!retryAfterFailure && guarded && started && !scopeChoice)
|
|
461
|
+
lastAnalysis.set(key, { at: ctx.now() });
|
|
437
462
|
}
|
|
438
463
|
}
|
|
439
464
|
}
|
|
@@ -22,7 +22,7 @@ export async function priorCompletion(folder, options) {
|
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
if (localMismatch && decision.action === "give-up") {
|
|
25
|
-
await discardCompletion(folder, item.path, options.identity);
|
|
25
|
+
await discardCompletion(folder, item.path, options.identity, { keepCopy: false });
|
|
26
26
|
if (decision.message)
|
|
27
27
|
notices.add(decision.message);
|
|
28
28
|
continue;
|
package/dist/cli/relay-check.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
1
2
|
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
3
|
import { analysisSkippedFiles, sizeSkippedFiles } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
4
|
import { loadAnalysisResults, loadPublicReport, saveAnalysisContext, settleAnalysisContext } from "../adapters/analysis-results.js";
|
|
@@ -7,17 +8,18 @@ import { randomUUID } from "node:crypto";
|
|
|
7
8
|
import { disabledSetupPings } from "../setup-pings/client.js";
|
|
8
9
|
import { fileScopeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
9
10
|
import { saveCompletion, discardCompletion, lockCompletion } from "../adapters/completion.js";
|
|
11
|
+
import { CompletionBusyError } from "../adapters/completion-lock.js";
|
|
10
12
|
import { saveLocalResults } from "../adapters/results.js";
|
|
11
|
-
import {
|
|
13
|
+
import { loadResultReceipt, saveResultReceipt, readResultReceipt, verifiedResultPaths } from "../adapters/result-receipt.js";
|
|
12
14
|
import { priorCompletion } from "./prior-completion.js";
|
|
13
|
-
import {
|
|
15
|
+
import { outputPaths, reportOutputDirectory } from "../adapters/output-paths.js";
|
|
14
16
|
import { validateClientRelease } from "../adapters/release.js";
|
|
15
17
|
import { selectAnalysisExecution } from "../adapters/models.js";
|
|
16
18
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
17
19
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
18
20
|
import { savedAnalysisUrl, savedArchitectureUrl } from "../contract/console-url.js";
|
|
19
21
|
import { prepareRelayCheck, prepareFolderChoice } from "./relay-preparation.js";
|
|
20
|
-
import { cliRefusals, refusalExitCode } from "../contract/cli-refusals.js";
|
|
22
|
+
import { cliRefusals, failureReason, refusalExitCode } from "../contract/cli-refusals.js";
|
|
21
23
|
import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
|
|
22
24
|
import { GatekeeperClient } from "../gatekeeper/client.js";
|
|
23
25
|
import { connectRelay, relayOrigin } from "../gatekeeper/connection.js";
|
|
@@ -37,6 +39,7 @@ import { SCOPE_CHOICE_EXIT_CODE, progressLine } from "../contract/scope-choice.j
|
|
|
37
39
|
import { ScopeChoiceError, scopeChoiceRefusal, selectionScopeChoices } from "../contract/selection-offer.js";
|
|
38
40
|
import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
|
|
39
41
|
const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
|
|
42
|
+
const reservationExpiredError = () => Object.assign(new Error(), { reason: "reservation_expired" });
|
|
40
43
|
export function runRelayCheck(folder, options) {
|
|
41
44
|
return runRelayLifecycle(folder, options);
|
|
42
45
|
}
|
|
@@ -47,10 +50,12 @@ export function runReservedRelayCheck(folder, options) {
|
|
|
47
50
|
async function runRelayLifecycle(folder, options, existing) {
|
|
48
51
|
const { config, stdout, stderr } = options;
|
|
49
52
|
const retainUnsaved = config.CF_LOCAL_RESULTS_RETRY !== "0";
|
|
53
|
+
const keepLocalResults = config.CF_LOCAL_RESULTS_KEEP !== "0";
|
|
50
54
|
let settings = options.settings;
|
|
51
55
|
const summaryOptions = { audienceEnabled: false, notActiveEnabled: false, skippedFilesEnabled: skippedFilesEnabled(settings), skippedOffer: !options.addToAnalysisId, analyzedFolder: options.analyzedFolder ?? folder };
|
|
52
56
|
let skippedFiles = [];
|
|
53
57
|
let client, reservationId, projectId;
|
|
58
|
+
let consoleSupportsFailureReason = options.consoleSupportsFailureReason ?? false;
|
|
54
59
|
let connection = existing?.connection;
|
|
55
60
|
let completedPayload;
|
|
56
61
|
let displayFindings;
|
|
@@ -67,6 +72,10 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
67
72
|
let releaseCompletion;
|
|
68
73
|
let localErrorMessage;
|
|
69
74
|
let completionStored = false, lostCompletionLock = false;
|
|
75
|
+
let localPaths;
|
|
76
|
+
let localReportReceipt;
|
|
77
|
+
let retainedPath;
|
|
78
|
+
let internalAbortCause;
|
|
70
79
|
let uploadOutcome;
|
|
71
80
|
let savedResult;
|
|
72
81
|
const controller = new AbortController();
|
|
@@ -75,11 +84,17 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
75
84
|
const uploadSignal = retainUnsaved ? options.signal ?? new AbortController().signal : signal;
|
|
76
85
|
const persist = async (payload, origin, uploaded = false, finished = false) => {
|
|
77
86
|
if (lostCompletionLock)
|
|
78
|
-
throw new
|
|
87
|
+
throw new CompletionBusyError();
|
|
79
88
|
const path = await saveCompletion(folder, payload, origin, { uploaded, finished, completedAt: completionTiming.completedAt,
|
|
80
89
|
absoluteExpiresAt: completionTiming.absoluteExpiresAt, retainUnsaved, identity: completionIdentity });
|
|
81
90
|
completionStored = true;
|
|
82
91
|
completionRecordPath = path;
|
|
92
|
+
if (localReportReceipt) {
|
|
93
|
+
try {
|
|
94
|
+
await saveResultReceipt(path, payload.checkId, localReportReceipt);
|
|
95
|
+
}
|
|
96
|
+
catch { /* A receipt is optional; the pending payload already preserves the results. */ }
|
|
97
|
+
}
|
|
83
98
|
return path;
|
|
84
99
|
};
|
|
85
100
|
const submit = (payload, alreadySaved = false) => {
|
|
@@ -144,14 +159,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
144
159
|
Object.assign(error, { localOperation: "read" });
|
|
145
160
|
throw error;
|
|
146
161
|
});
|
|
147
|
-
outputDir
|
|
162
|
+
outputDir = await reportOutputDirectory(folder, outputDir ?? selection?.outputDir);
|
|
148
163
|
if (selection?.kind === "file")
|
|
149
164
|
completionIdentity = {
|
|
150
165
|
consoleOrigin: new URL(config.CF_CONSOLE_URL).origin, selectionKey: selection.selectionKey,
|
|
151
166
|
};
|
|
152
167
|
signal.throwIfAborted();
|
|
153
168
|
if (retainUnsaved)
|
|
154
|
-
releaseCompletion = await lockCompletion(folder, () => {
|
|
169
|
+
releaseCompletion = await lockCompletion(folder, () => {
|
|
170
|
+
lostCompletionLock = true;
|
|
171
|
+
internalAbortCause = new CompletionBusyError();
|
|
172
|
+
controller.abort();
|
|
173
|
+
}, completionIdentity);
|
|
155
174
|
const recovery = await priorCompletion(folder, { retryEnabled: retainUnsaved,
|
|
156
175
|
consoleOrigin: new URL(config.CF_CONSOLE_URL).origin, projectId: existing?.expectedProjectId, retryUpload: options.retryUpload,
|
|
157
176
|
identity: completionIdentity });
|
|
@@ -173,6 +192,17 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
173
192
|
completedPayload = prior.payload;
|
|
174
193
|
skippedFiles = prior.payload.coverage?.skippedFiles ?? [];
|
|
175
194
|
completionOrigin = prior.consoleOrigin;
|
|
195
|
+
if (keepLocalResults) {
|
|
196
|
+
try {
|
|
197
|
+
const receipt = await loadResultReceipt(path, prior.payload.checkId);
|
|
198
|
+
if (receipt) {
|
|
199
|
+
localPaths = await verifiedResultPaths(folder, receipt);
|
|
200
|
+
if (localPaths)
|
|
201
|
+
localReportReceipt = receipt;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch { /* Missing or changed reports cannot justify deleting the pending payload. */ }
|
|
205
|
+
}
|
|
176
206
|
completionTiming = { completedAt: prior.completedAt, absoluteExpiresAt: prior.absoluteExpiresAt };
|
|
177
207
|
uploaded = prior.uploaded ?? false;
|
|
178
208
|
reservationId = prior.payload.reservationId;
|
|
@@ -187,12 +217,14 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
187
217
|
signal.throwIfAborted();
|
|
188
218
|
if (options.retryUpload)
|
|
189
219
|
throw new Error("The saved completion record is missing. Keep the local report and check the output directory.");
|
|
190
|
-
// Negotiate
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
// Negotiate before paid work: older strict intake schemas cannot accept coverage or additions.
|
|
221
|
+
if (options.consoleSupportsFailureReason === undefined) {
|
|
222
|
+
try {
|
|
223
|
+
consoleSupportsFailureReason = await client.supportsFailureReason();
|
|
224
|
+
}
|
|
225
|
+
catch { /* Older consoles omit optional metadata. */ }
|
|
226
|
+
}
|
|
227
|
+
if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? await client.supportsSkippedFiles())) {
|
|
196
228
|
if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
|
|
197
229
|
throw new ConsoleError("unavailable", "start");
|
|
198
230
|
settings = Object.freeze({ ...settings, CF_SKIPPED_FILES: "0" });
|
|
@@ -233,10 +265,17 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
233
265
|
if (renewal)
|
|
234
266
|
return;
|
|
235
267
|
renewal = activeClient.heartbeat(activeId).then(status => {
|
|
236
|
-
if (status.state !== "reserved")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
268
|
+
if (status.state !== "reserved") {
|
|
269
|
+
internalAbortCause = reservationExpiredError();
|
|
270
|
+
controller.abort();
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
lease = status;
|
|
274
|
+
}).catch(error => {
|
|
275
|
+
const expired = error instanceof ConsoleError && [401, 403, 404, 409].includes(error.httpStatus ?? 0);
|
|
276
|
+
internalAbortCause = expired ? reservationExpiredError() : new Error();
|
|
277
|
+
controller.abort();
|
|
278
|
+
}).finally(() => { renewal = undefined; });
|
|
240
279
|
}, 60000);
|
|
241
280
|
heartbeat.unref();
|
|
242
281
|
const execution = await selectAnalysisExecution(config, () => { throw new Error(); }, {
|
|
@@ -252,6 +291,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
252
291
|
const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
|
|
253
292
|
const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
|
|
254
293
|
completionTiming = observedCompletionTiming({ completedAt: Date.now() }, { kind: "found", status: reservation }, reservation);
|
|
294
|
+
// A final optional call can drain successfully after an internal abort.
|
|
295
|
+
controller.signal.throwIfAborted();
|
|
255
296
|
// The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
|
|
256
297
|
if (retainUnsaved)
|
|
257
298
|
clearInterval(heartbeat);
|
|
@@ -260,12 +301,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
260
301
|
let saveContext;
|
|
261
302
|
const saveReports = async () => {
|
|
262
303
|
const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
|
|
263
|
-
...analysisSaveOptions(selection
|
|
304
|
+
...analysisSaveOptions(selection), selection: membership, includedPaths,
|
|
264
305
|
...(options.addToAnalysisId ? { append: { readPaths: includedPaths, assessments } } : {}) });
|
|
306
|
+
localPaths = paths;
|
|
307
|
+
if (retainUnsaved && keepLocalResults && completedPayload && !savedResult) {
|
|
308
|
+
try {
|
|
309
|
+
localReportReceipt = (await readResultReceipt(folder, outputDir)).receipt;
|
|
310
|
+
await persist(completedPayload, completionOrigin);
|
|
311
|
+
}
|
|
312
|
+
catch { /* A missing receipt cannot undo reports already written in this invocation. */ }
|
|
313
|
+
}
|
|
265
314
|
try {
|
|
266
315
|
options.onLocalSaved?.(paths);
|
|
267
|
-
if (paths.ignoreAdded)
|
|
268
|
-
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
269
316
|
}
|
|
270
317
|
catch { /* Output failure must not change a server outcome. */ }
|
|
271
318
|
await saveContext?.();
|
|
@@ -274,7 +321,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
274
321
|
// The retry off path saves the full report even when intake validation refuses it.
|
|
275
322
|
let paths = retainUnsaved ? undefined : await saveReports();
|
|
276
323
|
const { payload, resultCheck, findingIndexes, findingActivities } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
277
|
-
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot, consoleSupportsActivity: options.consoleSupportsActivity,
|
|
324
|
+
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot, consoleSupportsActivity: options.consoleSupportsActivity,
|
|
278
325
|
...(!skippedFilesEnabled(settings) || !options.addToAnalysisId && !sizeSkippedFiles(skippedFiles).length ? {} : { coverage: { skippedFiles: sizeSkippedFiles(skippedFiles), readPaths: includedPaths },
|
|
279
326
|
addToAnalysisId: options.addToAnalysisId, findingAssessments: report.findingCheck.qualityAudit?.assessments ?? [] }) });
|
|
280
327
|
displayFindings = findingIndexes.map((index, acceptedIndex) => {
|
|
@@ -297,9 +344,9 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
297
344
|
if (resultCheck?.droppedFindings) {
|
|
298
345
|
const count = resultCheck.droppedFindings;
|
|
299
346
|
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
300
|
-
? `${count
|
|
347
|
+
? `${countForm(count, "it was", "they were")} too long or not in the expected format` : `${countForm(count, "its", "their")} quotes did not match your files`;
|
|
301
348
|
try {
|
|
302
|
-
stderr(`${count} ${count
|
|
349
|
+
stderr(`${count} ${countForm(count, "finding was", "findings were")} not uploaded because ${reason}.\n`);
|
|
303
350
|
}
|
|
304
351
|
catch { /* Output failure must not change a server outcome. */ }
|
|
305
352
|
}
|
|
@@ -335,12 +382,13 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
335
382
|
return 0;
|
|
336
383
|
}
|
|
337
384
|
catch (caught) {
|
|
338
|
-
|
|
385
|
+
// Reading passes can sanitize errors; this lifecycle owns internal abort causes.
|
|
386
|
+
let error = !completed && internalAbortCause && !options.signal?.aborted ? internalAbortCause : caught;
|
|
339
387
|
let status;
|
|
340
388
|
let statusLookup = { kind: "skipped" };
|
|
341
389
|
if (client && reservationId) {
|
|
342
390
|
try {
|
|
343
|
-
status = await (completed || uploaded ? client.status(reservationId) : client.fail(reservationId));
|
|
391
|
+
status = await (completed || uploaded ? client.status(reservationId) : client.fail(reservationId, consoleSupportsFailureReason && !(error instanceof AgentChoiceError) ? failureReason(error) : undefined));
|
|
344
392
|
statusLookup = { kind: "found", status };
|
|
345
393
|
}
|
|
346
394
|
catch (statusError) {
|
|
@@ -368,12 +416,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
368
416
|
}
|
|
369
417
|
if (error instanceof ResultsUploadError)
|
|
370
418
|
completionTiming = error.timing ?? completionTiming;
|
|
371
|
-
const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
372
|
-
timing: completionTiming, statusLookup, uploadOutcome, savedResult,
|
|
373
|
-
canWrite: !lostCompletionLock, completionStored,
|
|
419
|
+
const failure = await recoverRelayFailure({ error, cancelled: options.signal?.aborted, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
420
|
+
timing: completionTiming, statusLookup, reservationId, uploadOutcome, savedResult,
|
|
421
|
+
canWrite: !lostCompletionLock, completionStored, reportStored: localPaths !== undefined,
|
|
422
|
+
keepLocalResults, localErrorMessage, fileSelection: selection?.kind === "file", fileMode: fileScopeEnabled(settings) }, {
|
|
374
423
|
markUploaded: () => persist(completedPayload, completionOrigin, true),
|
|
375
424
|
keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
|
|
376
|
-
retire: () =>
|
|
425
|
+
retire: async () => {
|
|
426
|
+
if (!retainUnsaved)
|
|
427
|
+
return persist(completedPayload, completionOrigin, false, true);
|
|
428
|
+
if (completionRecordPath)
|
|
429
|
+
retainedPath = await discardCompletion(folder, completionRecordPath, completionIdentity, { keepCopy: keepLocalResults, reportStored: localPaths !== undefined });
|
|
430
|
+
},
|
|
377
431
|
});
|
|
378
432
|
if (failure.saved) {
|
|
379
433
|
try {
|
|
@@ -400,10 +454,14 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
400
454
|
}
|
|
401
455
|
catch { /* A failed local plan preserves the console's authoritative refusal. */ }
|
|
402
456
|
}
|
|
457
|
+
const savedPaths = localPaths ? [localPaths.findingsPath, localPaths.summaryPath] : retainedPath ? [retainedPath]
|
|
458
|
+
: completionStored && !lostCompletionLock && completionRecordPath ? [completionRecordPath] : [];
|
|
459
|
+
const message = failure.kept && savedPaths.length
|
|
460
|
+
? `${failure.message}\n${savedPaths.map(path => `Saved ${path}`).join("\n")}` : failure.message;
|
|
403
461
|
try {
|
|
404
|
-
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? failure.
|
|
462
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? message, ...(failure.charged ? { charged: true } : {}),
|
|
405
463
|
...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
406
|
-
stderr(`${scopeChoice?.message ??
|
|
464
|
+
stderr(`${scopeChoice?.message ?? message}\n`);
|
|
407
465
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
408
466
|
stderr(stateLine(status));
|
|
409
467
|
}
|
|
@@ -2,7 +2,7 @@ import { CompletionBusyError } from "../adapters/completion-lock.js";
|
|
|
2
2
|
import { OutputPathError } from "../adapters/output-paths.js";
|
|
3
3
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
4
4
|
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
5
|
-
import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE,
|
|
5
|
+
import { analysisFailureDetails, CliRefusalError, cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, START_FAILURE_MESSAGE, relayChargeStatus, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
6
6
|
import { isSelectionRefusal, selectionRefusalMessage } from "../contract/scope-choice.js";
|
|
7
7
|
import { ConsoleError } from "../gatekeeper/console.js";
|
|
8
8
|
import { recoverCompletedResults } from "./completion-delivery.js";
|
|
@@ -15,18 +15,26 @@ export async function recoverRelayFailure(input, operations) {
|
|
|
15
15
|
const recovery = input.payload && input.completed ? await recoverCompletedResults({
|
|
16
16
|
retryEnabled, payload: input.payload, timing: input.timing, statusLookup: input.statusLookup,
|
|
17
17
|
uploadOutcome: retryEnabled && input.savedResult ? { kind: "saved" } : carried ?? { kind: "cancelled" },
|
|
18
|
-
canWrite: input.canWrite, completionStored: input.completionStored,
|
|
18
|
+
canWrite: input.canWrite, completionStored: input.completionStored, reportStored: input.reportStored,
|
|
19
|
+
keepLocalResults: input.keepLocalResults, localErrorMessage: input.localErrorMessage,
|
|
19
20
|
}, operations) : undefined;
|
|
20
21
|
if (recovery?.decision.action === "saved")
|
|
21
|
-
return { saved: true,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
return { saved: true,
|
|
23
|
+
message: error instanceof CompletionBusyError ? error.message : recovery.message, reason: "results_saved_report_failed" };
|
|
24
|
+
const selectionRefusal = isSelectionRefusal(error, input.fileSelection ?? false);
|
|
25
|
+
const known = error instanceof CliRefusalError || error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError
|
|
26
|
+
|| selectionRefusal || error instanceof CompletionBusyError || error instanceof CompletionSettingsMismatchError || error instanceof Error
|
|
25
27
|
&& ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
26
28
|
|| error.message.startsWith("The saved completion"));
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const status = input.statusLookup.kind === "found" ? input.statusLookup.status : undefined;
|
|
30
|
+
const charge = relayChargeStatus(status, !input.reservationId && input.statusLookup.kind === "skipped");
|
|
31
|
+
const failure = error instanceof ConsoleError ? undefined : analysisFailureDetails(error, charge, { cancelled: input.cancelled, noReservation: !input.reservationId });
|
|
32
|
+
const knownMessage = known ? error.message : undefined;
|
|
33
|
+
const failureMessage = failure?.reason === "request_failed" ? knownMessage ?? failure.message : failure?.message;
|
|
34
|
+
const message = recovery?.message ?? input.localErrorMessage ?? failureMessage ?? knownMessage ?? (!input.reservationId && !input.completed ? START_FAILURE_MESSAGE : FINISH_FAILURE_MESSAGE);
|
|
35
|
+
const originalReason = failure?.reason ?? failureReason(error);
|
|
36
|
+
const reason = recovery && "reason" in recovery.decision ? recovery.decision.reason ?? originalReason : originalReason;
|
|
37
|
+
return { saved: false, message: selectionRefusalMessage(message, input.fileMode ?? true), reason,
|
|
38
|
+
...(recovery?.decision.action === "give-up" && recovery.decision.kept ? { kept: true } : {}),
|
|
39
|
+
...(reason !== "results_pending" && (charge === "charged" || charge === "free_used") ? { charged: true } : {}) };
|
|
32
40
|
}
|
|
@@ -7,7 +7,7 @@ import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
|
7
7
|
import { readLocalFolder } from "../adapters/files.js";
|
|
8
8
|
import { snapshotProvenance, sameFileHashes } from "../adapters/snapshot.js";
|
|
9
9
|
import { validateClientRelease } from "../adapters/release.js";
|
|
10
|
-
import { prepareRelayPlan } from "../adapters/
|
|
10
|
+
import { prepareRelayPlan } from "../adapters/relay-plan.js";
|
|
11
11
|
import { readAnalysisSource } from "../adapters/selection.js";
|
|
12
12
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
13
13
|
import { COMMITTED_FILES_MESSAGE } from "../contract/cli-refusals.js";
|
|
@@ -16,9 +16,12 @@ import { selectionStartIdentity } from "../contract/selection-metadata.js";
|
|
|
16
16
|
import { repositoryFacts } from "./git.js";
|
|
17
17
|
import { proposalWhollyRefused } from "../analysis-core/index.js";
|
|
18
18
|
/** Absolute checkout paths stay local; repository facts own the start's commit. */
|
|
19
|
-
export function buildRelayStartPayload(facts, files) {
|
|
19
|
+
export function buildRelayStartPayload(facts, files, coordinates = {}) {
|
|
20
20
|
const { remoteUrl, relPath, folderName, branch, commit, dirty } = facts;
|
|
21
|
-
|
|
21
|
+
// Older consoles placed repository-relative results under an empty selected folder.
|
|
22
|
+
const legacy = process.env.CF_CLI_RELPATH === "0";
|
|
23
|
+
const folder = legacy && coordinates.resultBase && coordinates.resultBase !== coordinates.readRoot ? "" : relPath;
|
|
24
|
+
return { execution: "local", remoteUrl, relPath: folder, folderName, branch, commit, dirty, files };
|
|
22
25
|
}
|
|
23
26
|
export function committedFilesOnly(settings) {
|
|
24
27
|
return settings.CF_LOCAL_COMMITTED_ONLY === "1";
|
|
@@ -66,7 +69,7 @@ export async function prepareFolderChoice(folder, options) {
|
|
|
66
69
|
}
|
|
67
70
|
/** Recovery can reuse signature validation without reading the folder again. */
|
|
68
71
|
export async function prepareRelayCheck(folder, options, validated = validateClientRelease(options.release, options.settings)) {
|
|
69
|
-
const { bundle } = validated;
|
|
72
|
+
const { bundle, tools } = validated;
|
|
70
73
|
options.signal?.throwIfAborted();
|
|
71
74
|
// Discovery must agree with Git before publishing repository-relative captures.
|
|
72
75
|
const selection = options.selection;
|
|
@@ -95,13 +98,13 @@ export async function prepareRelayCheck(folder, options, validated = validateCli
|
|
|
95
98
|
}
|
|
96
99
|
else if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
|
|
97
100
|
throw new Error(COMMITTED_FILES_MESSAGE);
|
|
98
|
-
const { plan, files } = prepareRelayPlan({ bundle, settings: validated.settings, relayVersion: options.release.relayVersion, source });
|
|
101
|
+
const { plan, files } = prepareRelayPlan({ bundle, tools, settings: validated.settings, relayVersion: options.release.relayVersion, source });
|
|
99
102
|
options.signal?.throwIfAborted();
|
|
100
103
|
const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(factsFolder);
|
|
101
104
|
options.signal?.throwIfAborted();
|
|
102
105
|
const startMetadata = { analysisSize: source.analysisSize, startAttempt: randomUUID(),
|
|
103
106
|
...(selection?.kind === "file" && membership
|
|
104
107
|
? { selection: selectionStartIdentity(selection.selectionKey, membership.snapshot) } : {}) };
|
|
105
|
-
return { source, provenance, plan, start: buildRelayStartPayload(
|
|
108
|
+
return { source, provenance, plan, start: buildRelayStartPayload(facts, files, { resultBase: selection?.kind !== "file" ? source.sourceBase : undefined, readRoot: folder }), startMetadata,
|
|
106
109
|
...(membership ? { membership } : {}) };
|
|
107
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { outputPaths,
|
|
3
|
+
import { outputPaths, reportOutputDirectory, writeReportFiles } from "../adapters/output-paths.js";
|
|
4
4
|
import { analysisScopeSchema, chooseScopeResultSchema, INCLUDED_FILES_CAPTION, OMITTED_IMPORTS_CAPTION, OMITTED_FILES_CAPTION, SCOPE_CHOICE_INSTRUCTION, INCOMPLETE_IMPORTS_MESSAGE, IMPORT_DISCOVERY_LIMIT_MESSAGE, INVALID_SCOPE_MESSAGE, SCOPE_RETRY_TEMPLATE } from "../contract/scope-choice.js";
|
|
5
5
|
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
6
6
|
import { scopeChoicesEnabled } from "../adapters/settings.js";
|
|
@@ -45,13 +45,14 @@ export function scopeChoiceText(value, retryPaths = {}) {
|
|
|
45
45
|
export async function renderScopeChoices(value, anchor) {
|
|
46
46
|
const result = chooseScopeResultSchema.parse(value);
|
|
47
47
|
// Validate the output directory and old entries before removing only generated scope files.
|
|
48
|
-
const
|
|
48
|
+
const outputDir = await reportOutputDirectory(anchor.outputAnchor, anchor.outputDir);
|
|
49
|
+
const [probe] = await outputPaths(anchor.outputAnchor, ["scope-1.json"], outputDir, true);
|
|
49
50
|
const old = (await readdir(dirname(probe))).filter(name => /^scope-.*\.json$/.test(name));
|
|
50
|
-
await outputPaths(anchor.outputAnchor, old,
|
|
51
|
+
await outputPaths(anchor.outputAnchor, old, outputDir);
|
|
51
52
|
for (const name of old)
|
|
52
53
|
await unlink(join(dirname(probe), name));
|
|
53
54
|
const runnable = result.choices.flatMap((choice, index) => choice.fits === false ? [] : [{ choice, index }]);
|
|
54
55
|
const files = Object.fromEntries(runnable.map(({ choice, index }) => [`scope-${index + 1}.json`, JSON.stringify(choice.scope, null, 2) + "\n"]));
|
|
55
|
-
const paths = await
|
|
56
|
+
const paths = await writeReportFiles(anchor.outputAnchor, files, outputDir);
|
|
56
57
|
return scopeChoiceText(result, Object.fromEntries(runnable.map(({ index }, position) => [index, paths[position]])));
|
|
57
58
|
}
|