@counterfact-labs/cli 0.2.7 → 0.2.10
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/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/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/2CWULBP2.js +1 -0
- package/dist/analysis-core/chunks/2M55DR2J.js +35 -0
- package/dist/analysis-core/chunks/34NVU26H.js +1 -0
- package/dist/analysis-core/chunks/{RNTMH4MU.js → 5BTATES5.js} +1 -1
- package/dist/analysis-core/chunks/5UWDUYHF.js +1 -0
- package/dist/analysis-core/chunks/{6RWEWBHK.js → 5VQ4VYIP.js} +1 -1
- package/dist/analysis-core/chunks/66JRJPJC.js +1 -0
- package/dist/analysis-core/chunks/6JTHFDH3.js +1 -0
- package/dist/analysis-core/chunks/6VHZNBII.js +19 -0
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/C77LGCKF.js +1 -0
- package/dist/analysis-core/chunks/CNQZLSAP.js +1 -0
- package/dist/analysis-core/chunks/DHVEGIU6.js +1 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/I4F56VMG.js +1 -0
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/{6B2HILSY.js → JAPVLU4Z.js} +1 -1
- package/dist/analysis-core/chunks/KBTM5MBC.js +14 -0
- package/dist/analysis-core/chunks/LFJQ4YLP.js +3 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/N2L4JMJE.js +1 -0
- package/dist/analysis-core/chunks/NCW5LJ5O.js +6 -0
- package/dist/analysis-core/chunks/P253B344.js +7 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QPIBHTON.js +67 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/TA2NIQAY.js +1 -0
- package/dist/analysis-core/chunks/UKX3A6AX.js +1 -0
- package/dist/analysis-core/chunks/VC3PYT2B.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/VWRFSVLI.js +1 -0
- package/dist/analysis-core/chunks/VX6LYNXV.js +1 -0
- package/dist/analysis-core/chunks/W73WF226.js +1 -0
- package/dist/analysis-core/chunks/{FBTOOUPL.js → XVGLY4EO.js} +1 -1
- 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 +55 -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 +25 -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 +103 -23
- package/dist/cli/relay-failure.js +4 -4
- package/dist/cli/relay-preparation.js +47 -9
- 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 +8 -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/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/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/analysis-core/chunks/{SKVEBOGA.js → 33EUZF25.js} +0 -0
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,20 @@ 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 before paid work: older strict intake schemas cannot accept coverage or additions.
|
|
191
|
+
if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? await client.supportsSkippedFiles())) {
|
|
192
|
+
if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
|
|
193
|
+
throw new ConsoleError("unavailable", "start");
|
|
194
|
+
settings = Object.freeze({ ...settings, CF_SKIPPED_FILES: "0" });
|
|
195
|
+
summaryOptions.skippedFilesEnabled = false;
|
|
196
|
+
}
|
|
155
197
|
try {
|
|
156
198
|
await outputPaths(folder, ["findings.json", "summary.md", ...(selection?.kind === "file" ? ["selection.json"] : [])], outputDir);
|
|
157
199
|
}
|
|
@@ -159,14 +201,18 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
159
201
|
if (error.code !== "ENOENT")
|
|
160
202
|
throw error;
|
|
161
203
|
}
|
|
162
|
-
const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
|
|
204
|
+
const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, settings, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, { ...validated, settings });
|
|
163
205
|
const { source, provenance, plan, start, startMetadata } = prepared;
|
|
164
206
|
membership = prepared.membership;
|
|
165
207
|
const includedPaths = Object.keys(source.files);
|
|
166
208
|
selectedPaths = includedPaths;
|
|
209
|
+
refusedCalls = plan.calls;
|
|
210
|
+
skippedFiles = source.skippedFiles;
|
|
167
211
|
let reservation = existing?.reservation;
|
|
168
212
|
if (!reservation) {
|
|
169
|
-
const started = await client.start(start, startMetadata);
|
|
213
|
+
const started = await client.start(start, { ...startMetadata, ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}) });
|
|
214
|
+
if (options.expectedProjectId && started.projectId !== options.expectedProjectId)
|
|
215
|
+
throw new ConsoleError("check_conflict");
|
|
170
216
|
projectId = started.projectId;
|
|
171
217
|
signal.throwIfAborted();
|
|
172
218
|
const created = await client.create({ ...started, checkId: randomUUID(), plan });
|
|
@@ -197,31 +243,53 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
197
243
|
options.onStart?.();
|
|
198
244
|
stderr(progressLine(selection));
|
|
199
245
|
(options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
|
|
200
|
-
const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
|
|
246
|
+
const report = await runAnalysisToEnd(source, execution.model, settings, { execution, ...(options.addToAnalysisId ? { findingsOnly: true } : {}) });
|
|
247
|
+
skippedFiles = !skippedFilesEnabled(settings) ? source.skippedFiles : analysisSkippedFiles(source.skippedFiles, report.skippedFiles);
|
|
248
|
+
const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
|
|
249
|
+
const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
|
|
201
250
|
completionTiming = observedCompletionTiming({ completedAt: Date.now() }, { kind: "found", status: reservation }, reservation);
|
|
202
251
|
// The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
|
|
203
252
|
if (retainUnsaved)
|
|
204
253
|
clearInterval(heartbeat);
|
|
205
254
|
else
|
|
206
255
|
signal.throwIfAborted();
|
|
256
|
+
let saveContext;
|
|
207
257
|
const saveReports = async () => {
|
|
208
|
-
const paths = await saveLocalResults(folder, report,
|
|
209
|
-
...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths
|
|
258
|
+
const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
|
|
259
|
+
...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths,
|
|
260
|
+
...(options.addToAnalysisId ? { append: { readPaths: includedPaths, assessments } } : {}) });
|
|
210
261
|
try {
|
|
211
262
|
options.onLocalSaved?.(paths);
|
|
212
263
|
if (paths.ignoreAdded)
|
|
213
264
|
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
214
265
|
}
|
|
215
266
|
catch { /* Output failure must not change a server outcome. */ }
|
|
267
|
+
await saveContext?.();
|
|
216
268
|
return paths;
|
|
217
269
|
};
|
|
270
|
+
// The retry off path saves the full report even when intake validation refuses it.
|
|
218
271
|
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
|
-
|
|
272
|
+
const { payload, resultCheck, findingIndexes, findingActivities } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
273
|
+
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot, consoleSupportsActivity: options.consoleSupportsActivity,
|
|
274
|
+
...(!skippedFilesEnabled(settings) || !options.addToAnalysisId && !sizeSkippedFiles(skippedFiles).length ? {} : { coverage: { skippedFiles: sizeSkippedFiles(skippedFiles), readPaths: includedPaths },
|
|
275
|
+
addToAnalysisId: options.addToAnalysisId, findingAssessments: report.findingCheck.qualityAudit?.assessments ?? [] }) });
|
|
276
|
+
displayFindings = findingIndexes.map((index, acceptedIndex) => {
|
|
222
277
|
const finding = report.findings[index];
|
|
223
|
-
|
|
278
|
+
const activity = findingActivities[acceptedIndex];
|
|
279
|
+
return { title: findingPresentation(finding).displayTitle, audience: finding.audience,
|
|
280
|
+
severity: finding.severity, site: finding.site, fixes: finding.fixes,
|
|
281
|
+
...(activity === undefined ? {} : { activity }) };
|
|
224
282
|
});
|
|
283
|
+
if (skippedFilesEnabled(settings) && (options.addToAnalysisId || sizeSkippedFiles(skippedFiles).length)) {
|
|
284
|
+
saveContext = async () => {
|
|
285
|
+
await saveAnalysisContext(folder, { projectId: projectId, analysisId: options.addToAnalysisId ?? reservation.checkId,
|
|
286
|
+
settings: priorResults?.context.settings ?? settings, assessments,
|
|
287
|
+
...(priorResults?.context.findings ? { findings: priorResults.context.findings } : {}),
|
|
288
|
+
pending: { checkId: reservation.checkId, fingerprints: findingIndexes.map(index => fingerprintOf(report.findings[index])), titles: [...displayFindings] } }, outputDir);
|
|
289
|
+
};
|
|
290
|
+
if (paths)
|
|
291
|
+
await saveContext();
|
|
292
|
+
}
|
|
225
293
|
if (resultCheck?.droppedFindings) {
|
|
226
294
|
const count = resultCheck.droppedFindings;
|
|
227
295
|
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
@@ -256,7 +324,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
256
324
|
uploaded = true;
|
|
257
325
|
await persist(payload, completionOrigin, true);
|
|
258
326
|
}
|
|
259
|
-
|
|
327
|
+
await settleSaved(saved);
|
|
328
|
+
stdout(formatSummary(options.addToAnalysisId ? await loadPublicReport(folder, outputDir) : { ...report, refusedFiles: skippedFiles }, summaryOptions));
|
|
260
329
|
stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
|
|
261
330
|
showSaved(saved);
|
|
262
331
|
return 0;
|
|
@@ -297,14 +366,16 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
297
366
|
completionTiming = error.timing ?? completionTiming;
|
|
298
367
|
const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
299
368
|
timing: completionTiming, statusLookup, uploadOutcome, savedResult,
|
|
300
|
-
canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file" }, {
|
|
369
|
+
canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file", fileMode: fileScopeEnabled(settings) }, {
|
|
301
370
|
markUploaded: () => persist(completedPayload, completionOrigin, true),
|
|
302
371
|
keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
|
|
303
372
|
retire: () => retainUnsaved ? completionRecordPath ? discardCompletion(folder, completionRecordPath, completionIdentity) : Promise.resolve() : persist(completedPayload, completionOrigin, false, true),
|
|
304
373
|
});
|
|
305
374
|
if (failure.saved) {
|
|
306
375
|
try {
|
|
307
|
-
|
|
376
|
+
const saved = savedResult ?? { projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings };
|
|
377
|
+
await settleSaved(saved);
|
|
378
|
+
showSaved(saved);
|
|
308
379
|
}
|
|
309
380
|
catch { /* Output failure must not change a server outcome. */ }
|
|
310
381
|
if (!failure.message)
|
|
@@ -316,10 +387,19 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
316
387
|
catch { /* Reporting failure cannot undo saved results. */ }
|
|
317
388
|
return 1;
|
|
318
389
|
}
|
|
390
|
+
let scopeChoice;
|
|
391
|
+
if (!reservationId && !existing && error instanceof ConsoleError && (error.code === "folder_too_large" || error.code === "plan_over_limit")) {
|
|
392
|
+
try {
|
|
393
|
+
const choice = await prepareFolderChoice(folder, { ...options, settings, signal, limit: error.code, refusedCalls });
|
|
394
|
+
if (choice)
|
|
395
|
+
scopeChoice = new AgentChoiceError(choice);
|
|
396
|
+
}
|
|
397
|
+
catch { /* A failed local plan preserves the console's authoritative refusal. */ }
|
|
398
|
+
}
|
|
319
399
|
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`);
|
|
400
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? failure.message,
|
|
401
|
+
...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
402
|
+
stderr(`${scopeChoice?.message ?? failure.message}\n`);
|
|
323
403
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
324
404
|
stderr(stateLine(status));
|
|
325
405
|
}
|
|
@@ -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
|
}
|