@counterfact-labs/cli 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bundle/client-release.json +1 -1
- package/dist/adapters/anthropic.js +11 -2
- package/dist/adapters/completion.js +3 -0
- package/dist/adapters/durable-write.js +9 -2
- package/dist/adapters/files.js +25 -13
- package/dist/adapters/git-ignore.js +28 -14
- package/dist/adapters/git.js +1 -1
- package/dist/adapters/gitignore-output.js +41 -0
- package/dist/adapters/models.js +1 -6
- package/dist/adapters/output-paths.js +5 -2
- package/dist/adapters/public-instructions.js +1 -1
- package/dist/adapters/relay-plan.js +2 -5
- package/dist/adapters/release.js +2 -1
- package/dist/adapters/repository-files.js +164 -4
- package/dist/adapters/repository-root.js +18 -2
- package/dist/adapters/results.js +8 -4
- package/dist/adapters/settings.js +26 -1
- package/dist/adapters/snapshot.js +86 -15
- package/dist/analysis-core/analysis/finding-audience-switch.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/analysis/source-notes/types.js +1 -1
- package/dist/analysis-core/chunks/377MLK4M.js +21 -0
- package/dist/analysis-core/chunks/{D6AWHLRI.js → 3JWEFCTJ.js} +1 -1
- package/dist/analysis-core/chunks/{3227KXHF.js → 4H23CSUM.js} +1 -1
- package/dist/analysis-core/chunks/5DGPLXYB.js +1 -0
- package/dist/analysis-core/chunks/5TATKWLG.js +1 -0
- package/dist/analysis-core/chunks/{EH3QQ3T4.js → 6B2HILSY.js} +1 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +14 -0
- package/dist/analysis-core/chunks/6U2IR4XB.js +1 -0
- package/dist/analysis-core/chunks/A7WJJMRY.js +1 -0
- package/dist/analysis-core/chunks/C6X5GDG4.js +1 -0
- package/dist/analysis-core/chunks/DLPVYWHZ.js +1 -0
- package/dist/analysis-core/chunks/DZZBQDGO.js +1 -0
- package/dist/analysis-core/chunks/F3R4J3ND.js +1 -0
- package/dist/analysis-core/chunks/FOKDE64Y.js +1 -0
- package/dist/analysis-core/chunks/K2GNDX3W.js +1 -0
- package/dist/analysis-core/chunks/KTNA2KTN.js +7 -0
- package/dist/analysis-core/chunks/KWECQJ6X.js +1 -0
- package/dist/analysis-core/chunks/{SL27GMEN.js → M6HYSTQD.js} +1 -1
- package/dist/analysis-core/chunks/{ZAA44GPI.js → N7U5N42V.js} +1 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +41 -0
- package/dist/analysis-core/chunks/{QMIXCT62.js → OPMA32A6.js} +1 -1
- package/dist/analysis-core/chunks/PECX3OXB.js +1 -0
- package/dist/analysis-core/chunks/QFY6A3MS.js +66 -0
- package/dist/analysis-core/chunks/SKVEBOGA.js +1 -0
- package/dist/analysis-core/chunks/{T66VDFXF.js → STI4JE7L.js} +1 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +1 -0
- package/dist/analysis-core/chunks/X7WZAA7S.js +1 -0
- package/dist/analysis-core/chunks/YDBO7P4X.js +1 -0
- package/dist/analysis-core/chunks/YUY436SM.js +1 -0
- package/dist/analysis-core/chunks/ZTOVBAG7.js +1 -0
- package/dist/analysis-core/contracts/file-safety.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -0
- 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/data/finding-types.js +1 -1
- package/dist/analysis-core/findings/sites.js +1 -0
- package/dist/analysis-core/github/repo-key.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/llm/output-limit.js +1 -1
- package/dist/analysis-core/llm/types.js +1 -0
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/headline-request.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -0
- package/dist/analysis-core/stages/execution-ports.js +1 -0
- package/dist/bundle/seal/deserialise.js +9 -1
- package/dist/bundle/seal/engine.js +76 -0
- package/dist/bundle/seal/freeze.js +9 -0
- package/dist/bundle/seal/metadata.js +3 -0
- package/dist/bundle/seal/open-engine.js +26 -0
- package/dist/bundle/seal/open.js +2 -8
- package/dist/cli/agent-folder.js +3 -3
- package/dist/cli/commands/analyze.js +39 -6
- package/dist/cli/commands/balance.js +6 -3
- package/dist/cli/commands/finding.js +9 -3
- package/dist/cli/context.js +6 -1
- package/dist/cli/finding-display.js +39 -0
- package/dist/cli/finding-lookup.js +92 -0
- package/dist/cli/finding-settings.js +5 -0
- package/dist/cli/fix-requests.js +42 -0
- package/dist/cli/intake-payload.js +19 -11
- package/dist/cli/main.js +9 -4
- package/dist/cli/mcp-failure.js +8 -0
- package/dist/cli/mcp.js +125 -37
- package/dist/cli/relay-check.js +47 -14
- package/dist/cli/relay-preparation.js +28 -5
- package/dist/cli/result-check.js +68 -0
- package/dist/cli/sign-in.js +61 -2
- package/dist/cli/summary.js +10 -3
- package/dist/contract/agent-choice.js +15 -0
- package/dist/contract/agent-proposal.js +38 -0
- package/dist/contract/attestation.js +106 -0
- package/dist/contract/cli-api-messages.js +2 -0
- package/dist/contract/cli-refusals.js +69 -1
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +21 -16
- package/dist/contract/email-sign-in-messages.js +4 -0
- package/dist/contract/engine.js +44 -0
- package/dist/contract/findings.js +28 -0
- package/dist/contract/fix-requests.js +30 -0
- package/dist/contract/messages.js +6 -94
- package/dist/contract/public-report.js +1 -0
- package/dist/contract/runtime-environment.js +12 -1
- package/dist/core/analysis/run-to-end.js +7 -52
- package/dist/gatekeeper/attestation.js +1 -1
- package/dist/gatekeeper/channel-binding.js +15 -0
- package/dist/gatekeeper/cli-api.js +28 -9
- package/dist/gatekeeper/connection-exchange.js +17 -12
- package/dist/gatekeeper/console.js +31 -17
- package/dist/gatekeeper/model.js +2 -1
- package/dist/gatekeeper/production-attestation.js +23 -17
- package/dist/gatekeeper/trust/policy.js +4 -1
- package/dist/gatekeeper/trust/schema.js +4 -0
- package/dist/transport/kms.js +25 -0
- package/package.json +4 -3
- package/dist/adapters/quality.js +0 -6
- package/dist/adapters/report.js +0 -29
- package/dist/analysis-core/analysis/analysis-flags.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/config.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +0 -1
- package/dist/analysis-core/analysis/headlines/attachment.js +0 -1
- package/dist/analysis-core/analysis/headlines/input.js +0 -1
- package/dist/analysis-core/chunks/3CKW5JIF.js +0 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +0 -1
- package/dist/analysis-core/chunks/7OYIBWX3.js +0 -1
- package/dist/analysis-core/chunks/DRGDIIFA.js +0 -1
- package/dist/analysis-core/chunks/FTODBFQW.js +0 -1
- package/dist/analysis-core/chunks/HPIAEQHE.js +0 -37
- package/dist/analysis-core/chunks/IVDDV6CU.js +0 -14
- package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
- package/dist/analysis-core/chunks/K5OTWBTT.js +0 -22
- package/dist/analysis-core/chunks/MXXNED6N.js +0 -7
- package/dist/analysis-core/chunks/NCWEOJVJ.js +0 -6
- package/dist/analysis-core/chunks/O652FQQH.js +0 -1
- package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
- package/dist/analysis-core/chunks/QV2EOS7U.js +0 -1
- package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
- package/dist/analysis-core/chunks/T2SAP7OV.js +0 -60
- package/dist/analysis-core/chunks/T5TREZCQ.js +0 -1
- package/dist/analysis-core/chunks/TDNTU4TF.js +0 -1
- package/dist/analysis-core/chunks/U5HOEVPE.js +0 -1
- package/dist/analysis-core/chunks/UEWL3GMQ.js +0 -1
- package/dist/analysis-core/chunks/V7BFUTM7.js +0 -1
- package/dist/core/analysis/finding-quality/config.js +0 -4
- package/dist/core/analysis/structural-run.js +0 -16
package/dist/cli/mcp.js
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
5
|
+
import { mcpFailure } from "./mcp-failure.js";
|
|
6
|
+
import { errorCode } from "./api.js";
|
|
6
7
|
import { commandContext } from "./context.js";
|
|
7
8
|
import { analyze } from "./commands/analyze.js";
|
|
8
9
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
9
10
|
import { finding } from "./commands/finding.js";
|
|
10
11
|
import { balance } from "./commands/balance.js";
|
|
11
12
|
import { ANALYZE_TOOL_NAME, analyzeToolAllowed } from "./claude-code-permission.js";
|
|
12
|
-
import { signInStatus, startSignIn } from "./sign-in.js";
|
|
13
|
+
import { signInStatus, startSignIn, waitForSignIn, SignInExpiredError, SignInDeniedError } from "./sign-in.js";
|
|
13
14
|
import { readEmailSignIn } from "../adapters/settings.js";
|
|
14
15
|
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
15
16
|
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
16
17
|
import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
|
|
17
18
|
import { suggestedEmail } from "./suggested-email.js";
|
|
19
|
+
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
20
|
+
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
21
|
+
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
22
|
+
import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
|
|
23
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
18
24
|
const legacyTools = [
|
|
19
25
|
{
|
|
20
26
|
name: ANALYZE_TOOL_NAME,
|
|
21
|
-
description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance, so call it only when the user asks for an analysis. After edits, suggest an analysis and wait for the user to ask. Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.",
|
|
22
|
-
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." } }, required: ["path"], additionalProperties: false },
|
|
27
|
+
description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance unless a free analysis is available, so call it 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. Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.",
|
|
28
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, proposal: { ...agentProposalInputSchema, description: "Optional: the files you believe make up the agent, with each file's role. Paths are relative to the agent folder." } }, required: ["path"], additionalProperties: false },
|
|
23
29
|
},
|
|
24
30
|
{ name: "get_finding", description: "Get a finding's explanation, quoted lines, and suggested fixes.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false } },
|
|
25
|
-
{ name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
31
|
+
{ name: "get_balance", description: "Get the balance and top-up link, and whether a free first analysis is available. A free first analysis needs no top-up.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
26
32
|
{ 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 } },
|
|
27
33
|
{ 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 } },
|
|
28
34
|
];
|
|
@@ -35,12 +41,17 @@ class ProtocolError extends Error {
|
|
|
35
41
|
}
|
|
36
42
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
37
43
|
export async function serveMcp(input, output, options = {}) {
|
|
44
|
+
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
38
45
|
const cwd = options.cwd ?? process.cwd();
|
|
39
46
|
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
47
|
+
const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
|
|
40
48
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
41
49
|
const lastAnalysis = new Map();
|
|
50
|
+
const findingSettings = readFindingSettings(options.env);
|
|
51
|
+
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
52
|
+
const references = new FindingReferences();
|
|
42
53
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
43
|
-
const
|
|
54
|
+
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
44
55
|
{
|
|
45
56
|
name: "sign_in_status",
|
|
46
57
|
description: "Check whether this computer is signed in to Counterfact and whether the user has allowed the analysis tool in Claude Code. Use path only to choose the agent folder for the local git email suggestion; otherwise use the launch folder. Analysis permission is always checked in the launch folder. A valid local git email is returned as suggestedEmail. Prefer the signed-in user's email already in your context; otherwise offer suggestedEmail. Ask the user to confirm an address before sending a code. With wait: true, wait only for browser sign-in to finish.",
|
|
@@ -65,7 +76,11 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
65
76
|
}, required: ["email", "code"], additionalProperties: false },
|
|
66
77
|
},
|
|
67
78
|
] : legacyTools;
|
|
79
|
+
const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
80
|
+
...tool, inputSchema: numberedFindingInputSchema,
|
|
81
|
+
} : tool).concat(fixesEnabled ? fixRequestTools : []);
|
|
68
82
|
let pendingSignIn;
|
|
83
|
+
let signInFailure;
|
|
69
84
|
const requests = new Map();
|
|
70
85
|
let queue = Promise.resolve();
|
|
71
86
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
@@ -76,15 +91,18 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
76
91
|
if (!object(args))
|
|
77
92
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
78
93
|
const name = params.name;
|
|
79
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path"] : name === "get_finding" ? ["id"]
|
|
80
|
-
: name === "
|
|
81
|
-
:
|
|
94
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
95
|
+
: name === "claim_fix" ? ["id"]
|
|
96
|
+
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
97
|
+
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
82
98
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
83
99
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
84
100
|
if (name === ANALYZE_TOOL_NAME && (typeof args.path !== "string" || !args.path.trim()))
|
|
85
101
|
throw new ProtocolError(-32602, "Provide a path.");
|
|
86
102
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
87
103
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
104
|
+
if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
|
|
105
|
+
throw new ProtocolError(-32602, FINDING_PROJECT_MESSAGE);
|
|
88
106
|
if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
|
|
89
107
|
throw new ProtocolError(-32602, "Provide an optional boolean wait.");
|
|
90
108
|
if (name === "sign_in_status" && "path" in args && (typeof args.path !== "string" || !args.path.trim()))
|
|
@@ -96,12 +114,33 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
96
114
|
if (name === "sign_in_with_code" && !validSignInCode(args.code))
|
|
97
115
|
throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
|
|
98
116
|
let text = "", exitCode = 0;
|
|
117
|
+
let reason;
|
|
118
|
+
let freeAnalysisAvailable = false;
|
|
119
|
+
let agentChoice;
|
|
99
120
|
let findingIds;
|
|
100
|
-
let
|
|
121
|
+
let findings, projectId;
|
|
122
|
+
let loadedFinding;
|
|
123
|
+
let fixRequests, claimed;
|
|
124
|
+
let waitingCount;
|
|
125
|
+
let url, analysisFolder;
|
|
126
|
+
let started = false;
|
|
101
127
|
let signIn;
|
|
102
128
|
try {
|
|
103
|
-
const ctx = commandContext({ ...options,
|
|
129
|
+
const ctx = commandContext({ ...options, env: { ...(options.env ?? process.env), CF_FINDING_AUDIENCE: audienceChoice },
|
|
130
|
+
onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
|
|
131
|
+
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
132
|
+
started = true;
|
|
133
|
+
if (guarded && analysisFolder)
|
|
134
|
+
lastAnalysis.set(analysisFolder, ctx.now());
|
|
135
|
+
options.onCheckStart?.();
|
|
136
|
+
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
104
137
|
findingIds = result.findingIds ?? [];
|
|
138
|
+
if (numbersEnabled) {
|
|
139
|
+
findings = findingDisplayRows(result.findings ?? [], { numbersEnabled,
|
|
140
|
+
audienceEnabled: result.audienceEnabled, labels: "reference" }).map(row => row.finding);
|
|
141
|
+
projectId = result.projectId;
|
|
142
|
+
references.remember(result.projectId);
|
|
143
|
+
}
|
|
105
144
|
url = result.url;
|
|
106
145
|
options.onCheckComplete?.(result);
|
|
107
146
|
}, write: chunk => {
|
|
@@ -109,43 +148,86 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
109
148
|
if (!chunk.startsWith("\r"))
|
|
110
149
|
text += chunk;
|
|
111
150
|
} });
|
|
112
|
-
if (name === "
|
|
113
|
-
await
|
|
151
|
+
if (name === "waiting_fixes") {
|
|
152
|
+
const waiting = await waitingFixes(ctx.api);
|
|
153
|
+
fixRequests = waiting;
|
|
154
|
+
const projectsNeeded = references.projectsNeeded(waiting);
|
|
155
|
+
text = formatWaitingFixes(waiting, numbersEnabled, item => projectsNeeded.has(item.findingId));
|
|
156
|
+
}
|
|
157
|
+
else if (name === "claim_fix") {
|
|
158
|
+
claimed = await claimFix(ctx.api, args.id);
|
|
159
|
+
text = "The fix request is marked as taken.";
|
|
160
|
+
}
|
|
161
|
+
else if (name === "get_finding") {
|
|
162
|
+
const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
|
|
163
|
+
await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
|
|
164
|
+
if (numbersEnabled)
|
|
165
|
+
loadedFinding = { ...value, ...(value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
|
|
166
|
+
} });
|
|
167
|
+
}
|
|
114
168
|
else if (name === "get_balance")
|
|
115
|
-
await balance(ctx);
|
|
169
|
+
await balance(ctx, { onLoaded: value => { freeAnalysisAvailable = value.freeAnalysisAvailable; } });
|
|
116
170
|
else if (name === "sign_in_status") {
|
|
117
171
|
const pending = pendingSignIn;
|
|
118
172
|
const allowed = await analyzeToolAllowed(ctx.homeDir, cwd);
|
|
119
173
|
if (args.wait === true && pending) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
if (pending.flow.retryEnabled) {
|
|
175
|
+
try {
|
|
176
|
+
await waitForSignIn(pending.flow, signal);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (signal.aborted)
|
|
180
|
+
throw error;
|
|
181
|
+
// The flow stores its failure; collect the usual status fields before reporting it.
|
|
182
|
+
}
|
|
126
183
|
}
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
else {
|
|
185
|
+
const cancel = () => pending.controller.abort();
|
|
186
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
187
|
+
try {
|
|
188
|
+
if (signal.aborted)
|
|
189
|
+
cancel();
|
|
190
|
+
await pending.flow.done;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
signal.removeEventListener("abort", cancel);
|
|
194
|
+
}
|
|
129
195
|
}
|
|
130
196
|
}
|
|
131
197
|
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
198
|
+
if (fixesEnabled)
|
|
199
|
+
waitingCount = signIn.signedIn ? await waitingFixCount(ctx.api) : 0;
|
|
132
200
|
text = signIn.signedIn ? "Signed in." : "Not signed in.";
|
|
133
201
|
if (emailSignIn) {
|
|
134
202
|
const suggestion = await suggestedEmail(resolveAgentFolder(args.path ?? ".", cwd), ctx.readEmail);
|
|
135
203
|
if (suggestion)
|
|
136
204
|
signIn.suggestedEmail = suggestion;
|
|
137
205
|
}
|
|
206
|
+
if (signInFailure)
|
|
207
|
+
signal.throwIfAborted();
|
|
208
|
+
const failure = signInFailure;
|
|
209
|
+
signInFailure = undefined;
|
|
210
|
+
if (failure && !signIn.signedIn) {
|
|
211
|
+
const shown = mcpFailure(failure.error, failureReasons);
|
|
212
|
+
text = (failure.error instanceof SignInExpiredError
|
|
213
|
+
? emailSignInMessages.mcp_expired : failure.error instanceof SignInDeniedError
|
|
214
|
+
? emailSignInMessages.mcp_cancelled : shown.message) + "\n";
|
|
215
|
+
exitCode = errorCode(failure.error);
|
|
216
|
+
reason = shown.reason;
|
|
217
|
+
}
|
|
138
218
|
}
|
|
139
219
|
else if (emailFlow) {
|
|
140
220
|
if (pendingSignIn)
|
|
141
221
|
throw new Error(emailSignInMessages.browser_sign_in_pending);
|
|
142
222
|
if (name === "sign_in") {
|
|
143
223
|
const start = await startEmailSignIn(ctx, email);
|
|
224
|
+
signInFailure = undefined;
|
|
144
225
|
signIn = { email: email, expiresIn: start.expiresIn };
|
|
145
226
|
text = `We sent a sign-in code to ${email}. Ask the user for the code from their email, then call sign_in_with_code with the same email and their code. Do not repeat the code.`;
|
|
146
227
|
}
|
|
147
228
|
else {
|
|
148
229
|
await signInWithCode(ctx, email, args.code);
|
|
230
|
+
signInFailure = undefined;
|
|
149
231
|
signIn = { signedIn: true };
|
|
150
232
|
text = "Signed in.";
|
|
151
233
|
}
|
|
@@ -156,20 +238,25 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
156
238
|
else {
|
|
157
239
|
let pending = pendingSignIn;
|
|
158
240
|
if (!pending) {
|
|
159
|
-
//
|
|
241
|
+
// With retry on, the browser flow belongs to the session, so a cancelled status wait leaves it running.
|
|
160
242
|
const controller = new AbortController();
|
|
161
243
|
const cancel = () => controller.abort();
|
|
162
244
|
signal.addEventListener("abort", cancel, { once: true });
|
|
163
245
|
try {
|
|
164
246
|
signal.throwIfAborted();
|
|
165
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
247
|
+
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
166
248
|
pending = { flow, controller, opened: false };
|
|
167
249
|
pendingSignIn = pending;
|
|
250
|
+
signInFailure = undefined;
|
|
168
251
|
const current = pending;
|
|
169
252
|
const clear = () => { if (pendingSignIn === current)
|
|
170
253
|
pendingSignIn = undefined; };
|
|
171
254
|
// Handle background failures even when the client never calls wait.
|
|
172
|
-
void flow.done.then(clear,
|
|
255
|
+
void flow.done.then(clear, error => {
|
|
256
|
+
if (flow.retryEnabled && pendingSignIn === current && !current.controller.signal.aborted)
|
|
257
|
+
signInFailure = { error };
|
|
258
|
+
clear();
|
|
259
|
+
});
|
|
173
260
|
pending.opened = await ctx.openBrowser(flow.pageUrl);
|
|
174
261
|
}
|
|
175
262
|
finally {
|
|
@@ -184,20 +271,15 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
184
271
|
else {
|
|
185
272
|
const launchFolder = await launch;
|
|
186
273
|
if (guarded && !launchFolder)
|
|
187
|
-
throw new
|
|
274
|
+
throw new CliRefusalError("launch_folder_unavailable");
|
|
188
275
|
const folder = guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd);
|
|
189
276
|
const last = lastAnalysis.get(folder);
|
|
190
277
|
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
191
|
-
throw new
|
|
278
|
+
throw new CliRefusalError("repeat_too_soon");
|
|
192
279
|
}
|
|
193
|
-
|
|
280
|
+
analysisFolder = folder;
|
|
194
281
|
try {
|
|
195
|
-
exitCode = await analyze(folder, {},
|
|
196
|
-
started = true;
|
|
197
|
-
if (guarded)
|
|
198
|
-
lastAnalysis.set(folder, ctx.now());
|
|
199
|
-
ctx.onCheckStart?.();
|
|
200
|
-
} });
|
|
282
|
+
exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
|
|
201
283
|
}
|
|
202
284
|
finally {
|
|
203
285
|
if (guarded && started)
|
|
@@ -206,13 +288,19 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
206
288
|
}
|
|
207
289
|
}
|
|
208
290
|
catch (error) {
|
|
209
|
-
|
|
291
|
+
const failure = mcpFailure(error, failureReasons);
|
|
292
|
+
text += failure.message + "\n";
|
|
293
|
+
reason = failure.reason;
|
|
210
294
|
exitCode = errorCode(error);
|
|
211
295
|
}
|
|
212
296
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
213
297
|
// repeats the text: without it the model cannot read the result.
|
|
214
298
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
215
|
-
structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn
|
|
299
|
+
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
300
|
+
...(exitCode !== 0 && failureReasons ? { reason: reason ?? failureReason(undefined) } : {}),
|
|
301
|
+
...(freeAnalysisAvailable ? { freeAnalysisAvailable } : {}),
|
|
302
|
+
...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
303
|
+
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
216
304
|
}
|
|
217
305
|
async function respond(message, id, signal) {
|
|
218
306
|
if (signal.aborted)
|
package/dist/cli/relay-check.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
2
|
-
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
3
1
|
import { randomUUID } from "node:crypto";
|
|
4
2
|
import { disabledSetupPings } from "../setup-pings/client.js";
|
|
5
3
|
import { saveCompletion, loadCompletion } from "../adapters/completion.js";
|
|
6
4
|
import { saveLocalResults } from "../adapters/results.js";
|
|
5
|
+
import { GITIGNORE_OUTPUT_NOTICE } from "../adapters/gitignore-output.js";
|
|
7
6
|
import { OutputPathError, outputPaths } from "../adapters/output-paths.js";
|
|
8
7
|
import { validateClientRelease } from "../adapters/release.js";
|
|
9
8
|
import { selectAnalysisExecution } from "../adapters/models.js";
|
|
9
|
+
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
10
|
+
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
11
|
+
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
10
12
|
import { prepareRelayCheck } from "./relay-preparation.js";
|
|
11
|
-
import { cliRefusals, COMMITTED_FILES_MESSAGE, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, refusalExitCode } from "../contract/cli-refusals.js";
|
|
13
|
+
import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, refusalExitCode } from "../contract/cli-refusals.js";
|
|
12
14
|
import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
|
|
13
15
|
import { GatekeeperClient } from "../gatekeeper/client.js";
|
|
14
16
|
import { connectRelay, relayOrigin } from "../gatekeeper/connection.js";
|
|
15
17
|
import productionTrust from "../gatekeeper/trust/production.json" with { type: "json" };
|
|
16
18
|
import { runAnalysisToEnd } from "../core/analysis/run-to-end.js";
|
|
19
|
+
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
17
20
|
import { formatSummary } from "./summary.js";
|
|
18
21
|
import { buildIntakePayload } from "./intake-payload.js";
|
|
19
22
|
import { completionRecoveryDecision, COMPLETION_SETTINGS_MISMATCH_MESSAGE, matchesCheck } from "./completion-recovery.js";
|
|
23
|
+
import { savedFindings } from "./finding-display.js";
|
|
24
|
+
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
25
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
20
26
|
const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
|
|
21
27
|
function requireIntakeReservation(status) {
|
|
22
28
|
if (status.state === "reserved")
|
|
@@ -31,7 +37,8 @@ function requireIntakeReservation(status) {
|
|
|
31
37
|
async function submit(client, payload, signal) {
|
|
32
38
|
signal.throwIfAborted();
|
|
33
39
|
const status = await client.status(payload.reservationId);
|
|
34
|
-
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
|
|
40
|
+
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
|
|
41
|
+
...(value.findings ? { findings: value.findings } : {}), settlement: value.settlement });
|
|
35
42
|
if (!matchesCheck(status, payload))
|
|
36
43
|
throw new ConsoleError("check_conflict");
|
|
37
44
|
if (status.uploadState === "saved")
|
|
@@ -65,11 +72,14 @@ export function runReservedRelayCheck(folder, options) {
|
|
|
65
72
|
return runRelayLifecycle(folder, options, options);
|
|
66
73
|
}
|
|
67
74
|
async function runRelayLifecycle(folder, options, existing) {
|
|
68
|
-
const { config,
|
|
75
|
+
const { config, stdout, stderr } = options;
|
|
76
|
+
let settings = options.settings;
|
|
77
|
+
const summaryOptions = { audienceEnabled: false };
|
|
69
78
|
let client, reservationId, projectId;
|
|
70
79
|
let connection = existing?.connection;
|
|
71
80
|
let identity;
|
|
72
81
|
let completedPayload;
|
|
82
|
+
let displayFindings;
|
|
73
83
|
let completionOrigin;
|
|
74
84
|
let completed = false, uploaded = false, heartbeat;
|
|
75
85
|
let renewal;
|
|
@@ -77,7 +87,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
77
87
|
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
78
88
|
const showSaved = (result) => {
|
|
79
89
|
const url = savedAnalysisUrl(result.projectId, config.CF_CONSOLE_URL);
|
|
80
|
-
|
|
90
|
+
const findings = savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
|
|
91
|
+
options.onSaved?.({ ...result, findings, url, audienceEnabled: summaryOptions.audienceEnabled });
|
|
81
92
|
stdout(`Saved analysis ${result.analysisId}: ${url}\n`);
|
|
82
93
|
};
|
|
83
94
|
try {
|
|
@@ -100,6 +111,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
100
111
|
// Signature verification precedes all folder reads, status calls and spending.
|
|
101
112
|
const validated = validateClientRelease(options.release, settings);
|
|
102
113
|
const { bundle } = validated;
|
|
114
|
+
settings = validated.settings;
|
|
115
|
+
summaryOptions.audienceEnabled = readFindingSettings(settings).audienceEnabled;
|
|
103
116
|
folder = await resolveFolderPath(folder);
|
|
104
117
|
signal.throwIfAborted();
|
|
105
118
|
const prior = await loadCompletion(folder);
|
|
@@ -134,7 +147,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
134
147
|
if (error.code !== "ENOENT")
|
|
135
148
|
throw error;
|
|
136
149
|
}
|
|
137
|
-
const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit }, validated);
|
|
150
|
+
const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
|
|
138
151
|
let reservation = existing?.reservation;
|
|
139
152
|
if (!reservation) {
|
|
140
153
|
const started = await client.start(start, startMetadata);
|
|
@@ -171,9 +184,28 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
171
184
|
(options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
|
|
172
185
|
const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
|
|
173
186
|
signal.throwIfAborted();
|
|
174
|
-
const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir);
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir, { ...summaryOptions, settings });
|
|
188
|
+
try {
|
|
189
|
+
options.onLocalSaved?.(paths);
|
|
190
|
+
if (paths.ignoreAdded)
|
|
191
|
+
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
192
|
+
}
|
|
193
|
+
catch { /* Output failure must not change a server outcome. */ }
|
|
194
|
+
const { payload, resultCheck, findingIndexes } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
195
|
+
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot });
|
|
196
|
+
displayFindings = findingIndexes.map(index => {
|
|
197
|
+
const finding = report.findings[index];
|
|
198
|
+
return { title: findingPresentation(finding).displayTitle, audience: finding.audience };
|
|
199
|
+
});
|
|
200
|
+
if (resultCheck?.droppedFindings) {
|
|
201
|
+
const count = resultCheck.droppedFindings;
|
|
202
|
+
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
203
|
+
? `${count === 1 ? "it was" : "they were"} too long or not in the expected format` : `${count === 1 ? "its" : "their"} quotes did not match your files`;
|
|
204
|
+
try {
|
|
205
|
+
stderr(`${count} ${count === 1 ? "finding was" : "findings were"} not uploaded because ${reason}.\n`);
|
|
206
|
+
}
|
|
207
|
+
catch { /* Output failure must not change a server outcome. */ }
|
|
208
|
+
}
|
|
177
209
|
completionOrigin = new URL(config.CF_CONSOLE_URL).origin;
|
|
178
210
|
await saveCompletion(folder, payload, completionOrigin);
|
|
179
211
|
completedPayload = payload;
|
|
@@ -181,7 +213,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
181
213
|
const saved = await submit(client, payload, signal);
|
|
182
214
|
uploaded = true;
|
|
183
215
|
await saveCompletion(folder, payload, completionOrigin, true);
|
|
184
|
-
stdout(formatSummary(report));
|
|
216
|
+
stdout(formatSummary(report, summaryOptions));
|
|
185
217
|
stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
|
|
186
218
|
showSaved(saved);
|
|
187
219
|
return 0;
|
|
@@ -204,7 +236,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
204
236
|
catch { /* Committed server state still survives a local write failure. */ }
|
|
205
237
|
}
|
|
206
238
|
try {
|
|
207
|
-
showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds });
|
|
239
|
+
showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings });
|
|
208
240
|
}
|
|
209
241
|
catch { /* Output failure must not change a server outcome. */ }
|
|
210
242
|
return 0;
|
|
@@ -216,11 +248,12 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
216
248
|
}
|
|
217
249
|
catch { /* Keep the pending record if the durable write cannot be confirmed. */ }
|
|
218
250
|
}
|
|
219
|
-
const known = error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE].includes(error.message)
|
|
251
|
+
const known = error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
220
252
|
|| error.message.startsWith("The saved completion") || error.message === COMPLETION_SETTINGS_MISMATCH_MESSAGE);
|
|
221
253
|
const message = known ? error.message : FINISH_FAILURE_MESSAGE;
|
|
222
254
|
try {
|
|
223
|
-
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined,
|
|
255
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failureReason(error), message,
|
|
256
|
+
...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
224
257
|
stderr(`${message}\n`);
|
|
225
258
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
226
259
|
stderr(stateLine(status));
|
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
2
3
|
import { outsideLoadersEnabled } from "../analysis-core/settings.js";
|
|
4
|
+
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
3
5
|
import { readLocalFolder } from "../adapters/files.js";
|
|
4
|
-
import { snapshotProvenance } from "../adapters/snapshot.js";
|
|
6
|
+
import { snapshotProvenance, sameFileHashes } from "../adapters/snapshot.js";
|
|
5
7
|
import { validateClientRelease } from "../adapters/release.js";
|
|
6
8
|
import { prepareRelayPlan } from "../adapters/relay-plan.js";
|
|
9
|
+
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
7
10
|
import { COMMITTED_FILES_MESSAGE } from "../contract/cli-refusals.js";
|
|
11
|
+
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
8
12
|
import { repositoryFacts } from "./git.js";
|
|
9
13
|
/** Absolute checkout paths stay local; repository facts own the start's commit. */
|
|
10
14
|
export function buildRelayStartPayload(facts, files) {
|
|
11
15
|
const { remoteUrl, relPath, folderName, branch, commit, dirty } = facts;
|
|
12
16
|
return { execution: "local", remoteUrl, relPath, folderName, branch, commit, dirty, files };
|
|
13
17
|
}
|
|
18
|
+
export function committedFilesOnly(settings) {
|
|
19
|
+
return settings.CF_LOCAL_COMMITTED_ONLY === "1";
|
|
20
|
+
}
|
|
14
21
|
/** Recovery can reuse signature validation without reading the folder again. */
|
|
15
22
|
export async function prepareRelayCheck(folder, options, validated = validateClientRelease(options.release, options.settings)) {
|
|
16
|
-
const { bundle, tools } = validated;
|
|
23
|
+
const { bundle, tools, settings } = validated;
|
|
17
24
|
options.signal?.throwIfAborted();
|
|
18
25
|
// Discovery must agree with Git before publishing repository-relative captures.
|
|
19
26
|
const earlyFacts = outsideLoadersEnabled(options.settings) ? await (options.facts ?? repositoryFacts)(folder) : undefined;
|
|
20
27
|
options.signal?.throwIfAborted();
|
|
21
28
|
const read = await readLocalFolder(options.settings, folder, options.trialAdmission, { captureSnapshot: true,
|
|
29
|
+
proposal: options.proposal,
|
|
22
30
|
...(earlyFacts ? { expectedRepositoryRoot: earlyFacts.repoRoot ?? null } : {}) });
|
|
31
|
+
if (read.proposalCounts?.fromProposal === 0 && read.proposalCounts.rejected > 0)
|
|
32
|
+
throw new Error(INVALID_PROPOSAL_MESSAGE);
|
|
33
|
+
if (read.agentChoice) {
|
|
34
|
+
const cwd = await resolveFolderPath(options.cwd ?? process.cwd());
|
|
35
|
+
const launchPath = (path) => relative(cwd, join(read.sourceBase ?? folder, path)).split(sep).join("/") || ".";
|
|
36
|
+
throw new AgentChoiceError({ ...read.agentChoice,
|
|
37
|
+
candidates: read.agentChoice.candidates.map(candidate => ({
|
|
38
|
+
folder: launchPath(candidate.folder), reason: launchPath(candidate.reason),
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
const source = { ...read, files: Object.freeze({ ...read.files }) };
|
|
24
|
-
const provenance = await snapshotProvenance(source.sourceBase ?? folder, source.snapshot,
|
|
25
|
-
if (options.expectedCommit
|
|
43
|
+
const provenance = await snapshotProvenance(source.sourceBase ?? folder, source.snapshot, { selectionFolder: folder, committedOnly: committedFilesOnly(options.settings) });
|
|
44
|
+
if (options.expectedCommit === null) {
|
|
45
|
+
if (!sameFileHashes(options.expectedFiles ?? [], source.snapshot))
|
|
46
|
+
throw new Error(COMMITTED_FILES_MESSAGE);
|
|
47
|
+
}
|
|
48
|
+
else if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
|
|
26
49
|
throw new Error(COMMITTED_FILES_MESSAGE);
|
|
27
|
-
const { plan, files } = prepareRelayPlan({ bundle, tools, settings
|
|
50
|
+
const { plan, files } = prepareRelayPlan({ bundle, tools, settings, relayVersion: options.release.relayVersion, source });
|
|
28
51
|
options.signal?.throwIfAborted();
|
|
29
52
|
const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(folder);
|
|
30
53
|
options.signal?.throwIfAborted();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { publicFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/relay-analysis.js";
|
|
2
|
+
import { findingSitesInOrder } from "../analysis-core/findings/sites.js";
|
|
3
|
+
import { localFindingSchema } from "../contract/console.js";
|
|
4
|
+
function evidenceQuotes(finding, repoKey) {
|
|
5
|
+
const primary = finding.site ? { repo: finding.siteRepo ?? repoKey, ...finding.site, excerpt: finding.excerpt } : null;
|
|
6
|
+
const sites = findingSitesInOrder(primary, finding.otherSites);
|
|
7
|
+
const quotes = [];
|
|
8
|
+
function add(site, text) {
|
|
9
|
+
if (typeof text === "string")
|
|
10
|
+
quotes.push({ repo: site.repo, path: site.path, text });
|
|
11
|
+
}
|
|
12
|
+
add(primary ?? {}, finding.excerpt);
|
|
13
|
+
finding.otherSites.forEach(site => add(site, site.excerpt));
|
|
14
|
+
for (const note of finding.sourceNotes?.sites ?? []) {
|
|
15
|
+
if (note.keyPhrase !== "")
|
|
16
|
+
add(sites[note.index] ?? {}, note.keyPhrase);
|
|
17
|
+
}
|
|
18
|
+
for (const site of finding.sourceNotes?.counterEvidence ?? [])
|
|
19
|
+
add(site, site.excerpt);
|
|
20
|
+
return quotes;
|
|
21
|
+
}
|
|
22
|
+
/** The intake boundary and offline measurement use exactly the same evidence decision. */
|
|
23
|
+
export function checkIntakeFindings(findings, context, onInvalidQuote) {
|
|
24
|
+
const accepted = [], acceptedIndexes = [];
|
|
25
|
+
const originals = new Map();
|
|
26
|
+
function original(path) {
|
|
27
|
+
if (!originals.has(path)) {
|
|
28
|
+
const captured = context.files[path];
|
|
29
|
+
try {
|
|
30
|
+
originals.set(path, typeof captured === "string" ? captured : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(captured));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
originals.set(path, null);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return originals.get(path);
|
|
37
|
+
}
|
|
38
|
+
let quotes = 0, droppedQuotes = 0, invalidQuotes = 0, schemaDroppedFindings = 0, capDroppedFindings = 0;
|
|
39
|
+
findings.forEach((finding, index) => {
|
|
40
|
+
const parsed = publicFinding.safeParse(finding);
|
|
41
|
+
const evidence = parsed.success ? evidenceQuotes(parsed.data, context.repoKey) : [];
|
|
42
|
+
quotes += evidence.length;
|
|
43
|
+
const invalid = evidence.filter(quote => {
|
|
44
|
+
const source = quote.repo === context.repoKey && typeof quote.path === "string" && Object.hasOwn(context.files, quote.path)
|
|
45
|
+
? original(quote.path) : null;
|
|
46
|
+
const invalid = !quote.text.length || source === null || !source.includes(quote.text);
|
|
47
|
+
if (invalid)
|
|
48
|
+
onInvalidQuote?.({ text: quote.text, source });
|
|
49
|
+
return invalid;
|
|
50
|
+
}).length;
|
|
51
|
+
invalidQuotes += invalid;
|
|
52
|
+
const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success && localFindingSchema.safeParse(parsed.data).success;
|
|
53
|
+
if (!schemaValid)
|
|
54
|
+
schemaDroppedFindings++;
|
|
55
|
+
if (index >= RESULT_CHECK_LIMITS.findings)
|
|
56
|
+
capDroppedFindings++;
|
|
57
|
+
if (!schemaValid || !parsed.success || invalid || index >= RESULT_CHECK_LIMITS.findings) {
|
|
58
|
+
// Removing one supporting quote could change the claim or invalidate source-note indices.
|
|
59
|
+
droppedQuotes += evidence.length;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
accepted.push(parsed.data);
|
|
63
|
+
acceptedIndexes.push(index);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return { findings: accepted, acceptedIndexes, droppedFindings: findings.length - accepted.length, quotes, droppedQuotes, invalidQuotes,
|
|
67
|
+
schemaDroppedFindings, capDroppedFindings };
|
|
68
|
+
}
|