@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.
Files changed (153) hide show
  1. package/README.md +7 -1
  2. package/bundle/client-release.json +1 -1
  3. package/dist/adapters/anthropic.js +11 -2
  4. package/dist/adapters/completion.js +3 -0
  5. package/dist/adapters/durable-write.js +9 -2
  6. package/dist/adapters/files.js +25 -13
  7. package/dist/adapters/git-ignore.js +28 -14
  8. package/dist/adapters/git.js +1 -1
  9. package/dist/adapters/gitignore-output.js +41 -0
  10. package/dist/adapters/models.js +1 -6
  11. package/dist/adapters/output-paths.js +5 -2
  12. package/dist/adapters/public-instructions.js +1 -1
  13. package/dist/adapters/relay-plan.js +2 -5
  14. package/dist/adapters/release.js +2 -1
  15. package/dist/adapters/repository-files.js +164 -4
  16. package/dist/adapters/repository-root.js +18 -2
  17. package/dist/adapters/results.js +8 -4
  18. package/dist/adapters/settings.js +26 -1
  19. package/dist/adapters/snapshot.js +86 -15
  20. package/dist/analysis-core/analysis/finding-audience-switch.js +1 -0
  21. package/dist/analysis-core/analysis/schemas.js +1 -1
  22. package/dist/analysis-core/analysis/source-notes/types.js +1 -1
  23. package/dist/analysis-core/chunks/377MLK4M.js +21 -0
  24. package/dist/analysis-core/chunks/{D6AWHLRI.js → 3JWEFCTJ.js} +1 -1
  25. package/dist/analysis-core/chunks/{3227KXHF.js → 4H23CSUM.js} +1 -1
  26. package/dist/analysis-core/chunks/5DGPLXYB.js +1 -0
  27. package/dist/analysis-core/chunks/5TATKWLG.js +1 -0
  28. package/dist/analysis-core/chunks/{EH3QQ3T4.js → 6B2HILSY.js} +1 -1
  29. package/dist/analysis-core/chunks/6QY5BV5B.js +14 -0
  30. package/dist/analysis-core/chunks/6U2IR4XB.js +1 -0
  31. package/dist/analysis-core/chunks/A7WJJMRY.js +1 -0
  32. package/dist/analysis-core/chunks/C6X5GDG4.js +1 -0
  33. package/dist/analysis-core/chunks/DLPVYWHZ.js +1 -0
  34. package/dist/analysis-core/chunks/DZZBQDGO.js +1 -0
  35. package/dist/analysis-core/chunks/F3R4J3ND.js +1 -0
  36. package/dist/analysis-core/chunks/FOKDE64Y.js +1 -0
  37. package/dist/analysis-core/chunks/K2GNDX3W.js +1 -0
  38. package/dist/analysis-core/chunks/KTNA2KTN.js +7 -0
  39. package/dist/analysis-core/chunks/KWECQJ6X.js +1 -0
  40. package/dist/analysis-core/chunks/{SL27GMEN.js → M6HYSTQD.js} +1 -1
  41. package/dist/analysis-core/chunks/{ZAA44GPI.js → N7U5N42V.js} +1 -1
  42. package/dist/analysis-core/chunks/NIOS7U6C.js +41 -0
  43. package/dist/analysis-core/chunks/{QMIXCT62.js → OPMA32A6.js} +1 -1
  44. package/dist/analysis-core/chunks/PECX3OXB.js +1 -0
  45. package/dist/analysis-core/chunks/QFY6A3MS.js +66 -0
  46. package/dist/analysis-core/chunks/SKVEBOGA.js +1 -0
  47. package/dist/analysis-core/chunks/{T66VDFXF.js → STI4JE7L.js} +1 -1
  48. package/dist/analysis-core/chunks/SVMEWGD3.js +1 -0
  49. package/dist/analysis-core/chunks/X7WZAA7S.js +1 -0
  50. package/dist/analysis-core/chunks/YDBO7P4X.js +1 -0
  51. package/dist/analysis-core/chunks/YUY436SM.js +1 -0
  52. package/dist/analysis-core/chunks/ZTOVBAG7.js +1 -0
  53. package/dist/analysis-core/contracts/file-safety.js +1 -0
  54. package/dist/analysis-core/contracts/relay-analysis.js +1 -0
  55. package/dist/analysis-core/contracts/requests.js +1 -1
  56. package/dist/analysis-core/conventions/analysis-fit.js +1 -1
  57. package/dist/analysis-core/conventions/file-admission.js +1 -1
  58. package/dist/analysis-core/data/finding-types.js +1 -1
  59. package/dist/analysis-core/findings/sites.js +1 -0
  60. package/dist/analysis-core/github/repo-key.js +1 -1
  61. package/dist/analysis-core/import/outside-loader-sources.js +1 -1
  62. package/dist/analysis-core/index.js +3 -3
  63. package/dist/analysis-core/llm/output-limit.js +1 -1
  64. package/dist/analysis-core/llm/types.js +1 -0
  65. package/dist/analysis-core/requests/answer-formats.js +1 -1
  66. package/dist/analysis-core/requests/headline-request.js +1 -1
  67. package/dist/analysis-core/requests/local-plan.js +1 -1
  68. package/dist/analysis-core/requests/registry.js +1 -1
  69. package/dist/analysis-core/requests/template-version.js +1 -1
  70. package/dist/analysis-core/requests/tools.js +1 -1
  71. package/dist/analysis-core/settings.js +1 -1
  72. package/dist/analysis-core/stages/engine-entry.js +1 -0
  73. package/dist/analysis-core/stages/execution-ports.js +1 -0
  74. package/dist/bundle/seal/deserialise.js +9 -1
  75. package/dist/bundle/seal/engine.js +76 -0
  76. package/dist/bundle/seal/freeze.js +9 -0
  77. package/dist/bundle/seal/metadata.js +3 -0
  78. package/dist/bundle/seal/open-engine.js +26 -0
  79. package/dist/bundle/seal/open.js +2 -8
  80. package/dist/cli/agent-folder.js +3 -3
  81. package/dist/cli/commands/analyze.js +39 -6
  82. package/dist/cli/commands/balance.js +6 -3
  83. package/dist/cli/commands/finding.js +9 -3
  84. package/dist/cli/context.js +6 -1
  85. package/dist/cli/finding-display.js +39 -0
  86. package/dist/cli/finding-lookup.js +92 -0
  87. package/dist/cli/finding-settings.js +5 -0
  88. package/dist/cli/fix-requests.js +42 -0
  89. package/dist/cli/intake-payload.js +19 -11
  90. package/dist/cli/main.js +9 -4
  91. package/dist/cli/mcp-failure.js +8 -0
  92. package/dist/cli/mcp.js +125 -37
  93. package/dist/cli/relay-check.js +47 -14
  94. package/dist/cli/relay-preparation.js +28 -5
  95. package/dist/cli/result-check.js +68 -0
  96. package/dist/cli/sign-in.js +61 -2
  97. package/dist/cli/summary.js +10 -3
  98. package/dist/contract/agent-choice.js +15 -0
  99. package/dist/contract/agent-proposal.js +38 -0
  100. package/dist/contract/attestation.js +106 -0
  101. package/dist/contract/cli-api-messages.js +2 -0
  102. package/dist/contract/cli-refusals.js +69 -1
  103. package/dist/contract/cli-version.js +1 -1
  104. package/dist/contract/console.js +21 -16
  105. package/dist/contract/email-sign-in-messages.js +4 -0
  106. package/dist/contract/engine.js +44 -0
  107. package/dist/contract/findings.js +28 -0
  108. package/dist/contract/fix-requests.js +30 -0
  109. package/dist/contract/messages.js +6 -94
  110. package/dist/contract/public-report.js +1 -0
  111. package/dist/contract/runtime-environment.js +12 -1
  112. package/dist/core/analysis/run-to-end.js +7 -52
  113. package/dist/gatekeeper/attestation.js +1 -1
  114. package/dist/gatekeeper/channel-binding.js +15 -0
  115. package/dist/gatekeeper/cli-api.js +28 -9
  116. package/dist/gatekeeper/connection-exchange.js +17 -12
  117. package/dist/gatekeeper/console.js +31 -17
  118. package/dist/gatekeeper/model.js +2 -1
  119. package/dist/gatekeeper/production-attestation.js +23 -17
  120. package/dist/gatekeeper/trust/policy.js +4 -1
  121. package/dist/gatekeeper/trust/schema.js +4 -0
  122. package/dist/transport/kms.js +25 -0
  123. package/package.json +4 -3
  124. package/dist/adapters/quality.js +0 -6
  125. package/dist/adapters/report.js +0 -29
  126. package/dist/analysis-core/analysis/analysis-flags.js +0 -1
  127. package/dist/analysis-core/analysis/finding-quality/config.js +0 -1
  128. package/dist/analysis-core/analysis/finding-quality/policy.js +0 -1
  129. package/dist/analysis-core/analysis/headlines/attachment.js +0 -1
  130. package/dist/analysis-core/analysis/headlines/input.js +0 -1
  131. package/dist/analysis-core/chunks/3CKW5JIF.js +0 -1
  132. package/dist/analysis-core/chunks/6YCABS5T.js +0 -1
  133. package/dist/analysis-core/chunks/7OYIBWX3.js +0 -1
  134. package/dist/analysis-core/chunks/DRGDIIFA.js +0 -1
  135. package/dist/analysis-core/chunks/FTODBFQW.js +0 -1
  136. package/dist/analysis-core/chunks/HPIAEQHE.js +0 -37
  137. package/dist/analysis-core/chunks/IVDDV6CU.js +0 -14
  138. package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
  139. package/dist/analysis-core/chunks/K5OTWBTT.js +0 -22
  140. package/dist/analysis-core/chunks/MXXNED6N.js +0 -7
  141. package/dist/analysis-core/chunks/NCWEOJVJ.js +0 -6
  142. package/dist/analysis-core/chunks/O652FQQH.js +0 -1
  143. package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
  144. package/dist/analysis-core/chunks/QV2EOS7U.js +0 -1
  145. package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
  146. package/dist/analysis-core/chunks/T2SAP7OV.js +0 -60
  147. package/dist/analysis-core/chunks/T5TREZCQ.js +0 -1
  148. package/dist/analysis-core/chunks/TDNTU4TF.js +0 -1
  149. package/dist/analysis-core/chunks/U5HOEVPE.js +0 -1
  150. package/dist/analysis-core/chunks/UEWL3GMQ.js +0 -1
  151. package/dist/analysis-core/chunks/V7BFUTM7.js +0 -1
  152. package/dist/core/analysis/finding-quality/config.js +0 -4
  153. package/dist/core/analysis/structural-run.js +0 -16
@@ -6,6 +6,11 @@ import { cliApiMessages } from "../../contract/cli-api-messages.js";
6
6
  import { ApiError } from "../api.js";
7
7
  import { resolveAgentFolder } from "../agent-folder.js";
8
8
  import { runRelayCheck } from "../relay-check.js";
9
+ import { GITIGNORE_OUTPUT_NOTICE } from "../../adapters/gitignore-output.js";
10
+ import { readFile } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+ import { loadAgentProposal } from "../../contract/agent-proposal.js";
13
+ import { formatSavedFindings } from "../finding-display.js";
9
14
  // Only a real pause says "paused"; an unreviewed release or an upload-only server cannot start.
10
15
  const unavailable = START_FAILURE_MESSAGE;
11
16
  function requireLocal(value) {
@@ -26,16 +31,35 @@ function requireLocal(value) {
26
31
  if (value.mode !== "local")
27
32
  throw new ApiError(503, "unavailable", unavailable);
28
33
  }
29
- export async function analyze(path, _options, ctx) {
34
+ export function parseAnalyzeOptions(args) {
35
+ let path, proposalFile;
36
+ for (let index = 0; index < args.length; index++) {
37
+ const arg = args[index];
38
+ if (arg === "--proposal" && proposalFile === undefined) {
39
+ const value = args[++index];
40
+ if (!value || value.startsWith("-"))
41
+ throw new Error();
42
+ proposalFile = value;
43
+ }
44
+ else if (!arg.startsWith("-") && path === undefined)
45
+ path = arg;
46
+ else
47
+ throw new Error();
48
+ }
49
+ return { path: path ?? ".", options: proposalFile === undefined ? {} : { proposalFile } };
50
+ }
51
+ export async function analyze(path, options, ctx) {
30
52
  const startup = ctx.startup;
53
+ const proposal = await loadAgentProposal(async () => options.proposalFile === undefined ? options.proposal
54
+ : JSON.parse(await readFile(resolve(ctx.cwd ?? process.cwd(), options.proposalFile), "utf8")), startup.settings);
31
55
  requireLocal(await ctx.api.request("checks/mode"));
32
56
  const development = startup.transport.CF_RELAY_DEV === "1" ? await loadDevelopmentRelay(startup.transport) : undefined;
33
57
  const release = development?.release ?? ctx.relayRuntime?.release ?? await loadInstalledRelease();
34
58
  if (release.projectContext !== "checks")
35
- throw new Error(unavailable);
59
+ throw new ApiError(503, "unavailable", unavailable);
36
60
  const token = await ctx.token?.();
37
61
  if (!token)
38
- throw new Error("Not signed in. Run counterfact-labs login.");
62
+ throw new ApiError(401, "unauthorized", cliApiMessages.unauthorized);
39
63
  const folder = resolveAgentFolder(path, ctx.cwd);
40
64
  const config = Object.freeze({ ...startup.transport, CF_RELAY_URL: startup.transport.CF_RELAY_URL ?? release.relayUrl,
41
65
  CF_LOCAL_RELAY: "1", CF_CONSOLE_URL: ctx.api.baseUrl, CF_CONSOLE_TOKEN: token });
@@ -50,19 +74,28 @@ export async function analyze(path, _options, ctx) {
50
74
  } };
51
75
  try {
52
76
  return await (ctx.relayRuntime?.run ?? runRelayCheck)(folder, {
53
- settings: startup.settings, config, release, connect: development?.connect ?? ctx.relayRuntime?.connect,
77
+ settings: startup.settings, config, release, cwd: ctx.cwd, connect: development?.connect ?? ctx.relayRuntime?.connect,
78
+ ...(proposal ? { proposal } : {}),
54
79
  signal: ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal,
55
80
  stdout() { }, stderr() { },
56
81
  onStart: () => { ctx.onCheckStart?.(); ctx.write("\r\u001b[2KAnalyzing the agent…"); progressShown = true; },
82
+ onLocalSaved: result => {
83
+ if (result.ignoreAdded) {
84
+ clearProgress();
85
+ ctx.write(GITIGNORE_OUTPUT_NOTICE);
86
+ }
87
+ },
57
88
  onError: failure => {
89
+ ctx.onCheckFailure?.(failure);
90
+ if (failure.agentChoice)
91
+ ctx.onAgentChoice?.(failure.agentChoice);
58
92
  clearProgress();
59
93
  ctx.write(`${failure.message}\n`);
60
94
  },
61
95
  onSaved: result => {
62
96
  clearProgress();
63
97
  ctx.onCheckComplete?.(result);
64
- if (result.findingIds?.length)
65
- ctx.write(`Finding ids: ${result.findingIds.join(", ")}\n`);
98
+ ctx.write(formatSavedFindings(result, { numbersEnabled: ctx.findingNumbers, audienceEnabled: result.audienceEnabled }));
66
99
  ctx.write(`Full results: ${result.url}\n`);
67
100
  },
68
101
  });
@@ -1,6 +1,9 @@
1
1
  import { money } from "../output.js";
2
- export async function balance(ctx) {
3
- const value = await ctx.api.request("balance");
4
- ctx.write(`Balance: ${money(value.balanceMicro)}\nTop up: ${value.topUpUrl}\n`);
2
+ export async function balance(ctx, options = {}) {
3
+ const response = await ctx.api.request("balance");
4
+ const value = { ...response, freeAnalysisAvailable: response.freeAnalysisAvailable ?? false };
5
+ options.onLoaded?.(value);
6
+ const freeAnalysis = value.freeAnalysisAvailable ? "Your first analysis is free.\n" : "";
7
+ ctx.write(`Balance: ${money(value.balanceMicro)}\n${freeAnalysis}Top up: ${value.topUpUrl}\n`);
5
8
  return 0;
6
9
  }
@@ -1,6 +1,12 @@
1
- export async function finding(id, ctx) {
2
- const value = await ctx.api.request(`findings/${encodeURIComponent(id)}`);
3
- ctx.write(`${value.title}\n${value.explanation}\n`);
1
+ import { loadFinding } from "../finding-lookup.js";
2
+ import { formatFindingNumber } from "../finding-display.js";
3
+ import { findingSequenceSchema } from "../../contract/findings.js";
4
+ export async function finding(id, ctx, options = {}) {
5
+ const value = await loadFinding(id, ctx.api, options.projectId, options.numbers ?? false);
6
+ const showNumber = ctx.findingNumbers !== false && findingSequenceSchema.safeParse(value.seq).success;
7
+ options.onLoaded?.(value);
8
+ const number = showNumber ? `${formatFindingNumber(value.seq)} ` : "";
9
+ ctx.write(`${number}${value.title}\n${value.explanation}\n`);
4
10
  for (const quote of value.quotes) {
5
11
  ctx.write(`\n${quote.path}${quote.line === null ? "" : `:${quote.line}`}\n`);
6
12
  if (quote.text !== null)
@@ -6,15 +6,20 @@ import { openBrowser } from "./browser.js";
6
6
  import { readCredentials } from "./credentials.js";
7
7
  import { userEmail } from "./git.js";
8
8
  import { terminalSignInInput } from "./sign-in-input.js";
9
+ import { readFindingSettings } from "./finding-settings.js";
9
10
  export function commandContext(options = {}) {
10
11
  const startup = readCliSettings(options.env, options.emailSignIn);
11
12
  const homeDir = options.homeDir ?? homedir();
12
13
  const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
13
14
  const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
14
15
  const write = options.write ?? (text => { process.stdout.write(text); });
16
+ const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
15
17
  return {
18
+ findingNumbers: findingSettings.numbersEnabled,
19
+ signInRetry: (options.env ?? process.env).CF_SIGN_IN_RETRY !== "0",
16
20
  startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
17
- token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart,
21
+ token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
22
+ onCheckFailure: options.onCheckFailure,
18
23
  api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
19
24
  signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
20
25
  sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
@@ -0,0 +1,39 @@
1
+ import { groupByAudience } from "../analysis-core/analysis/schemas.js";
2
+ export const TEAM_WORKFLOW_HEADING = "Team workflow";
3
+ export const formatFindingNumber = (seq) => `F-${seq}`;
4
+ /** Intake returns one id per payload finding in order; the optional number mapping is joined by id. */
5
+ export function savedFindings(result, report) {
6
+ const numbers = new Map(result.findings?.map(value => [value.id, value]));
7
+ return result.findingIds.map((id, index) => {
8
+ const numbered = numbers.get(id), local = report[index];
9
+ return { id, ...(numbered ? { seq: numbered.seq, number: formatFindingNumber(numbered.seq) } : {}),
10
+ title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }) };
11
+ });
12
+ }
13
+ function findingLine(value, enabled, index, labels, audienceEnabled) {
14
+ // The off switch preserves the original Markdown summary, including whitespace.
15
+ if (labels === "position" && !audienceEnabled)
16
+ return `${index + 1}. ${value.title}`;
17
+ const title = value.title.replace(/[\r\n\u2028\u2029]+/g, " ");
18
+ const label = labels === "position" ? `${index + 1}.` : enabled && value.seq !== undefined ? formatFindingNumber(value.seq) : value.id ?? `${index + 1}.`;
19
+ return `${label}${title ? ` ${title}` : ""}`;
20
+ }
21
+ export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, labels }) {
22
+ const groups = audienceEnabled ? groupByAudience(findings) : { users: findings, team: [] };
23
+ return [...groups.users, ...groups.team].map((finding, index) => ({ finding,
24
+ label: findingLine(finding, numbersEnabled, index, labels, audienceEnabled), startsTeamGroup: groups.team.length > 0 && index === groups.users.length }));
25
+ }
26
+ export function formatSavedFindings(result, options) {
27
+ if (options.numbersEnabled !== false && result.findings?.some(value => value.seq !== undefined)) {
28
+ return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("");
29
+ }
30
+ return result.findingIds?.length ? `Finding ids: ${result.findingIds.join(", ")}\n` : "";
31
+ }
32
+ export function formatWaitingFixes(items, enabled, needsProject) {
33
+ if (!items.length)
34
+ return "No waiting fixes.";
35
+ return "Waiting fixes:\n" + findingDisplayRows(items.map(item => ({ ...item, id: item.findingId })), { numbersEnabled: enabled, audienceEnabled: false, labels: "reference" }).map(({ finding: item, label }) => {
36
+ const scope = [item.repoKey, ...(enabled && needsProject(item) ? [`project ${item.projectId}`] : [])].filter(Boolean);
37
+ return `${label}${scope.length ? ` (${scope.join("; ")})` : ""}\n`;
38
+ }).join("");
39
+ }
@@ -0,0 +1,92 @@
1
+ import { canonicalUuidSchema, findingNumberParamsSchema, findingNumberResponseSchema, findingSequenceSchema } from "../contract/findings.js";
2
+ import { cliApiMessages } from "../contract/cli-api-messages.js";
3
+ export const FINDING_PROJECT_MESSAGE = "Give the project for this finding number, which analysis results and the waiting_fixes tool list.";
4
+ export const numberedFindingInputSchema = { type: "object", properties: {
5
+ id: { type: "string", description: "A finding id, or a number such as F-10." },
6
+ projectId: { type: "string", description: "The project the finding number belongs to, needed unless this session knows only one project." },
7
+ }, required: ["id"], additionalProperties: false };
8
+ export function findingNumber(value) {
9
+ const match = /^F-?([1-9][0-9]*)$/i.exec(value.trim());
10
+ const seq = match ? Number(match[1]) : undefined;
11
+ return seq !== undefined && Number.isSafeInteger(seq) ? seq : undefined;
12
+ }
13
+ export class FindingReferences {
14
+ projects = new Set();
15
+ analysedProjects = new Set();
16
+ remember(projectId, source = "analysis") {
17
+ const canonical = projectId.toLowerCase();
18
+ this.projects.add(canonical);
19
+ if (source === "analysis")
20
+ this.analysedProjects.add(canonical);
21
+ }
22
+ // Observed findings cannot prove another project lacks the same historical number.
23
+ project() { return this.projects.size === 1 ? [...this.projects][0] : undefined; }
24
+ async resolve(reference, projectId, readWaiting) {
25
+ const seq = findingNumber(reference);
26
+ if (projectId || seq === undefined)
27
+ return projectId;
28
+ let waiting = [];
29
+ try {
30
+ waiting = await readWaiting?.() ?? [];
31
+ }
32
+ catch { /* A known project can still resolve a finding when the queue is unavailable. */ }
33
+ return this.resolveNumber(this.waitingByNumber(waiting).get(seq) ?? []);
34
+ }
35
+ projectsNeeded(waiting) {
36
+ const needed = new Set();
37
+ for (const matches of this.waitingByNumber(waiting).values()) {
38
+ const project = this.resolveNumber(matches);
39
+ for (const item of matches)
40
+ if (project !== item.projectId.toLowerCase())
41
+ needed.add(item.findingId);
42
+ }
43
+ return needed;
44
+ }
45
+ waitingByNumber(waiting) {
46
+ const numbers = new Map();
47
+ for (const item of waiting) {
48
+ this.remember(item.projectId, "waiting");
49
+ const matches = numbers.get(item.seq);
50
+ if (matches)
51
+ matches.push(item);
52
+ else
53
+ numbers.set(item.seq, [item]);
54
+ }
55
+ return numbers;
56
+ }
57
+ resolveNumber(matches) {
58
+ if (!matches.length && this.analysedProjects.size === 1)
59
+ return [...this.analysedProjects][0];
60
+ if (matches.length !== 1)
61
+ return this.project();
62
+ const candidate = matches[0].projectId.toLowerCase();
63
+ // A queued fix may be older than the project the user just analysed.
64
+ if (this.analysedProjects.size > 0 && (this.analysedProjects.size !== 1 || !this.analysedProjects.has(candidate)))
65
+ return undefined;
66
+ return candidate;
67
+ }
68
+ }
69
+ export async function loadFinding(reference, api, projectId, numbered = true) {
70
+ const seq = numbered ? findingNumber(reference) : undefined;
71
+ let id = reference;
72
+ if (seq !== undefined) {
73
+ if (!projectId)
74
+ throw new Error(FINDING_PROJECT_MESSAGE);
75
+ const params = findingNumberParamsSchema.safeParse({ projectId, seq: String(seq) });
76
+ if (!params.success)
77
+ throw new Error(FINDING_PROJECT_MESSAGE);
78
+ const result = findingNumberResponseSchema.safeParse(await api.request(`projects/${params.data.projectId}/findings/${params.data.seq}`));
79
+ if (!result.success || result.data.seq !== seq)
80
+ throw new Error(cliApiMessages.invalid_response);
81
+ id = result.data.id;
82
+ }
83
+ const value = await api.request(`findings/${encodeURIComponent(id)}`);
84
+ const canonicalId = canonicalUuidSchema.safeParse(value.id);
85
+ if (seq !== undefined && (!canonicalId.success || canonicalId.data !== id || value.seq !== undefined && value.seq !== seq)) {
86
+ throw new Error(cliApiMessages.invalid_response);
87
+ }
88
+ if (seq !== undefined)
89
+ return { ...value, id, seq };
90
+ const { seq: detailSeq, ...detail } = value;
91
+ return numbered && detailSeq !== undefined && !findingSequenceSchema.safeParse(detailSeq).success ? detail : value;
92
+ }
@@ -0,0 +1,5 @@
1
+ import { findingAudienceEnabled } from "../analysis-core/analysis/finding-audience-switch.js";
2
+ export function readFindingSettings(env = process.env, bundle) {
3
+ return { numbersEnabled: env.CF_FINDING_NUMBERS !== "0", fixesEnabled: env.CF_WAITING_FIXES !== "0",
4
+ audienceEnabled: findingAudienceEnabled(env, bundle) };
5
+ }
@@ -0,0 +1,42 @@
1
+ import { waitingFixesResponseSchema, claimFixRequestParamsSchema, claimFixRequestResponseSchema } from "../contract/fix-requests.js";
2
+ import { cliApiMessages } from "../contract/cli-api-messages.js";
3
+ import { ApiError } from "./api.js";
4
+ export const FIX_DISCOVERY_TIMEOUT_MS = 2_000;
5
+ export const fixRequestTools = [
6
+ { name: "waiting_fixes", description: "Lists the fixes you asked for in the console that are still waiting. It is read-only and free.",
7
+ annotations: { readOnlyHint: true },
8
+ inputSchema: { type: "object", properties: {}, additionalProperties: false } },
9
+ { name: "claim_fix", description: "Marks a waiting fix as taken when you start it, without editing files or running an analysis.",
10
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "The findingId returned by waiting_fixes." } }, required: ["id"], additionalProperties: false } },
11
+ ];
12
+ export async function waitingFixes(api, options = {}) {
13
+ try {
14
+ const result = waitingFixesResponseSchema.safeParse(await api.request("fix-requests", undefined, true, options));
15
+ if (!result.success)
16
+ throw new Error(cliApiMessages.invalid_response);
17
+ return result.data;
18
+ }
19
+ catch (error) {
20
+ // An older console has no queue; read-only discovery still works during rollout.
21
+ if (error instanceof ApiError && error.status === 404)
22
+ return [];
23
+ throw error;
24
+ }
25
+ }
26
+ export async function waitingFixCount(api) {
27
+ try {
28
+ return (await waitingFixes(api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS })).length;
29
+ }
30
+ catch {
31
+ return 0;
32
+ }
33
+ }
34
+ export async function claimFix(api, id) {
35
+ const params = claimFixRequestParamsSchema.safeParse({ id });
36
+ if (!params.success)
37
+ throw new Error("Provide a finding id.");
38
+ const result = claimFixRequestResponseSchema.safeParse(await api.request(`fix-requests/${params.data.id}/claim`, {}));
39
+ if (!result.success || result.data.findingId !== params.data.id)
40
+ throw new Error(cliApiMessages.invalid_response);
41
+ return result.data;
42
+ }
@@ -1,18 +1,26 @@
1
+ import { resultSafetyCheckEnabled } from "../analysis-core/settings.js";
1
2
  import { localFindingsPayloadSchema } from "../contract/console.js";
3
+ import { projectPublicFinding } from "../contract/public-report.js";
4
+ import { checkIntakeFindings } from "./result-check.js";
2
5
  import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
3
- export function buildIntakePayload(identity, agent, report) {
6
+ export function buildIntakePayload(identity, agent, report, options = {}) {
4
7
  const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
5
- const findings = report.findings.map(finding => {
6
- // The local report may add presentation fields; the intake accepts cleaned evidence only.
7
- const { kind, severity, title, cause, summary, category, question, site, excerpt, otherSites, fixes, sourceNotes } = finding;
8
- return { kind, severity, title, cause, summary, category, question, site, excerpt,
9
- fixes: fixes.map(({ title, approach, kind, targetFiles, expectedOutcome, summary, impact, confidence, objective, status, blockedBy, alsoFixes, delta }) => ({ title, approach, kind, targetFiles, expectedOutcome, summary, impact, confidence, objective, status, blockedBy, alsoFixes, delta })),
10
- siteRepo: repo(finding.siteRepo), otherSites: otherSites.map(site => ({ ...site, repo: repo(site.repo) })),
11
- ...(sourceNotes === undefined ? {} : { sourceNotes: sourceNotes && { ...sourceNotes,
12
- counterEvidence: sourceNotes.counterEvidence.map(site => ({ ...site, repo: repo(site.repo) })) } }) };
13
- });
8
+ let findings = report.findings.map(finding => projectPublicFinding(finding, repo));
9
+ let resultCheck = null;
10
+ let findingIndexes = report.findings.map((_, index) => index);
11
+ if (resultSafetyCheckEnabled(options.settings ?? {})) {
12
+ const captured = options.files;
13
+ if (!captured)
14
+ throw new Error("Result check requires a captured file snapshot");
15
+ const admitted = Object.fromEntries(agent.files.filter(file => Object.hasOwn(captured, file.path))
16
+ .map(file => [file.path, captured[file.path]]));
17
+ const { findings: accepted, acceptedIndexes, ...counts } = checkIntakeFindings(findings, { repoKey: agent.repository, files: admitted });
18
+ findings = accepted;
19
+ resultCheck = counts;
20
+ findingIndexes = acceptedIndexes;
21
+ }
14
22
  const result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings });
15
23
  if (!result.success)
16
24
  throw new Error(RESULTS_TOO_LARGE_MESSAGE);
17
- return result.data;
25
+ return { payload: result.data, resultCheck, findingIndexes };
18
26
  }
package/dist/cli/main.js CHANGED
@@ -4,13 +4,13 @@ import { realpath } from "node:fs/promises";
4
4
  import { errorCode, errorText } from "./api.js";
5
5
  import { readEmailSignIn } from "../adapters/settings.js";
6
6
  import { commandContext } from "./context.js";
7
- import { analyze } from "./commands/analyze.js";
7
+ import { analyze, parseAnalyzeOptions } from "./commands/analyze.js";
8
8
  import { finding } from "./commands/finding.js";
9
9
  import { balance } from "./commands/balance.js";
10
10
  import { login, parseLoginOptions } from "./commands/login.js";
11
11
  import { logout } from "./commands/logout.js";
12
12
  import { serveMcp } from "./mcp.js";
13
- const usage = "Usage: counterfact-labs analyze [path]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
13
+ const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
14
14
  export async function run(args, options = {}) {
15
15
  const write = options.write ?? (text => { process.stdout.write(text); });
16
16
  const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
@@ -22,9 +22,14 @@ export async function run(args, options = {}) {
22
22
  return 0;
23
23
  }
24
24
  if (command === "analyze") {
25
- if (rest.length > 1 || rest.some(arg => arg.startsWith("-")))
25
+ let parsed;
26
+ try {
27
+ parsed = parseAnalyzeOptions(rest);
28
+ }
29
+ catch {
26
30
  throw new Error(usage.trimEnd());
27
- return await analyze(rest[0] ?? ".", {}, commandContext(commandOptions));
31
+ }
32
+ return await analyze(parsed.path, parsed.options, commandContext(commandOptions));
28
33
  }
29
34
  if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
30
35
  return await finding(rest[0], commandContext(commandOptions));
@@ -0,0 +1,8 @@
1
+ import { CliRefusalError, failureReason, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
2
+ import { errorText } from "./api.js";
3
+ export function mcpFailure(error, enabled) {
4
+ // Only outside_project changed its sentence; the other local refusals already use the old sentences.
5
+ const message = !enabled && error instanceof CliRefusalError && error.code === "outside_project" ? START_FAILURE_MESSAGE
6
+ : errorText(error);
7
+ return { message, ...(enabled ? { reason: failureReason(error) } : {}) };
8
+ }