@counterfact-labs/cli 0.2.10 → 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/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/schemas.js +1 -1
- package/dist/analysis-core/chunks/{5VQ4VYIP.js → 2IQN26TC.js} +1 -1
- package/dist/analysis-core/chunks/{N2L4JMJE.js → 2J3PWUPG.js} +1 -1
- package/dist/analysis-core/chunks/{3GUKUEEJ.js → 5JG3ZRNJ.js} +1 -1
- package/dist/analysis-core/chunks/{LFJQ4YLP.js → 6E4K7JTI.js} +1 -1
- package/dist/analysis-core/chunks/{66JRJPJC.js → 6W6B7VSP.js} +1 -1
- package/dist/analysis-core/chunks/{P253B344.js → AIS3FHQR.js} +7 -7
- package/dist/analysis-core/chunks/{YWL52FVW.js → EJ2IZUN2.js} +2 -2
- package/dist/analysis-core/chunks/{5UWDUYHF.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/{TA2NIQAY.js → I5F7S63K.js} +1 -1
- package/dist/analysis-core/chunks/ICJV6KIZ.js +11 -0
- package/dist/analysis-core/chunks/{2CWULBP2.js → JCVB6IC4.js} +1 -1
- package/dist/analysis-core/chunks/{DHVEGIU6.js → JLW44GIU.js} +1 -1
- package/dist/analysis-core/chunks/JS6HTVYX.js +1 -0
- package/dist/analysis-core/chunks/{UKX3A6AX.js → JWQXG5RB.js} +1 -1
- package/dist/analysis-core/chunks/LOODGUMW.js +1 -0
- package/dist/analysis-core/chunks/{XVGLY4EO.js → NWYNJHY4.js} +1 -1
- package/dist/analysis-core/chunks/{KBTM5MBC.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/{6VHZNBII.js → QENYFKG3.js} +8 -8
- package/dist/analysis-core/chunks/{W73WF226.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/{VWRFSVLI.js → VBGQKYIT.js} +1 -1
- package/dist/analysis-core/chunks/{5BTATES5.js → VVSHUUWX.js} +1 -1
- 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/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/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 +27 -21
- 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/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 +85 -23
- package/dist/cli/relay-failure.js +19 -11
- package/dist/cli/relay-preparation.js +6 -3
- 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 +8 -4
- package/dist/gatekeeper/model.js +13 -3
- package/package.json +1 -1
- package/dist/adapters/gitignore-output.js +0 -41
- package/dist/analysis-core/chunks/2M55DR2J.js +0 -35
- package/dist/analysis-core/chunks/34NVU26H.js +0 -1
- package/dist/analysis-core/chunks/C77LGCKF.js +0 -1
- package/dist/analysis-core/chunks/HCPO6QPU.js +0 -1
- package/dist/analysis-core/chunks/J4AXGE5W.js +0 -1
- package/dist/analysis-core/chunks/M5YWJY7P.js +0 -1
- package/dist/analysis-core/chunks/NCW5LJ5O.js +0 -6
- package/dist/analysis-core/chunks/QPIBHTON.js +0 -67
- package/dist/analysis-core/chunks/SJESFYBA.js +0 -10
- package/dist/analysis-core/chunks/VC3PYT2B.js +0 -1
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;
|
|
@@ -188,6 +218,12 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
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
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
|
+
}
|
|
191
227
|
if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? await client.supportsSkippedFiles())) {
|
|
192
228
|
if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
|
|
193
229
|
throw new ConsoleError("unavailable", "start");
|
|
@@ -229,10 +265,17 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
229
265
|
if (renewal)
|
|
230
266
|
return;
|
|
231
267
|
renewal = activeClient.heartbeat(activeId).then(status => {
|
|
232
|
-
if (status.state !== "reserved")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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; });
|
|
236
279
|
}, 60000);
|
|
237
280
|
heartbeat.unref();
|
|
238
281
|
const execution = await selectAnalysisExecution(config, () => { throw new Error(); }, {
|
|
@@ -248,6 +291,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
248
291
|
const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
|
|
249
292
|
const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
|
|
250
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();
|
|
251
296
|
// The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
|
|
252
297
|
if (retainUnsaved)
|
|
253
298
|
clearInterval(heartbeat);
|
|
@@ -256,12 +301,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
256
301
|
let saveContext;
|
|
257
302
|
const saveReports = async () => {
|
|
258
303
|
const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
|
|
259
|
-
...analysisSaveOptions(selection
|
|
304
|
+
...analysisSaveOptions(selection), selection: membership, includedPaths,
|
|
260
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
|
+
}
|
|
261
314
|
try {
|
|
262
315
|
options.onLocalSaved?.(paths);
|
|
263
|
-
if (paths.ignoreAdded)
|
|
264
|
-
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
265
316
|
}
|
|
266
317
|
catch { /* Output failure must not change a server outcome. */ }
|
|
267
318
|
await saveContext?.();
|
|
@@ -293,9 +344,9 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
293
344
|
if (resultCheck?.droppedFindings) {
|
|
294
345
|
const count = resultCheck.droppedFindings;
|
|
295
346
|
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
296
|
-
? `${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`;
|
|
297
348
|
try {
|
|
298
|
-
stderr(`${count} ${count
|
|
349
|
+
stderr(`${count} ${countForm(count, "finding was", "findings were")} not uploaded because ${reason}.\n`);
|
|
299
350
|
}
|
|
300
351
|
catch { /* Output failure must not change a server outcome. */ }
|
|
301
352
|
}
|
|
@@ -331,12 +382,13 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
331
382
|
return 0;
|
|
332
383
|
}
|
|
333
384
|
catch (caught) {
|
|
334
|
-
|
|
385
|
+
// Reading passes can sanitize errors; this lifecycle owns internal abort causes.
|
|
386
|
+
let error = !completed && internalAbortCause && !options.signal?.aborted ? internalAbortCause : caught;
|
|
335
387
|
let status;
|
|
336
388
|
let statusLookup = { kind: "skipped" };
|
|
337
389
|
if (client && reservationId) {
|
|
338
390
|
try {
|
|
339
|
-
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));
|
|
340
392
|
statusLookup = { kind: "found", status };
|
|
341
393
|
}
|
|
342
394
|
catch (statusError) {
|
|
@@ -364,12 +416,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
364
416
|
}
|
|
365
417
|
if (error instanceof ResultsUploadError)
|
|
366
418
|
completionTiming = error.timing ?? completionTiming;
|
|
367
|
-
const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
368
|
-
timing: completionTiming, statusLookup, uploadOutcome, savedResult,
|
|
369
|
-
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) }, {
|
|
370
423
|
markUploaded: () => persist(completedPayload, completionOrigin, true),
|
|
371
424
|
keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
|
|
372
|
-
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
|
+
},
|
|
373
431
|
});
|
|
374
432
|
if (failure.saved) {
|
|
375
433
|
try {
|
|
@@ -396,10 +454,14 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
396
454
|
}
|
|
397
455
|
catch { /* A failed local plan preserves the console's authoritative refusal. */ }
|
|
398
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;
|
|
399
461
|
try {
|
|
400
|
-
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 } : {}),
|
|
401
463
|
...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
402
|
-
stderr(`${scopeChoice?.message ??
|
|
464
|
+
stderr(`${scopeChoice?.message ?? message}\n`);
|
|
403
465
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
404
466
|
stderr(stateLine(status));
|
|
405
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
|
}
|
|
@@ -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";
|
|
@@ -102,6 +105,6 @@ export async function prepareRelayCheck(folder, options, validated = validateCli
|
|
|
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
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
1
2
|
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
3
|
import { skippedFilesNotice, ANALYZE_SKIPPED_FILES_OFFER } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
4
|
import { analyzeCommand } from "../contract/agent-choice.js";
|
|
@@ -15,7 +16,7 @@ export function skippedResultText(files, settings = {}, options = {}) {
|
|
|
15
16
|
}
|
|
16
17
|
export function followupPlanText(plan) {
|
|
17
18
|
const count = plan.groups.length;
|
|
18
|
-
return [count ? `This will run ${count} ${count
|
|
19
|
+
return [count ? `This will run ${count} ${countForm(count, "analysis", "analyses")}.` : "No skipped files remain to analyze.",
|
|
19
20
|
...plan.tooLarge.map(path => `${path}: too large to analyze.`),
|
|
20
21
|
...plan.unavailable.map(path => `${path}: could not be read.`),
|
|
21
22
|
...(plan.missingContext ?? []).map(file => `${file.path}: some imported files could not be included: ${file.imports.join(", ")}.`), ""].join("\n");
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { normalizeSignInEmail } from "./email-sign-in.js";
|
|
2
2
|
import { userEmail } from "./git.js";
|
|
3
|
+
function isNoreplyEmail(email) {
|
|
4
|
+
// These addresses cannot receive the sign-in code.
|
|
5
|
+
return /(?:^|[.@+_-])(?:no[._-]?reply|do[._-]?not[._-]?reply)(?:$|[.@+_-])/.test(email);
|
|
6
|
+
}
|
|
3
7
|
export async function suggestedEmail(folder, readEmail = userEmail) {
|
|
4
8
|
try {
|
|
5
|
-
|
|
9
|
+
const email = normalizeSignInEmail(await readEmail(folder));
|
|
10
|
+
return email && !isNoreplyEmail(email) ? email : undefined;
|
|
6
11
|
}
|
|
7
12
|
catch {
|
|
8
13
|
return undefined;
|
package/dist/cli/summary.js
CHANGED
|
@@ -2,9 +2,10 @@ import { skippedResultText } from "./skipped-files.js";
|
|
|
2
2
|
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
3
3
|
import { activityAdvice, findingDisplayRows, notActiveCountLine, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
|
|
4
4
|
import { groupByActivity } from "../analysis-core/analysis/schemas.js";
|
|
5
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
5
6
|
export function formatSummary(report, options) {
|
|
6
7
|
const lines = ["# Counterfact analysis", "",
|
|
7
|
-
`${report.findings.length} findings; ${report.structures.length} structural
|
|
8
|
+
`${report.findings.length} ${countForm(report.findings.length, "finding")}; ${report.structures.length} ${countForm(report.structures.length, "structural proposal")}.`, ""];
|
|
8
9
|
const skipped = skippedResultText(report.refusedFiles ?? [], { CF_SKIPPED_FILES: options.skippedFilesEnabled === false ? "0" : "1" }, { offer: options.skippedOffer, folder: options.analyzedFolder });
|
|
9
10
|
if (skipped)
|
|
10
11
|
lines.push(skipped.trimEnd(), "");
|
|
@@ -8,9 +8,11 @@ export function analyzeCommand(folder) {
|
|
|
8
8
|
/** The local importer supplies the choice; transport keeps its data and wording together. */
|
|
9
9
|
export class AgentChoiceError extends Error {
|
|
10
10
|
agentChoice;
|
|
11
|
+
reason;
|
|
11
12
|
constructor(agentChoice) {
|
|
12
13
|
super(agentChoiceMessage(agentChoice));
|
|
13
14
|
this.agentChoice = agentChoice;
|
|
15
|
+
this.reason = agentChoice.kind;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
export function agentChoiceMessage(agentChoice, surface = "terminal") {
|