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
@@ -0,0 +1,602 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
3
+ import { shareGitInventory } from "../adapters/git-inventory.js";
4
+ import { readLocateFile, scanRange } from "../adapters/locate-file.js";
5
+ import { loadSyntaxParser } from "../adapters/syntax.js";
6
+ import { hostUsage } from "../adapters/usage.js";
7
+ import { CHOICE_MAX_OPTIONS, LOCATE_GRAY_MIN, LOCATE_MIN_KB, LOCATE_SECTION_MAX_LINES, LOCATE_SHRINK_TOP, LOCATE_VERDICT_MIN, STATE_MAX_CHARS, } from "../constants.js";
8
+ import { locateDisplay, outlineEvidence, sectionOutline, sectionState, sectionStateFits, } from "../core/locate.js";
9
+ import { buildEnvelope, } from "../core/output.js";
10
+ import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.js";
11
+ import { answerItemFromInput, buildResultReport, contextFromEvidence, known, unknown, } from "../core/result-report.js";
12
+ import { sectionFile } from "../core/sections.js";
13
+ import { interpolate } from "../describe.js";
14
+ import { renderResultReport } from "../render.js";
15
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
16
+ import { LOCATE_DESCRIPTION } from "../texts/locate.js";
17
+ const parameters = Type.Object({
18
+ root: Type.Optional(Type.String()),
19
+ path: Type.String({ minLength: 1 }),
20
+ goal: Type.String({ minLength: 1 }),
21
+ }, { additionalProperties: false });
22
+ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
23
+ const { host, runtime, exec: execute } = dependencies;
24
+ return {
25
+ name: "jev_locate_in_file",
26
+ label: "Jev locate in file",
27
+ description: interpolate(LOCATE_DESCRIPTION.replace("{{ompOutline}}", host.isOmp
28
+ ? " read already outlines code files; use this when the outline does not tell you which range to open."
29
+ : ""), host.names),
30
+ parameters,
31
+ ...(host.isOmp
32
+ ? { approval: "read", loadMode: "essential" }
33
+ : {
34
+ promptSnippet: "Find which line range of one large file serves a goal",
35
+ }),
36
+ async execute(_id, args, signal, _update, ctx) {
37
+ const client = dependencies.client;
38
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
39
+ exec: execute,
40
+ signal,
41
+ origin: dependencies.evidenceOrigin,
42
+ });
43
+ const evidenceContext = evidence.context;
44
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
45
+ const started = performance.now();
46
+ const exec = shareGitInventory(execute);
47
+ const results = [];
48
+ const limitations = [];
49
+ const context = contextFromEvidence(evidenceContext);
50
+ const diagnostics = [];
51
+ const actions = [];
52
+ const auxiliary = {
53
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
54
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
55
+ };
56
+ let primary;
57
+ let control;
58
+ let requiredControl = false;
59
+ let failureCause = "invalid_response";
60
+ const raw = (answer) => answer.type === "choice"
61
+ ? Object.entries(answer.probabilities).map(([label, probability]) => ({
62
+ label,
63
+ value: label,
64
+ probability: known(probability),
65
+ }))
66
+ : [];
67
+ const diagnose = (cause, fact, members = []) => {
68
+ const id = `d${diagnostics.length + 1}`, actionId = `a${actions.length + 1}`;
69
+ const code = cause === "invalid_root"
70
+ ? "correct_context"
71
+ : cause === "not_configured"
72
+ ? "configure_client"
73
+ : cause === "transport_failure" || cause === "service_unavailable"
74
+ ? "recover_service"
75
+ : "inspect_native";
76
+ actions.push({
77
+ id: actionId,
78
+ code,
79
+ target: known(args.path),
80
+ scope: { kind: "call" },
81
+ condition: code === "correct_context"
82
+ ? "If the requested root is incorrect"
83
+ : code === "recover_service"
84
+ ? "Only after service recovery is observed"
85
+ : "If the range decision remains open",
86
+ instruction: code === "correct_context"
87
+ ? "Use the exact registered worktree top-level; no fallback was performed."
88
+ : code === "configure_client"
89
+ ? "Configure the Jev client or inspect the file natively."
90
+ : "Read or search the cited file within existing permissions; revisit Jev only with decisive new evidence or materially changed context.",
91
+ repeatUnchanged: false,
92
+ });
93
+ diagnostics.push({
94
+ id,
95
+ cause,
96
+ fact,
97
+ target: known(cause === "invalid_root" ? (args.root ?? cwd) : args.path),
98
+ origin: cause === "invalid_root" || cause === "forbidden_path"
99
+ ? "input"
100
+ : cause === "control_failure"
101
+ ? "control"
102
+ : cause === "session_budget"
103
+ ? "budget"
104
+ : cause === "service_unavailable" ||
105
+ cause === "transport_failure" ||
106
+ cause === "invalid_response" ||
107
+ cause === "provider_context_refusal" ||
108
+ cause === "not_configured"
109
+ ? "provider"
110
+ : "collection",
111
+ scope: { kind: "call" },
112
+ effect: "blocking",
113
+ material: true,
114
+ omittedMembers: members,
115
+ memberCount: known(members.length),
116
+ actionIds: [actionId],
117
+ });
118
+ };
119
+ const finish = (input) => {
120
+ const usage = results.reduce((sum, result) => ({
121
+ inputTokens: sum.inputTokens + (result.usage?.inputTokens ?? 0),
122
+ costUsd: sum.costUsd + (result.usage?.costUsd ?? 0),
123
+ }), { inputTokens: 0, costUsd: 0 });
124
+ const metrics = {
125
+ calls: 0,
126
+ questions: 0,
127
+ cacheHits: 0,
128
+ cacheRequests: 0,
129
+ };
130
+ for (const result of results) {
131
+ metrics.calls += result.calls ?? 0;
132
+ metrics.questions += result.questions ?? 0;
133
+ metrics.cacheHits += result.cacheHits ?? 0;
134
+ metrics.cacheRequests += result.cacheRequests ?? 0;
135
+ }
136
+ const envelope = buildEnvelope({
137
+ ...input,
138
+ limitations: [...limitations, ...(input.limitations ?? [])],
139
+ yield: {
140
+ ...metrics,
141
+ costUsd: usage.costUsd,
142
+ elapsedMs: performance.now() - started,
143
+ },
144
+ });
145
+ if (input.refusal)
146
+ diagnose(failureCause, input.refusal);
147
+ if (input.answers?.[0] && primary?.source)
148
+ actions.push({
149
+ id: `a${actions.length + 1}`,
150
+ code: "inspect_native",
151
+ target: known(input.answers[0].label === "none"
152
+ ? args.path
153
+ : input.answers[0].label),
154
+ scope: { kind: "group", groupIds: ["pointer"] },
155
+ condition: "If the range decision remains open",
156
+ instruction: input.answers[0].label === "none"
157
+ ? "Search elsewhere for the goal; read any cited alternative range if the result is unsure. Do not retry unchanged evidence."
158
+ : "Read the current primary and alternative ranges; retain uncertainty if no decisive new evidence is available. Do not retry unchanged evidence.",
159
+ repeatUnchanged: false,
160
+ });
161
+ if (!input.refusal &&
162
+ (!primary?.source || (requiredControl && !control?.source)))
163
+ diagnose("invalid_response", "pointer decision lacks established answer provenance or its required control");
164
+ const common = {
165
+ id: "pointer",
166
+ kind: "pointer",
167
+ label: args.path,
168
+ groupId: "pointer",
169
+ evidence: primary
170
+ ? [
171
+ {
172
+ target: args.path,
173
+ canonicalPath: evidenceContext.effectiveRoot
174
+ ? known(`${evidenceContext.effectiveRoot.path}/${args.path}`)
175
+ : unknown("file admission not established"),
176
+ aliases: [],
177
+ side: "current",
178
+ revision: context.resolvedBase,
179
+ },
180
+ ]
181
+ : [],
182
+ diagnosticIds: diagnostics.map((d) => d.id),
183
+ actionIds: actions.map((a) => a.id),
184
+ };
185
+ if (input.answers?.[0])
186
+ common.label = input.answers[0].label;
187
+ const displayed = input.answers?.[0];
188
+ let item = { ...common, treatment: "not_judged", source: "none" };
189
+ if (displayed &&
190
+ primary?.type === "choice" &&
191
+ primary.source &&
192
+ (!requiredControl || (control?.type === "choice" && control.source))) {
193
+ auxiliary.passages[primary.source]--;
194
+ item = answerItemFromInput(common, {
195
+ ...displayed,
196
+ answer: primary,
197
+ publicResult: displayed.value?.head ?? primary.choice,
198
+ reason: [...limitations, ...(input.limitations ?? [])]
199
+ .map((limit) => limit.fact)
200
+ .join("; "),
201
+ rawValues: raw(primary),
202
+ reportControls: control?.type === "choice" && control.source
203
+ ? [
204
+ {
205
+ id: "pointer-order",
206
+ kind: "order",
207
+ source: control.source,
208
+ outcome: displayed.orderDependent
209
+ ? "order-dependent"
210
+ : "consistent",
211
+ rawValues: raw(control),
212
+ },
213
+ ]
214
+ : [],
215
+ });
216
+ }
217
+ const report = buildResultReport({
218
+ tool: "jev_locate_in_file",
219
+ context,
220
+ items: [item],
221
+ diagnostics,
222
+ actions,
223
+ metrics: {
224
+ ...metrics,
225
+ costUsd: results.every((r) => !r.calls || r.usage !== undefined)
226
+ ? usage.costUsd
227
+ : undefined,
228
+ elapsedMs: performance.now() - started,
229
+ },
230
+ auxiliary,
231
+ total: known(1),
232
+ refused: Boolean(input.refusal &&
233
+ (failureCause === "invalid_root" ||
234
+ failureCause === "forbidden_path" ||
235
+ failureCause === "outside_inventory" ||
236
+ failureCause === "group_too_large" ||
237
+ failureCause === "evidence_too_large")),
238
+ });
239
+ runtime.session.record(envelope);
240
+ runtime.guide.deliver(ctx);
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: renderResultReport(report, {
246
+ details: {
247
+ lines: [
248
+ ...envelope.lines,
249
+ {
250
+ type: "list",
251
+ title: "pointer reservations",
252
+ items: [...limitations, ...(input.limitations ?? [])].map((limit) => limit.fact),
253
+ },
254
+ ...(displayed
255
+ ? [
256
+ {
257
+ type: "list",
258
+ title: "read ranges",
259
+ items: [
260
+ ...(displayed.label === "none"
261
+ ? []
262
+ : [displayed.label]),
263
+ ...(displayed.candidates ?? [])
264
+ .filter((candidate) => candidate.label !== "none")
265
+ .map((candidate) => `${candidate.label} (${candidate.value.p})`),
266
+ ],
267
+ },
268
+ ]
269
+ : []),
270
+ ],
271
+ },
272
+ }),
273
+ },
274
+ ],
275
+ details: { results, evidenceContext, result: report },
276
+ ...hostUsage(host.isOmp, results.some((r) => r.usage) ? usage : undefined),
277
+ };
278
+ };
279
+ if (!evidence.ok) {
280
+ failureCause = evidence.cause ?? "invalid_root";
281
+ return finish({ refusal: evidence.error });
282
+ }
283
+ failureCause = "file_unavailable";
284
+ const file = await readLocateFile(cwd, args.path, signal, exec);
285
+ if (!file.ok) {
286
+ failureCause = file.cause ?? "file_unavailable";
287
+ return finish({ refusal: file.error });
288
+ }
289
+ failureCause = "outside_inventory";
290
+ if (file.bytes < LOCATE_MIN_KB * 1000)
291
+ return finish({
292
+ refusal: `small file (${file.lines} lines): read it directly`,
293
+ });
294
+ if (!client) {
295
+ failureCause = "not_configured";
296
+ return finish({ refusal: NOT_CONFIGURED });
297
+ }
298
+ let sections;
299
+ if (file.kind === "whole") {
300
+ const parser = await loadParser();
301
+ const declarations = parser?.supports(args.path)
302
+ ? parser.declarations(args.path, file.text, {
303
+ descendAbove: LOCATE_SECTION_MAX_LINES,
304
+ })
305
+ : undefined;
306
+ if (args.path.endsWith(".py") && !parser?.supports(args.path))
307
+ limitations.push({
308
+ fact: "Python: unsupported language (plugin missing), heuristic sections",
309
+ next: "install @ast-grep/lang-python",
310
+ });
311
+ for (const limit of declarations?.limits ?? [])
312
+ limitations.push({
313
+ fact: limit.kind === "parse_failed"
314
+ ? "syntax parse failed: heuristic sections"
315
+ : `parse_partial: ${args.path}${limit.declaration ? `: ${limit.declaration}` : ""}`,
316
+ next: limit.kind === "parse_failed"
317
+ ? "read the selected range directly"
318
+ : "read the complete source and check its syntax",
319
+ });
320
+ sections = sectionFile(args.path, file.text, declarations?.limits.some((limit) => limit.kind === "parse_failed")
321
+ ? undefined
322
+ : declarations?.declarations).sections;
323
+ }
324
+ else {
325
+ sections = file.windows.map((window, index) => ({
326
+ id: `S${index + 1}`,
327
+ ...window,
328
+ }));
329
+ limitations.push({
330
+ fact: "large file: window outline, not declarations",
331
+ next: "read the selected range directly",
332
+ });
333
+ }
334
+ const sectionCount = sections.length;
335
+ context.inventories.push({
336
+ id: "sections",
337
+ kind: "sections",
338
+ rules: [
339
+ file.kind === "whole"
340
+ ? "declarations or heuristic sections from complete file"
341
+ : "bounded window outline",
342
+ ],
343
+ restrictions: [
344
+ args.path,
345
+ ...sections.map((section) => `${section.id}: ${section.start}-${section.end}`),
346
+ ],
347
+ discovered: known(sectionCount),
348
+ considered: known(sectionCount),
349
+ scopeRestricted: true,
350
+ criteria: [],
351
+ });
352
+ if (!sectionCount) {
353
+ diagnose("collection_empty", "no sections discovered; no pointer judgment");
354
+ return finish({});
355
+ }
356
+ failureCause = "group_too_large";
357
+ const judge = async (candidates, outline = false) => {
358
+ primary = undefined;
359
+ control = undefined;
360
+ requiredControl = false;
361
+ const inventory = context.inventories.find((inventory) => inventory.id === "pointer-candidates");
362
+ const restricted = {
363
+ id: "pointer-candidates",
364
+ kind: "sections",
365
+ rules: ["actual candidates for the latest pointer stage"],
366
+ restrictions: [
367
+ args.path,
368
+ ...candidates.map((section) => `${section.id}: ${section.start}-${section.end}`),
369
+ ],
370
+ discovered: known(candidates.length),
371
+ considered: known(candidates.length),
372
+ scopeRestricted: true,
373
+ criteria: [],
374
+ };
375
+ if (inventory)
376
+ Object.assign(inventory, restricted);
377
+ else
378
+ context.inventories.push(restricted);
379
+ const state = withEvidenceContext(sectionState(args.path, args.goal, candidates, outline), evidenceContext);
380
+ if (JSON.stringify(state).length > STATE_MAX_CHARS)
381
+ return {
382
+ ok: false,
383
+ error: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
384
+ };
385
+ const ask = async (reverse) => {
386
+ const result = await client.judge(state, {
387
+ pointer: pointerQuestion({
388
+ instructions: `Which section serves this goal: ${args.goal}? Choose none if no section fits. Judge the supplied text, not the label alone.`,
389
+ noneLabel: "No section serves the goal",
390
+ candidates,
391
+ reverse,
392
+ }),
393
+ }, {
394
+ signal,
395
+ ...runtime.session.requestGate(),
396
+ beforeRequest: (n) => runtime.session.admit(n),
397
+ admissionCause: () => "session_budget",
398
+ onUsage: (usage) => runtime.session.recordUsage(usage),
399
+ });
400
+ results.push(result);
401
+ if (!result.ok) {
402
+ failureCause = result.failureCause ?? "invalid_response";
403
+ if (reverse)
404
+ auxiliary.controls.notJudged++;
405
+ else
406
+ auxiliary.passages.notJudged++;
407
+ }
408
+ if (!result.ok)
409
+ return { ok: false, error: result.error };
410
+ const answer = result.answers.pointer;
411
+ if (reverse)
412
+ control = answer;
413
+ else
414
+ primary = answer;
415
+ if (answer?.type === "unjudged")
416
+ failureCause = answer.cause ?? "invalid_response";
417
+ if (answer?.type === "choice" && answer.source) {
418
+ if (reverse)
419
+ auxiliary.controls[answer.source]++;
420
+ else
421
+ auxiliary.passages[answer.source]++;
422
+ }
423
+ else if (reverse)
424
+ auxiliary.controls.notJudged++;
425
+ else
426
+ auxiliary.passages.notJudged++;
427
+ return answer?.type === "choice"
428
+ ? { ok: true, probabilities: answer.probabilities }
429
+ : {
430
+ ok: false,
431
+ error: answer?.type === "unjudged"
432
+ ? answer.reason
433
+ : "Pointer answer missing.",
434
+ };
435
+ };
436
+ const first = await ask(false);
437
+ if (!first.ok)
438
+ return first;
439
+ let pointer = readPointer(first.probabilities);
440
+ if (needsReverse(first.probabilities, candidates.length)) {
441
+ requiredControl = true;
442
+ const second = await ask(true);
443
+ if (!second.ok) {
444
+ if (failureCause === "invalid_response")
445
+ failureCause = "control_failure";
446
+ return second;
447
+ }
448
+ pointer = readPointer(first.probabilities, second.probabilities);
449
+ }
450
+ return { ok: true, ...pointer };
451
+ };
452
+ const emptyState = sectionState(args.path, args.goal, []);
453
+ const contextChars = JSON.stringify(withEvidenceContext(emptyState, evidenceContext))
454
+ .length - JSON.stringify(emptyState).length;
455
+ const evidenceMaxChars = STATE_MAX_CHARS - contextChars;
456
+ const fits = (candidates) => sectionStateFits(args.path, args.goal, candidates, false, evidenceMaxChars);
457
+ const twoStage = file.kind === "outline" || !fits(sections);
458
+ let planUnsure = false;
459
+ if (twoStage) {
460
+ const outline = sectionOutline(args.path, args.goal, sections, evidenceMaxChars);
461
+ if (outline.length < 2 || outline.length >= CHOICE_MAX_OPTIONS)
462
+ return finish({
463
+ refusal: "No usable multi-section plan fits the budget; use grep or read directly.",
464
+ });
465
+ const evidence = outlineEvidence(args.path, args.goal, outline, evidenceMaxChars);
466
+ if (!fits(evidence))
467
+ return finish({
468
+ refusal: "Block plan exceeds the state budget; use grep.",
469
+ });
470
+ if (outline.length < sections.length)
471
+ limitations.push({
472
+ fact: "adjacent sections grouped; a class may span outline blocks",
473
+ next: "read neighboring blocks when needed",
474
+ });
475
+ const plan = await judge(evidence);
476
+ if (!plan.ok)
477
+ return finish({ refusal: plan.error });
478
+ const head = plan.ranked[0];
479
+ if (!head)
480
+ return finish({ refusal: "Pointer probabilities missing." });
481
+ if (head.id === "none") {
482
+ const display = locateDisplay(args.path, outline, plan.ranked, false);
483
+ if (!display)
484
+ return finish({ refusal: "Pointer probabilities missing." });
485
+ return finish({
486
+ answers: [
487
+ {
488
+ ...display.primary,
489
+ band: display.band,
490
+ candidates: display.candidates,
491
+ orderDependent: plan.orderDependent,
492
+ },
493
+ ],
494
+ limitations: [
495
+ {
496
+ fact: "no block serves the goal",
497
+ next: display.band === "unsure"
498
+ ? "read the alternative block or search elsewhere"
499
+ : "search elsewhere",
500
+ },
501
+ ],
502
+ });
503
+ }
504
+ const block = outline.find((s) => s.id === head.id);
505
+ if (!block)
506
+ return finish({ refusal: "Pointer named an unknown block." });
507
+ planUnsure = head.p < LOCATE_VERDICT_MIN;
508
+ if (planUnsure) {
509
+ const alternative = outline.find((s) => s.id === plan.ranked[1]?.id);
510
+ limitations.push({
511
+ fact: `plan unsure: refined only the top block; also consider ${alternative ? `${args.path}:${alternative.start}-${alternative.end}` : "none"}`,
512
+ next: "read the other block if needed",
513
+ });
514
+ }
515
+ if (file.kind === "outline") {
516
+ const selected = await scanRange(cwd, args.path, block, signal, exec);
517
+ if (!selected.ok) {
518
+ failureCause = selected.cause ?? "file_unavailable";
519
+ return finish({ refusal: selected.error });
520
+ }
521
+ const lines = selected.text.split("\n");
522
+ sections = sections
523
+ .filter((s) => s.start >= block.start && s.end <= block.end)
524
+ .map((section) => ({
525
+ ...section,
526
+ label: `lines ${section.start}-${section.end}`,
527
+ text: lines
528
+ .slice(section.start - block.start, section.end - block.start + 1)
529
+ .join("\n"),
530
+ }));
531
+ }
532
+ else
533
+ sections = sections.filter((s) => s.start >= block.start && s.end <= block.end);
534
+ if (sections.length < 2)
535
+ return finish({
536
+ refusal: "Selected block has fewer than two sections; use grep or read directly.",
537
+ });
538
+ }
539
+ if (!fits(sections))
540
+ return finish({
541
+ refusal: `Selected block exceeds ${STATE_MAX_CHARS} chars or the choice limit; read ${args.path}:${sections[0]?.start}-${sections.at(-1)?.end} directly.`,
542
+ });
543
+ let pointer = await judge(sections);
544
+ if (!pointer.ok)
545
+ return finish({ refusal: pointer.error });
546
+ let shrunk = false;
547
+ if ((pointer.ranked[0]?.p ?? 0) < LOCATE_GRAY_MIN) {
548
+ const ids = pointer.ranked
549
+ .filter((s) => s.id !== "none")
550
+ .slice(0, LOCATE_SHRINK_TOP)
551
+ .map((s) => s.id);
552
+ sections = ids.flatMap((id) => sections.filter((s) => s.id === id));
553
+ pointer = await judge(sections);
554
+ if (!pointer.ok)
555
+ return finish({ refusal: pointer.error });
556
+ shrunk = true;
557
+ if (twoStage)
558
+ limitations.push({
559
+ fact: "two-stage pointer: shrinking re-ask not measured for outlines",
560
+ next: "read the selected evidence",
561
+ });
562
+ limitations.push({
563
+ fact: "pointer below gray threshold: narrowed to top three plus none",
564
+ next: "read both candidates or search elsewhere",
565
+ });
566
+ }
567
+ const head = pointer.ranked[0];
568
+ if (!head)
569
+ return finish({ refusal: "Pointer probabilities missing." });
570
+ const displayed = locateDisplay(args.path, sections, pointer.ranked, shrunk || planUnsure);
571
+ if (!displayed)
572
+ return finish({ refusal: "Pointer probabilities missing." });
573
+ const { primary: displayedPrimary, band, candidates } = displayed;
574
+ if (band === "unsure" || head.id === "none")
575
+ limitations.push({
576
+ fact: head.id === "none"
577
+ ? "no section fits"
578
+ : "two sections share the mass",
579
+ next: head.id === "none" ? "search elsewhere" : "read both",
580
+ });
581
+ return finish({
582
+ answers: [
583
+ {
584
+ ...displayedPrimary,
585
+ band,
586
+ orderDependent: pointer.orderDependent,
587
+ candidates,
588
+ },
589
+ ],
590
+ lines: head.id !== "none"
591
+ ? [{ type: "list", title: "read", items: [displayedPrimary.label] }]
592
+ : [],
593
+ limitations: [
594
+ {
595
+ fact: `${file.lines} lines, ${sectionCount} sections`,
596
+ next: "open the cited line range",
597
+ },
598
+ ],
599
+ });
600
+ },
601
+ };
602
+ }