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.
Files changed (180) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/CONTRIBUTING.md +43 -0
  3. package/README.md +58 -17
  4. package/SECURITY.md +43 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +198 -0
  7. package/dist/adapters/ask-proof.js +200 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +234 -0
  11. package/dist/adapters/docs.js +192 -0
  12. package/dist/adapters/evidence-context.js +119 -0
  13. package/dist/adapters/exec.js +207 -0
  14. package/dist/adapters/files.js +418 -0
  15. package/dist/adapters/find.js +150 -0
  16. package/dist/adapters/git-base.js +32 -0
  17. package/dist/adapters/git-inventory.js +71 -0
  18. package/dist/adapters/git.js +483 -0
  19. package/dist/adapters/locate-file.js +197 -0
  20. package/dist/adapters/output-lines.js +46 -0
  21. package/dist/adapters/private-storage.js +106 -0
  22. package/dist/adapters/risk-callers.js +429 -0
  23. package/dist/adapters/runner-version.js +78 -0
  24. package/dist/adapters/shell.js +92 -0
  25. package/dist/adapters/syntax.js +187 -0
  26. package/dist/adapters/test-inventory.js +139 -0
  27. package/dist/adapters/usage.js +20 -0
  28. package/dist/adapters/utf8.js +47 -0
  29. package/dist/configuration.js +267 -0
  30. package/dist/constants.js +140 -0
  31. package/dist/core/ask-closure.js +282 -0
  32. package/dist/core/ask-proof.js +1 -0
  33. package/dist/core/ask-references.js +278 -0
  34. package/dist/core/asks.js +507 -0
  35. package/dist/core/batches.js +65 -0
  36. package/dist/core/command-output.js +224 -0
  37. package/dist/core/diff.js +178 -0
  38. package/dist/core/docs.js +302 -0
  39. package/dist/core/find.js +108 -0
  40. package/dist/core/git.js +1 -0
  41. package/dist/core/imports.js +550 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/lexical.js +132 -0
  44. package/dist/core/locate.js +169 -0
  45. package/dist/core/output.js +137 -0
  46. package/dist/core/pointer.js +29 -0
  47. package/dist/core/result-report.js +302 -0
  48. package/dist/core/risk-callers.js +851 -0
  49. package/dist/core/runner-version.js +45 -0
  50. package/dist/core/secret-path.js +34 -0
  51. package/dist/core/sections.js +230 -0
  52. package/dist/core/state.js +51 -0
  53. package/dist/core/syntax.js +1 -0
  54. package/dist/core/test-commands.js +334 -0
  55. package/dist/core/test-coverage.js +74 -0
  56. package/dist/core/test-discovery.js +1382 -0
  57. package/dist/core/test-evidence.js +527 -0
  58. package/dist/core/test-state.js +81 -0
  59. package/dist/core/truncate.js +12 -0
  60. package/dist/core/units.js +349 -0
  61. package/dist/describe.js +23 -0
  62. package/dist/guide.js +33 -0
  63. package/dist/host.js +24 -0
  64. package/dist/jev/client.js +456 -0
  65. package/dist/jev/pool.js +54 -0
  66. package/dist/jev/types.js +1 -0
  67. package/dist/mcp/main.js +124 -0
  68. package/dist/mcp/protocol.js +210 -0
  69. package/dist/mcp/tools.js +129 -0
  70. package/dist/presets/docs.js +62 -0
  71. package/dist/presets/risk.js +179 -0
  72. package/dist/presets/spec.js +81 -0
  73. package/dist/presets/witnesses.js +249 -0
  74. package/dist/render.js +114 -0
  75. package/dist/report-schema.js +1356 -0
  76. package/dist/result-types.js +1 -0
  77. package/dist/result.js +3 -0
  78. package/dist/runtime.js +1 -0
  79. package/dist/session.js +147 -0
  80. package/dist/texts/ask-files.js +3 -0
  81. package/dist/texts/ask.js +4 -0
  82. package/dist/texts/check-diff.js +20 -0
  83. package/dist/texts/configuration.js +1 -0
  84. package/dist/texts/find.js +19 -0
  85. package/dist/texts/guide.js +3 -0
  86. package/dist/texts/instructions.js +72 -0
  87. package/dist/texts/locate.js +15 -0
  88. package/dist/texts/select-tests.js +4 -0
  89. package/dist/tools/ask-files.js +450 -0
  90. package/dist/tools/ask-schema.js +70 -0
  91. package/dist/tools/ask.js +1147 -0
  92. package/dist/tools/check-diff.js +594 -0
  93. package/dist/tools/docs-check.js +408 -0
  94. package/dist/tools/find.js +682 -0
  95. package/dist/tools/locate.js +602 -0
  96. package/dist/tools/review-report.js +230 -0
  97. package/dist/tools/select-tests.js +821 -0
  98. package/dist/tools/spec-check.js +263 -0
  99. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  100. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  101. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  102. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  103. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  104. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  105. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  106. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  107. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  108. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  109. package/docs/agent-instructions.md +120 -0
  110. package/docs/design.md +16 -4
  111. package/docs/mcp.md +233 -0
  112. package/docs/tools/jev_ask.md +8 -5
  113. package/docs/tools/jev_ask_files.md +2 -1
  114. package/docs/tools/jev_check_diff.md +4 -1
  115. package/docs/tools/jev_find_files.md +2 -1
  116. package/docs/tools/jev_locate_in_file.md +5 -0
  117. package/docs/tools/jev_select_tests.md +4 -1
  118. package/package.json +19 -4
  119. package/rules/jev-ask.md +22 -1
  120. package/server.json +57 -0
  121. package/src/adapters/ask-files.ts +11 -3
  122. package/src/adapters/ask-proof.ts +69 -11
  123. package/src/adapters/canonical-path.ts +18 -0
  124. package/src/adapters/command.ts +102 -36
  125. package/src/adapters/docs.ts +33 -14
  126. package/src/adapters/evidence-context.ts +169 -0
  127. package/src/adapters/exec.ts +226 -0
  128. package/src/adapters/files.ts +146 -16
  129. package/src/adapters/find.ts +37 -7
  130. package/src/adapters/git-base.ts +7 -1
  131. package/src/adapters/git.ts +61 -8
  132. package/src/adapters/locate-file.ts +51 -9
  133. package/src/adapters/private-storage.ts +155 -0
  134. package/src/adapters/risk-callers.ts +7 -2
  135. package/src/adapters/shell.ts +113 -0
  136. package/src/adapters/test-inventory.ts +12 -4
  137. package/src/configuration.ts +55 -14
  138. package/src/constants.ts +37 -5
  139. package/src/core/ask-references.ts +262 -146
  140. package/src/core/asks.ts +79 -7
  141. package/src/core/command-output.ts +17 -1
  142. package/src/core/import-boundaries.ts +8 -3
  143. package/src/core/locate.ts +8 -5
  144. package/src/core/output.ts +34 -0
  145. package/src/core/result-report.ts +410 -0
  146. package/src/core/secret-path.ts +37 -0
  147. package/src/core/state.ts +8 -1
  148. package/src/core/units.ts +3 -2
  149. package/src/host.ts +11 -0
  150. package/src/index.ts +3 -0
  151. package/src/jev/client.ts +66 -16
  152. package/src/jev/types.ts +24 -3
  153. package/src/mcp/main.ts +135 -0
  154. package/src/mcp/protocol.ts +332 -0
  155. package/src/mcp/tools.ts +179 -0
  156. package/src/render.ts +109 -0
  157. package/src/report-schema.ts +1380 -0
  158. package/src/result-types.ts +234 -0
  159. package/src/result.ts +4 -1
  160. package/src/runtime.ts +6 -0
  161. package/src/session.ts +59 -0
  162. package/src/setup.ts +13 -5
  163. package/src/texts/ask-files.ts +4 -1
  164. package/src/texts/ask.ts +8 -1
  165. package/src/texts/check-diff.ts +7 -4
  166. package/src/texts/find.ts +8 -2
  167. package/src/texts/guide.ts +8 -16
  168. package/src/texts/instructions.ts +98 -0
  169. package/src/texts/locate.ts +8 -2
  170. package/src/texts/run-end.ts +2 -2
  171. package/src/texts/select-tests.ts +4 -1
  172. package/src/tools/ask-files.ts +311 -18
  173. package/src/tools/ask.ts +722 -95
  174. package/src/tools/check-diff.ts +337 -31
  175. package/src/tools/docs-check.ts +241 -38
  176. package/src/tools/find.ts +389 -29
  177. package/src/tools/locate.ts +387 -25
  178. package/src/tools/review-report.ts +308 -0
  179. package/src/tools/select-tests.ts +484 -23
  180. package/src/tools/spec-check.ts +194 -19
package/src/tools/find.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { type Static, Type } from "@sinclair/typebox";
2
+ import {
3
+ resolveEvidenceContext,
4
+ withEvidenceContext,
5
+ } from "../adapters/evidence-context.ts";
2
6
  import { collectSearchExcerpts } from "../adapters/files.ts";
3
7
  import { prefilter } from "../adapters/find.ts";
4
8
  import { shareGitInventory } from "../adapters/git-inventory.ts";
@@ -18,16 +22,29 @@ import {
18
22
  type EnvelopeInput,
19
23
  } from "../core/output.ts";
20
24
  import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.ts";
25
+ import {
26
+ type Action,
27
+ answerItemFromInput,
28
+ buildResultReport,
29
+ type Cause,
30
+ contextFromEvidence,
31
+ type Diagnostic,
32
+ type Item,
33
+ known,
34
+ type RawValue,
35
+ } from "../core/result-report.ts";
36
+ import { secretRefusal } from "../core/secret-path.ts";
21
37
  import { interpolate } from "../describe.ts";
22
38
  import type { GuideContext } from "../guide.ts";
23
- import type { Judgment, Question, State } from "../jev/types.ts";
24
- import { renderEnvelope } from "../render.ts";
39
+ import type { Answer, Judgment, Question, State } from "../jev/types.ts";
40
+ import { renderResultReport } from "../render.ts";
25
41
  import type { ToolDependencies } from "../runtime.ts";
26
42
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
27
43
  import { FIND_DESCRIPTION } from "../texts/find.ts";
28
44
 
29
45
  export const findParameters = Type.Object(
30
46
  {
47
+ root: Type.Optional(Type.String()),
31
48
  goal: Type.String({ minLength: 1 }),
32
49
  keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
33
50
  scope: Type.Optional(
@@ -74,6 +91,13 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
74
91
  ctx: { cwd: string } & GuideContext,
75
92
  ) {
76
93
  const client = dependencies.client;
94
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
95
+ exec: execute,
96
+ signal,
97
+ origin: dependencies.evidenceOrigin,
98
+ });
99
+ const evidenceContext = evidence.context;
100
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
77
101
  const started = performance.now();
78
102
  const exec = shareGitInventory(execute);
79
103
  const totals = {
@@ -88,6 +112,101 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
88
112
  const unchecked: string[] = [];
89
113
  let budget: BudgetRefusal | undefined;
90
114
  let sent = 0;
115
+ const context = contextFromEvidence(evidenceContext);
116
+ const diagnostics: Diagnostic[] = [];
117
+ const actions: Action[] = [];
118
+ const auxiliary = {
119
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
120
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
121
+ };
122
+ let entryPaths: string[] = [];
123
+ let costObserved = true;
124
+ let failureCause: Cause = "invalid_response";
125
+ let primary: Answer | undefined;
126
+ let confirmation: Answer | undefined;
127
+ let confirmationRequired = false;
128
+ const raw = (answer: Answer): RawValue[] =>
129
+ answer.type === "choice"
130
+ ? Object.entries(answer.probabilities).map(
131
+ ([label, probability]) => ({
132
+ label,
133
+ value: label,
134
+ probability: known(probability),
135
+ }),
136
+ )
137
+ : [];
138
+ const diagnostic = (
139
+ cause: Cause,
140
+ fact: string,
141
+ members: string[] = [],
142
+ ) => {
143
+ const id = `d${diagnostics.length + 1}`;
144
+ const actionId = `a${actions.length + 1}`;
145
+ const code =
146
+ cause === "invalid_root"
147
+ ? "correct_context"
148
+ : cause === "not_configured"
149
+ ? "configure_client"
150
+ : cause === "service_unavailable" || cause === "transport_failure"
151
+ ? "recover_service"
152
+ : "inspect_native";
153
+ actions.push({
154
+ id: actionId,
155
+ code,
156
+ target: known(members.join(", ") || "entry"),
157
+ scope: { kind: "call" },
158
+ condition:
159
+ code === "correct_context"
160
+ ? "If the requested root is incorrect"
161
+ : code === "configure_client"
162
+ ? "If Jev judgment is still useful after configuring the client"
163
+ : code === "recover_service"
164
+ ? "Only after service recovery is observed"
165
+ : "If the entry decision remains open",
166
+ instruction:
167
+ code === "correct_context"
168
+ ? "Provide the exact registered worktree top-level; no fallback was performed."
169
+ : code === "configure_client"
170
+ ? "Configure the Jev client, or continue with native discovery."
171
+ : code === "recover_service"
172
+ ? "Continue natively while unavailable; revisit only after a material service change."
173
+ : "Inspect the candidate paths or collected passages natively; retain uncertainty without repeating unchanged evidence.",
174
+ repeatUnchanged: false,
175
+ });
176
+ diagnostics.push({
177
+ id,
178
+ cause,
179
+ fact,
180
+ target: known(
181
+ members.join(", ") ||
182
+ (cause === "invalid_root"
183
+ ? (args.root ?? cwd)
184
+ : cause === "forbidden_path"
185
+ ? JSON.stringify({ scope: args.scope, exclude: args.exclude })
186
+ : "entry"),
187
+ ),
188
+ origin:
189
+ cause === "invalid_root" || cause === "forbidden_path"
190
+ ? "input"
191
+ : cause === "call_budget" || cause === "session_budget"
192
+ ? "budget"
193
+ : cause === "control_failure"
194
+ ? "control"
195
+ : cause === "service_unavailable" ||
196
+ cause === "transport_failure" ||
197
+ cause === "invalid_response" ||
198
+ cause === "provider_context_refusal" ||
199
+ cause === "not_configured"
200
+ ? "provider"
201
+ : "collection",
202
+ scope: { kind: "call" },
203
+ effect: "blocking",
204
+ material: true,
205
+ omittedMembers: members,
206
+ memberCount: known(members.length),
207
+ actionIds: [actionId],
208
+ });
209
+ };
91
210
  const finish = (input: Omit<EnvelopeInput, "yield">) => {
92
211
  const envelope = buildEnvelope({
93
212
  ...input,
@@ -99,15 +218,166 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
99
218
  elapsedMs: performance.now() - started,
100
219
  },
101
220
  });
221
+ if (input.refusal) diagnostic(failureCause, input.refusal);
222
+ if (
223
+ !input.refusal &&
224
+ !diagnostics.length &&
225
+ !budget &&
226
+ !unchecked.length &&
227
+ !primary?.source
228
+ )
229
+ diagnostic(
230
+ "invalid_response",
231
+ "entry decision was not received with established answer provenance",
232
+ );
233
+ if (budget)
234
+ diagnostic(
235
+ budget.kind === "max_calls" ? "call_budget" : "session_budget",
236
+ budget.message,
237
+ [...new Set(unchecked)],
238
+ );
239
+ if (unchecked.length && !budget)
240
+ diagnostic(
241
+ failureCause,
242
+ "candidate ranking or entry work was not judged",
243
+ [...new Set(unchecked)],
244
+ );
245
+ const entryLine = input.lines?.find((line) => line.type === "entry");
246
+ if (entryLine?.type === "entry" && primary?.source)
247
+ actions.push({
248
+ id: `a${actions.length + 1}`,
249
+ code: "inspect_native",
250
+ target: known(
251
+ (entryLine.nextRead ?? []).join(", ") || "candidate inventory",
252
+ ),
253
+ scope: { kind: "group", groupIds: ["entry"] },
254
+ condition:
255
+ "If the entry decision remains open after this bounded judgment",
256
+ instruction:
257
+ entryLine.candidate.label === "none"
258
+ ? "Search elsewhere or widen the admitted scope for the goal; inspect any named alternative before collecting genuinely new evidence. Do not retry unchanged evidence."
259
+ : "Read the named candidate passages; add only genuinely discriminating evidence if needed. Do not reword unchanged evidence to retry.",
260
+ repeatUnchanged: false,
261
+ });
262
+ const common = {
263
+ id: "entry",
264
+ kind: "entry" as const,
265
+ label: "entry",
266
+ groupId: "entry",
267
+ evidence: entryPaths.map((path) => ({
268
+ target: path,
269
+ canonicalPath: known(`${cwd}/${path}`),
270
+ aliases: [],
271
+ side: "current" as const,
272
+ revision: context.resolvedBase,
273
+ })),
274
+ diagnosticIds: diagnostics.map((d) => d.id),
275
+ actionIds: actions.map((a) => a.id),
276
+ };
277
+ let item: Item = { ...common, treatment: "not_judged", source: "none" };
278
+ if (
279
+ entryLine?.type === "entry" &&
280
+ primary?.type === "choice" &&
281
+ primary.source &&
282
+ (!confirmationRequired ||
283
+ (confirmation?.type === "choice" && confirmation.source))
284
+ ) {
285
+ auxiliary.passages[primary.source]--;
286
+ item = answerItemFromInput(common, {
287
+ label: "entry",
288
+ answer: primary,
289
+ value: entryLine.candidate.value,
290
+ publicResult: entryLine.candidate.value.head,
291
+ band: entryLine.band,
292
+ reason: limitations.map((limit) => limit.fact).join("; "),
293
+ rawValues: raw(primary),
294
+ reportControls:
295
+ confirmation?.type === "choice" && confirmation.source
296
+ ? [
297
+ {
298
+ id: "entry-order",
299
+ kind: "order",
300
+ source: confirmation.source,
301
+ outcome: entryLine.orderDependent
302
+ ? "order-dependent"
303
+ : "consistent",
304
+ rawValues: raw(confirmation),
305
+ },
306
+ ]
307
+ : [],
308
+ });
309
+ }
310
+ const report = buildResultReport({
311
+ tool: "jev_find_files",
312
+ context,
313
+ items: [item],
314
+ diagnostics,
315
+ actions,
316
+ metrics: {
317
+ ...totals,
318
+ costUsd: costObserved ? totals.usage.costUsd : undefined,
319
+ elapsedMs: performance.now() - started,
320
+ },
321
+ auxiliary,
322
+ total: known(1),
323
+ refused: Boolean(
324
+ input.refusal &&
325
+ (failureCause === "invalid_root" ||
326
+ failureCause === "forbidden_path"),
327
+ ),
328
+ missingWork: unchecked.length > 0,
329
+ });
102
330
  runtime.session.record(envelope);
103
331
  runtime.guide.deliver(ctx);
104
332
  return {
105
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
106
- details: totals,
333
+ content: [
334
+ {
335
+ type: "text" as const,
336
+ text: renderResultReport(report, {
337
+ details: {
338
+ lines: [
339
+ ...envelope.lines,
340
+ ...(entryLine?.type === "entry"
341
+ ? [
342
+ {
343
+ type: "list" as const,
344
+ title: "read",
345
+ items: [...(entryLine.nextRead ?? [])],
346
+ },
347
+ {
348
+ type: "list" as const,
349
+ title: "auxiliary ranking",
350
+ items: (entryLine.relevant ?? []).map(
351
+ (candidate) =>
352
+ `${candidate.label} (${candidate.value.p})`,
353
+ ),
354
+ },
355
+ ]
356
+ : []),
357
+ {
358
+ type: "list" as const,
359
+ title: "discovery reservations",
360
+ items: [...limitations, ...(input.limitations ?? [])].map(
361
+ (limit) => limit.fact,
362
+ ),
363
+ },
364
+ ],
365
+ },
366
+ }),
367
+ },
368
+ ],
369
+ details: { ...totals, evidenceContext, result: report },
107
370
  ...hostUsage(host.isOmp, totals.usage),
108
371
  };
109
372
  };
110
- if (!client) return finish({ refusal: NOT_CONFIGURED });
373
+ if (!evidence.ok) {
374
+ failureCause = evidence.cause ?? "invalid_root";
375
+ return finish({ refusal: evidence.error });
376
+ }
377
+ if (!client) {
378
+ failureCause = "not_configured";
379
+ return finish({ refusal: NOT_CONFIGURED });
380
+ }
111
381
  const words = contentWords(args.goal);
112
382
  const short = words.length < C.FIND_GOAL_MIN_WORDS;
113
383
  if (short)
@@ -130,14 +400,37 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
130
400
  : C.FIND_FILES_READ;
131
401
  const candidates = await prefilter(
132
402
  exec,
133
- ctx.cwd,
403
+ cwd,
134
404
  keywords,
135
405
  candidateLimit,
136
406
  args.scope,
137
407
  args.exclude,
138
408
  signal,
139
409
  );
140
- if (!candidates.ok) return finish({ refusal: candidates.error });
410
+ if (!candidates.ok) {
411
+ failureCause = candidates.cause ?? "git_failure";
412
+ return finish({ refusal: candidates.error });
413
+ }
414
+ for (const path of candidates.secret)
415
+ diagnostic("secret_pattern", secretRefusal(path), [path]);
416
+ context.inventories.push({
417
+ id: "candidates",
418
+ kind: "repository",
419
+ rules: [
420
+ "Git tracked and non-ignored untracked files",
421
+ "keyword prefilter with existing effort limit",
422
+ ],
423
+ restrictions: [
424
+ ...(typeof args.scope === "string"
425
+ ? [args.scope]
426
+ : (args.scope ?? [])),
427
+ ...(args.exclude ?? []).map((path) => `exclude ${path}`),
428
+ ],
429
+ discovered: known(candidates.scopeFiles),
430
+ considered: known(candidates.paths.length),
431
+ scopeRestricted: Boolean(args.scope || args.exclude?.length),
432
+ criteria: [],
433
+ });
141
434
  const entry = (
142
435
  path: string,
143
436
  p: number,
@@ -173,23 +466,27 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
173
466
  ],
174
467
  });
175
468
  if (!candidates.paths.length) {
176
- limitations.push({
177
- fact:
178
- candidates.scopeFiles === 0
179
- ? "scope matched 0 files"
180
- : "no file shares a word with the goal",
181
- next: "rephrase goal or widen scope",
182
- });
183
- return finish({
184
- lines: [entry("none", 1, short ? "unsure" : "verdict")],
185
- });
469
+ diagnostic(
470
+ "collection_empty",
471
+ candidates.scopeFiles === 0
472
+ ? "scope matched 0 files; no entry judgment"
473
+ : "no file shares a word with the goal; no entry judgment",
474
+ );
475
+ return finish({});
186
476
  }
187
477
  const judge = async (
188
478
  state: State,
189
479
  questions: Record<string, Question>,
190
480
  ): Promise<Judgment> => {
481
+ state = withEvidenceContext(state, evidenceContext);
482
+ if (JSON.stringify(state).length > C.STATE_MAX_CHARS)
483
+ return {
484
+ ok: false,
485
+ error: `required evidence exceeds STATE_MAX_CHARS=${C.STATE_MAX_CHARS}`,
486
+ };
191
487
  const result = await client.judge(state, questions, {
192
488
  signal,
489
+ ...runtime.session.requestGate(),
193
490
  beforeRequest: (count) => {
194
491
  if (args.max_calls !== undefined && sent >= args.max_calls) {
195
492
  budget = {
@@ -207,6 +504,8 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
207
504
  return admission;
208
505
  },
209
506
  onUsage: (usage) => runtime.session.recordUsage(usage),
507
+ admissionCause: () =>
508
+ budget?.kind === "max_calls" ? "call_budget" : "session_budget",
210
509
  });
211
510
  for (const key of [
212
511
  "calls",
@@ -216,7 +515,21 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
216
515
  ] as const)
217
516
  totals[key] += result[key] ?? 0;
218
517
  totals.usage.inputTokens += result.usage?.inputTokens ?? 0;
518
+ if (result.calls && !result.usage) costObserved = false;
519
+ if (!result.ok) {
520
+ failureCause = result.failureCause ?? "invalid_response";
521
+ auxiliary.passages.notJudged += Object.keys(questions).length;
522
+ }
219
523
  totals.usage.costUsd += result.usage?.costUsd ?? 0;
524
+ if (result.ok)
525
+ for (const id of Object.keys(questions)) {
526
+ const answer = result.answers[id];
527
+ if (answer?.type === "unjudged") {
528
+ failureCause = answer.cause ?? "invalid_response";
529
+ auxiliary.passages.notJudged++;
530
+ } else if (answer?.source) auxiliary.passages[answer.source]++;
531
+ else auxiliary.passages.notJudged++;
532
+ }
220
533
  return result;
221
534
  };
222
535
  const boolQuestions = (
@@ -267,6 +580,12 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
267
580
  const rankedNames = rankFiles(names);
268
581
  let bestRanking = rankedNames;
269
582
  const fallback = () => {
583
+ primary = undefined;
584
+ if (bestRanking.length)
585
+ limitations.push({
586
+ fact: `auxiliary ranking retained: ${bestRanking.map((item) => `${item.path} (${item.p})`).join(" · ")}; entry was not judged`,
587
+ next: "inspect these paths natively",
588
+ });
270
589
  const top = bestRanking.slice(0, 2);
271
590
  return finish({
272
591
  lines: [
@@ -308,13 +627,16 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
308
627
  const contentResults = await Promise.all(
309
628
  contentBatches.map(async (paths) => {
310
629
  const read = await collectSearchExcerpts(
311
- ctx.cwd,
630
+ cwd,
312
631
  paths,
313
632
  keywords,
314
633
  signal,
315
634
  exec,
316
635
  );
317
- if (!read.ok) return { paths, result: read };
636
+ if (!read.ok) {
637
+ failureCause = read.cause ?? "file_unavailable";
638
+ return { paths, result: read };
639
+ }
318
640
  Object.assign(files, read.pointers);
319
641
  limitations.push(
320
642
  ...read.limits.map((limit) => ({
@@ -322,11 +644,18 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
322
644
  next: "provide a UTF-8 text file",
323
645
  })),
324
646
  );
325
- if (read.ignored.length)
647
+ if (read.ignored.length) {
648
+ for (const omission of read.omissions)
649
+ diagnostic(
650
+ omission.cause,
651
+ `${omission.path}: ${omission.reason}`,
652
+ [omission.path],
653
+ );
326
654
  limitations.push({
327
655
  fact: `${read.ignored.length} binary, empty or unreadable paths ignored: ${read.ignored.join(", ")}`,
328
656
  next: "narrow scope to text files",
329
657
  });
658
+ }
330
659
  const textPaths = paths.filter(
331
660
  (path) => read.files[path] !== undefined,
332
661
  );
@@ -395,12 +724,26 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
395
724
  ],
396
725
  });
397
726
  if (!retained.length) {
398
- limitations.push({
399
- fact: "no file retained by content or names guard",
400
- next: "rephrase goal or widen scope",
401
- });
402
- return output("none", 1, short ? "unsure" : "verdict");
727
+ diagnostic(
728
+ "collection_empty",
729
+ "no file retained by content or names guard; no entry decision was judged",
730
+ );
731
+ return finish({});
403
732
  }
733
+ entryPaths = retained.map((item) => item.path);
734
+ context.inventories.push({
735
+ id: "entry-passages",
736
+ kind: "repository",
737
+ rules: [
738
+ "name ranking then bounded passage ranking",
739
+ "existing content and names guard",
740
+ ],
741
+ restrictions: entryPaths,
742
+ discovered: known(content.length),
743
+ considered: known(retained.length),
744
+ scopeRestricted: true,
745
+ criteria: [],
746
+ });
404
747
  const idByPath = Object.fromEntries(
405
748
  retained.map((item, index) => [item.path, `c${index + 1}`]),
406
749
  );
@@ -434,6 +777,7 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
434
777
  if (!firstResult.ok && !budget)
435
778
  return finish({ refusal: firstResult.error });
436
779
  const first = firstResult.ok ? firstResult.answers.entry : undefined;
780
+ primary = first;
437
781
  if (first?.type !== "choice") {
438
782
  unchecked.push("entry");
439
783
  return fallback();
@@ -456,14 +800,30 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
456
800
  (args.effort !== "quick" &&
457
801
  needsReverse(first.probabilities, retained.length)))
458
802
  ) {
803
+ confirmationRequired = true;
459
804
  const secondResult = await pointer([...paths].reverse());
460
- if (!secondResult.ok && !budget)
805
+ if (!secondResult.ok && !budget) {
806
+ auxiliary.passages.notJudged--;
807
+ auxiliary.controls.notJudged++;
461
808
  return finish({ refusal: secondResult.error });
809
+ }
462
810
  const second = secondResult.ok ? secondResult.answers.entry : undefined;
463
- if (second?.type !== "choice") {
464
- unchecked.push("entry confirmation");
465
- band = "unsure";
811
+ confirmation = second;
812
+ if (second?.type === "choice" && second.source) {
813
+ auxiliary.passages[second.source]--;
814
+ auxiliary.controls[second.source]++;
466
815
  } else {
816
+ auxiliary.passages.notJudged--;
817
+ auxiliary.controls.notJudged++;
818
+ diagnostic(
819
+ second?.type === "unjudged"
820
+ ? (second.cause ?? "control_failure")
821
+ : "control_failure",
822
+ "required reversed-order entry control was not judged",
823
+ );
824
+ return fallback();
825
+ }
826
+ {
467
827
  const secondRanks = ranked(second.probabilities);
468
828
  const combined = readPointer(
469
829
  first.probabilities,