jev-agent-tools 0.1.4 → 0.3.0
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/CHANGELOG.md +106 -1
- package/CONTRIBUTING.md +43 -0
- package/README.md +58 -17
- package/SECURITY.md +43 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +198 -0
- package/dist/adapters/ask-proof.js +200 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +234 -0
- package/dist/adapters/docs.js +192 -0
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +418 -0
- package/dist/adapters/find.js +150 -0
- package/dist/adapters/git-base.js +32 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +483 -0
- package/dist/adapters/locate-file.js +197 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +106 -0
- package/dist/adapters/risk-callers.js +429 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +92 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +139 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +267 -0
- package/dist/constants.js +140 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +278 -0
- package/dist/core/asks.js +507 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +137 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +51 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +456 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +210 -0
- package/dist/mcp/tools.js +129 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +114 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +3 -0
- package/dist/texts/ask.js +4 -0
- package/dist/texts/check-diff.js +20 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +19 -0
- package/dist/texts/guide.js +3 -0
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +15 -0
- package/dist/texts/select-tests.js +4 -0
- package/dist/tools/ask-files.js +450 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +1147 -0
- package/dist/tools/check-diff.js +594 -0
- package/dist/tools/docs-check.js +408 -0
- package/dist/tools/find.js +682 -0
- package/dist/tools/locate.js +602 -0
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +821 -0
- package/dist/tools/spec-check.js +263 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +120 -0
- package/docs/design.md +16 -4
- package/docs/mcp.md +233 -0
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +19 -4
- package/rules/jev-ask.md +22 -1
- package/server.json +57 -0
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +102 -36
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +155 -0
- package/src/adapters/risk-callers.ts +7 -2
- package/src/adapters/shell.ts +113 -0
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +55 -14
- package/src/constants.ts +37 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/command-output.ts +17 -1
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/host.ts +11 -0
- package/src/index.ts +3 -0
- package/src/jev/client.ts +66 -16
- package/src/jev/types.ts +24 -3
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +332 -0
- package/src/mcp/tools.ts +179 -0
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +311 -18
- package/src/tools/ask.ts +722 -95
- package/src/tools/check-diff.ts +337 -31
- package/src/tools/docs-check.ts +241 -38
- package/src/tools/find.ts +389 -29
- package/src/tools/locate.ts +387 -25
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +484 -23
- package/src/tools/spec-check.ts +194 -19
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
|
|
3
|
+
import { collectSearchExcerpts } from "../adapters/files.js";
|
|
4
|
+
import { prefilter } from "../adapters/find.js";
|
|
5
|
+
import { shareGitInventory } from "../adapters/git-inventory.js";
|
|
6
|
+
import { hostUsage } from "../adapters/usage.js";
|
|
7
|
+
import * as C from "../constants.js";
|
|
8
|
+
import { contentWords, findKeywords, rankFiles, retainFiles, } from "../core/find.js";
|
|
9
|
+
import { buildEnvelope, } from "../core/output.js";
|
|
10
|
+
import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.js";
|
|
11
|
+
import { answerItemFromInput, buildResultReport, contextFromEvidence, known, } from "../core/result-report.js";
|
|
12
|
+
import { secretRefusal } from "../core/secret-path.js";
|
|
13
|
+
import { interpolate } from "../describe.js";
|
|
14
|
+
import { renderResultReport } from "../render.js";
|
|
15
|
+
import { NOT_CONFIGURED } from "../texts/configuration.js";
|
|
16
|
+
import { FIND_DESCRIPTION } from "../texts/find.js";
|
|
17
|
+
export const findParameters = Type.Object({
|
|
18
|
+
root: Type.Optional(Type.String()),
|
|
19
|
+
goal: Type.String({ minLength: 1 }),
|
|
20
|
+
keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
21
|
+
scope: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
|
|
22
|
+
exclude: Type.Optional(Type.Array(Type.String())),
|
|
23
|
+
effort: Type.Optional(Type.Union([
|
|
24
|
+
Type.Literal("quick"),
|
|
25
|
+
Type.Literal("default"),
|
|
26
|
+
Type.Literal("thorough"),
|
|
27
|
+
])),
|
|
28
|
+
max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
29
|
+
}, { additionalProperties: false });
|
|
30
|
+
export function createFindFilesTool(dependencies) {
|
|
31
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
32
|
+
return {
|
|
33
|
+
name: "jev_find_files",
|
|
34
|
+
label: "Jev find files",
|
|
35
|
+
description: interpolate(FIND_DESCRIPTION, host.names),
|
|
36
|
+
parameters: findParameters,
|
|
37
|
+
...(host.isOmp
|
|
38
|
+
? {
|
|
39
|
+
approval: "read",
|
|
40
|
+
loadMode: "essential",
|
|
41
|
+
defaultInactive: true,
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
44
|
+
promptSnippet: "Find the files for a goal described in plain words, ranked, with an entry point",
|
|
45
|
+
promptGuidelines: [
|
|
46
|
+
"jev_find_files: describe the behavior in one sentence of five words or more, not a guessed identifier",
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
async execute(_id, args, signal, _update, ctx) {
|
|
50
|
+
const client = dependencies.client;
|
|
51
|
+
const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
|
|
52
|
+
exec: execute,
|
|
53
|
+
signal,
|
|
54
|
+
origin: dependencies.evidenceOrigin,
|
|
55
|
+
});
|
|
56
|
+
const evidenceContext = evidence.context;
|
|
57
|
+
const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
const exec = shareGitInventory(execute);
|
|
60
|
+
const totals = {
|
|
61
|
+
calls: 0,
|
|
62
|
+
questions: 0,
|
|
63
|
+
cacheHits: 0,
|
|
64
|
+
cacheRequests: 0,
|
|
65
|
+
usage: { inputTokens: 0, costUsd: 0 },
|
|
66
|
+
};
|
|
67
|
+
const limitations = [];
|
|
68
|
+
const unchecked = [];
|
|
69
|
+
let budget;
|
|
70
|
+
let sent = 0;
|
|
71
|
+
const context = contextFromEvidence(evidenceContext);
|
|
72
|
+
const diagnostics = [];
|
|
73
|
+
const actions = [];
|
|
74
|
+
const auxiliary = {
|
|
75
|
+
controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
|
|
76
|
+
passages: { fresh: 0, cache: 0, notJudged: 0 },
|
|
77
|
+
};
|
|
78
|
+
let entryPaths = [];
|
|
79
|
+
let costObserved = true;
|
|
80
|
+
let failureCause = "invalid_response";
|
|
81
|
+
let primary;
|
|
82
|
+
let confirmation;
|
|
83
|
+
let confirmationRequired = false;
|
|
84
|
+
const raw = (answer) => answer.type === "choice"
|
|
85
|
+
? Object.entries(answer.probabilities).map(([label, probability]) => ({
|
|
86
|
+
label,
|
|
87
|
+
value: label,
|
|
88
|
+
probability: known(probability),
|
|
89
|
+
}))
|
|
90
|
+
: [];
|
|
91
|
+
const diagnostic = (cause, fact, members = []) => {
|
|
92
|
+
const id = `d${diagnostics.length + 1}`;
|
|
93
|
+
const actionId = `a${actions.length + 1}`;
|
|
94
|
+
const code = cause === "invalid_root"
|
|
95
|
+
? "correct_context"
|
|
96
|
+
: cause === "not_configured"
|
|
97
|
+
? "configure_client"
|
|
98
|
+
: cause === "service_unavailable" || cause === "transport_failure"
|
|
99
|
+
? "recover_service"
|
|
100
|
+
: "inspect_native";
|
|
101
|
+
actions.push({
|
|
102
|
+
id: actionId,
|
|
103
|
+
code,
|
|
104
|
+
target: known(members.join(", ") || "entry"),
|
|
105
|
+
scope: { kind: "call" },
|
|
106
|
+
condition: code === "correct_context"
|
|
107
|
+
? "If the requested root is incorrect"
|
|
108
|
+
: code === "configure_client"
|
|
109
|
+
? "If Jev judgment is still useful after configuring the client"
|
|
110
|
+
: code === "recover_service"
|
|
111
|
+
? "Only after service recovery is observed"
|
|
112
|
+
: "If the entry decision remains open",
|
|
113
|
+
instruction: code === "correct_context"
|
|
114
|
+
? "Provide the exact registered worktree top-level; no fallback was performed."
|
|
115
|
+
: code === "configure_client"
|
|
116
|
+
? "Configure the Jev client, or continue with native discovery."
|
|
117
|
+
: code === "recover_service"
|
|
118
|
+
? "Continue natively while unavailable; revisit only after a material service change."
|
|
119
|
+
: "Inspect the candidate paths or collected passages natively; retain uncertainty without repeating unchanged evidence.",
|
|
120
|
+
repeatUnchanged: false,
|
|
121
|
+
});
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
id,
|
|
124
|
+
cause,
|
|
125
|
+
fact,
|
|
126
|
+
target: known(members.join(", ") ||
|
|
127
|
+
(cause === "invalid_root"
|
|
128
|
+
? (args.root ?? cwd)
|
|
129
|
+
: cause === "forbidden_path"
|
|
130
|
+
? JSON.stringify({ scope: args.scope, exclude: args.exclude })
|
|
131
|
+
: "entry")),
|
|
132
|
+
origin: cause === "invalid_root" || cause === "forbidden_path"
|
|
133
|
+
? "input"
|
|
134
|
+
: cause === "call_budget" || cause === "session_budget"
|
|
135
|
+
? "budget"
|
|
136
|
+
: cause === "control_failure"
|
|
137
|
+
? "control"
|
|
138
|
+
: cause === "service_unavailable" ||
|
|
139
|
+
cause === "transport_failure" ||
|
|
140
|
+
cause === "invalid_response" ||
|
|
141
|
+
cause === "provider_context_refusal" ||
|
|
142
|
+
cause === "not_configured"
|
|
143
|
+
? "provider"
|
|
144
|
+
: "collection",
|
|
145
|
+
scope: { kind: "call" },
|
|
146
|
+
effect: "blocking",
|
|
147
|
+
material: true,
|
|
148
|
+
omittedMembers: members,
|
|
149
|
+
memberCount: known(members.length),
|
|
150
|
+
actionIds: [actionId],
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const finish = (input) => {
|
|
154
|
+
const envelope = buildEnvelope({
|
|
155
|
+
...input,
|
|
156
|
+
limitations: [...limitations, ...(input.limitations ?? [])],
|
|
157
|
+
...(budget ? { budget, unchecked: [...new Set(unchecked)] } : {}),
|
|
158
|
+
yield: {
|
|
159
|
+
...totals,
|
|
160
|
+
costUsd: totals.usage.costUsd,
|
|
161
|
+
elapsedMs: performance.now() - started,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
if (input.refusal)
|
|
165
|
+
diagnostic(failureCause, input.refusal);
|
|
166
|
+
if (!input.refusal &&
|
|
167
|
+
!diagnostics.length &&
|
|
168
|
+
!budget &&
|
|
169
|
+
!unchecked.length &&
|
|
170
|
+
!primary?.source)
|
|
171
|
+
diagnostic("invalid_response", "entry decision was not received with established answer provenance");
|
|
172
|
+
if (budget)
|
|
173
|
+
diagnostic(budget.kind === "max_calls" ? "call_budget" : "session_budget", budget.message, [...new Set(unchecked)]);
|
|
174
|
+
if (unchecked.length && !budget)
|
|
175
|
+
diagnostic(failureCause, "candidate ranking or entry work was not judged", [...new Set(unchecked)]);
|
|
176
|
+
const entryLine = input.lines?.find((line) => line.type === "entry");
|
|
177
|
+
if (entryLine?.type === "entry" && primary?.source)
|
|
178
|
+
actions.push({
|
|
179
|
+
id: `a${actions.length + 1}`,
|
|
180
|
+
code: "inspect_native",
|
|
181
|
+
target: known((entryLine.nextRead ?? []).join(", ") || "candidate inventory"),
|
|
182
|
+
scope: { kind: "group", groupIds: ["entry"] },
|
|
183
|
+
condition: "If the entry decision remains open after this bounded judgment",
|
|
184
|
+
instruction: entryLine.candidate.label === "none"
|
|
185
|
+
? "Search elsewhere or widen the admitted scope for the goal; inspect any named alternative before collecting genuinely new evidence. Do not retry unchanged evidence."
|
|
186
|
+
: "Read the named candidate passages; add only genuinely discriminating evidence if needed. Do not reword unchanged evidence to retry.",
|
|
187
|
+
repeatUnchanged: false,
|
|
188
|
+
});
|
|
189
|
+
const common = {
|
|
190
|
+
id: "entry",
|
|
191
|
+
kind: "entry",
|
|
192
|
+
label: "entry",
|
|
193
|
+
groupId: "entry",
|
|
194
|
+
evidence: entryPaths.map((path) => ({
|
|
195
|
+
target: path,
|
|
196
|
+
canonicalPath: known(`${cwd}/${path}`),
|
|
197
|
+
aliases: [],
|
|
198
|
+
side: "current",
|
|
199
|
+
revision: context.resolvedBase,
|
|
200
|
+
})),
|
|
201
|
+
diagnosticIds: diagnostics.map((d) => d.id),
|
|
202
|
+
actionIds: actions.map((a) => a.id),
|
|
203
|
+
};
|
|
204
|
+
let item = { ...common, treatment: "not_judged", source: "none" };
|
|
205
|
+
if (entryLine?.type === "entry" &&
|
|
206
|
+
primary?.type === "choice" &&
|
|
207
|
+
primary.source &&
|
|
208
|
+
(!confirmationRequired ||
|
|
209
|
+
(confirmation?.type === "choice" && confirmation.source))) {
|
|
210
|
+
auxiliary.passages[primary.source]--;
|
|
211
|
+
item = answerItemFromInput(common, {
|
|
212
|
+
label: "entry",
|
|
213
|
+
answer: primary,
|
|
214
|
+
value: entryLine.candidate.value,
|
|
215
|
+
publicResult: entryLine.candidate.value.head,
|
|
216
|
+
band: entryLine.band,
|
|
217
|
+
reason: limitations.map((limit) => limit.fact).join("; "),
|
|
218
|
+
rawValues: raw(primary),
|
|
219
|
+
reportControls: confirmation?.type === "choice" && confirmation.source
|
|
220
|
+
? [
|
|
221
|
+
{
|
|
222
|
+
id: "entry-order",
|
|
223
|
+
kind: "order",
|
|
224
|
+
source: confirmation.source,
|
|
225
|
+
outcome: entryLine.orderDependent
|
|
226
|
+
? "order-dependent"
|
|
227
|
+
: "consistent",
|
|
228
|
+
rawValues: raw(confirmation),
|
|
229
|
+
},
|
|
230
|
+
]
|
|
231
|
+
: [],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const report = buildResultReport({
|
|
235
|
+
tool: "jev_find_files",
|
|
236
|
+
context,
|
|
237
|
+
items: [item],
|
|
238
|
+
diagnostics,
|
|
239
|
+
actions,
|
|
240
|
+
metrics: {
|
|
241
|
+
...totals,
|
|
242
|
+
costUsd: costObserved ? totals.usage.costUsd : undefined,
|
|
243
|
+
elapsedMs: performance.now() - started,
|
|
244
|
+
},
|
|
245
|
+
auxiliary,
|
|
246
|
+
total: known(1),
|
|
247
|
+
refused: Boolean(input.refusal &&
|
|
248
|
+
(failureCause === "invalid_root" ||
|
|
249
|
+
failureCause === "forbidden_path")),
|
|
250
|
+
missingWork: unchecked.length > 0,
|
|
251
|
+
});
|
|
252
|
+
runtime.session.record(envelope);
|
|
253
|
+
runtime.guide.deliver(ctx);
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: renderResultReport(report, {
|
|
259
|
+
details: {
|
|
260
|
+
lines: [
|
|
261
|
+
...envelope.lines,
|
|
262
|
+
...(entryLine?.type === "entry"
|
|
263
|
+
? [
|
|
264
|
+
{
|
|
265
|
+
type: "list",
|
|
266
|
+
title: "read",
|
|
267
|
+
items: [...(entryLine.nextRead ?? [])],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: "list",
|
|
271
|
+
title: "auxiliary ranking",
|
|
272
|
+
items: (entryLine.relevant ?? []).map((candidate) => `${candidate.label} (${candidate.value.p})`),
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
: []),
|
|
276
|
+
{
|
|
277
|
+
type: "list",
|
|
278
|
+
title: "discovery reservations",
|
|
279
|
+
items: [...limitations, ...(input.limitations ?? [])].map((limit) => limit.fact),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
details: { ...totals, evidenceContext, result: report },
|
|
287
|
+
...hostUsage(host.isOmp, totals.usage),
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
if (!evidence.ok) {
|
|
291
|
+
failureCause = evidence.cause ?? "invalid_root";
|
|
292
|
+
return finish({ refusal: evidence.error });
|
|
293
|
+
}
|
|
294
|
+
if (!client) {
|
|
295
|
+
failureCause = "not_configured";
|
|
296
|
+
return finish({ refusal: NOT_CONFIGURED });
|
|
297
|
+
}
|
|
298
|
+
const words = contentWords(args.goal);
|
|
299
|
+
const short = words.length < C.FIND_GOAL_MIN_WORDS;
|
|
300
|
+
if (short)
|
|
301
|
+
limitations.push({
|
|
302
|
+
fact: `goal has ${words.length} content words: verdicts are capped at unsure`,
|
|
303
|
+
next: "describe the behavior in one sentence",
|
|
304
|
+
});
|
|
305
|
+
const keywords = findKeywords(args.goal, args.keywords);
|
|
306
|
+
const candidateLimit = args.effort === "quick"
|
|
307
|
+
? C.FIND_QUICK_CANDIDATES
|
|
308
|
+
: args.effort === "thorough"
|
|
309
|
+
? C.FIND_THOROUGH_CANDIDATES
|
|
310
|
+
: C.FIND_NAME_CANDIDATES;
|
|
311
|
+
const readLimit = args.effort === "quick"
|
|
312
|
+
? C.FIND_QUICK_READ
|
|
313
|
+
: args.effort === "thorough"
|
|
314
|
+
? C.FIND_THOROUGH_READ
|
|
315
|
+
: C.FIND_FILES_READ;
|
|
316
|
+
const candidates = await prefilter(exec, cwd, keywords, candidateLimit, args.scope, args.exclude, signal);
|
|
317
|
+
if (!candidates.ok) {
|
|
318
|
+
failureCause = candidates.cause ?? "git_failure";
|
|
319
|
+
return finish({ refusal: candidates.error });
|
|
320
|
+
}
|
|
321
|
+
for (const path of candidates.secret)
|
|
322
|
+
diagnostic("secret_pattern", secretRefusal(path), [path]);
|
|
323
|
+
context.inventories.push({
|
|
324
|
+
id: "candidates",
|
|
325
|
+
kind: "repository",
|
|
326
|
+
rules: [
|
|
327
|
+
"Git tracked and non-ignored untracked files",
|
|
328
|
+
"keyword prefilter with existing effort limit",
|
|
329
|
+
],
|
|
330
|
+
restrictions: [
|
|
331
|
+
...(typeof args.scope === "string"
|
|
332
|
+
? [args.scope]
|
|
333
|
+
: (args.scope ?? [])),
|
|
334
|
+
...(args.exclude ?? []).map((path) => `exclude ${path}`),
|
|
335
|
+
],
|
|
336
|
+
discovered: known(candidates.scopeFiles),
|
|
337
|
+
considered: known(candidates.paths.length),
|
|
338
|
+
scopeRestricted: Boolean(args.scope || args.exclude?.length),
|
|
339
|
+
criteria: [],
|
|
340
|
+
});
|
|
341
|
+
const entry = (path, p, band, alternatives = [], orderDependent = false, orderProbabilities, relevant = []) => ({
|
|
342
|
+
type: "entry",
|
|
343
|
+
candidate: { label: path, value: { head: path, p } },
|
|
344
|
+
band,
|
|
345
|
+
candidates: alternatives.map((item) => ({
|
|
346
|
+
label: item.path,
|
|
347
|
+
value: { head: item.path, p: item.p },
|
|
348
|
+
})),
|
|
349
|
+
orderDependent,
|
|
350
|
+
orderProbabilities,
|
|
351
|
+
relevant: relevant.map((item) => ({
|
|
352
|
+
label: item.path,
|
|
353
|
+
value: { head: item.path, p: item.p },
|
|
354
|
+
})),
|
|
355
|
+
nextRead: path === "none"
|
|
356
|
+
? []
|
|
357
|
+
: [
|
|
358
|
+
path,
|
|
359
|
+
...(band === "unsure"
|
|
360
|
+
? alternatives
|
|
361
|
+
.map((item) => item.path)
|
|
362
|
+
.filter((path) => path !== "none")
|
|
363
|
+
: []),
|
|
364
|
+
],
|
|
365
|
+
});
|
|
366
|
+
if (!candidates.paths.length) {
|
|
367
|
+
diagnostic("collection_empty", candidates.scopeFiles === 0
|
|
368
|
+
? "scope matched 0 files; no entry judgment"
|
|
369
|
+
: "no file shares a word with the goal; no entry judgment");
|
|
370
|
+
return finish({});
|
|
371
|
+
}
|
|
372
|
+
const judge = async (state, questions) => {
|
|
373
|
+
state = withEvidenceContext(state, evidenceContext);
|
|
374
|
+
if (JSON.stringify(state).length > C.STATE_MAX_CHARS)
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
error: `required evidence exceeds STATE_MAX_CHARS=${C.STATE_MAX_CHARS}`,
|
|
378
|
+
};
|
|
379
|
+
const result = await client.judge(state, questions, {
|
|
380
|
+
signal,
|
|
381
|
+
...runtime.session.requestGate(),
|
|
382
|
+
beforeRequest: (count) => {
|
|
383
|
+
if (args.max_calls !== undefined && sent >= args.max_calls) {
|
|
384
|
+
budget = {
|
|
385
|
+
kind: "max_calls",
|
|
386
|
+
message: `max_calls=${args.max_calls} reached`,
|
|
387
|
+
};
|
|
388
|
+
return { ok: false, error: budget.message };
|
|
389
|
+
}
|
|
390
|
+
const admission = runtime.session.admit(count);
|
|
391
|
+
if (!admission.ok) {
|
|
392
|
+
budget = { kind: "session", message: admission.error };
|
|
393
|
+
return admission;
|
|
394
|
+
}
|
|
395
|
+
sent++;
|
|
396
|
+
return admission;
|
|
397
|
+
},
|
|
398
|
+
onUsage: (usage) => runtime.session.recordUsage(usage),
|
|
399
|
+
admissionCause: () => budget?.kind === "max_calls" ? "call_budget" : "session_budget",
|
|
400
|
+
});
|
|
401
|
+
for (const key of [
|
|
402
|
+
"calls",
|
|
403
|
+
"questions",
|
|
404
|
+
"cacheHits",
|
|
405
|
+
"cacheRequests",
|
|
406
|
+
])
|
|
407
|
+
totals[key] += result[key] ?? 0;
|
|
408
|
+
totals.usage.inputTokens += result.usage?.inputTokens ?? 0;
|
|
409
|
+
if (result.calls && !result.usage)
|
|
410
|
+
costObserved = false;
|
|
411
|
+
if (!result.ok) {
|
|
412
|
+
failureCause = result.failureCause ?? "invalid_response";
|
|
413
|
+
auxiliary.passages.notJudged += Object.keys(questions).length;
|
|
414
|
+
}
|
|
415
|
+
totals.usage.costUsd += result.usage?.costUsd ?? 0;
|
|
416
|
+
if (result.ok)
|
|
417
|
+
for (const id of Object.keys(questions)) {
|
|
418
|
+
const answer = result.answers[id];
|
|
419
|
+
if (answer?.type === "unjudged") {
|
|
420
|
+
failureCause = answer.cause ?? "invalid_response";
|
|
421
|
+
auxiliary.passages.notJudged++;
|
|
422
|
+
}
|
|
423
|
+
else if (answer?.source)
|
|
424
|
+
auxiliary.passages[answer.source]++;
|
|
425
|
+
else
|
|
426
|
+
auxiliary.passages.notJudged++;
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
};
|
|
430
|
+
const boolQuestions = (paths, content) => Object.fromEntries(paths.map((path) => [
|
|
431
|
+
path,
|
|
432
|
+
{
|
|
433
|
+
type: "bool",
|
|
434
|
+
instructions: content
|
|
435
|
+
? `Does the passage from ${path} implement or document behavior that helps with this goal: ${args.goal}?`
|
|
436
|
+
: `Could the file ${path} help with this goal: ${args.goal}?`,
|
|
437
|
+
criteria: {
|
|
438
|
+
true: "The file could help with the goal",
|
|
439
|
+
false: "The file does not help with the goal",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
]));
|
|
443
|
+
const names = [];
|
|
444
|
+
const nameBatches = Array.from({ length: Math.ceil(candidates.paths.length / C.FIND_NAME_BATCH) }, (_, index) => candidates.paths.slice(index * C.FIND_NAME_BATCH, (index + 1) * C.FIND_NAME_BATCH));
|
|
445
|
+
const nameResults = await Promise.all(nameBatches.map(async (paths) => ({
|
|
446
|
+
paths,
|
|
447
|
+
result: await judge({ goal: args.goal, paths }, boolQuestions(paths, false)),
|
|
448
|
+
})));
|
|
449
|
+
for (const { paths, result } of nameResults) {
|
|
450
|
+
if (!result.ok && !budget)
|
|
451
|
+
return finish({ refusal: result.error });
|
|
452
|
+
for (const path of paths) {
|
|
453
|
+
const answer = result.ok ? result.answers[path] : undefined;
|
|
454
|
+
if (answer?.type === "bool")
|
|
455
|
+
names.push({ path, p: answer.p });
|
|
456
|
+
else
|
|
457
|
+
unchecked.push(path);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const rankedNames = rankFiles(names);
|
|
461
|
+
let bestRanking = rankedNames;
|
|
462
|
+
const fallback = () => {
|
|
463
|
+
primary = undefined;
|
|
464
|
+
if (bestRanking.length)
|
|
465
|
+
limitations.push({
|
|
466
|
+
fact: `auxiliary ranking retained: ${bestRanking.map((item) => `${item.path} (${item.p})`).join(" · ")}; entry was not judged`,
|
|
467
|
+
next: "inspect these paths natively",
|
|
468
|
+
});
|
|
469
|
+
const top = bestRanking.slice(0, 2);
|
|
470
|
+
return finish({
|
|
471
|
+
lines: [
|
|
472
|
+
entry(top[0]?.path ?? "none", top[0]?.p ?? 0, "unsure", top.slice(1)),
|
|
473
|
+
],
|
|
474
|
+
answers: bestRanking.map((item) => ({
|
|
475
|
+
label: item.path,
|
|
476
|
+
value: { head: "relevant", p: item.p },
|
|
477
|
+
band: "unsure",
|
|
478
|
+
})),
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
if (budget) {
|
|
482
|
+
unchecked.push(...candidates.paths.filter((path) => !names.some((item) => item.path === path)));
|
|
483
|
+
return fallback();
|
|
484
|
+
}
|
|
485
|
+
const readPaths = rankedNames
|
|
486
|
+
.slice(0, readLimit)
|
|
487
|
+
.map((item) => item.path);
|
|
488
|
+
const files = {};
|
|
489
|
+
const content = [];
|
|
490
|
+
const contentBatches = Array.from({ length: Math.ceil(readPaths.length / C.FIND_READ_PER_CALL) }, (_, index) => readPaths.slice(index * C.FIND_READ_PER_CALL, (index + 1) * C.FIND_READ_PER_CALL));
|
|
491
|
+
const contentResults = await Promise.all(contentBatches.map(async (paths) => {
|
|
492
|
+
const read = await collectSearchExcerpts(cwd, paths, keywords, signal, exec);
|
|
493
|
+
if (!read.ok) {
|
|
494
|
+
failureCause = read.cause ?? "file_unavailable";
|
|
495
|
+
return { paths, result: read };
|
|
496
|
+
}
|
|
497
|
+
Object.assign(files, read.pointers);
|
|
498
|
+
limitations.push(...read.limits.map((limit) => ({
|
|
499
|
+
fact: `${limit.path}: ${limit.reason}`,
|
|
500
|
+
next: "provide a UTF-8 text file",
|
|
501
|
+
})));
|
|
502
|
+
if (read.ignored.length) {
|
|
503
|
+
for (const omission of read.omissions)
|
|
504
|
+
diagnostic(omission.cause, `${omission.path}: ${omission.reason}`, [omission.path]);
|
|
505
|
+
limitations.push({
|
|
506
|
+
fact: `${read.ignored.length} binary, empty or unreadable paths ignored: ${read.ignored.join(", ")}`,
|
|
507
|
+
next: "narrow scope to text files",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const textPaths = paths.filter((path) => read.files[path] !== undefined);
|
|
511
|
+
if (!textPaths.length)
|
|
512
|
+
return { paths: [], result: { ok: true, answers: {} } };
|
|
513
|
+
return {
|
|
514
|
+
paths: textPaths,
|
|
515
|
+
result: await judge({
|
|
516
|
+
goal: args.goal,
|
|
517
|
+
files: Object.fromEntries(Object.entries(read.files).sort(([a], [b]) => a.localeCompare(b))),
|
|
518
|
+
}, boolQuestions(textPaths, true)),
|
|
519
|
+
};
|
|
520
|
+
}));
|
|
521
|
+
for (const { paths, result } of contentResults) {
|
|
522
|
+
if (!result.ok && !budget)
|
|
523
|
+
return finish({ refusal: result.error });
|
|
524
|
+
for (const path of paths) {
|
|
525
|
+
const answer = result.ok ? result.answers[path] : undefined;
|
|
526
|
+
if (answer?.type === "bool")
|
|
527
|
+
content.push({ path, p: answer.p });
|
|
528
|
+
else
|
|
529
|
+
unchecked.push(path);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (content.length)
|
|
533
|
+
bestRanking = rankFiles(content);
|
|
534
|
+
if (budget) {
|
|
535
|
+
unchecked.push(...readPaths.filter((path) => !content.some((item) => item.path === path)));
|
|
536
|
+
return fallback();
|
|
537
|
+
}
|
|
538
|
+
const retained = retainFiles(content, rankedNames);
|
|
539
|
+
limitations.push({
|
|
540
|
+
fact: `cascade: ${names.length} names judged → ${content.length} files read → ${retained.length} shown to the pointer`,
|
|
541
|
+
next: "",
|
|
542
|
+
});
|
|
543
|
+
const relevant = rankFiles(content).filter((item) => retained.some((file) => file.path === item.path));
|
|
544
|
+
const output = (path, p, band, alternatives = [], orderDependent = false, orderProbabilities) => finish({
|
|
545
|
+
lines: [
|
|
546
|
+
entry(path, p, band, alternatives, orderDependent, orderProbabilities, relevant),
|
|
547
|
+
],
|
|
548
|
+
});
|
|
549
|
+
if (!retained.length) {
|
|
550
|
+
diagnostic("collection_empty", "no file retained by content or names guard; no entry decision was judged");
|
|
551
|
+
return finish({});
|
|
552
|
+
}
|
|
553
|
+
entryPaths = retained.map((item) => item.path);
|
|
554
|
+
context.inventories.push({
|
|
555
|
+
id: "entry-passages",
|
|
556
|
+
kind: "repository",
|
|
557
|
+
rules: [
|
|
558
|
+
"name ranking then bounded passage ranking",
|
|
559
|
+
"existing content and names guard",
|
|
560
|
+
],
|
|
561
|
+
restrictions: entryPaths,
|
|
562
|
+
discovered: known(content.length),
|
|
563
|
+
considered: known(retained.length),
|
|
564
|
+
scopeRestricted: true,
|
|
565
|
+
criteria: [],
|
|
566
|
+
});
|
|
567
|
+
const idByPath = Object.fromEntries(retained.map((item, index) => [item.path, `c${index + 1}`]));
|
|
568
|
+
const pathById = Object.fromEntries(retained.map((item, index) => [`c${index + 1}`, item.path]));
|
|
569
|
+
const pointer = async (paths) => {
|
|
570
|
+
return judge({
|
|
571
|
+
goal: args.goal,
|
|
572
|
+
candidates: paths.map((path) => ({
|
|
573
|
+
id: idByPath[path] ?? "",
|
|
574
|
+
path,
|
|
575
|
+
excerpt: files[path] ?? "",
|
|
576
|
+
})),
|
|
577
|
+
}, {
|
|
578
|
+
entry: pointerQuestion({
|
|
579
|
+
instructions: `Which candidate is the best entry point for this goal: ${args.goal}? Choose none if no passage helps.`,
|
|
580
|
+
noneLabel: "No candidate implements or documents the goal",
|
|
581
|
+
candidates: paths.map((path) => ({
|
|
582
|
+
id: idByPath[path] ?? "",
|
|
583
|
+
label: `Candidate ${idByPath[path]}`,
|
|
584
|
+
})),
|
|
585
|
+
}),
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
const paths = retained.map((item) => item.path);
|
|
589
|
+
const firstResult = await pointer(paths);
|
|
590
|
+
if (!firstResult.ok && !budget)
|
|
591
|
+
return finish({ refusal: firstResult.error });
|
|
592
|
+
const first = firstResult.ok ? firstResult.answers.entry : undefined;
|
|
593
|
+
primary = first;
|
|
594
|
+
if (first?.type !== "choice") {
|
|
595
|
+
unchecked.push("entry");
|
|
596
|
+
return fallback();
|
|
597
|
+
}
|
|
598
|
+
const ranked = (probabilities) => readPointer(probabilities).ranked.map(({ id, p }) => ({ path: id, p }));
|
|
599
|
+
const firstRanks = ranked(first.probabilities);
|
|
600
|
+
const firstTop = firstRanks[0];
|
|
601
|
+
if (!firstTop)
|
|
602
|
+
return finish({ refusal: "Pointer returned no probabilities." });
|
|
603
|
+
let ranks = firstRanks;
|
|
604
|
+
let band = firstTop.p >= C.ORDER_REVERSE_BELOW ? "verdict" : "unsure";
|
|
605
|
+
let disagreement = false;
|
|
606
|
+
let orderDependent = false;
|
|
607
|
+
let orderProbabilities;
|
|
608
|
+
if (retained.length > 1 &&
|
|
609
|
+
(args.effort === "thorough" ||
|
|
610
|
+
(args.effort !== "quick" &&
|
|
611
|
+
needsReverse(first.probabilities, retained.length)))) {
|
|
612
|
+
confirmationRequired = true;
|
|
613
|
+
const secondResult = await pointer([...paths].reverse());
|
|
614
|
+
if (!secondResult.ok && !budget) {
|
|
615
|
+
auxiliary.passages.notJudged--;
|
|
616
|
+
auxiliary.controls.notJudged++;
|
|
617
|
+
return finish({ refusal: secondResult.error });
|
|
618
|
+
}
|
|
619
|
+
const second = secondResult.ok ? secondResult.answers.entry : undefined;
|
|
620
|
+
confirmation = second;
|
|
621
|
+
if (second?.type === "choice" && second.source) {
|
|
622
|
+
auxiliary.passages[second.source]--;
|
|
623
|
+
auxiliary.controls[second.source]++;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
auxiliary.passages.notJudged--;
|
|
627
|
+
auxiliary.controls.notJudged++;
|
|
628
|
+
diagnostic(second?.type === "unjudged"
|
|
629
|
+
? (second.cause ?? "control_failure")
|
|
630
|
+
: "control_failure", "required reversed-order entry control was not judged");
|
|
631
|
+
return fallback();
|
|
632
|
+
}
|
|
633
|
+
{
|
|
634
|
+
const secondRanks = ranked(second.probabilities);
|
|
635
|
+
const combined = readPointer(first.probabilities, second.probabilities);
|
|
636
|
+
ranks = combined.ranked.map(({ id, p }) => ({ path: id, p }));
|
|
637
|
+
disagreement = firstTop.path !== secondRanks[0]?.path;
|
|
638
|
+
const chosen = ranks[0]?.path ?? firstTop.path;
|
|
639
|
+
orderProbabilities = [
|
|
640
|
+
first.probabilities[chosen] ?? 0,
|
|
641
|
+
second.probabilities[chosen] ?? 0,
|
|
642
|
+
];
|
|
643
|
+
orderDependent = combined.orderDependent;
|
|
644
|
+
band =
|
|
645
|
+
!disagreement && (ranks[0]?.p ?? 0) >= C.FIND_CONFIRM_MIN
|
|
646
|
+
? "verdict"
|
|
647
|
+
: "unsure";
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (disagreement)
|
|
651
|
+
limitations.push({
|
|
652
|
+
fact: "the two orders disagree",
|
|
653
|
+
next: ranks.slice(0, 2).every((item) => item.path !== "none")
|
|
654
|
+
? "read both"
|
|
655
|
+
: "read the named file if present; rephrase goal or widen scope",
|
|
656
|
+
});
|
|
657
|
+
else if (band === "unsure")
|
|
658
|
+
limitations.push({
|
|
659
|
+
fact: "entry probability below confirmation threshold",
|
|
660
|
+
next: ranks.slice(0, 2).every((item) => item.path !== "none")
|
|
661
|
+
? "read both"
|
|
662
|
+
: "read the named file if present; rephrase goal or widen scope",
|
|
663
|
+
});
|
|
664
|
+
const top = ranks[0];
|
|
665
|
+
if (!top)
|
|
666
|
+
return finish({ refusal: "Pointer returned no probabilities." });
|
|
667
|
+
if (top.path === "none")
|
|
668
|
+
limitations.push({
|
|
669
|
+
fact: "pointer chose none",
|
|
670
|
+
next: "rephrase goal or widen scope",
|
|
671
|
+
});
|
|
672
|
+
return output(top.path === "none" ? "none" : (pathById[top.path] ?? top.path), top.p, short || budget ? "unsure" : band, band === "unsure" || short
|
|
673
|
+
? ranks.slice(1, 2).map((item) => ({
|
|
674
|
+
path: item.path === "none"
|
|
675
|
+
? "none"
|
|
676
|
+
: (pathById[item.path] ?? item.path),
|
|
677
|
+
p: item.p,
|
|
678
|
+
}))
|
|
679
|
+
: [], orderDependent, orderProbabilities);
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|