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
@@ -1,5 +1,9 @@
1
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
+ import {
4
+ resolveEvidenceContext,
5
+ withEvidenceContext,
6
+ } from "../adapters/evidence-context.ts";
3
7
  import { shareGitInventory } from "../adapters/git-inventory.ts";
4
8
  import { readLocateFile, scanRange } from "../adapters/locate-file.ts";
5
9
  import { loadSyntaxParser } from "../adapters/syntax.ts";
@@ -26,18 +30,34 @@ import {
26
30
  type Limitation,
27
31
  } from "../core/output.ts";
28
32
  import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.ts";
33
+ import {
34
+ type Action,
35
+ answerItemFromInput,
36
+ buildResultReport,
37
+ type Cause,
38
+ contextFromEvidence,
39
+ type Diagnostic,
40
+ type Item,
41
+ known,
42
+ type RawValue,
43
+ unknown,
44
+ } from "../core/result-report.ts";
29
45
  import { type Section, sectionFile } from "../core/sections.ts";
30
46
  import type { SyntaxParser } from "../core/units.ts";
31
47
  import { interpolate } from "../describe.ts";
32
48
  import type { GuideContext } from "../guide.ts";
33
- import type { Judgment } from "../jev/types.ts";
34
- import { renderEnvelope } from "../render.ts";
49
+ import type { Answer, Judgment } from "../jev/types.ts";
50
+ import { renderResultReport } from "../render.ts";
35
51
  import type { ToolDependencies } from "../runtime.ts";
36
52
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
37
53
  import { LOCATE_DESCRIPTION } from "../texts/locate.ts";
38
54
 
39
55
  const parameters = Type.Object(
40
- { path: Type.String({ minLength: 1 }), goal: Type.String({ minLength: 1 }) },
56
+ {
57
+ root: Type.Optional(Type.String()),
58
+ path: Type.String({ minLength: 1 }),
59
+ goal: Type.String({ minLength: 1 }),
60
+ },
41
61
  { additionalProperties: false },
42
62
  );
43
63
  type LocateArgs = Static<typeof parameters>;
@@ -74,10 +94,97 @@ export function createLocateTool(
74
94
  ctx: { cwd: string } & GuideContext,
75
95
  ) {
76
96
  const client = dependencies.client;
97
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
98
+ exec: execute,
99
+ signal,
100
+ origin: dependencies.evidenceOrigin,
101
+ });
102
+ const evidenceContext = evidence.context;
103
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
77
104
  const started = performance.now();
78
105
  const exec = shareGitInventory(execute);
79
106
  const results: Judgment[] = [];
80
107
  const limitations: Limitation[] = [];
108
+ const context = contextFromEvidence(evidenceContext);
109
+ const diagnostics: Diagnostic[] = [];
110
+ const actions: Action[] = [];
111
+ const auxiliary = {
112
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
113
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
114
+ };
115
+ let primary: Answer | undefined;
116
+ let control: Answer | undefined;
117
+ let requiredControl = false;
118
+ let failureCause: Cause = "invalid_response";
119
+ const raw = (answer: Answer): RawValue[] =>
120
+ answer.type === "choice"
121
+ ? Object.entries(answer.probabilities).map(
122
+ ([label, probability]) => ({
123
+ label,
124
+ value: label,
125
+ probability: known(probability),
126
+ }),
127
+ )
128
+ : [];
129
+ const diagnose = (cause: Cause, fact: string, members: string[] = []) => {
130
+ const id = `d${diagnostics.length + 1}`,
131
+ actionId = `a${actions.length + 1}`;
132
+ const code =
133
+ cause === "invalid_root"
134
+ ? "correct_context"
135
+ : cause === "not_configured"
136
+ ? "configure_client"
137
+ : cause === "transport_failure" || cause === "service_unavailable"
138
+ ? "recover_service"
139
+ : "inspect_native";
140
+ actions.push({
141
+ id: actionId,
142
+ code,
143
+ target: known(args.path),
144
+ scope: { kind: "call" },
145
+ condition:
146
+ code === "correct_context"
147
+ ? "If the requested root is incorrect"
148
+ : code === "recover_service"
149
+ ? "Only after service recovery is observed"
150
+ : "If the range decision remains open",
151
+ instruction:
152
+ code === "correct_context"
153
+ ? "Use the exact registered worktree top-level; no fallback was performed."
154
+ : code === "configure_client"
155
+ ? "Configure the Jev client or inspect the file natively."
156
+ : "Read or search the cited file within existing permissions; revisit Jev only with decisive new evidence or materially changed context.",
157
+ repeatUnchanged: false,
158
+ });
159
+ diagnostics.push({
160
+ id,
161
+ cause,
162
+ fact,
163
+ target: known(
164
+ cause === "invalid_root" ? (args.root ?? cwd) : args.path,
165
+ ),
166
+ origin:
167
+ cause === "invalid_root" || cause === "forbidden_path"
168
+ ? "input"
169
+ : cause === "control_failure"
170
+ ? "control"
171
+ : cause === "session_budget"
172
+ ? "budget"
173
+ : cause === "service_unavailable" ||
174
+ cause === "transport_failure" ||
175
+ cause === "invalid_response" ||
176
+ cause === "provider_context_refusal" ||
177
+ cause === "not_configured"
178
+ ? "provider"
179
+ : "collection",
180
+ scope: { kind: "call" },
181
+ effect: "blocking",
182
+ material: true,
183
+ omittedMembers: members,
184
+ memberCount: known(members.length),
185
+ actionIds: [actionId],
186
+ });
187
+ };
81
188
  const finish = (input: Omit<EnvelopeInput, "yield">) => {
82
189
  const usage = results.reduce(
83
190
  (sum, result) => ({
@@ -107,24 +214,182 @@ export function createLocateTool(
107
214
  elapsedMs: performance.now() - started,
108
215
  },
109
216
  });
217
+ if (input.refusal) diagnose(failureCause, input.refusal);
218
+ if (input.answers?.[0] && primary?.source)
219
+ actions.push({
220
+ id: `a${actions.length + 1}`,
221
+ code: "inspect_native",
222
+ target: known(
223
+ input.answers[0].label === "none"
224
+ ? args.path
225
+ : input.answers[0].label,
226
+ ),
227
+ scope: { kind: "group", groupIds: ["pointer"] },
228
+ condition: "If the range decision remains open",
229
+ instruction:
230
+ input.answers[0].label === "none"
231
+ ? "Search elsewhere for the goal; read any cited alternative range if the result is unsure. Do not retry unchanged evidence."
232
+ : "Read the current primary and alternative ranges; retain uncertainty if no decisive new evidence is available. Do not retry unchanged evidence.",
233
+ repeatUnchanged: false,
234
+ });
235
+ if (
236
+ !input.refusal &&
237
+ (!primary?.source || (requiredControl && !control?.source))
238
+ )
239
+ diagnose(
240
+ "invalid_response",
241
+ "pointer decision lacks established answer provenance or its required control",
242
+ );
243
+ const common = {
244
+ id: "pointer",
245
+ kind: "pointer" as const,
246
+ label: args.path,
247
+ groupId: "pointer",
248
+ evidence: primary
249
+ ? [
250
+ {
251
+ target: args.path,
252
+ canonicalPath: evidenceContext.effectiveRoot
253
+ ? known(
254
+ `${evidenceContext.effectiveRoot.path}/${args.path}`,
255
+ )
256
+ : unknown("file admission not established"),
257
+ aliases: [],
258
+ side: "current" as const,
259
+ revision: context.resolvedBase,
260
+ },
261
+ ]
262
+ : [],
263
+ diagnosticIds: diagnostics.map((d) => d.id),
264
+ actionIds: actions.map((a) => a.id),
265
+ };
266
+ if (input.answers?.[0]) common.label = input.answers[0].label;
267
+ const displayed = input.answers?.[0];
268
+ let item: Item = { ...common, treatment: "not_judged", source: "none" };
269
+ if (
270
+ displayed &&
271
+ primary?.type === "choice" &&
272
+ primary.source &&
273
+ (!requiredControl || (control?.type === "choice" && control.source))
274
+ ) {
275
+ auxiliary.passages[primary.source]--;
276
+ item = answerItemFromInput(common, {
277
+ ...displayed,
278
+ answer: primary,
279
+ publicResult: displayed.value?.head ?? primary.choice,
280
+ reason: [...limitations, ...(input.limitations ?? [])]
281
+ .map((limit) => limit.fact)
282
+ .join("; "),
283
+ rawValues: raw(primary),
284
+ reportControls:
285
+ control?.type === "choice" && control.source
286
+ ? [
287
+ {
288
+ id: "pointer-order",
289
+ kind: "order",
290
+ source: control.source,
291
+ outcome: displayed.orderDependent
292
+ ? "order-dependent"
293
+ : "consistent",
294
+ rawValues: raw(control),
295
+ },
296
+ ]
297
+ : [],
298
+ });
299
+ }
300
+ const report = buildResultReport({
301
+ tool: "jev_locate_in_file",
302
+ context,
303
+ items: [item],
304
+ diagnostics,
305
+ actions,
306
+ metrics: {
307
+ ...metrics,
308
+ costUsd: results.every((r) => !r.calls || r.usage !== undefined)
309
+ ? usage.costUsd
310
+ : undefined,
311
+ elapsedMs: performance.now() - started,
312
+ },
313
+ auxiliary,
314
+ total: known(1),
315
+ refused: Boolean(
316
+ input.refusal &&
317
+ (failureCause === "invalid_root" ||
318
+ failureCause === "forbidden_path" ||
319
+ failureCause === "outside_inventory" ||
320
+ failureCause === "group_too_large" ||
321
+ failureCause === "evidence_too_large"),
322
+ ),
323
+ });
110
324
  runtime.session.record(envelope);
111
325
  runtime.guide.deliver(ctx);
112
326
  return {
113
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
114
- details: { results },
327
+ content: [
328
+ {
329
+ type: "text" as const,
330
+ text: renderResultReport(report, {
331
+ details: {
332
+ lines: [
333
+ ...envelope.lines,
334
+ {
335
+ type: "list" as const,
336
+ title: "pointer reservations",
337
+ items: [...limitations, ...(input.limitations ?? [])].map(
338
+ (limit) => limit.fact,
339
+ ),
340
+ },
341
+ ...(displayed
342
+ ? [
343
+ {
344
+ type: "list" as const,
345
+ title: "read ranges",
346
+ items: [
347
+ ...(displayed.label === "none"
348
+ ? []
349
+ : [displayed.label]),
350
+ ...(displayed.candidates ?? [])
351
+ .filter(
352
+ (candidate) => candidate.label !== "none",
353
+ )
354
+ .map(
355
+ (candidate) =>
356
+ `${candidate.label} (${candidate.value.p})`,
357
+ ),
358
+ ],
359
+ },
360
+ ]
361
+ : []),
362
+ ],
363
+ },
364
+ }),
365
+ },
366
+ ],
367
+ details: { results, evidenceContext, result: report },
115
368
  ...hostUsage(
116
369
  host.isOmp,
117
370
  results.some((r) => r.usage) ? usage : undefined,
118
371
  ),
119
372
  };
120
373
  };
121
- const file = await readLocateFile(ctx.cwd, args.path, signal, exec);
122
- if (!file.ok) return finish({ refusal: file.error });
374
+ if (!evidence.ok) {
375
+ failureCause = evidence.cause ?? "invalid_root";
376
+ return finish({ refusal: evidence.error });
377
+ }
378
+ failureCause = "file_unavailable";
379
+ const file = await readLocateFile(cwd, args.path, signal, exec);
380
+ if (!file.ok) {
381
+ failureCause = file.cause ?? "file_unavailable";
382
+ return finish({ refusal: file.error });
383
+ }
384
+ failureCause = "outside_inventory";
123
385
  if (file.bytes < LOCATE_MIN_KB * 1000)
124
386
  return finish({
125
387
  refusal: `small file (${file.lines} lines): read it directly`,
126
388
  });
127
- if (!client) return finish({ refusal: NOT_CONFIGURED });
389
+ if (!client) {
390
+ failureCause = "not_configured";
391
+ return finish({ refusal: NOT_CONFIGURED });
392
+ }
128
393
  let sections: Section[];
129
394
  if (file.kind === "whole") {
130
395
  const parser = await loadParser();
@@ -167,8 +432,66 @@ export function createLocateTool(
167
432
  });
168
433
  }
169
434
  const sectionCount = sections.length;
435
+ context.inventories.push({
436
+ id: "sections",
437
+ kind: "sections",
438
+ rules: [
439
+ file.kind === "whole"
440
+ ? "declarations or heuristic sections from complete file"
441
+ : "bounded window outline",
442
+ ],
443
+ restrictions: [
444
+ args.path,
445
+ ...sections.map(
446
+ (section) => `${section.id}: ${section.start}-${section.end}`,
447
+ ),
448
+ ],
449
+ discovered: known(sectionCount),
450
+ considered: known(sectionCount),
451
+ scopeRestricted: true,
452
+ criteria: [],
453
+ });
454
+ if (!sectionCount) {
455
+ diagnose(
456
+ "collection_empty",
457
+ "no sections discovered; no pointer judgment",
458
+ );
459
+ return finish({});
460
+ }
461
+ failureCause = "group_too_large";
170
462
  const judge = async (candidates: Section[], outline = false) => {
171
- const state = sectionState(args.path, args.goal, candidates, outline);
463
+ primary = undefined;
464
+ control = undefined;
465
+ requiredControl = false;
466
+ const inventory = context.inventories.find(
467
+ (inventory) => inventory.id === "pointer-candidates",
468
+ );
469
+ const restricted = {
470
+ id: "pointer-candidates",
471
+ kind: "sections" as const,
472
+ rules: ["actual candidates for the latest pointer stage"],
473
+ restrictions: [
474
+ args.path,
475
+ ...candidates.map(
476
+ (section) => `${section.id}: ${section.start}-${section.end}`,
477
+ ),
478
+ ],
479
+ discovered: known(candidates.length),
480
+ considered: known(candidates.length),
481
+ scopeRestricted: true,
482
+ criteria: [],
483
+ };
484
+ if (inventory) Object.assign(inventory, restricted);
485
+ else context.inventories.push(restricted);
486
+ const state = withEvidenceContext(
487
+ sectionState(args.path, args.goal, candidates, outline),
488
+ evidenceContext,
489
+ );
490
+ if (JSON.stringify(state).length > STATE_MAX_CHARS)
491
+ return {
492
+ ok: false as const,
493
+ error: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
494
+ };
172
495
  const ask = async (reverse: boolean) => {
173
496
  const result = await client.judge(
174
497
  state,
@@ -182,13 +505,29 @@ export function createLocateTool(
182
505
  },
183
506
  {
184
507
  signal,
508
+ ...runtime.session.requestGate(),
185
509
  beforeRequest: (n) => runtime.session.admit(n),
510
+ admissionCause: () => "session_budget",
186
511
  onUsage: (usage) => runtime.session.recordUsage(usage),
187
512
  },
188
513
  );
189
514
  results.push(result);
515
+ if (!result.ok) {
516
+ failureCause = result.failureCause ?? "invalid_response";
517
+ if (reverse) auxiliary.controls.notJudged++;
518
+ else auxiliary.passages.notJudged++;
519
+ }
190
520
  if (!result.ok) return { ok: false as const, error: result.error };
191
521
  const answer = result.answers.pointer;
522
+ if (reverse) control = answer;
523
+ else primary = answer;
524
+ if (answer?.type === "unjudged")
525
+ failureCause = answer.cause ?? "invalid_response";
526
+ if (answer?.type === "choice" && answer.source) {
527
+ if (reverse) auxiliary.controls[answer.source]++;
528
+ else auxiliary.passages[answer.source]++;
529
+ } else if (reverse) auxiliary.controls.notJudged++;
530
+ else auxiliary.passages.notJudged++;
192
531
  return answer?.type === "choice"
193
532
  ? { ok: true as const, probabilities: answer.probabilities }
194
533
  : {
@@ -203,25 +542,51 @@ export function createLocateTool(
203
542
  if (!first.ok) return first;
204
543
  let pointer = readPointer(first.probabilities);
205
544
  if (needsReverse(first.probabilities, candidates.length)) {
545
+ requiredControl = true;
206
546
  const second = await ask(true);
207
- if (!second.ok) return second;
547
+ if (!second.ok) {
548
+ if (failureCause === "invalid_response")
549
+ failureCause = "control_failure";
550
+ return second;
551
+ }
208
552
  pointer = readPointer(first.probabilities, second.probabilities);
209
553
  }
210
554
  return { ok: true as const, ...pointer };
211
555
  };
556
+ const emptyState = sectionState(args.path, args.goal, []);
557
+ const contextChars =
558
+ JSON.stringify(withEvidenceContext(emptyState, evidenceContext))
559
+ .length - JSON.stringify(emptyState).length;
560
+ const evidenceMaxChars = STATE_MAX_CHARS - contextChars;
212
561
  const fits = (candidates: Section[]) =>
213
- sectionStateFits(args.path, args.goal, candidates);
562
+ sectionStateFits(
563
+ args.path,
564
+ args.goal,
565
+ candidates,
566
+ false,
567
+ evidenceMaxChars,
568
+ );
214
569
  const twoStage = file.kind === "outline" || !fits(sections);
215
570
  let planUnsure = false;
216
571
  if (twoStage) {
217
- const outline = sectionOutline(args.path, args.goal, sections);
572
+ const outline = sectionOutline(
573
+ args.path,
574
+ args.goal,
575
+ sections,
576
+ evidenceMaxChars,
577
+ );
218
578
  if (outline.length < 2 || outline.length >= CHOICE_MAX_OPTIONS)
219
579
  return finish({
220
580
  refusal:
221
581
  "No usable multi-section plan fits the budget; use grep or read directly.",
222
582
  });
223
- const evidence = outlineEvidence(args.path, args.goal, outline);
224
- if (!sectionStateFits(args.path, args.goal, evidence))
583
+ const evidence = outlineEvidence(
584
+ args.path,
585
+ args.goal,
586
+ outline,
587
+ evidenceMaxChars,
588
+ );
589
+ if (!fits(evidence))
225
590
  return finish({
226
591
  refusal: "Block plan exceeds the state budget; use grep.",
227
592
  });
@@ -270,14 +635,11 @@ export function createLocateTool(
270
635
  });
271
636
  }
272
637
  if (file.kind === "outline") {
273
- const selected = await scanRange(
274
- ctx.cwd,
275
- args.path,
276
- block,
277
- signal,
278
- exec,
279
- );
280
- if (!selected.ok) return finish({ refusal: selected.error });
638
+ const selected = await scanRange(cwd, args.path, block, signal, exec);
639
+ if (!selected.ok) {
640
+ failureCause = selected.cause ?? "file_unavailable";
641
+ return finish({ refusal: selected.error });
642
+ }
281
643
  const lines = selected.text.split("\n");
282
644
  sections = sections
283
645
  .filter((s) => s.start >= block.start && s.end <= block.end)
@@ -337,7 +699,7 @@ export function createLocateTool(
337
699
  );
338
700
  if (!displayed)
339
701
  return finish({ refusal: "Pointer probabilities missing." });
340
- const { primary, band, candidates } = displayed;
702
+ const { primary: displayedPrimary, band, candidates } = displayed;
341
703
  if (band === "unsure" || head.id === "none")
342
704
  limitations.push({
343
705
  fact:
@@ -349,7 +711,7 @@ export function createLocateTool(
349
711
  return finish({
350
712
  answers: [
351
713
  {
352
- ...primary,
714
+ ...displayedPrimary,
353
715
  band,
354
716
  orderDependent: pointer.orderDependent,
355
717
  candidates,
@@ -357,7 +719,7 @@ export function createLocateTool(
357
719
  ],
358
720
  lines:
359
721
  head.id !== "none"
360
- ? [{ type: "list", title: "read", items: [primary.label] }]
722
+ ? [{ type: "list", title: "read", items: [displayedPrimary.label] }]
361
723
  : [],
362
724
  limitations: [
363
725
  {