jev-agent-tools 0.2.0 → 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 (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
@@ -1,6 +1,9 @@
1
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
- import { canonicalPath } from "../adapters/canonical-path.ts";
3
+ import {
4
+ resolveEvidenceContext,
5
+ withEvidenceContext,
6
+ } from "../adapters/evidence-context.ts";
4
7
  import { shareGitInventory } from "../adapters/git-inventory.ts";
5
8
  import { readLocateFile, scanRange } from "../adapters/locate-file.ts";
6
9
  import { loadSyntaxParser } from "../adapters/syntax.ts";
@@ -27,18 +30,34 @@ import {
27
30
  type Limitation,
28
31
  } from "../core/output.ts";
29
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";
30
45
  import { type Section, sectionFile } from "../core/sections.ts";
31
46
  import type { SyntaxParser } from "../core/units.ts";
32
47
  import { interpolate } from "../describe.ts";
33
48
  import type { GuideContext } from "../guide.ts";
34
- import type { Judgment } from "../jev/types.ts";
35
- import { renderEnvelope } from "../render.ts";
49
+ import type { Answer, Judgment } from "../jev/types.ts";
50
+ import { renderResultReport } from "../render.ts";
36
51
  import type { ToolDependencies } from "../runtime.ts";
37
52
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
38
53
  import { LOCATE_DESCRIPTION } from "../texts/locate.ts";
39
54
 
40
55
  const parameters = Type.Object(
41
- { 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
+ },
42
61
  { additionalProperties: false },
43
62
  );
44
63
  type LocateArgs = Static<typeof parameters>;
@@ -75,11 +94,97 @@ export function createLocateTool(
75
94
  ctx: { cwd: string } & GuideContext,
76
95
  ) {
77
96
  const client = dependencies.client;
78
- const cwd = await canonicalPath(ctx.cwd);
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;
79
104
  const started = performance.now();
80
105
  const exec = shareGitInventory(execute);
81
106
  const results: Judgment[] = [];
82
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
+ };
83
188
  const finish = (input: Omit<EnvelopeInput, "yield">) => {
84
189
  const usage = results.reduce(
85
190
  (sum, result) => ({
@@ -109,24 +214,182 @@ export function createLocateTool(
109
214
  elapsedMs: performance.now() - started,
110
215
  },
111
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
+ });
112
324
  runtime.session.record(envelope);
113
325
  runtime.guide.deliver(ctx);
114
326
  return {
115
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
116
- 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 },
117
368
  ...hostUsage(
118
369
  host.isOmp,
119
370
  results.some((r) => r.usage) ? usage : undefined,
120
371
  ),
121
372
  };
122
373
  };
374
+ if (!evidence.ok) {
375
+ failureCause = evidence.cause ?? "invalid_root";
376
+ return finish({ refusal: evidence.error });
377
+ }
378
+ failureCause = "file_unavailable";
123
379
  const file = await readLocateFile(cwd, args.path, signal, exec);
124
- if (!file.ok) return finish({ refusal: file.error });
380
+ if (!file.ok) {
381
+ failureCause = file.cause ?? "file_unavailable";
382
+ return finish({ refusal: file.error });
383
+ }
384
+ failureCause = "outside_inventory";
125
385
  if (file.bytes < LOCATE_MIN_KB * 1000)
126
386
  return finish({
127
387
  refusal: `small file (${file.lines} lines): read it directly`,
128
388
  });
129
- if (!client) return finish({ refusal: NOT_CONFIGURED });
389
+ if (!client) {
390
+ failureCause = "not_configured";
391
+ return finish({ refusal: NOT_CONFIGURED });
392
+ }
130
393
  let sections: Section[];
131
394
  if (file.kind === "whole") {
132
395
  const parser = await loadParser();
@@ -169,8 +432,66 @@ export function createLocateTool(
169
432
  });
170
433
  }
171
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";
172
462
  const judge = async (candidates: Section[], outline = false) => {
173
- 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
+ };
174
495
  const ask = async (reverse: boolean) => {
175
496
  const result = await client.judge(
176
497
  state,
@@ -186,12 +507,27 @@ export function createLocateTool(
186
507
  signal,
187
508
  ...runtime.session.requestGate(),
188
509
  beforeRequest: (n) => runtime.session.admit(n),
510
+ admissionCause: () => "session_budget",
189
511
  onUsage: (usage) => runtime.session.recordUsage(usage),
190
512
  },
191
513
  );
192
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
+ }
193
520
  if (!result.ok) return { ok: false as const, error: result.error };
194
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++;
195
531
  return answer?.type === "choice"
196
532
  ? { ok: true as const, probabilities: answer.probabilities }
197
533
  : {
@@ -206,25 +542,51 @@ export function createLocateTool(
206
542
  if (!first.ok) return first;
207
543
  let pointer = readPointer(first.probabilities);
208
544
  if (needsReverse(first.probabilities, candidates.length)) {
545
+ requiredControl = true;
209
546
  const second = await ask(true);
210
- if (!second.ok) return second;
547
+ if (!second.ok) {
548
+ if (failureCause === "invalid_response")
549
+ failureCause = "control_failure";
550
+ return second;
551
+ }
211
552
  pointer = readPointer(first.probabilities, second.probabilities);
212
553
  }
213
554
  return { ok: true as const, ...pointer };
214
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;
215
561
  const fits = (candidates: Section[]) =>
216
- sectionStateFits(args.path, args.goal, candidates);
562
+ sectionStateFits(
563
+ args.path,
564
+ args.goal,
565
+ candidates,
566
+ false,
567
+ evidenceMaxChars,
568
+ );
217
569
  const twoStage = file.kind === "outline" || !fits(sections);
218
570
  let planUnsure = false;
219
571
  if (twoStage) {
220
- const outline = sectionOutline(args.path, args.goal, sections);
572
+ const outline = sectionOutline(
573
+ args.path,
574
+ args.goal,
575
+ sections,
576
+ evidenceMaxChars,
577
+ );
221
578
  if (outline.length < 2 || outline.length >= CHOICE_MAX_OPTIONS)
222
579
  return finish({
223
580
  refusal:
224
581
  "No usable multi-section plan fits the budget; use grep or read directly.",
225
582
  });
226
- const evidence = outlineEvidence(args.path, args.goal, outline);
227
- 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))
228
590
  return finish({
229
591
  refusal: "Block plan exceeds the state budget; use grep.",
230
592
  });
@@ -274,7 +636,10 @@ export function createLocateTool(
274
636
  }
275
637
  if (file.kind === "outline") {
276
638
  const selected = await scanRange(cwd, args.path, block, signal, exec);
277
- if (!selected.ok) return finish({ refusal: selected.error });
639
+ if (!selected.ok) {
640
+ failureCause = selected.cause ?? "file_unavailable";
641
+ return finish({ refusal: selected.error });
642
+ }
278
643
  const lines = selected.text.split("\n");
279
644
  sections = sections
280
645
  .filter((s) => s.start >= block.start && s.end <= block.end)
@@ -334,7 +699,7 @@ export function createLocateTool(
334
699
  );
335
700
  if (!displayed)
336
701
  return finish({ refusal: "Pointer probabilities missing." });
337
- const { primary, band, candidates } = displayed;
702
+ const { primary: displayedPrimary, band, candidates } = displayed;
338
703
  if (band === "unsure" || head.id === "none")
339
704
  limitations.push({
340
705
  fact:
@@ -346,7 +711,7 @@ export function createLocateTool(
346
711
  return finish({
347
712
  answers: [
348
713
  {
349
- ...primary,
714
+ ...displayedPrimary,
350
715
  band,
351
716
  orderDependent: pointer.orderDependent,
352
717
  candidates,
@@ -354,7 +719,7 @@ export function createLocateTool(
354
719
  ],
355
720
  lines:
356
721
  head.id !== "none"
357
- ? [{ type: "list", title: "read", items: [primary.label] }]
722
+ ? [{ type: "list", title: "read", items: [displayedPrimary.label] }]
358
723
  : [],
359
724
  limitations: [
360
725
  {