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
package/src/tools/find.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { type Static, Type } from "@sinclair/typebox";
2
- import { canonicalPath } from "../adapters/canonical-path.ts";
2
+ import {
3
+ resolveEvidenceContext,
4
+ withEvidenceContext,
5
+ } from "../adapters/evidence-context.ts";
3
6
  import { collectSearchExcerpts } from "../adapters/files.ts";
4
7
  import { prefilter } from "../adapters/find.ts";
5
8
  import { shareGitInventory } from "../adapters/git-inventory.ts";
@@ -19,16 +22,29 @@ import {
19
22
  type EnvelopeInput,
20
23
  } from "../core/output.ts";
21
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";
22
37
  import { interpolate } from "../describe.ts";
23
38
  import type { GuideContext } from "../guide.ts";
24
- import type { Judgment, Question, State } from "../jev/types.ts";
25
- import { renderEnvelope } from "../render.ts";
39
+ import type { Answer, Judgment, Question, State } from "../jev/types.ts";
40
+ import { renderResultReport } from "../render.ts";
26
41
  import type { ToolDependencies } from "../runtime.ts";
27
42
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
28
43
  import { FIND_DESCRIPTION } from "../texts/find.ts";
29
44
 
30
45
  export const findParameters = Type.Object(
31
46
  {
47
+ root: Type.Optional(Type.String()),
32
48
  goal: Type.String({ minLength: 1 }),
33
49
  keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
34
50
  scope: Type.Optional(
@@ -75,7 +91,13 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
75
91
  ctx: { cwd: string } & GuideContext,
76
92
  ) {
77
93
  const client = dependencies.client;
78
- const cwd = await canonicalPath(ctx.cwd);
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;
79
101
  const started = performance.now();
80
102
  const exec = shareGitInventory(execute);
81
103
  const totals = {
@@ -90,6 +112,101 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
90
112
  const unchecked: string[] = [];
91
113
  let budget: BudgetRefusal | undefined;
92
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
+ };
93
210
  const finish = (input: Omit<EnvelopeInput, "yield">) => {
94
211
  const envelope = buildEnvelope({
95
212
  ...input,
@@ -101,15 +218,166 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
101
218
  elapsedMs: performance.now() - started,
102
219
  },
103
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
+ });
104
330
  runtime.session.record(envelope);
105
331
  runtime.guide.deliver(ctx);
106
332
  return {
107
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
108
- 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 },
109
370
  ...hostUsage(host.isOmp, totals.usage),
110
371
  };
111
372
  };
112
- 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
+ }
113
381
  const words = contentWords(args.goal);
114
382
  const short = words.length < C.FIND_GOAL_MIN_WORDS;
115
383
  if (short)
@@ -139,7 +407,30 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
139
407
  args.exclude,
140
408
  signal,
141
409
  );
142
- 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
+ });
143
434
  const entry = (
144
435
  path: string,
145
436
  p: number,
@@ -175,21 +466,24 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
175
466
  ],
176
467
  });
177
468
  if (!candidates.paths.length) {
178
- limitations.push({
179
- fact:
180
- candidates.scopeFiles === 0
181
- ? "scope matched 0 files"
182
- : "no file shares a word with the goal",
183
- next: "rephrase goal or widen scope",
184
- });
185
- return finish({
186
- lines: [entry("none", 1, short ? "unsure" : "verdict")],
187
- });
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({});
188
476
  }
189
477
  const judge = async (
190
478
  state: State,
191
479
  questions: Record<string, Question>,
192
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
+ };
193
487
  const result = await client.judge(state, questions, {
194
488
  signal,
195
489
  ...runtime.session.requestGate(),
@@ -210,6 +504,8 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
210
504
  return admission;
211
505
  },
212
506
  onUsage: (usage) => runtime.session.recordUsage(usage),
507
+ admissionCause: () =>
508
+ budget?.kind === "max_calls" ? "call_budget" : "session_budget",
213
509
  });
214
510
  for (const key of [
215
511
  "calls",
@@ -219,7 +515,21 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
219
515
  ] as const)
220
516
  totals[key] += result[key] ?? 0;
221
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
+ }
222
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
+ }
223
533
  return result;
224
534
  };
225
535
  const boolQuestions = (
@@ -270,6 +580,12 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
270
580
  const rankedNames = rankFiles(names);
271
581
  let bestRanking = rankedNames;
272
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
+ });
273
589
  const top = bestRanking.slice(0, 2);
274
590
  return finish({
275
591
  lines: [
@@ -317,7 +633,10 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
317
633
  signal,
318
634
  exec,
319
635
  );
320
- if (!read.ok) return { paths, result: read };
636
+ if (!read.ok) {
637
+ failureCause = read.cause ?? "file_unavailable";
638
+ return { paths, result: read };
639
+ }
321
640
  Object.assign(files, read.pointers);
322
641
  limitations.push(
323
642
  ...read.limits.map((limit) => ({
@@ -325,11 +644,18 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
325
644
  next: "provide a UTF-8 text file",
326
645
  })),
327
646
  );
328
- 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
+ );
329
654
  limitations.push({
330
655
  fact: `${read.ignored.length} binary, empty or unreadable paths ignored: ${read.ignored.join(", ")}`,
331
656
  next: "narrow scope to text files",
332
657
  });
658
+ }
333
659
  const textPaths = paths.filter(
334
660
  (path) => read.files[path] !== undefined,
335
661
  );
@@ -398,12 +724,26 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
398
724
  ],
399
725
  });
400
726
  if (!retained.length) {
401
- limitations.push({
402
- fact: "no file retained by content or names guard",
403
- next: "rephrase goal or widen scope",
404
- });
405
- 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({});
406
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
+ });
407
747
  const idByPath = Object.fromEntries(
408
748
  retained.map((item, index) => [item.path, `c${index + 1}`]),
409
749
  );
@@ -437,6 +777,7 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
437
777
  if (!firstResult.ok && !budget)
438
778
  return finish({ refusal: firstResult.error });
439
779
  const first = firstResult.ok ? firstResult.answers.entry : undefined;
780
+ primary = first;
440
781
  if (first?.type !== "choice") {
441
782
  unchecked.push("entry");
442
783
  return fallback();
@@ -459,14 +800,30 @@ export function createFindFilesTool(dependencies: ToolDependencies) {
459
800
  (args.effort !== "quick" &&
460
801
  needsReverse(first.probabilities, retained.length)))
461
802
  ) {
803
+ confirmationRequired = true;
462
804
  const secondResult = await pointer([...paths].reverse());
463
- if (!secondResult.ok && !budget)
805
+ if (!secondResult.ok && !budget) {
806
+ auxiliary.passages.notJudged--;
807
+ auxiliary.controls.notJudged++;
464
808
  return finish({ refusal: secondResult.error });
809
+ }
465
810
  const second = secondResult.ok ? secondResult.answers.entry : undefined;
466
- if (second?.type !== "choice") {
467
- unchecked.push("entry confirmation");
468
- band = "unsure";
811
+ confirmation = second;
812
+ if (second?.type === "choice" && second.source) {
813
+ auxiliary.passages[second.source]--;
814
+ auxiliary.controls[second.source]++;
469
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
+ {
470
827
  const secondRanks = ranked(second.probabilities);
471
828
  const combined = readPointer(
472
829
  first.probabilities,