@counterfact-labs/cli 0.2.7 → 0.2.9
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/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/entry-files.js +5 -2
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +13 -1
- package/dist/adapters/files.js +110 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +9 -2
- package/dist/adapters/selection.js +19 -9
- package/dist/adapters/settings.js +37 -6
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{6RWEWBHK.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{FBTOOUPL.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/{RNTMH4MU.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -1
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.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/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -1
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- 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/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +47 -5
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +58 -15
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/context.js +2 -1
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +21 -7
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +134 -28
- package/dist/cli/relay-check.js +107 -23
- package/dist/cli/relay-failure.js +4 -4
- package/dist/cli/relay-preparation.js +50 -12
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +3 -3
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +31 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +17 -38
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +31 -11
- package/dist/contract/selection-offer.js +5 -8
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +19 -3
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +4 -2
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/2PZF54KH.js +0 -10
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/7NEWZ74F.js +0 -1
- package/dist/analysis-core/chunks/CGCJN73D.js +0 -14
- package/dist/analysis-core/chunks/CWYVUGG4.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/GOQVKWP3.js +0 -32
- package/dist/analysis-core/chunks/IHAVDX4L.js +0 -66
- package/dist/analysis-core/chunks/IVULXWQN.js +0 -21
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/K5FUABWW.js +0 -1
- package/dist/analysis-core/chunks/KGQYR62R.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/PY5M22CL.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/SLJ6CW7W.js +0 -1
- package/dist/analysis-core/chunks/UEORNVQB.js +0 -1
- package/dist/analysis-core/chunks/WYLIV2P2.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
package/dist/cli/mcp.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { readSkippedFilesSetting, readFolderPartsSetting } from "../adapters/settings.js";
|
|
1
3
|
import { createInterface } from "node:readline";
|
|
2
4
|
import { once } from "node:events";
|
|
3
5
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
4
|
-
import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
6
|
+
import { cliRefusals, CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
5
7
|
import { mcpFailure } from "./mcp-failure.js";
|
|
8
|
+
import { agentChoiceMessage } from "../contract/agent-choice.js";
|
|
6
9
|
import { errorCode } from "./api.js";
|
|
7
10
|
import { commandContext } from "./context.js";
|
|
8
11
|
import { analyze } from "./commands/analyze.js";
|
|
12
|
+
import { skipped } from "./commands/skipped.js";
|
|
9
13
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
10
14
|
import { finding } from "./commands/finding.js";
|
|
11
15
|
import { balance } from "./commands/balance.js";
|
|
@@ -19,10 +23,12 @@ import { suggestedEmail } from "./suggested-email.js";
|
|
|
19
23
|
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
20
24
|
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
21
25
|
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
22
|
-
import {
|
|
26
|
+
import { formatFindingNumber, formatWaitingFixes, formatFindingActivity, presentSavedFindings, findingDetailReference, formatFindingContinuation } from "./finding-display.js";
|
|
27
|
+
import { groupByActivity } from "../analysis-core/analysis/schemas.js";
|
|
23
28
|
import { readFindingSettings } from "./finding-settings.js";
|
|
29
|
+
import { sizeSkippedFiles } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
24
30
|
import { z } from "zod";
|
|
25
|
-
import { readCliSettings, readSelectionSettings, selectionModeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
31
|
+
import { fileScopeEnabled, readCliSettings, readSelectionSettings, selectionModeEnabled, scopeChoicesEnabled, multiEntryEnabled } from "../adapters/settings.js";
|
|
26
32
|
import { resolveAnalysisSelection } from "./analysis-selection.js";
|
|
27
33
|
import { analysisScopeSchema, chooseScopeMcpResultSchema } from "../contract/scope-choice.js";
|
|
28
34
|
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
@@ -34,7 +40,12 @@ const filePathDescription = "The agent folder or agent file on this computer, in
|
|
|
34
40
|
const fileScopeDescription = "Optional: a scope returned by Counterfact, using the same snapshot and selected file. Include all imports, no imports, or the listed imports chosen by the user.";
|
|
35
41
|
const fileProposalDescription = "Optional: the files you believe make up the agent, with each file's role. For a folder, paths are relative to that folder. For a file, paths are relative to the launch folder and must belong to the selected file's import scope.";
|
|
36
42
|
const fileProposalPathDescription = "Paths are relative to the selected folder, or to the launch folder when selecting a file.";
|
|
37
|
-
|
|
43
|
+
const multiAnalysisDescription = "Analyze an agent folder, one agent file, or several agent files on this computer. Pass path for one folder or file, or paths for several files inside the launch folder. Counterfact includes each file's project imports and reads shared files once. Up to 5 agent files run as one analysis with one result list and one charge unless a free analysis is available. If the selection is too large or has more than 5 agent files, Counterfact returns scope choices. Call only when the user asks for an analysis. A free first analysis needs no top-up. After edits, suggest an analysis and wait for the user to ask.";
|
|
44
|
+
const multiPathsDescription = "Optional: several agent file paths inside the launch folder, analyzed together. Use either path or paths. Folders cannot be mixed into paths.";
|
|
45
|
+
const partChoiceDescription = " If parts are returned, ask the user to choose one, then call analyze_agent again with that part's files as scope.";
|
|
46
|
+
const partScopeSchema = { type: "array", items: { type: "string" }, minItems: 1,
|
|
47
|
+
description: "The files in the part the user chose. Ask the user to pick a returned part before supplying scope." };
|
|
48
|
+
function fileAnalysisTool(choices, parts, multiple) {
|
|
38
49
|
const proposal = structuredClone(agentProposalInputSchema);
|
|
39
50
|
if (typeof proposal.properties?.files === "object" && proposal.properties.files && "items" in proposal.properties.files) {
|
|
40
51
|
const files = proposal.properties.files.items;
|
|
@@ -43,11 +54,16 @@ function fileAnalysisTool(choices) {
|
|
|
43
54
|
}
|
|
44
55
|
const scope = z.toJSONSchema(analysisScopeSchema);
|
|
45
56
|
delete scope.$schema;
|
|
57
|
+
if (!multiple && scope.properties)
|
|
58
|
+
delete scope.properties.entries;
|
|
59
|
+
const scopeSchema = parts && choices ? { anyOf: [scope, partScopeSchema] } : parts ? partScopeSchema : scope;
|
|
46
60
|
return { name: ANALYZE_TOOL_NAME,
|
|
47
|
-
description: [fileAnalysisDescription, ...(choices ? [scopeChoiceDescription] : []), fileAnalysisEnding].join(" "),
|
|
61
|
+
description: [multiple ? multiAnalysisDescription : fileAnalysisDescription, ...(choices ? [scopeChoiceDescription] : []), fileAnalysisEnding].join(" ") + (parts ? partChoiceDescription : ""),
|
|
48
62
|
inputSchema: { type: "object", properties: { path: { type: "string", description: filePathDescription },
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
...(multiple ? { paths: { type: "array", items: { type: "string" }, minItems: 1, description: multiPathsDescription } } : {}),
|
|
64
|
+
proposal: { ...proposal, description: fileProposalDescription }, ...(choices || parts ? { scope: { ...scopeSchema, description: choices && parts
|
|
65
|
+
? `${fileScopeDescription} ${partScopeSchema.description}` : parts ? partScopeSchema.description : fileScopeDescription } } : {}) },
|
|
66
|
+
...(multiple ? {} : { required: ["path"] }), additionalProperties: false } };
|
|
51
67
|
}
|
|
52
68
|
const legacyTools = [
|
|
53
69
|
{
|
|
@@ -60,6 +76,12 @@ const legacyTools = [
|
|
|
60
76
|
{ name: "sign_in_status", description: "Check whether this computer is signed in to Counterfact, and whether the user has allowed the analysis tool in Claude Code. With wait: true, first wait for a sign-in started by sign_in to finish.", inputSchema: { type: "object", properties: { wait: { type: "boolean" } }, additionalProperties: false } },
|
|
61
77
|
{ name: "sign_in", description: "Open the Counterfact sign-in page in the user's browser. Then call sign_in_status with wait: true.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
62
78
|
];
|
|
79
|
+
const skippedTool = {
|
|
80
|
+
name: "analyze_skipped_files",
|
|
81
|
+
description: "Handle files skipped by the saved analysis of a local folder. Use action files to list their names, or plan to find the fewest analyses that fit, including imported files. Tell the user that count before starting. Use action analyze only after the user accepts the offer to analyze the skipped files. Findings are added to the same results.",
|
|
82
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: "The original agent folder on this computer." },
|
|
83
|
+
action: { type: "string", enum: ["files", "plan", "analyze"] } }, required: ["path", "action"], additionalProperties: false },
|
|
84
|
+
};
|
|
63
85
|
class ProtocolError extends Error {
|
|
64
86
|
code;
|
|
65
87
|
constructor(code, message) {
|
|
@@ -67,6 +89,7 @@ class ProtocolError extends Error {
|
|
|
67
89
|
this.code = code;
|
|
68
90
|
}
|
|
69
91
|
}
|
|
92
|
+
const MAX_ANALYSIS_FINDING_HISTORY = 5;
|
|
70
93
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
71
94
|
export async function serveMcp(input, output, options = {}) {
|
|
72
95
|
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
@@ -75,13 +98,21 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
75
98
|
const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
|
|
76
99
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
77
100
|
const lastAnalysis = new Map();
|
|
78
|
-
const
|
|
79
|
-
const
|
|
101
|
+
const startupSettings = options.startup?.settings ?? options.env ?? process.env;
|
|
102
|
+
const skippedSettings = Object.freeze({ CF_SKIPPED_FILES: readSkippedFilesSetting(startupSettings.CF_SKIPPED_FILES) });
|
|
103
|
+
const skippedEnabled = skippedFilesEnabled(skippedSettings);
|
|
104
|
+
const partSettings = Object.freeze({ CF_FOLDER_PARTS: readFolderPartsSetting(startupSettings.CF_FOLDER_PARTS) });
|
|
105
|
+
const scopeEnabled = folderPartsEnabled({ ...skippedSettings, ...partSettings });
|
|
106
|
+
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
107
|
+
const notActiveFindings = new Map();
|
|
108
|
+
const analysisFindings = new Map();
|
|
109
|
+
const { numbersEnabled, fixesEnabled, agentReadableResults } = findingSettings;
|
|
110
|
+
const analysisSwitches = readSelectionSettings(startupSettings);
|
|
80
111
|
const choicesEnabled = scopeChoicesEnabled(analysisSwitches);
|
|
81
112
|
const selectionEnabled = selectionModeEnabled(analysisSwitches);
|
|
113
|
+
const multipleEnabled = multiEntryEnabled(analysisSwitches);
|
|
82
114
|
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches } : undefined;
|
|
83
115
|
let analysisStartup = options.startup;
|
|
84
|
-
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
85
116
|
const references = new FindingReferences();
|
|
86
117
|
const emailEnv = analysisEnv && Object.hasOwn(analysisEnv, "CF_EMAIL_SIGN_IN") ? analysisEnv : options.env;
|
|
87
118
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(emailEnv);
|
|
@@ -110,9 +141,19 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
110
141
|
}, required: ["email", "code"], additionalProperties: false },
|
|
111
142
|
},
|
|
112
143
|
] : legacyTools;
|
|
113
|
-
const tools = signInTools.map(tool =>
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
const tools = [...signInTools.map(tool => {
|
|
145
|
+
if (tool.name === ANALYZE_TOOL_NAME) {
|
|
146
|
+
if (selectionEnabled)
|
|
147
|
+
return fileAnalysisTool(choicesEnabled, scopeEnabled, multipleEnabled);
|
|
148
|
+
if (scopeEnabled)
|
|
149
|
+
return { ...tool,
|
|
150
|
+
description: tool.description + partChoiceDescription,
|
|
151
|
+
inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, scope: partScopeSchema } } };
|
|
152
|
+
}
|
|
153
|
+
return tool;
|
|
154
|
+
}).map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
155
|
+
...tool, inputSchema: numberedFindingInputSchema,
|
|
156
|
+
} : tool), ...(fixesEnabled ? fixRequestTools : []), ...(skippedEnabled ? [skippedTool] : [])];
|
|
116
157
|
let pendingSignIn;
|
|
117
158
|
let signInFailure;
|
|
118
159
|
const requests = new Map();
|
|
@@ -125,14 +166,21 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
125
166
|
if (!object(args))
|
|
126
167
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
127
168
|
const name = params.name;
|
|
128
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal", ...(choicesEnabled ? ["scope"] : [])] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
169
|
+
const allowed = name === "analyze_skipped_files" ? ["path", "action"] : name === ANALYZE_TOOL_NAME ? ["path", "proposal", ...(multipleEnabled ? ["paths"] : []), ...((scopeEnabled || choicesEnabled) ? ["scope"] : [])] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
129
170
|
: name === "claim_fix" ? ["id"]
|
|
130
171
|
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
131
172
|
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
132
173
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
133
174
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
134
|
-
if (name === ANALYZE_TOOL_NAME && (
|
|
175
|
+
if (name === ANALYZE_TOOL_NAME && (args.paths === undefined
|
|
176
|
+
? typeof args.path !== "string" || !args.path.trim()
|
|
177
|
+
: args.path !== undefined || !Array.isArray(args.paths) || !args.paths.length || args.paths.some(path => typeof path !== "string" || !path.trim()))) {
|
|
178
|
+
throw new ProtocolError(-32602, "Provide a path.");
|
|
179
|
+
}
|
|
180
|
+
if (name === "analyze_skipped_files" && (typeof args.path !== "string" || !args.path.trim()))
|
|
135
181
|
throw new ProtocolError(-32602, "Provide a path.");
|
|
182
|
+
if (name === "analyze_skipped_files" && !["files", "plan", "analyze"].includes(args.action))
|
|
183
|
+
throw new ProtocolError(-32602, "Choose files, plan, or analyze.");
|
|
136
184
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
137
185
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
138
186
|
if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
|
|
@@ -154,7 +202,12 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
154
202
|
let scopeChoice;
|
|
155
203
|
let findingIds;
|
|
156
204
|
let findings, projectId;
|
|
205
|
+
let skippedFilesCount;
|
|
206
|
+
let topFindingIds;
|
|
207
|
+
let notActiveCount, analysisId;
|
|
208
|
+
let nextFinding;
|
|
157
209
|
let loadedFinding;
|
|
210
|
+
let notActiveHint = "";
|
|
158
211
|
let fixRequests, claimed;
|
|
159
212
|
let waitingCount;
|
|
160
213
|
let url, analysisFolder;
|
|
@@ -165,7 +218,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
165
218
|
if (selectionEnabled)
|
|
166
219
|
analysisStartup ??= readCliSettings(analysisEnv, emailSignIn);
|
|
167
220
|
const analysisSettings = analysisStartup?.settings ?? {};
|
|
168
|
-
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, CF_FINDING_AUDIENCE: audienceChoice },
|
|
221
|
+
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, ...skippedSettings, ...partSettings, CF_FINDING_AUDIENCE: audienceChoice },
|
|
169
222
|
onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
|
|
170
223
|
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
171
224
|
started = true;
|
|
@@ -179,9 +232,30 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
179
232
|
options.onScopeChoice?.(choice);
|
|
180
233
|
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
181
234
|
findingIds = result.findingIds ?? [];
|
|
235
|
+
topFindingIds = result.topFindingIds;
|
|
236
|
+
if (skippedEnabled && result.skippedFiles)
|
|
237
|
+
skippedFilesCount = sizeSkippedFiles(result.skippedFiles).length;
|
|
238
|
+
const groups = groupByActivity(result.findings ?? [], result.notActiveEnabled);
|
|
239
|
+
notActiveFindings.set(result.projectId, groups.notActive);
|
|
240
|
+
if (groups.notActive.length) {
|
|
241
|
+
notActiveCount = groups.notActive.length;
|
|
242
|
+
analysisId = result.analysisId;
|
|
243
|
+
const ids = groups.notActive.map(finding => numbersEnabled && finding.seq !== undefined ? formatFindingNumber(finding.seq) : finding.id);
|
|
244
|
+
notActiveHint = `Not active details: use get_finding for ${ids.join(", ")}.\n`;
|
|
245
|
+
}
|
|
182
246
|
if (numbersEnabled) {
|
|
183
|
-
|
|
184
|
-
audienceEnabled: result.audienceEnabled,
|
|
247
|
+
const displayed = presentSavedFindings(result, { numbersEnabled, agentReadableResults,
|
|
248
|
+
audienceEnabled: result.audienceEnabled, notActiveEnabled: result.notActiveEnabled, projectId: result.projectId, surface: "mcp" });
|
|
249
|
+
findings = displayed.findings;
|
|
250
|
+
findingIds = displayed.findingIds;
|
|
251
|
+
nextFinding = displayed.nextFinding;
|
|
252
|
+
if (agentReadableResults && result.analysisId !== undefined) {
|
|
253
|
+
analysisFindings.delete(result.analysisId);
|
|
254
|
+
analysisFindings.set(result.analysisId, { projectId: result.projectId, findings: displayed.orderedReferences });
|
|
255
|
+
// Older details remain available from the console without keeping every chain in memory.
|
|
256
|
+
if (analysisFindings.size > MAX_ANALYSIS_FINDING_HISTORY)
|
|
257
|
+
analysisFindings.delete(analysisFindings.keys().next().value);
|
|
258
|
+
}
|
|
185
259
|
projectId = result.projectId;
|
|
186
260
|
references.remember(result.projectId);
|
|
187
261
|
}
|
|
@@ -205,9 +279,23 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
205
279
|
else if (name === "get_finding") {
|
|
206
280
|
const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
|
|
207
281
|
await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
|
|
208
|
-
|
|
209
|
-
|
|
282
|
+
// Older consoles retain the finding and fixes but may not store its activity yet.
|
|
283
|
+
const activity = [...notActiveFindings.values()].flat().find(saved => saved.id === value.id)?.activity;
|
|
284
|
+
if (activity)
|
|
285
|
+
ctx.write(formatFindingActivity(activity));
|
|
286
|
+
if (numbersEnabled || activity)
|
|
287
|
+
loadedFinding = { ...value, ...(activity ? { activity } : {}),
|
|
288
|
+
...(numbersEnabled && value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
|
|
289
|
+
if (agentReadableResults && numbersEnabled) {
|
|
290
|
+
for (const { projectId, findings } of analysisFindings.values()) {
|
|
291
|
+
const index = findings.findIndex(item => item.id === value.id);
|
|
292
|
+
if (index >= 0 && findings[index + 1])
|
|
293
|
+
nextFinding = findingDetailReference(findings[index + 1], projectId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
210
296
|
} });
|
|
297
|
+
if (nextFinding)
|
|
298
|
+
text += formatFindingContinuation(nextFinding);
|
|
211
299
|
}
|
|
212
300
|
else if (name === "get_balance")
|
|
213
301
|
await balance(ctx, { onLoaded: value => { freeAnalysisAvailable = value.freeAnalysisAvailable; } });
|
|
@@ -252,7 +340,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
252
340
|
const failure = signInFailure;
|
|
253
341
|
signInFailure = undefined;
|
|
254
342
|
if (failure && !signIn.signedIn) {
|
|
255
|
-
const shown = mcpFailure(failure.error, failureReasons);
|
|
343
|
+
const shown = mcpFailure(failure.error, failureReasons, fileScopeEnabled(analysisSwitches));
|
|
256
344
|
text = (failure.error instanceof SignInExpiredError
|
|
257
345
|
? emailSignInMessages.mcp_expired : failure.error instanceof SignInDeniedError
|
|
258
346
|
? emailSignInMessages.mcp_cancelled : shown.message) + "\n";
|
|
@@ -316,19 +404,30 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
316
404
|
const launchFolder = await launch;
|
|
317
405
|
if (guarded && !launchFolder)
|
|
318
406
|
throw new CliRefusalError("launch_folder_unavailable");
|
|
319
|
-
const
|
|
320
|
-
|
|
407
|
+
const partScope = Array.isArray(args.scope) ? args.scope : undefined;
|
|
408
|
+
if (partScope && (!scopeEnabled || args.proposal !== undefined))
|
|
409
|
+
throw new Error(cliRefusals.invalid_body);
|
|
410
|
+
const scope = partScope ? undefined : await loadAnalysisScope({ scope: args.scope }, cwd, analysisSettings);
|
|
411
|
+
const selection = selectionEnabled ? await resolveAnalysisSelection((args.paths ?? args.path), launchFolder ?? cwd, analysisSettings, scope) : undefined;
|
|
321
412
|
if (guarded && selection?.kind === "folder")
|
|
322
413
|
await resolveMcpAgentFolder(selection.path, launchFolder, ctx.homeDir);
|
|
323
414
|
const folder = selection?.path ?? (guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd));
|
|
324
|
-
const
|
|
415
|
+
const request = selection?.request;
|
|
416
|
+
const originalEntries = request?.kind === "file" ? request.entries ?? [request.entry] : undefined;
|
|
417
|
+
const chosen = request?.kind === "file" ? request.scope : undefined;
|
|
418
|
+
const includedEntries = originalEntries && (request?.kind === "file" && request.entries && chosen && chosen.imports !== "all"
|
|
419
|
+
? originalEntries.filter(entry => entry === chosen.entry || Array.isArray(chosen.imports) && chosen.imports.includes(entry))
|
|
420
|
+
: originalEntries);
|
|
421
|
+
const key = selection ? includedEntries
|
|
422
|
+
? JSON.stringify([selection.launchRoot, [...includedEntries].sort()]) : JSON.stringify([selection.launchRoot, selection.path]) : folder;
|
|
325
423
|
const last = lastAnalysis.get(key);
|
|
326
|
-
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
424
|
+
if (name === ANALYZE_TOOL_NAME && guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
327
425
|
throw new CliRefusalError("repeat_too_soon");
|
|
328
426
|
}
|
|
329
427
|
analysisFolder = key;
|
|
330
428
|
try {
|
|
331
|
-
exitCode = await
|
|
429
|
+
exitCode = name === "analyze_skipped_files" ? await skipped(folder, args.action, ctx, analyze, "mcp")
|
|
430
|
+
: await analyze(folder, { proposal: args.proposal, scope: partScope ?? scope, selection, surface: "mcp", analyzedFolder: args.path }, ctx);
|
|
332
431
|
}
|
|
333
432
|
finally {
|
|
334
433
|
if (reason === "results_pending")
|
|
@@ -339,11 +438,15 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
339
438
|
}
|
|
340
439
|
}
|
|
341
440
|
catch (error) {
|
|
342
|
-
const failure = mcpFailure(error, failureReasons);
|
|
441
|
+
const failure = mcpFailure(error, failureReasons, fileScopeEnabled(analysisSwitches));
|
|
343
442
|
text += failure.message + "\n";
|
|
344
443
|
reason = failure.reason;
|
|
345
444
|
exitCode = errorCode(error);
|
|
346
445
|
}
|
|
446
|
+
// Choices return before other output today; replace the terminal command list with the names-only MCP list.
|
|
447
|
+
if (agentChoice?.kind === "choose_scope")
|
|
448
|
+
text = agentChoiceMessage(agentChoice, "mcp") + "\n";
|
|
449
|
+
text += notActiveHint;
|
|
347
450
|
if (scopeChoice) {
|
|
348
451
|
const refusal = scopeChoiceRefusal(scopeChoice);
|
|
349
452
|
if (!refusal)
|
|
@@ -358,7 +461,10 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
358
461
|
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
359
462
|
...(exitCode !== 0 && failureReasons ? { reason: reason ?? failureReason(undefined) } : {}),
|
|
360
463
|
...(freeAnalysisAvailable ? { freeAnalysisAvailable } : {}),
|
|
361
|
-
...(
|
|
464
|
+
...(skippedFilesCount ? { skippedFilesCount } : {}),
|
|
465
|
+
...(topFindingIds ? { topFindingIds } : {}),
|
|
466
|
+
...(notActiveCount === undefined ? {} : { notActiveCount }), ...(analysisId === undefined ? {} : { analysisId }),
|
|
467
|
+
...(findings ? { findings, projectId } : {}), ...(nextFinding ? { nextFinding } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
362
468
|
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
363
469
|
}
|
|
364
470
|
async function respond(message, id, signal) {
|
package/dist/cli/relay-check.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { analysisSkippedFiles, sizeSkippedFiles } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
|
+
import { loadAnalysisResults, loadPublicReport, saveAnalysisContext, settleAnalysisContext } from "../adapters/analysis-results.js";
|
|
4
|
+
import { fingerprintOf } from "../analysis-core/analysis/finding-identity.js";
|
|
5
|
+
import { combineAssessments } from "../adapters/combined-results.js";
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
2
7
|
import { disabledSetupPings } from "../setup-pings/client.js";
|
|
3
|
-
import { scopeChoicesEnabled } from "../adapters/settings.js";
|
|
8
|
+
import { fileScopeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
4
9
|
import { saveCompletion, discardCompletion, lockCompletion } from "../adapters/completion.js";
|
|
5
10
|
import { saveLocalResults } from "../adapters/results.js";
|
|
6
11
|
import { GITIGNORE_OUTPUT_NOTICE } from "../adapters/gitignore-output.js";
|
|
@@ -10,8 +15,8 @@ import { validateClientRelease } from "../adapters/release.js";
|
|
|
10
15
|
import { selectAnalysisExecution } from "../adapters/models.js";
|
|
11
16
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
12
17
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
13
|
-
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
14
|
-
import { prepareRelayCheck } from "./relay-preparation.js";
|
|
18
|
+
import { savedAnalysisUrl, savedArchitectureUrl } from "../contract/console-url.js";
|
|
19
|
+
import { prepareRelayCheck, prepareFolderChoice } from "./relay-preparation.js";
|
|
15
20
|
import { cliRefusals, refusalExitCode } from "../contract/cli-refusals.js";
|
|
16
21
|
import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
|
|
17
22
|
import { GatekeeperClient } from "../gatekeeper/client.js";
|
|
@@ -30,6 +35,7 @@ import { recoverRelayFailure } from "./relay-failure.js";
|
|
|
30
35
|
import { analysisSaveOptions, resolveRunSelection } from "./analysis-selection.js";
|
|
31
36
|
import { SCOPE_CHOICE_EXIT_CODE, progressLine } from "../contract/scope-choice.js";
|
|
32
37
|
import { ScopeChoiceError, scopeChoiceRefusal, selectionScopeChoices } from "../contract/selection-offer.js";
|
|
38
|
+
import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
|
|
33
39
|
const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
|
|
34
40
|
export function runRelayCheck(folder, options) {
|
|
35
41
|
return runRelayLifecycle(folder, options);
|
|
@@ -42,13 +48,17 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
42
48
|
const { config, stdout, stderr } = options;
|
|
43
49
|
const retainUnsaved = config.CF_LOCAL_RESULTS_RETRY !== "0";
|
|
44
50
|
let settings = options.settings;
|
|
45
|
-
const summaryOptions = { audienceEnabled: false };
|
|
51
|
+
const summaryOptions = { audienceEnabled: false, notActiveEnabled: false, skippedFilesEnabled: skippedFilesEnabled(settings), skippedOffer: !options.addToAnalysisId, analyzedFolder: options.analyzedFolder ?? folder };
|
|
52
|
+
let skippedFiles = [];
|
|
46
53
|
let client, reservationId, projectId;
|
|
47
54
|
let connection = existing?.connection;
|
|
48
55
|
let completedPayload;
|
|
49
56
|
let displayFindings;
|
|
50
57
|
let completionOrigin, completionRecordPath;
|
|
51
58
|
let completionTiming = {};
|
|
59
|
+
let combinedFindings;
|
|
60
|
+
let topFindingIds;
|
|
61
|
+
let refusedCalls;
|
|
52
62
|
let completed = false, uploaded = false, heartbeat;
|
|
53
63
|
let selection, membership;
|
|
54
64
|
let selectedPaths, completionIdentity;
|
|
@@ -84,9 +94,25 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
84
94
|
};
|
|
85
95
|
const showSaved = (result) => {
|
|
86
96
|
const url = savedAnalysisUrl(result.projectId, config.CF_CONSOLE_URL);
|
|
87
|
-
const
|
|
88
|
-
|
|
97
|
+
const architectureUrl = completedPayload?.architecture ? savedArchitectureUrl(result.projectId, config.CF_CONSOLE_URL) : undefined;
|
|
98
|
+
const findings = combinedFindings ?? savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
|
|
99
|
+
options.onSaved?.({ ...result, ...(combinedFindings ? { findingIds: combinedFindings.map(f => f.id), topFindingIds } : {}), ...(!skippedFilesEnabled(settings) ? {} : { skippedFiles }), findings, url, ...(architectureUrl ? { architectureUrl } : {}), ...summaryOptions });
|
|
89
100
|
stdout(result.analysisId ? `Saved analysis ${result.analysisId}: ${url}\n` : `Full results: ${url}\n`);
|
|
101
|
+
if (architectureUrl)
|
|
102
|
+
stdout(`Architecture: ${architectureUrl}\n`);
|
|
103
|
+
};
|
|
104
|
+
const settleSaved = async (result) => {
|
|
105
|
+
const payload = completedPayload;
|
|
106
|
+
if (!result.analysisId || !skippedFilesEnabled(settings) || !payload?.coverage || !payload.addToAnalysisId && !sizeSkippedFiles(payload.coverage.skippedFiles).length)
|
|
107
|
+
return;
|
|
108
|
+
const context = await settleAnalysisContext(folder, { ...result, analysisId: result.analysisId, findingIds: result.findingIds ?? [] }, payload.checkId, payload.addToAnalysisId, outputDir);
|
|
109
|
+
const { report } = await loadAnalysisResults(folder, outputDir);
|
|
110
|
+
skippedFiles = report.refusedFiles;
|
|
111
|
+
if (payload.addToAnalysisId) {
|
|
112
|
+
topFindingIds = context.topFindingIds;
|
|
113
|
+
const references = new Map(context.findings?.map(f => [f.fingerprint, f]));
|
|
114
|
+
combinedFindings = report.findings.flatMap(finding => { const saved = references.get(fingerprintOf(finding)); return saved ? [saved] : []; });
|
|
115
|
+
}
|
|
90
116
|
};
|
|
91
117
|
try {
|
|
92
118
|
if (!config.CF_CONSOLE_URL || !existing && (!config.CF_RELAY_URL || !config.CF_CONSOLE_TOKEN)) {
|
|
@@ -108,9 +134,16 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
108
134
|
const validated = validateClientRelease(options.release, settings);
|
|
109
135
|
const { bundle } = validated;
|
|
110
136
|
settings = validated.settings;
|
|
111
|
-
|
|
137
|
+
const findingSettings = readFindingSettings(settings);
|
|
138
|
+
summaryOptions.audienceEnabled = findingSettings.audienceEnabled;
|
|
139
|
+
summaryOptions.notActiveEnabled = findingSettings.notActiveEnabled;
|
|
112
140
|
selection = await resolveRunSelection(folder, options.cwd ?? process.cwd(), settings, options);
|
|
113
|
-
folder
|
|
141
|
+
// Verify the selected folder before output and recovery operations can fail for their own reasons.
|
|
142
|
+
folder = await resolveFolderPath(selection?.outputAnchor ?? folder).catch(error => {
|
|
143
|
+
if (error instanceof Error)
|
|
144
|
+
Object.assign(error, { localOperation: "read" });
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
114
147
|
outputDir ??= selection?.outputDir;
|
|
115
148
|
if (selection?.kind === "file")
|
|
116
149
|
completionIdentity = {
|
|
@@ -138,6 +171,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
138
171
|
completionStored = true;
|
|
139
172
|
completionRecordPath = path;
|
|
140
173
|
completedPayload = prior.payload;
|
|
174
|
+
skippedFiles = prior.payload.coverage?.skippedFiles ?? [];
|
|
141
175
|
completionOrigin = prior.consoleOrigin;
|
|
142
176
|
completionTiming = { completedAt: prior.completedAt, absoluteExpiresAt: prior.absoluteExpiresAt };
|
|
143
177
|
uploaded = prior.uploaded ?? false;
|
|
@@ -146,12 +180,24 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
146
180
|
const saved = await submit(prior.payload, retainUnsaved && uploaded);
|
|
147
181
|
uploaded = true;
|
|
148
182
|
await persist(prior.payload, prior.consoleOrigin, true);
|
|
183
|
+
await settleSaved(saved);
|
|
149
184
|
showSaved(saved);
|
|
150
185
|
return 0;
|
|
151
186
|
}
|
|
152
187
|
signal.throwIfAborted();
|
|
153
188
|
if (options.retryUpload)
|
|
154
189
|
throw new Error("The saved completion record is missing. Keep the local report and check the output directory.");
|
|
190
|
+
// Negotiate once before paid work: older strict intake schemas cannot accept optional metadata.
|
|
191
|
+
const capabilities = skippedFilesEnabled(settings) && options.skippedFilesSupported === undefined
|
|
192
|
+
|| settings.CF_LOCAL_ARCHITECTURE === "1" && options.consoleSupportsArchitecture === undefined
|
|
193
|
+
? await client.capabilities() : undefined;
|
|
194
|
+
const consoleSupportsArchitecture = options.consoleSupportsArchitecture ?? capabilities?.architecture ?? false;
|
|
195
|
+
if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? capabilities?.skippedFiles)) {
|
|
196
|
+
if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
|
|
197
|
+
throw new ConsoleError("unavailable", "start");
|
|
198
|
+
settings = Object.freeze({ ...settings, CF_SKIPPED_FILES: "0" });
|
|
199
|
+
summaryOptions.skippedFilesEnabled = false;
|
|
200
|
+
}
|
|
155
201
|
try {
|
|
156
202
|
await outputPaths(folder, ["findings.json", "summary.md", ...(selection?.kind === "file" ? ["selection.json"] : [])], outputDir);
|
|
157
203
|
}
|
|
@@ -159,14 +205,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
159
205
|
if (error.code !== "ENOENT")
|
|
160
206
|
throw error;
|
|
161
207
|
}
|
|
162
|
-
const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
|
|
208
|
+
const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, settings, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, { ...validated, settings });
|
|
163
209
|
const { source, provenance, plan, start, startMetadata } = prepared;
|
|
164
210
|
membership = prepared.membership;
|
|
165
211
|
const includedPaths = Object.keys(source.files);
|
|
166
212
|
selectedPaths = includedPaths;
|
|
213
|
+
refusedCalls = plan.calls;
|
|
214
|
+
skippedFiles = source.skippedFiles;
|
|
167
215
|
let reservation = existing?.reservation;
|
|
168
216
|
if (!reservation) {
|
|
169
|
-
const started = await client.start(start, startMetadata);
|
|
217
|
+
const started = await client.start(start, { ...startMetadata, ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}) });
|
|
218
|
+
if (options.expectedProjectId && started.projectId !== options.expectedProjectId)
|
|
219
|
+
throw new ConsoleError("check_conflict");
|
|
170
220
|
projectId = started.projectId;
|
|
171
221
|
signal.throwIfAborted();
|
|
172
222
|
const created = await client.create({ ...started, checkId: randomUUID(), plan });
|
|
@@ -197,31 +247,53 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
197
247
|
options.onStart?.();
|
|
198
248
|
stderr(progressLine(selection));
|
|
199
249
|
(options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
|
|
200
|
-
const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
|
|
250
|
+
const report = await runAnalysisToEnd(source, execution.model, settings, { execution, ...(options.addToAnalysisId ? { findingsOnly: true } : {}) });
|
|
251
|
+
skippedFiles = !skippedFilesEnabled(settings) ? source.skippedFiles : analysisSkippedFiles(source.skippedFiles, report.skippedFiles);
|
|
252
|
+
const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
|
|
253
|
+
const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
|
|
201
254
|
completionTiming = observedCompletionTiming({ completedAt: Date.now() }, { kind: "found", status: reservation }, reservation);
|
|
202
255
|
// The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
|
|
203
256
|
if (retainUnsaved)
|
|
204
257
|
clearInterval(heartbeat);
|
|
205
258
|
else
|
|
206
259
|
signal.throwIfAborted();
|
|
260
|
+
let saveContext;
|
|
207
261
|
const saveReports = async () => {
|
|
208
|
-
const paths = await saveLocalResults(folder, report,
|
|
209
|
-
...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths
|
|
262
|
+
const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
|
|
263
|
+
...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths,
|
|
264
|
+
...(options.addToAnalysisId ? { append: { readPaths: includedPaths, assessments } } : {}) });
|
|
210
265
|
try {
|
|
211
266
|
options.onLocalSaved?.(paths);
|
|
212
267
|
if (paths.ignoreAdded)
|
|
213
268
|
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
214
269
|
}
|
|
215
270
|
catch { /* Output failure must not change a server outcome. */ }
|
|
271
|
+
await saveContext?.();
|
|
216
272
|
return paths;
|
|
217
273
|
};
|
|
274
|
+
// The retry off path saves the full report even when intake validation refuses it.
|
|
218
275
|
let paths = retainUnsaved ? undefined : await saveReports();
|
|
219
|
-
const { payload, resultCheck, findingIndexes } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
220
|
-
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot
|
|
221
|
-
|
|
276
|
+
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, consoleSupportsArchitecture,
|
|
278
|
+
...(!skippedFilesEnabled(settings) || !options.addToAnalysisId && !sizeSkippedFiles(skippedFiles).length ? {} : { coverage: { skippedFiles: sizeSkippedFiles(skippedFiles), readPaths: includedPaths },
|
|
279
|
+
addToAnalysisId: options.addToAnalysisId, findingAssessments: report.findingCheck.qualityAudit?.assessments ?? [] }) });
|
|
280
|
+
displayFindings = findingIndexes.map((index, acceptedIndex) => {
|
|
222
281
|
const finding = report.findings[index];
|
|
223
|
-
|
|
282
|
+
const activity = findingActivities[acceptedIndex];
|
|
283
|
+
return { title: findingPresentation(finding).displayTitle, audience: finding.audience,
|
|
284
|
+
severity: finding.severity, site: finding.site, fixes: finding.fixes,
|
|
285
|
+
...(activity === undefined ? {} : { activity }) };
|
|
224
286
|
});
|
|
287
|
+
if (skippedFilesEnabled(settings) && (options.addToAnalysisId || sizeSkippedFiles(skippedFiles).length)) {
|
|
288
|
+
saveContext = async () => {
|
|
289
|
+
await saveAnalysisContext(folder, { projectId: projectId, analysisId: options.addToAnalysisId ?? reservation.checkId,
|
|
290
|
+
settings: priorResults?.context.settings ?? settings, assessments,
|
|
291
|
+
...(priorResults?.context.findings ? { findings: priorResults.context.findings } : {}),
|
|
292
|
+
pending: { checkId: reservation.checkId, fingerprints: findingIndexes.map(index => fingerprintOf(report.findings[index])), titles: [...displayFindings] } }, outputDir);
|
|
293
|
+
};
|
|
294
|
+
if (paths)
|
|
295
|
+
await saveContext();
|
|
296
|
+
}
|
|
225
297
|
if (resultCheck?.droppedFindings) {
|
|
226
298
|
const count = resultCheck.droppedFindings;
|
|
227
299
|
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
@@ -256,7 +328,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
256
328
|
uploaded = true;
|
|
257
329
|
await persist(payload, completionOrigin, true);
|
|
258
330
|
}
|
|
259
|
-
|
|
331
|
+
await settleSaved(saved);
|
|
332
|
+
stdout(formatSummary(options.addToAnalysisId ? await loadPublicReport(folder, outputDir) : { ...report, refusedFiles: skippedFiles }, summaryOptions));
|
|
260
333
|
stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
|
|
261
334
|
showSaved(saved);
|
|
262
335
|
return 0;
|
|
@@ -297,14 +370,16 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
297
370
|
completionTiming = error.timing ?? completionTiming;
|
|
298
371
|
const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
299
372
|
timing: completionTiming, statusLookup, uploadOutcome, savedResult,
|
|
300
|
-
canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file" }, {
|
|
373
|
+
canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file", fileMode: fileScopeEnabled(settings) }, {
|
|
301
374
|
markUploaded: () => persist(completedPayload, completionOrigin, true),
|
|
302
375
|
keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
|
|
303
376
|
retire: () => retainUnsaved ? completionRecordPath ? discardCompletion(folder, completionRecordPath, completionIdentity) : Promise.resolve() : persist(completedPayload, completionOrigin, false, true),
|
|
304
377
|
});
|
|
305
378
|
if (failure.saved) {
|
|
306
379
|
try {
|
|
307
|
-
|
|
380
|
+
const saved = savedResult ?? { projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings };
|
|
381
|
+
await settleSaved(saved);
|
|
382
|
+
showSaved(saved);
|
|
308
383
|
}
|
|
309
384
|
catch { /* Output failure must not change a server outcome. */ }
|
|
310
385
|
if (!failure.message)
|
|
@@ -316,10 +391,19 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
316
391
|
catch { /* Reporting failure cannot undo saved results. */ }
|
|
317
392
|
return 1;
|
|
318
393
|
}
|
|
394
|
+
let scopeChoice;
|
|
395
|
+
if (!reservationId && !existing && error instanceof ConsoleError && (error.code === "folder_too_large" || error.code === "plan_over_limit")) {
|
|
396
|
+
try {
|
|
397
|
+
const choice = await prepareFolderChoice(folder, { ...options, settings, signal, limit: error.code, refusedCalls });
|
|
398
|
+
if (choice)
|
|
399
|
+
scopeChoice = new AgentChoiceError(choice);
|
|
400
|
+
}
|
|
401
|
+
catch { /* A failed local plan preserves the console's authoritative refusal. */ }
|
|
402
|
+
}
|
|
319
403
|
try {
|
|
320
|
-
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: failure.message,
|
|
321
|
-
...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
322
|
-
stderr(`${failure.message}\n`);
|
|
404
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? failure.message,
|
|
405
|
+
...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
406
|
+
stderr(`${scopeChoice?.message ?? failure.message}\n`);
|
|
323
407
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
324
408
|
stderr(stateLine(status));
|
|
325
409
|
}
|
|
@@ -2,8 +2,8 @@ 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, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
6
|
-
import { isSelectionRefusal } from "../contract/scope-choice.js";
|
|
5
|
+
import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, localFailure, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
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";
|
|
9
9
|
import { CompletionSettingsMismatchError } from "./prior-completion.js";
|
|
@@ -24,9 +24,9 @@ export async function recoverRelayFailure(input, operations) {
|
|
|
24
24
|
|| error instanceof CompletionBusyError || error instanceof CompletionSettingsMismatchError || error instanceof Error
|
|
25
25
|
&& ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
26
26
|
|| error.message.startsWith("The saved completion"));
|
|
27
|
-
const message = recovery?.message ?? input.localErrorMessage ?? (known ? error.message : FINISH_FAILURE_MESSAGE);
|
|
27
|
+
const message = recovery?.message ?? input.localErrorMessage ?? localFailure(error)?.message ?? (known ? error.message : FINISH_FAILURE_MESSAGE);
|
|
28
28
|
const reason = !retryEnabled ? failureReason(error)
|
|
29
29
|
: recovery?.decision.action === "retry" && input.completionStored ? "results_pending"
|
|
30
30
|
: recovery?.decision.action === "give-up" ? "results_discarded" : failureReason(error);
|
|
31
|
-
return { saved: false, message, reason };
|
|
31
|
+
return { saved: false, message: selectionRefusalMessage(message, input.fileMode ?? true), reason };
|
|
32
32
|
}
|