@counterfact-labs/cli 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { join, relative, sep } from "node:path";
|
|
3
|
-
import { outsideLoadersEnabled } from "../analysis-core/settings.js";
|
|
4
|
+
import { outsideLoadersEnabled, folderPartsEnabled } from "../analysis-core/settings.js";
|
|
5
|
+
import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
|
|
4
6
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
5
7
|
import { readLocalFolder } from "../adapters/files.js";
|
|
6
8
|
import { snapshotProvenance, sameFileHashes } from "../adapters/snapshot.js";
|
|
7
9
|
import { validateClientRelease } from "../adapters/release.js";
|
|
8
|
-
import { prepareRelayPlan } from "../adapters/relay-plan.js";
|
|
10
|
+
import { prepareRelayPlan } from "../adapters/source-relay-plan.js";
|
|
11
|
+
import { readAnalysisSource } from "../adapters/selection.js";
|
|
9
12
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
10
13
|
import { COMMITTED_FILES_MESSAGE } from "../contract/cli-refusals.js";
|
|
11
14
|
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
15
|
+
import { selectionStartIdentity } from "../contract/selection-metadata.js";
|
|
12
16
|
import { repositoryFacts } from "./git.js";
|
|
17
|
+
import { proposalWhollyRefused } from "../analysis-core/index.js";
|
|
13
18
|
/** Absolute checkout paths stay local; repository facts own the start's commit. */
|
|
14
19
|
export function buildRelayStartPayload(facts, files) {
|
|
15
20
|
const { remoteUrl, relPath, folderName, branch, commit, dirty } = facts;
|
|
@@ -18,39 +23,85 @@ export function buildRelayStartPayload(facts, files) {
|
|
|
18
23
|
export function committedFilesOnly(settings) {
|
|
19
24
|
return settings.CF_LOCAL_COMMITTED_ONLY === "1";
|
|
20
25
|
}
|
|
26
|
+
/** Every local choice uses the same canonical launch folder and source coordinates. */
|
|
27
|
+
async function launchChoice(choice, folder, sourceBase, cwd) {
|
|
28
|
+
const launch = await resolveFolderPath(cwd), base = sourceBase ?? await resolveFolderPath(folder);
|
|
29
|
+
const launchPath = (path) => relative(launch, join(base, path)).split(sep).join("/") || ".";
|
|
30
|
+
return { ...choice, candidates: choice.candidates.map(candidate => ({ ...candidate,
|
|
31
|
+
folder: launchPath(candidate.folder), reason: launchPath(candidate.reason),
|
|
32
|
+
})) };
|
|
33
|
+
}
|
|
34
|
+
/** Every part must run alone; cost refusals also need a smaller normal call plan. */
|
|
35
|
+
export async function prepareFolderChoice(folder, options) {
|
|
36
|
+
const { settings, signal } = options;
|
|
37
|
+
if (!folderPartsEnabled(settings) || fileScopeOf(settings) !== undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (options.limit === "plan_over_limit" && !options.refusedCalls)
|
|
40
|
+
return undefined;
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
const read = await readLocalFolder(settings, folder, options.trialAdmission, { planParts: true, captureSnapshot: true });
|
|
43
|
+
const count = (calls) => calls.reduce((sum, call) => sum + call.calls, 0);
|
|
44
|
+
const refusedCount = count(options.refusedCalls ?? []);
|
|
45
|
+
const candidates = [];
|
|
46
|
+
// Finish each validation before starting another walk, so ten choices bound the extra reads.
|
|
47
|
+
for (const part of read.analysisParts ?? []) {
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
try {
|
|
50
|
+
const scopedSettings = { ...settings, CF_PART_FILES: JSON.stringify(part.files) };
|
|
51
|
+
const source = await readLocalFolder(scopedSettings, folder, options.trialAdmission, { captureSnapshot: true });
|
|
52
|
+
if (source.agentChoice || part.files.some(path => !Object.hasOwn(source.files, path)))
|
|
53
|
+
continue;
|
|
54
|
+
if (options.limit === "plan_over_limit" && count(localSourcePlan(scopedSettings, source).calls) >= refusedCount)
|
|
55
|
+
continue;
|
|
56
|
+
candidates.push({ folder: ".", ...part });
|
|
57
|
+
if (candidates.length === 10)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
return candidates.length ? launchChoice({ kind: "choose_scope", candidates }, folder, read.sourceBase, options.cwd ?? process.cwd()) : undefined;
|
|
66
|
+
}
|
|
21
67
|
/** Recovery can reuse signature validation without reading the folder again. */
|
|
22
68
|
export async function prepareRelayCheck(folder, options, validated = validateClientRelease(options.release, options.settings)) {
|
|
23
|
-
const { bundle
|
|
69
|
+
const { bundle } = validated;
|
|
24
70
|
options.signal?.throwIfAborted();
|
|
25
71
|
// Discovery must agree with Git before publishing repository-relative captures.
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
const selection = options.selection;
|
|
73
|
+
const factsFolder = selection?.factsFolder ?? folder;
|
|
74
|
+
let earlyFacts;
|
|
75
|
+
const { source: read, membership } = await readAnalysisSource(options.settings, folder, {
|
|
76
|
+
selection, provider: options.selectionProvider, proposal: options.proposal, admission: options.trialAdmission, signal: options.signal,
|
|
77
|
+
readFolder: async () => {
|
|
78
|
+
if (outsideLoadersEnabled(options.settings))
|
|
79
|
+
earlyFacts = await (options.facts ?? repositoryFacts)(factsFolder);
|
|
80
|
+
options.signal?.throwIfAborted();
|
|
81
|
+
return readLocalFolder(options.settings, folder, options.trialAdmission, { captureSnapshot: true, proposal: options.proposal,
|
|
82
|
+
...(earlyFacts ? { expectedRepositoryRoot: earlyFacts.repoRoot ?? null } : {}) });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (proposalWhollyRefused(read.proposalCounts))
|
|
32
86
|
throw new Error(INVALID_PROPOSAL_MESSAGE);
|
|
33
87
|
if (read.agentChoice) {
|
|
34
|
-
|
|
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
|
-
});
|
|
88
|
+
throw new AgentChoiceError(await launchChoice(read.agentChoice, folder, read.sourceBase, options.cwd ?? process.cwd()));
|
|
41
89
|
}
|
|
42
90
|
const source = { ...read, files: Object.freeze({ ...read.files }) };
|
|
43
|
-
const provenance = await snapshotProvenance(source.sourceBase ?? folder, source.snapshot, { selectionFolder:
|
|
91
|
+
const provenance = await snapshotProvenance(selection?.kind === "file" ? selection.launchRoot : source.sourceBase ?? folder, source.snapshot, { selectionFolder: factsFolder, committedOnly: committedFilesOnly(options.settings) });
|
|
44
92
|
if (options.expectedCommit === null) {
|
|
45
93
|
if (!sameFileHashes(options.expectedFiles ?? [], source.snapshot))
|
|
46
94
|
throw new Error(COMMITTED_FILES_MESSAGE);
|
|
47
95
|
}
|
|
48
96
|
else if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
|
|
49
97
|
throw new Error(COMMITTED_FILES_MESSAGE);
|
|
50
|
-
const { plan, files } = prepareRelayPlan({ bundle,
|
|
98
|
+
const { plan, files } = prepareRelayPlan({ bundle, settings: validated.settings, relayVersion: options.release.relayVersion, source });
|
|
51
99
|
options.signal?.throwIfAborted();
|
|
52
|
-
const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(
|
|
100
|
+
const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(factsFolder);
|
|
53
101
|
options.signal?.throwIfAborted();
|
|
54
|
-
const startMetadata = { analysisSize: source.analysisSize, startAttempt: randomUUID()
|
|
55
|
-
|
|
102
|
+
const startMetadata = { analysisSize: source.analysisSize, startAttempt: randomUUID(),
|
|
103
|
+
...(selection?.kind === "file" && membership
|
|
104
|
+
? { selection: selectionStartIdentity(selection.selectionKey, membership.snapshot) } : {}) };
|
|
105
|
+
return { source, provenance, plan, start: buildRelayStartPayload(source.sourceBase && selection?.kind !== "file" ? { ...facts, relPath: "" } : facts, files), startMetadata,
|
|
106
|
+
...(membership ? { membership } : {}) };
|
|
56
107
|
}
|
package/dist/cli/result-check.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { publicFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/
|
|
1
|
+
import { publicFinding, intakeFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/intake-finding.js";
|
|
2
2
|
import { findingSitesInOrder } from "../analysis-core/findings/sites.js";
|
|
3
|
-
import { localFindingSchema } from "../contract/console.js";
|
|
4
3
|
function evidenceQuotes(finding, repoKey) {
|
|
5
4
|
const primary = finding.site ? { repo: finding.siteRepo ?? repoKey, ...finding.site, excerpt: finding.excerpt } : null;
|
|
6
5
|
const sites = findingSitesInOrder(primary, finding.otherSites);
|
|
@@ -36,27 +35,47 @@ export function checkIntakeFindings(findings, context, onInvalidQuote) {
|
|
|
36
35
|
return originals.get(path);
|
|
37
36
|
}
|
|
38
37
|
let quotes = 0, droppedQuotes = 0, invalidQuotes = 0, schemaDroppedFindings = 0, capDroppedFindings = 0;
|
|
38
|
+
function invalidQuote(quote) {
|
|
39
|
+
const source = quote.repo === context.repoKey && typeof quote.path === "string" && Object.hasOwn(context.files, quote.path)
|
|
40
|
+
? original(quote.path) : null;
|
|
41
|
+
const invalid = !quote.text.length || source === null || !source.includes(quote.text);
|
|
42
|
+
if (invalid)
|
|
43
|
+
onInvalidQuote?.({ text: quote.text, source });
|
|
44
|
+
return invalid;
|
|
45
|
+
}
|
|
39
46
|
findings.forEach((finding, index) => {
|
|
40
47
|
const parsed = publicFinding.safeParse(finding);
|
|
48
|
+
let activityQuoteCount = 0;
|
|
49
|
+
if (parsed.success && parsed.data.activity) {
|
|
50
|
+
const activityQuotes = parsed.data.activity.evidence.map(site => ({ repo: parsed.data.siteRepo ?? context.repoKey, path: site.path, text: site.excerpt }));
|
|
51
|
+
// Optional activity cannot weaken the original-byte evidence boundary or cost a substantive finding.
|
|
52
|
+
activityQuoteCount = activityQuotes.length;
|
|
53
|
+
quotes += activityQuoteCount;
|
|
54
|
+
const invalid = activityQuotes.filter(invalidQuote).length;
|
|
55
|
+
invalidQuotes += invalid;
|
|
56
|
+
if (invalid)
|
|
57
|
+
delete parsed.data.activity;
|
|
58
|
+
if (parsed.data.activity && (!resultCheckFinding.safeParse(parsed.data).success || !intakeFinding.safeParse(parsed.data).success)) {
|
|
59
|
+
const substantive = { ...parsed.data };
|
|
60
|
+
delete substantive.activity;
|
|
61
|
+
if (resultCheckFinding.safeParse(substantive).success && intakeFinding.safeParse(substantive).success)
|
|
62
|
+
delete parsed.data.activity;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (parsed.success && !parsed.data.activity)
|
|
66
|
+
droppedQuotes += activityQuoteCount;
|
|
41
67
|
const evidence = parsed.success ? evidenceQuotes(parsed.data, context.repoKey) : [];
|
|
42
68
|
quotes += evidence.length;
|
|
43
|
-
const invalid = evidence.filter(
|
|
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;
|
|
69
|
+
const invalid = evidence.filter(invalidQuote).length;
|
|
51
70
|
invalidQuotes += invalid;
|
|
52
|
-
const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success &&
|
|
71
|
+
const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success && intakeFinding.safeParse(parsed.data).success;
|
|
53
72
|
if (!schemaValid)
|
|
54
73
|
schemaDroppedFindings++;
|
|
55
74
|
if (index >= RESULT_CHECK_LIMITS.findings)
|
|
56
75
|
capDroppedFindings++;
|
|
57
76
|
if (!schemaValid || !parsed.success || invalid || index >= RESULT_CHECK_LIMITS.findings) {
|
|
58
77
|
// Removing one supporting quote could change the claim or invalidate source-note indices.
|
|
59
|
-
droppedQuotes += evidence.length;
|
|
78
|
+
droppedQuotes += evidence.length + (parsed.success && parsed.data.activity ? activityQuoteCount : 0);
|
|
60
79
|
}
|
|
61
80
|
else {
|
|
62
81
|
accepted.push(parsed.data);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { ConsoleError } from "../gatekeeper/console.js";
|
|
3
|
+
import { completionRecoveryDecision, completionRetryDeadline, observedCompletionTiming, matchesCheck } from "./completion-recovery.js";
|
|
4
|
+
export function requireIntakeReservation(status) {
|
|
5
|
+
if (status.state === "reserved")
|
|
6
|
+
return;
|
|
7
|
+
if (status.state === "closing")
|
|
8
|
+
throw new ConsoleError("reservation_closing");
|
|
9
|
+
if (status.state === "closed")
|
|
10
|
+
throw new ConsoleError("reservation_closed");
|
|
11
|
+
throw new ConsoleError("reservation_expired", "intake");
|
|
12
|
+
}
|
|
13
|
+
/** Carry the owner's answer so cleanup cannot turn a definite refusal into an endless retry. */
|
|
14
|
+
export class ResultsUploadError extends ConsoleError {
|
|
15
|
+
decision;
|
|
16
|
+
timing;
|
|
17
|
+
outcome;
|
|
18
|
+
constructor(error, decision, timing, httpStatus = error instanceof ConsoleError ? error.httpStatus : undefined) {
|
|
19
|
+
super(error instanceof ConsoleError ? error.code : "unavailable", "intake", error instanceof ConsoleError ? { localReason: error.localReason, responseCode: error.responseCode, httpStatus: error.httpStatus } : {});
|
|
20
|
+
this.decision = decision;
|
|
21
|
+
this.timing = timing;
|
|
22
|
+
this.outcome = { kind: "decided", decision, ...(httpStatus === undefined ? {} : { httpStatus }) };
|
|
23
|
+
if (error instanceof Error)
|
|
24
|
+
this.name = error.name;
|
|
25
|
+
if (error instanceof ConsoleError)
|
|
26
|
+
this.message = error.message;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const uploadFailure = (error) => {
|
|
30
|
+
if (!(error instanceof ConsoleError))
|
|
31
|
+
return { kind: "refused", code: "unavailable", transportFailure: true };
|
|
32
|
+
return { kind: "refused", code: error.responseCode ?? error.code, httpStatus: error.httpStatus };
|
|
33
|
+
};
|
|
34
|
+
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId,
|
|
35
|
+
analysisId: value.analysisId, findingIds: value.findingIds,
|
|
36
|
+
...(value.findings ? { findings: value.findings } : {}), settlement: value.settlement });
|
|
37
|
+
/** Status confirms committed identity; intake owns whether a completed payload can still be saved. */
|
|
38
|
+
export async function submitResults(client, payload, signal, options) {
|
|
39
|
+
const now = options.now ?? Date.now;
|
|
40
|
+
const wait = options.wait ?? ((ms, signal) => sleep(ms, undefined, { signal }));
|
|
41
|
+
let timing = { ...options.timing, completedAt: options.timing?.completedAt ?? now(), retryStartedAt: now() };
|
|
42
|
+
let lookup = { kind: "skipped" };
|
|
43
|
+
let outcome = { kind: options.retry && options.alreadySaved ? "saved" : "pending" };
|
|
44
|
+
const uploadHttpStatus = () => "httpStatus" in outcome ? outcome.httpStatus : undefined;
|
|
45
|
+
const failure = (error, decision) => new ResultsUploadError(error, decision, timing, uploadHttpStatus());
|
|
46
|
+
const deadline = () => completionRetryDeadline(timing, lookup, payload, outcome);
|
|
47
|
+
const receipt = (value) => value && matchesCheck(value, payload) && value.uploadState === "saved"
|
|
48
|
+
? committed(value) : { checkId: payload.checkId, projectId: payload.projectId };
|
|
49
|
+
const decide = () => completionRecoveryDecision({ retryEnabled: options.retry, payload, statusLookup: lookup, uploadOutcome: outcome, timing, now: now() });
|
|
50
|
+
const status = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const value = await client.status(payload.reservationId);
|
|
53
|
+
lookup = { kind: "found", status: value };
|
|
54
|
+
timing = observedCompletionTiming(timing, lookup, payload);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
lookup = { ...uploadFailure(error), kind: "failed" };
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
const before = await status().catch(error => {
|
|
65
|
+
const decision = decide();
|
|
66
|
+
if (options.retry && decision.action === "saved")
|
|
67
|
+
return undefined;
|
|
68
|
+
if (!options.retry)
|
|
69
|
+
throw error;
|
|
70
|
+
return undefined;
|
|
71
|
+
});
|
|
72
|
+
const initial = decide();
|
|
73
|
+
if (initial.action === "saved")
|
|
74
|
+
return receipt(before);
|
|
75
|
+
if (options.retry && initial.action === "give-up")
|
|
76
|
+
throw failure(new ConsoleError(initial.code === "check_conflict" ? "check_conflict" : initial.code === "reservation_expired" ? "reservation_expired" : "reservation_closed", "intake"), initial);
|
|
77
|
+
if (before && !options.retry) {
|
|
78
|
+
if (!matchesCheck(before, payload))
|
|
79
|
+
throw new ConsoleError("check_conflict");
|
|
80
|
+
requireIntakeReservation(before);
|
|
81
|
+
}
|
|
82
|
+
if (options.retry && now() >= deadline())
|
|
83
|
+
throw failure(new ConsoleError(), initial);
|
|
84
|
+
let delay = 1000;
|
|
85
|
+
for (;;) {
|
|
86
|
+
signal.throwIfAborted();
|
|
87
|
+
try {
|
|
88
|
+
const saved = await client.intake(payload, { acceptAnySuccess: options.retry });
|
|
89
|
+
outcome = { kind: "saved" };
|
|
90
|
+
decide();
|
|
91
|
+
return saved;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
outcome = uploadFailure(error);
|
|
95
|
+
// A lost response may have committed; confirm it before retrying the same payload.
|
|
96
|
+
const after = await status().catch(() => undefined);
|
|
97
|
+
const decision = decide();
|
|
98
|
+
if (decision.action === "saved")
|
|
99
|
+
return receipt(after);
|
|
100
|
+
if (!options.retry) {
|
|
101
|
+
if (after && !matchesCheck(after, payload))
|
|
102
|
+
throw new ConsoleError("check_conflict");
|
|
103
|
+
if (error instanceof ConsoleError && error.code === "unavailable" && after) {
|
|
104
|
+
requireIntakeReservation(after);
|
|
105
|
+
signal.throwIfAborted();
|
|
106
|
+
return await client.intake(payload, { acceptAnySuccess: options.retry });
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
const remaining = deadline() - now();
|
|
111
|
+
if (decision.action !== "retry" || !decision.automatic || remaining <= 0)
|
|
112
|
+
throw failure(error, decision);
|
|
113
|
+
await wait(Math.min(delay, remaining), signal);
|
|
114
|
+
signal.throwIfAborted();
|
|
115
|
+
if (now() >= deadline())
|
|
116
|
+
throw failure(error, decide());
|
|
117
|
+
delay = Math.min(delay * 2, 30_000);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof ResultsUploadError)
|
|
123
|
+
throw error;
|
|
124
|
+
if (signal.aborted && outcome.kind !== "saved")
|
|
125
|
+
outcome = { kind: "cancelled", httpStatus: uploadHttpStatus() };
|
|
126
|
+
const decision = decide();
|
|
127
|
+
if (decision.action === "saved")
|
|
128
|
+
return receipt();
|
|
129
|
+
throw failure(error, decision);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
2
|
+
import { formatSavedFindings } from "./finding-display.js";
|
|
3
|
+
import { skippedResultText } from "./skipped-files.js";
|
|
4
|
+
/** Terminal and grouped follow-ups use the same final result presentation. */
|
|
5
|
+
export function formatSavedResults(result, settings, numbersEnabled, offer = true, options = {}) {
|
|
6
|
+
return formatSavedFindings(result, { numbersEnabled, audienceEnabled: result.audienceEnabled, notActiveEnabled: result.notActiveEnabled,
|
|
7
|
+
agentReadableResults: options.agentReadableResults ?? readFindingSettings(settings).agentReadableResults, projectId: result.projectId, surface: options.surface })
|
|
8
|
+
+ skippedResultText(result.skippedFiles ?? [], settings, { offer, surface: options.surface, folder: options.analyzedFolder }) + `Full results: ${result.url}\n`
|
|
9
|
+
+ (result.architectureUrl ? `Architecture: ${result.architectureUrl}\n` : "");
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdir, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { outputPaths, writeOutputFiles } from "../adapters/output-paths.js";
|
|
4
|
+
import { analysisScopeSchema, chooseScopeResultSchema, INCLUDED_FILES_CAPTION, OMITTED_IMPORTS_CAPTION, OMITTED_FILES_CAPTION, SCOPE_CHOICE_INSTRUCTION, INCOMPLETE_IMPORTS_MESSAGE, IMPORT_DISCOVERY_LIMIT_MESSAGE, INVALID_SCOPE_MESSAGE, SCOPE_RETRY_TEMPLATE } from "../contract/scope-choice.js";
|
|
5
|
+
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
6
|
+
import { scopeChoicesEnabled } from "../adapters/settings.js";
|
|
7
|
+
export async function loadAnalysisScope(options, cwd, settings) {
|
|
8
|
+
if (options.scope === undefined && options.scopeFile === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (!scopeChoicesEnabled(settings))
|
|
11
|
+
throw new Error(INVALID_SCOPE_MESSAGE);
|
|
12
|
+
try {
|
|
13
|
+
const value = options.scopeFile === undefined ? options.scope : JSON.parse(await readFile(resolve(cwd, options.scopeFile), "utf8"));
|
|
14
|
+
return analysisScopeSchema.parse(value);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(INVALID_SCOPE_MESSAGE);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function shellQuote(value) { return `'${value.replaceAll("'", `'"'"'`)}'`; }
|
|
21
|
+
/** MCP and terminal output share the same file coverage; only the terminal adds retry commands. */
|
|
22
|
+
export function scopeChoiceText(value, retryPaths = {}) {
|
|
23
|
+
const result = chooseScopeResultSchema.parse(value);
|
|
24
|
+
const refusal = scopeChoiceRefusal(result);
|
|
25
|
+
if (refusal)
|
|
26
|
+
return refusal + "\n";
|
|
27
|
+
const lines = [result.text];
|
|
28
|
+
if (!result.coverageComplete && result.refusedEdges.length > 0)
|
|
29
|
+
lines.push(INCOMPLETE_IMPORTS_MESSAGE);
|
|
30
|
+
if (!result.coverageComplete && result.frontierCount > 0)
|
|
31
|
+
lines.push(IMPORT_DISCOVERY_LIMIT_MESSAGE);
|
|
32
|
+
result.choices.forEach((choice, index) => {
|
|
33
|
+
lines.push(`${index + 1}. ${choice.label}`, choice.description, INCLUDED_FILES_CAPTION, ...choice.includes.map(path => ` ${path}`));
|
|
34
|
+
if (choice.omitted.length > 0)
|
|
35
|
+
lines.push(choice.scope.entries ? OMITTED_FILES_CAPTION : OMITTED_IMPORTS_CAPTION, ...choice.omitted.map(path => ` ${path}`));
|
|
36
|
+
if (choice.fits !== false && retryPaths[index]) {
|
|
37
|
+
const entry = choice.scope.entry.startsWith("-") ? `./${choice.scope.entry}` : choice.scope.entry;
|
|
38
|
+
lines.push(SCOPE_RETRY_TEMPLATE.replace("{quoted-entry}", () => shellQuote(entry))
|
|
39
|
+
.replace("{quoted-scope-file}", () => shellQuote(retryPaths[index])));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
lines.push(SCOPE_CHOICE_INSTRUCTION);
|
|
43
|
+
return lines.join("\n") + "\n";
|
|
44
|
+
}
|
|
45
|
+
export async function renderScopeChoices(value, anchor) {
|
|
46
|
+
const result = chooseScopeResultSchema.parse(value);
|
|
47
|
+
// Validate the output directory and old entries before removing only generated scope files.
|
|
48
|
+
const [probe] = await outputPaths(anchor.outputAnchor, ["scope-1.json"], anchor.outputDir, true);
|
|
49
|
+
const old = (await readdir(dirname(probe))).filter(name => /^scope-.*\.json$/.test(name));
|
|
50
|
+
await outputPaths(anchor.outputAnchor, old, anchor.outputDir);
|
|
51
|
+
for (const name of old)
|
|
52
|
+
await unlink(join(dirname(probe), name));
|
|
53
|
+
const runnable = result.choices.flatMap((choice, index) => choice.fits === false ? [] : [{ choice, index }]);
|
|
54
|
+
const files = Object.fromEntries(runnable.map(({ choice, index }) => [`scope-${index + 1}.json`, JSON.stringify(choice.scope, null, 2) + "\n"]));
|
|
55
|
+
const paths = await writeOutputFiles(anchor.outputAnchor, files, anchor.outputDir);
|
|
56
|
+
return scopeChoiceText(result, Object.fromEntries(runnable.map(({ index }, position) => [index, paths[position]])));
|
|
57
|
+
}
|
package/dist/cli/sign-in.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiError } from "./api.js";
|
|
1
|
+
import { ApiError, SavedSignInError } from "./api.js";
|
|
2
2
|
import { readCredentials, saveCredentials } from "./credentials.js";
|
|
3
3
|
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
4
4
|
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
@@ -89,7 +89,7 @@ export async function signInStatus(ctx) {
|
|
|
89
89
|
return { signedIn: true };
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
92
|
-
if (error instanceof ApiError && error.status === 401)
|
|
92
|
+
if (error instanceof SavedSignInError || error instanceof ApiError && error.status === 401)
|
|
93
93
|
return { signedIn: false };
|
|
94
94
|
throw error;
|
|
95
95
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { skippedFilesNotice, ANALYZE_SKIPPED_FILES_OFFER } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
|
+
import { analyzeCommand } from "../contract/agent-choice.js";
|
|
4
|
+
function terminalOffer(folder) {
|
|
5
|
+
return `To analyze them separately, run ${analyzeCommand(folder)} --skipped.`;
|
|
6
|
+
}
|
|
7
|
+
export function skippedResultText(files, settings = {}, options = {}) {
|
|
8
|
+
if (!skippedFilesEnabled(settings))
|
|
9
|
+
return "";
|
|
10
|
+
const notice = skippedFilesNotice(files);
|
|
11
|
+
if (!notice)
|
|
12
|
+
return "";
|
|
13
|
+
const offer = options.offer === false ? "" : `${options.surface === "mcp" ? ANALYZE_SKIPPED_FILES_OFFER : terminalOffer(options.folder ?? ".")}\n`;
|
|
14
|
+
return `${notice}\n${offer}`;
|
|
15
|
+
}
|
|
16
|
+
export function followupPlanText(plan) {
|
|
17
|
+
const count = plan.groups.length;
|
|
18
|
+
return [count ? `This will run ${count} ${count === 1 ? "analysis" : "analyses"}.` : "No skipped files remain to analyze.",
|
|
19
|
+
...plan.tooLarge.map(path => `${path}: too large to analyze.`),
|
|
20
|
+
...plan.unavailable.map(path => `${path}: could not be read.`),
|
|
21
|
+
...(plan.missingContext ?? []).map(file => `${file.path}: some imported files could not be included: ${file.imports.join(", ")}.`), ""].join("\n");
|
|
22
|
+
}
|
package/dist/cli/summary.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { skippedResultText } from "./skipped-files.js";
|
|
1
2
|
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
2
|
-
import { findingDisplayRows, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
|
|
3
|
+
import { activityAdvice, findingDisplayRows, notActiveCountLine, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
|
|
4
|
+
import { groupByActivity } from "../analysis-core/analysis/schemas.js";
|
|
3
5
|
export function formatSummary(report, options) {
|
|
4
6
|
const lines = ["# Counterfact analysis", "",
|
|
5
7
|
`${report.findings.length} findings; ${report.structures.length} structural proposals.`, ""];
|
|
8
|
+
const skipped = skippedResultText(report.refusedFiles ?? [], { CF_SKIPPED_FILES: options.skippedFilesEnabled === false ? "0" : "1" }, { offer: options.skippedOffer, folder: options.analyzedFolder });
|
|
9
|
+
if (skipped)
|
|
10
|
+
lines.push(skipped.trimEnd(), "");
|
|
6
11
|
if (!report.findings.length)
|
|
7
12
|
lines.push("No findings.", "");
|
|
8
13
|
const findings = report.findings.map(finding => {
|
|
@@ -25,6 +30,15 @@ export function formatSummary(report, options) {
|
|
|
25
30
|
if (finding.fixes.length)
|
|
26
31
|
lines.push(...finding.fixes.map(fix => `- ${fix.title}`), "");
|
|
27
32
|
}
|
|
33
|
+
const { notActive } = groupByActivity(findings, options.notActiveEnabled === true);
|
|
34
|
+
if (notActive.length) {
|
|
35
|
+
lines.push("<details>", `<summary>${notActiveCountLine(notActive.length)}</summary>`, "");
|
|
36
|
+
for (const finding of notActive) {
|
|
37
|
+
const activity = finding.activity;
|
|
38
|
+
lines.push(`### ${finding.title}`, "", `${activity.state === "dead" ? "Dead" : "Switched off"}: ${activity.reason}`, "", activityAdvice(activity), "", finding.cause, "", ...finding.fixes.map(fix => `- ${fix.title}: ${fix.approach}`), "");
|
|
39
|
+
}
|
|
40
|
+
lines.push("</details>", "");
|
|
41
|
+
}
|
|
28
42
|
if (report.structures.length)
|
|
29
43
|
lines.push("## Structural proposals", "", ...report.structures.flatMap(proposal => {
|
|
30
44
|
const presentation = findingPresentation({ ...proposal, title: proposal.body.title });
|
|
@@ -1,14 +1,37 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
1
2
|
export const CHOOSE_AGENT_MESSAGE = "Counterfact couldn't tell which folder holds your agent. Run it again on that folder, for example";
|
|
2
3
|
export const NO_AGENT_MESSAGE = "Counterfact couldn't find an agent prompt in this folder. Pass a folder containing an agent prompt or a model call with a system prompt.";
|
|
4
|
+
const ANALYZE_COMMAND = `npx -y @counterfact-labs/cli@${CLI_VERSION} analyze`;
|
|
5
|
+
export function analyzeCommand(folder) {
|
|
6
|
+
return `${ANALYZE_COMMAND} ${shellArgument(folder)}`;
|
|
7
|
+
}
|
|
3
8
|
/** The local importer supplies the choice; transport keeps its data and wording together. */
|
|
4
9
|
export class AgentChoiceError extends Error {
|
|
5
10
|
agentChoice;
|
|
6
11
|
constructor(agentChoice) {
|
|
7
|
-
|
|
8
|
-
super(agentChoice.kind === "choose_agent" ? [`${CHOOSE_AGENT_MESSAGE} counterfact-labs analyze ${shellArgument(folders[0])}.`, "These folders look like agents:", ...folders].join("\n") : NO_AGENT_MESSAGE);
|
|
12
|
+
super(agentChoiceMessage(agentChoice));
|
|
9
13
|
this.agentChoice = agentChoice;
|
|
10
14
|
}
|
|
11
15
|
}
|
|
16
|
+
export function agentChoiceMessage(agentChoice, surface = "terminal") {
|
|
17
|
+
if (agentChoice.kind === "choose_scope")
|
|
18
|
+
return scopeChoiceMessage(agentChoice, surface);
|
|
19
|
+
if (agentChoice.kind === "choose_agent")
|
|
20
|
+
return folderChoiceMessage(agentChoice);
|
|
21
|
+
return NO_AGENT_MESSAGE;
|
|
22
|
+
}
|
|
23
|
+
function scopeChoiceMessage(agentChoice, surface) {
|
|
24
|
+
return ["Choose one part to analyze:", ...agentChoice.candidates.map((candidate, index) => {
|
|
25
|
+
const label = `${index + 1}. ${candidate.label}`;
|
|
26
|
+
if (surface === "mcp")
|
|
27
|
+
return label;
|
|
28
|
+
return `${label}\n ${analyzeCommand(candidate.folder)} --scope ${shellArgument(JSON.stringify(candidate.files))}`;
|
|
29
|
+
})].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function folderChoiceMessage(agentChoice) {
|
|
32
|
+
const folders = [...new Set(agentChoice.candidates.map(candidate => candidate.folder))];
|
|
33
|
+
return [`${CHOOSE_AGENT_MESSAGE} ${analyzeCommand(folders[0])}.`, "These folders look like agents:", ...folders].join("\n");
|
|
34
|
+
}
|
|
12
35
|
/** Folder names must remain literal when a user pastes the suggested command. */
|
|
13
36
|
function shellArgument(folder) {
|
|
14
37
|
return /^[a-zA-Z0-9_./-]+$/.test(folder) ? folder : `'${folder.replaceAll("'", "'\\''")}'`;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
1
2
|
export const cliApiMessages = Object.freeze({
|
|
2
3
|
unreachable: "Can't reach Counterfact. Try again shortly.",
|
|
3
4
|
invalid_response: "Counterfact returned an invalid response. Try again shortly.",
|
|
4
5
|
request_failed: "The request could not be completed. Try again shortly.",
|
|
6
|
+
unreadable_sign_in: "Your saved sign-in can't be read. Sign in again.",
|
|
7
|
+
unreadable_sign_in_terminal: `Your saved sign-in can't be read. Sign in again with npx -y @counterfact-labs/cli@${CLI_VERSION} login.`,
|
|
5
8
|
unauthorized: "Not signed in. Run counterfact-labs login.",
|
|
6
9
|
invalid_console_url: "COUNTERFACT_URL must be an HTTP or HTTPS address without credentials or query fields.",
|
|
7
10
|
});
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { FOLDER_TOO_LARGE_MESSAGE } from "../analysis-core/errors.js";
|
|
2
2
|
import { cliApiMessages } from "./cli-api-messages.js";
|
|
3
3
|
import { emailSignInMessages } from "./email-sign-in-messages.js";
|
|
4
|
+
import { ANALYSIS_TOO_LARGE_MESSAGE, INVALID_SELECTION_MESSAGE } from "./scope-choice.js";
|
|
4
5
|
export const START_FAILURE_MESSAGE = "This couldn't start. Try again.";
|
|
5
6
|
export const RESERVATION_EXPIRED_MESSAGE = "This reservation expired. Start a new analysis.";
|
|
6
7
|
export const FINISH_FAILURE_MESSAGE = "The analysis couldn't finish. Try again.";
|
|
8
|
+
export const NOT_ALLOWED_MESSAGE = "Your account can't run analyses. Ask Counterfact for access.";
|
|
9
|
+
export const SIGN_IN_EXPIRED_MESSAGE = "Your sign-in has expired. Run counterfact-labs login, then try again.";
|
|
10
|
+
export const FOLDER_BUSY_MESSAGE = "Another analysis is running on this folder. Try again when it finishes.";
|
|
11
|
+
export const CHECK_CONFLICT_MESSAGE = "Saved results from an earlier run don't match this analysis. Start a new analysis.";
|
|
12
|
+
export const UNSAVED_RESULTS_MESSAGE = "The analysis ended before its results were saved. Start a new analysis.";
|
|
13
|
+
export const PENDING_RESULTS_MESSAGE = "Your results are kept on this computer. Run the same command on the same folder again to save them at no extra charge.";
|
|
7
14
|
export const RESULTS_TOO_LARGE_MESSAGE = "The results were too large to save. Try again.";
|
|
8
15
|
export const COMMITTED_FILES_MESSAGE = "Counterfact analyzes the committed version of the agent's files. Commit your changes, then try again.";
|
|
9
16
|
const unsupportedVersion = "This version of Counterfact can't run analyses. Update it: npm install -g @counterfact-labs/cli@latest.";
|
|
10
17
|
// The console separates over-limit costs from version or route mismatches, so each code gets its own recovery guidance.
|
|
11
|
-
const
|
|
18
|
+
const localReadFailure = "Counterfact couldn't read the selected file or folder. Check that it exists and is readable, then try again.";
|
|
12
19
|
export const localRefusals = {
|
|
13
|
-
outside_project:
|
|
14
|
-
launch_folder_unavailable:
|
|
15
|
-
folder_unavailable:
|
|
20
|
+
outside_project: INVALID_SELECTION_MESSAGE,
|
|
21
|
+
launch_folder_unavailable: "Counterfact couldn't read the launch folder. Check that it exists and is readable, then try again.",
|
|
22
|
+
folder_unavailable: localReadFailure,
|
|
23
|
+
local_read_failed: localReadFailure,
|
|
24
|
+
local_git_failed: "Counterfact couldn't read this folder's Git information. Check that Git works in this folder, then try again.",
|
|
16
25
|
repeat_too_soon: FINISH_FAILURE_MESSAGE,
|
|
17
26
|
};
|
|
18
27
|
/** Both console clients use the same public messages for server refusals. */
|
|
@@ -22,7 +31,7 @@ export const cliRefusals = {
|
|
|
22
31
|
local_checks_paused: "Analyses are paused right now. Try again later.",
|
|
23
32
|
cli_update_required: "Update Counterfact: run npm install -g @counterfact-labs/cli@latest.",
|
|
24
33
|
release_unavailable: unsupportedVersion,
|
|
25
|
-
plan_over_limit:
|
|
34
|
+
plan_over_limit: ANALYSIS_TOO_LARGE_MESSAGE,
|
|
26
35
|
invalid_plan: unsupportedVersion,
|
|
27
36
|
invalid_body: "Counterfact couldn't read this request. Update Counterfact, then try again.",
|
|
28
37
|
body_too_large: "This request is too large. The limit is 2 MB.",
|
|
@@ -70,14 +79,39 @@ const reasonValues = [
|
|
|
70
79
|
...Object.keys(localRefusals),
|
|
71
80
|
...Object.values(otherReasons), ...Object.values(messageReasons),
|
|
72
81
|
"console_unreachable", "invalid_console_url", "relay_unreachable", "results_not_saved",
|
|
82
|
+
"results_saved_report_failed", "results_pending", "results_discarded",
|
|
83
|
+
"folder_busy", "completion_settings_mismatch",
|
|
73
84
|
];
|
|
74
85
|
const ownedReasons = new Set(reasonValues);
|
|
86
|
+
const readErrorCodes = new Set(["ENOENT", "EACCES", "EPERM", "ENOTDIR", "EISDIR", "ELOOP", "EMFILE", "ENFILE", "EIO", "ESTALE", "EBADF", "EINVAL", "ENOMEM", "EAGAIN", "EBUSY"]);
|
|
87
|
+
/** Classify local failures without copying paths, command output or file contents into public errors. */
|
|
88
|
+
export function localFailure(error) {
|
|
89
|
+
if (!(error instanceof Error))
|
|
90
|
+
return undefined;
|
|
91
|
+
if ("reason" in error && error.reason === "local_git_failed")
|
|
92
|
+
return { message: localRefusals.local_git_failed, reason: "local_git_failed" };
|
|
93
|
+
// Only the selection reader knows whether a filesystem operation belongs to the agent.
|
|
94
|
+
if (!("localOperation" in error) || error.localOperation !== "read")
|
|
95
|
+
return undefined;
|
|
96
|
+
const syscall = "syscall" in error ? error.syscall : undefined;
|
|
97
|
+
const code = "code" in error ? error.code : undefined;
|
|
98
|
+
if (typeof code === "string" && readErrorCodes.has(code) && typeof syscall === "string"
|
|
99
|
+
&& (/^(?:read|stat|lstat|scandir|opendir|realpath|readlink|access)$/.test(syscall)
|
|
100
|
+
|| syscall === "open")
|
|
101
|
+
|| "code" in error && error.code === "ERR_ACCESS_DENIED" && "permission" in error && error.permission === "FileSystemRead") {
|
|
102
|
+
return { message: localRefusals.local_read_failed, reason: "local_read_failed" };
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
75
106
|
/** Only fixed reasons may cross the MCP boundary; diagnostics can contain customer data. */
|
|
76
107
|
export function failureReason(error) {
|
|
77
108
|
if (!(error instanceof Error))
|
|
78
109
|
return "request_failed";
|
|
79
110
|
if (error instanceof CliRefusalError)
|
|
80
111
|
return error.code;
|
|
112
|
+
const local = localFailure(error);
|
|
113
|
+
if (local)
|
|
114
|
+
return local.reason;
|
|
81
115
|
if ("reason" in error && typeof error.reason === "string" && ownedReasons.has(error.reason))
|
|
82
116
|
return error.reason;
|
|
83
117
|
const code = "code" in error ? error.code : undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.9";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export function savedAnalysisUrl(projectId, consoleOrigin) {
|
|
2
2
|
return new URL(`/projects/${projectId}/findings`, consoleOrigin).href;
|
|
3
3
|
}
|
|
4
|
+
export function savedArchitectureUrl(projectId, consoleOrigin) {
|
|
5
|
+
return new URL(`/projects/${projectId}/architecture/overview`, consoleOrigin).href;
|
|
6
|
+
}
|
|
4
7
|
import { isLoopbackHost } from "../gatekeeper/loopback.js";
|
|
5
8
|
/** Bearer credentials may use plain HTTP only on this machine. */
|
|
6
9
|
export function consoleSchemeAllowed(url) {
|