@counterfact-labs/cli 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/bundle/client-release.json +1 -1
  2. package/dist/adapters/analysis-results.js +87 -0
  3. package/dist/adapters/combined-results.js +27 -0
  4. package/dist/adapters/entry-files.js +5 -2
  5. package/dist/adapters/entry-packing.js +161 -0
  6. package/dist/adapters/entry-selection.js +13 -1
  7. package/dist/adapters/files.js +110 -5
  8. package/dist/adapters/git.js +10 -1
  9. package/dist/adapters/relay-plan.js +5 -7
  10. package/dist/adapters/release.js +4 -1
  11. package/dist/adapters/results.js +9 -2
  12. package/dist/adapters/selection.js +19 -9
  13. package/dist/adapters/settings.js +37 -6
  14. package/dist/adapters/source-relay-plan.js +7 -0
  15. package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
  16. package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
  17. package/dist/analysis-core/analysis/file-dependencies.js +1 -0
  18. package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
  19. package/dist/analysis-core/analysis/finding-identity.js +1 -0
  20. package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
  21. package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
  22. package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
  23. package/dist/analysis-core/analysis/project-architecture.js +1 -0
  24. package/dist/analysis-core/analysis/schemas.js +1 -1
  25. package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
  26. package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
  27. package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
  28. package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
  29. package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
  30. package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
  31. package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
  32. package/dist/analysis-core/chunks/{6RWEWBHK.js → 5B5UCWD3.js} +1 -1
  33. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  34. package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
  35. package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
  36. package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
  37. package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
  38. package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
  39. package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
  40. package/dist/analysis-core/chunks/{FBTOOUPL.js → HVQJKPQ4.js} +1 -1
  41. package/dist/analysis-core/chunks/INVTU73D.js +1 -0
  42. package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
  43. package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
  44. package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
  45. package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
  46. package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
  47. package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
  48. package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
  49. package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
  50. package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
  51. package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
  52. package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
  53. package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
  54. package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
  55. package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
  56. package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
  57. package/dist/analysis-core/chunks/{RNTMH4MU.js → S27Y2TP5.js} +1 -1
  58. package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
  59. package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
  60. package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
  61. package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
  62. package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
  63. package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
  64. package/dist/analysis-core/contracts/analysis-selection.js +1 -1
  65. package/dist/analysis-core/contracts/analysis-size.js +1 -0
  66. package/dist/analysis-core/contracts/execution-request.js +1 -0
  67. package/dist/analysis-core/contracts/intake-finding.js +1 -0
  68. package/dist/analysis-core/contracts/model-call-identity.js +1 -0
  69. package/dist/analysis-core/contracts/relay-analysis.js +1 -1
  70. package/dist/analysis-core/contracts/requests.js +1 -1
  71. package/dist/analysis-core/conventions/analysis-fit.js +1 -1
  72. package/dist/analysis-core/conventions/file-admission.js +1 -1
  73. package/dist/analysis-core/conventions/read-limits.js +1 -0
  74. package/dist/analysis-core/import/calls/modules.js +1 -1
  75. package/dist/analysis-core/import/entry-selection.js +1 -0
  76. package/dist/analysis-core/import/outside-loader-sources.js +1 -1
  77. package/dist/analysis-core/index.js +3 -3
  78. package/dist/analysis-core/requests/answer-formats.js +1 -1
  79. package/dist/analysis-core/requests/local-plan.js +1 -1
  80. package/dist/analysis-core/requests/planned-limits.js +1 -0
  81. package/dist/analysis-core/requests/registry.js +1 -1
  82. package/dist/analysis-core/requests/template-version.js +1 -1
  83. package/dist/analysis-core/requests/tools.js +1 -1
  84. package/dist/analysis-core/settings.js +1 -1
  85. package/dist/analysis-core/stages/engine-entry.js +1 -1
  86. package/dist/bundle/seal/engine.js +1 -1
  87. package/dist/cli/analysis-selection.js +47 -5
  88. package/dist/cli/api.js +1 -1
  89. package/dist/cli/commands/analyze.js +58 -15
  90. package/dist/cli/commands/skipped.js +65 -0
  91. package/dist/cli/context.js +2 -1
  92. package/dist/cli/finding-display.js +102 -7
  93. package/dist/cli/finding-settings.js +3 -1
  94. package/dist/cli/intake-payload.js +26 -2
  95. package/dist/cli/main.js +21 -7
  96. package/dist/cli/mcp-failure.js +6 -5
  97. package/dist/cli/mcp.js +134 -28
  98. package/dist/cli/relay-check.js +107 -23
  99. package/dist/cli/relay-failure.js +4 -4
  100. package/dist/cli/relay-preparation.js +50 -12
  101. package/dist/cli/result-check.js +31 -12
  102. package/dist/cli/saved-results.js +10 -0
  103. package/dist/cli/scope-choices.js +3 -3
  104. package/dist/cli/sign-in.js +2 -2
  105. package/dist/cli/skipped-files.js +22 -0
  106. package/dist/cli/summary.js +15 -1
  107. package/dist/contract/agent-choice.js +25 -2
  108. package/dist/contract/cli-api-messages.js +3 -0
  109. package/dist/contract/cli-refusals.js +31 -5
  110. package/dist/contract/cli-version.js +1 -1
  111. package/dist/contract/console-url.js +3 -0
  112. package/dist/contract/console.js +17 -38
  113. package/dist/contract/engine.js +1 -1
  114. package/dist/contract/messages.js +4 -3
  115. package/dist/contract/project-architecture.js +1 -0
  116. package/dist/contract/scope-choice.js +31 -11
  117. package/dist/contract/selection-offer.js +5 -8
  118. package/dist/gatekeeper/cli-api.js +70 -4
  119. package/dist/gatekeeper/console.js +19 -3
  120. package/dist/gatekeeper/model.js +5 -23
  121. package/dist/gatekeeper/trust/policy.js +2 -1
  122. package/package.json +4 -2
  123. package/dist/analysis-core/analysis/source-notes/types.js +0 -1
  124. package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
  125. package/dist/analysis-core/chunks/2PZF54KH.js +0 -10
  126. package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
  127. package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
  128. package/dist/analysis-core/chunks/7NEWZ74F.js +0 -1
  129. package/dist/analysis-core/chunks/CGCJN73D.js +0 -14
  130. package/dist/analysis-core/chunks/CWYVUGG4.js +0 -1
  131. package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
  132. package/dist/analysis-core/chunks/GOQVKWP3.js +0 -32
  133. package/dist/analysis-core/chunks/IHAVDX4L.js +0 -66
  134. package/dist/analysis-core/chunks/IVULXWQN.js +0 -21
  135. package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
  136. package/dist/analysis-core/chunks/K5FUABWW.js +0 -1
  137. package/dist/analysis-core/chunks/KGQYR62R.js +0 -7
  138. package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
  139. package/dist/analysis-core/chunks/PY5M22CL.js +0 -1
  140. package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
  141. package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
  142. package/dist/analysis-core/chunks/SLJ6CW7W.js +0 -1
  143. package/dist/analysis-core/chunks/UEORNVQB.js +0 -1
  144. package/dist/analysis-core/chunks/WYLIV2P2.js +0 -1
  145. package/dist/analysis-core/data/finding-types.js +0 -1
package/dist/cli/mcp.js CHANGED
@@ -1,11 +1,15 @@
1
+ import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
2
+ import { readSkippedFilesSetting, readFolderPartsSetting } from "../adapters/settings.js";
1
3
  import { createInterface } from "node:readline";
2
4
  import { once } from "node:events";
3
5
  import { resolveFolderPath } from "../adapters/folder-path.js";
4
- import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
6
+ import { cliRefusals, CliRefusalError, failureReason } from "../contract/cli-refusals.js";
5
7
  import { mcpFailure } from "./mcp-failure.js";
8
+ import { agentChoiceMessage } from "../contract/agent-choice.js";
6
9
  import { errorCode } from "./api.js";
7
10
  import { commandContext } from "./context.js";
8
11
  import { analyze } from "./commands/analyze.js";
12
+ import { skipped } from "./commands/skipped.js";
9
13
  import { CLI_VERSION } from "../contract/cli-version.js";
10
14
  import { finding } from "./commands/finding.js";
11
15
  import { balance } from "./commands/balance.js";
@@ -19,10 +23,12 @@ import { suggestedEmail } from "./suggested-email.js";
19
23
  import { agentProposalInputSchema } from "../contract/agent-proposal.js";
20
24
  import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
21
25
  import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
22
- import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
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
- function fileAnalysisTool(choices) {
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
- proposal: { ...proposal, description: fileProposalDescription }, ...(choices ? { scope: { ...scope, description: fileScopeDescription } } : {}) },
50
- required: ["path"], additionalProperties: false } };
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 findingSettings = readFindingSettings(options.env);
79
- const analysisSwitches = readSelectionSettings(options.startup?.settings ?? options.env ?? process.env);
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 => tool.name === ANALYZE_TOOL_NAME && selectionEnabled ? fileAnalysisTool(choicesEnabled) : tool).map(tool => tool.name === "get_finding" && numbersEnabled ? {
114
- ...tool, inputSchema: numberedFindingInputSchema,
115
- } : tool).concat(fixesEnabled ? fixRequestTools : []);
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 && (typeof args.path !== "string" || !args.path.trim()))
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
- findings = findingDisplayRows(result.findings ?? [], { numbersEnabled,
184
- audienceEnabled: result.audienceEnabled, labels: "reference" }).map(row => row.finding);
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
- if (numbersEnabled)
209
- loadedFinding = { ...value, ...(value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
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 scope = await loadAnalysisScope({ scope: args.scope }, cwd, analysisSettings);
320
- const selection = selectionEnabled ? await resolveAnalysisSelection(args.path, launchFolder ?? cwd, analysisSettings, scope) : undefined;
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 key = selection ? JSON.stringify([selection.launchRoot, selection.path]) : folder;
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 analyze(folder, { proposal: args.proposal, scope, selection }, ctx);
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
- ...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
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) {
@@ -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 findings = savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
88
- options.onSaved?.({ ...result, findings, url, audienceEnabled: summaryOptions.audienceEnabled });
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
- summaryOptions.audienceEnabled = readFindingSettings(settings).audienceEnabled;
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 = selection?.outputAnchor ?? await resolveFolderPath(folder);
141
+ // Verify the selected folder before output and recovery operations can fail for their own reasons.
142
+ folder = await resolveFolderPath(selection?.outputAnchor ?? folder).catch(error => {
143
+ if (error instanceof Error)
144
+ Object.assign(error, { localOperation: "read" });
145
+ throw error;
146
+ });
114
147
  outputDir ??= selection?.outputDir;
115
148
  if (selection?.kind === "file")
116
149
  completionIdentity = {
@@ -138,6 +171,7 @@ async function runRelayLifecycle(folder, options, existing) {
138
171
  completionStored = true;
139
172
  completionRecordPath = path;
140
173
  completedPayload = prior.payload;
174
+ skippedFiles = prior.payload.coverage?.skippedFiles ?? [];
141
175
  completionOrigin = prior.consoleOrigin;
142
176
  completionTiming = { completedAt: prior.completedAt, absoluteExpiresAt: prior.absoluteExpiresAt };
143
177
  uploaded = prior.uploaded ?? false;
@@ -146,12 +180,24 @@ async function runRelayLifecycle(folder, options, existing) {
146
180
  const saved = await submit(prior.payload, retainUnsaved && uploaded);
147
181
  uploaded = true;
148
182
  await persist(prior.payload, prior.consoleOrigin, true);
183
+ await settleSaved(saved);
149
184
  showSaved(saved);
150
185
  return 0;
151
186
  }
152
187
  signal.throwIfAborted();
153
188
  if (options.retryUpload)
154
189
  throw new Error("The saved completion record is missing. Keep the local report and check the output directory.");
190
+ // Negotiate once before paid work: older strict intake schemas cannot accept optional metadata.
191
+ const capabilities = skippedFilesEnabled(settings) && options.skippedFilesSupported === undefined
192
+ || settings.CF_LOCAL_ARCHITECTURE === "1" && options.consoleSupportsArchitecture === undefined
193
+ ? await client.capabilities() : undefined;
194
+ const consoleSupportsArchitecture = options.consoleSupportsArchitecture ?? capabilities?.architecture ?? false;
195
+ if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? capabilities?.skippedFiles)) {
196
+ if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
197
+ throw new ConsoleError("unavailable", "start");
198
+ settings = Object.freeze({ ...settings, CF_SKIPPED_FILES: "0" });
199
+ summaryOptions.skippedFilesEnabled = false;
200
+ }
155
201
  try {
156
202
  await outputPaths(folder, ["findings.json", "summary.md", ...(selection?.kind === "file" ? ["selection.json"] : [])], outputDir);
157
203
  }
@@ -159,14 +205,18 @@ async function runRelayLifecycle(folder, options, existing) {
159
205
  if (error.code !== "ENOENT")
160
206
  throw error;
161
207
  }
162
- const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
208
+ const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, settings, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, { ...validated, settings });
163
209
  const { source, provenance, plan, start, startMetadata } = prepared;
164
210
  membership = prepared.membership;
165
211
  const includedPaths = Object.keys(source.files);
166
212
  selectedPaths = includedPaths;
213
+ refusedCalls = plan.calls;
214
+ skippedFiles = source.skippedFiles;
167
215
  let reservation = existing?.reservation;
168
216
  if (!reservation) {
169
- const started = await client.start(start, startMetadata);
217
+ const started = await client.start(start, { ...startMetadata, ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}) });
218
+ if (options.expectedProjectId && started.projectId !== options.expectedProjectId)
219
+ throw new ConsoleError("check_conflict");
170
220
  projectId = started.projectId;
171
221
  signal.throwIfAborted();
172
222
  const created = await client.create({ ...started, checkId: randomUUID(), plan });
@@ -197,31 +247,53 @@ async function runRelayLifecycle(folder, options, existing) {
197
247
  options.onStart?.();
198
248
  stderr(progressLine(selection));
199
249
  (options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
200
- const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
250
+ const report = await runAnalysisToEnd(source, execution.model, settings, { execution, ...(options.addToAnalysisId ? { findingsOnly: true } : {}) });
251
+ skippedFiles = !skippedFilesEnabled(settings) ? source.skippedFiles : analysisSkippedFiles(source.skippedFiles, report.skippedFiles);
252
+ const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
253
+ const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
201
254
  completionTiming = observedCompletionTiming({ completedAt: Date.now() }, { kind: "found", status: reservation }, reservation);
202
255
  // The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
203
256
  if (retainUnsaved)
204
257
  clearInterval(heartbeat);
205
258
  else
206
259
  signal.throwIfAborted();
260
+ let saveContext;
207
261
  const saveReports = async () => {
208
- const paths = await saveLocalResults(folder, report, source.skippedFiles, outputDir, { ...summaryOptions, settings,
209
- ...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths });
262
+ const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
263
+ ...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths,
264
+ ...(options.addToAnalysisId ? { append: { readPaths: includedPaths, assessments } } : {}) });
210
265
  try {
211
266
  options.onLocalSaved?.(paths);
212
267
  if (paths.ignoreAdded)
213
268
  stderr(GITIGNORE_OUTPUT_NOTICE);
214
269
  }
215
270
  catch { /* Output failure must not change a server outcome. */ }
271
+ await saveContext?.();
216
272
  return paths;
217
273
  };
274
+ // The retry off path saves the full report even when intake validation refuses it.
218
275
  let paths = retainUnsaved ? undefined : await saveReports();
219
- const { payload, resultCheck, findingIndexes } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
220
- bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot });
221
- displayFindings = findingIndexes.map(index => {
276
+ const { payload, resultCheck, findingIndexes, findingActivities } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
277
+ bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot, consoleSupportsActivity: options.consoleSupportsActivity, consoleSupportsArchitecture,
278
+ ...(!skippedFilesEnabled(settings) || !options.addToAnalysisId && !sizeSkippedFiles(skippedFiles).length ? {} : { coverage: { skippedFiles: sizeSkippedFiles(skippedFiles), readPaths: includedPaths },
279
+ addToAnalysisId: options.addToAnalysisId, findingAssessments: report.findingCheck.qualityAudit?.assessments ?? [] }) });
280
+ displayFindings = findingIndexes.map((index, acceptedIndex) => {
222
281
  const finding = report.findings[index];
223
- return { title: findingPresentation(finding).displayTitle, audience: finding.audience };
282
+ const activity = findingActivities[acceptedIndex];
283
+ return { title: findingPresentation(finding).displayTitle, audience: finding.audience,
284
+ severity: finding.severity, site: finding.site, fixes: finding.fixes,
285
+ ...(activity === undefined ? {} : { activity }) };
224
286
  });
287
+ if (skippedFilesEnabled(settings) && (options.addToAnalysisId || sizeSkippedFiles(skippedFiles).length)) {
288
+ saveContext = async () => {
289
+ await saveAnalysisContext(folder, { projectId: projectId, analysisId: options.addToAnalysisId ?? reservation.checkId,
290
+ settings: priorResults?.context.settings ?? settings, assessments,
291
+ ...(priorResults?.context.findings ? { findings: priorResults.context.findings } : {}),
292
+ pending: { checkId: reservation.checkId, fingerprints: findingIndexes.map(index => fingerprintOf(report.findings[index])), titles: [...displayFindings] } }, outputDir);
293
+ };
294
+ if (paths)
295
+ await saveContext();
296
+ }
225
297
  if (resultCheck?.droppedFindings) {
226
298
  const count = resultCheck.droppedFindings;
227
299
  const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
@@ -256,7 +328,8 @@ async function runRelayLifecycle(folder, options, existing) {
256
328
  uploaded = true;
257
329
  await persist(payload, completionOrigin, true);
258
330
  }
259
- stdout(formatSummary(report, summaryOptions));
331
+ await settleSaved(saved);
332
+ stdout(formatSummary(options.addToAnalysisId ? await loadPublicReport(folder, outputDir) : { ...report, refusedFiles: skippedFiles }, summaryOptions));
260
333
  stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
261
334
  showSaved(saved);
262
335
  return 0;
@@ -297,14 +370,16 @@ async function runRelayLifecycle(folder, options, existing) {
297
370
  completionTiming = error.timing ?? completionTiming;
298
371
  const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
299
372
  timing: completionTiming, statusLookup, uploadOutcome, savedResult,
300
- canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file" }, {
373
+ canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file", fileMode: fileScopeEnabled(settings) }, {
301
374
  markUploaded: () => persist(completedPayload, completionOrigin, true),
302
375
  keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
303
376
  retire: () => retainUnsaved ? completionRecordPath ? discardCompletion(folder, completionRecordPath, completionIdentity) : Promise.resolve() : persist(completedPayload, completionOrigin, false, true),
304
377
  });
305
378
  if (failure.saved) {
306
379
  try {
307
- showSaved(savedResult ?? { projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings });
380
+ const saved = savedResult ?? { projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings };
381
+ await settleSaved(saved);
382
+ showSaved(saved);
308
383
  }
309
384
  catch { /* Output failure must not change a server outcome. */ }
310
385
  if (!failure.message)
@@ -316,10 +391,19 @@ async function runRelayLifecycle(folder, options, existing) {
316
391
  catch { /* Reporting failure cannot undo saved results. */ }
317
392
  return 1;
318
393
  }
394
+ let scopeChoice;
395
+ if (!reservationId && !existing && error instanceof ConsoleError && (error.code === "folder_too_large" || error.code === "plan_over_limit")) {
396
+ try {
397
+ const choice = await prepareFolderChoice(folder, { ...options, settings, signal, limit: error.code, refusedCalls });
398
+ if (choice)
399
+ scopeChoice = new AgentChoiceError(choice);
400
+ }
401
+ catch { /* A failed local plan preserves the console's authoritative refusal. */ }
402
+ }
319
403
  try {
320
- options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: failure.message,
321
- ...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
322
- stderr(`${failure.message}\n`);
404
+ options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? failure.message,
405
+ ...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
406
+ stderr(`${scopeChoice?.message ?? failure.message}\n`);
323
407
  if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
324
408
  stderr(stateLine(status));
325
409
  }
@@ -2,8 +2,8 @@ import { CompletionBusyError } from "../adapters/completion-lock.js";
2
2
  import { OutputPathError } from "../adapters/output-paths.js";
3
3
  import { AgentChoiceError } from "../contract/agent-choice.js";
4
4
  import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
5
- import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
6
- import { isSelectionRefusal } from "../contract/scope-choice.js";
5
+ import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, localFailure, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
6
+ import { isSelectionRefusal, selectionRefusalMessage } from "../contract/scope-choice.js";
7
7
  import { ConsoleError } from "../gatekeeper/console.js";
8
8
  import { recoverCompletedResults } from "./completion-delivery.js";
9
9
  import { CompletionSettingsMismatchError } from "./prior-completion.js";
@@ -24,9 +24,9 @@ export async function recoverRelayFailure(input, operations) {
24
24
  || error instanceof CompletionBusyError || error instanceof CompletionSettingsMismatchError || error instanceof Error
25
25
  && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
26
26
  || error.message.startsWith("The saved completion"));
27
- const message = recovery?.message ?? input.localErrorMessage ?? (known ? error.message : FINISH_FAILURE_MESSAGE);
27
+ const message = recovery?.message ?? input.localErrorMessage ?? localFailure(error)?.message ?? (known ? error.message : FINISH_FAILURE_MESSAGE);
28
28
  const reason = !retryEnabled ? failureReason(error)
29
29
  : recovery?.decision.action === "retry" && input.completionStored ? "results_pending"
30
30
  : recovery?.decision.action === "give-up" ? "results_discarded" : failureReason(error);
31
- return { saved: false, message, reason };
31
+ return { saved: false, message: selectionRefusalMessage(message, input.fileMode ?? true), reason };
32
32
  }