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,7 @@
1
+ import { RECOVERY_POLICY, USE_POLICY } from "./instructions.ts";
2
+
1
3
  export const LOCATE_DESCRIPTION = `Find which line range of ONE big file (over {{LOCATE_MIN_KB}} KB) serves a goal, instead of reading it whole; known string or symbol -> {{names.grep}}.
2
- Use for: a file too big to read whole (over {{LOCATE_MIN_KB}} KB) when you know what you need from it ("where are retries scheduled", "the part that parses flags"). Prefer it over reading the file in chunks.
4
+ Use for: a file too big to read whole (over {{LOCATE_MIN_KB}} KB) when you know what you need from it ("where are retries scheduled", "the part that parses flags") and finding a range can focus the next inspection.
3
5
  Not for: an exact string or symbol → {{names.grep}}. Files under {{LOCATE_MIN_KB}} KB → read them directly. Several files → jev_ask_files or {{names.semantic}}.{{ompOutline}}
4
6
 
5
7
  How Jev sees it: code cuts the file into declarations or sections, and Jev reads them all at a glance and picks the one that serves the goal. Several sections often share the answer (a getter and its setter), so the mass splits and the pick looks unsure even when both are right. It reads what is written, not what a function does at run time.
@@ -7,4 +9,8 @@ How Jev sees it: code cuts the file into declarations or sections, and Jev reads
7
9
  path: one repo-relative file.
8
10
  goal: what you need from the file, as a sentence.
9
11
 
10
- Result: the range as path:start-end with its label and probability, and the read call to make next. An unmarked line above {{LOCATE_VERDICT_MIN}} is a verdict: read that range. unsure ({{LOCATE_GRAY_MIN}} to {{LOCATE_VERDICT_MIN}}) lists the two best by probability, often adjacent code that shares the answer, not the next section of the file: read both. Below {{LOCATE_GRAY_MIN}} Jev is asked once more among the three best plus none; the original unsure band is retained. A "none" means no section fits and the goal is probably not in this file: search elsewhere. Bracket lines say what limited the call. The reading guide defines every mark once.`;
12
+ Result: the range as path:start-end with its label and probability, and the read call to make next. An unmarked line above {{LOCATE_VERDICT_MIN}} is a verdict: read that range. unsure ({{LOCATE_GRAY_MIN}} to {{LOCATE_VERDICT_MIN}}) lists the two best by probability, often adjacent code that shares the answer, not the next section of the file: read both. Below {{LOCATE_GRAY_MIN}} Jev is asked once more among the three best plus none; the original unsure band is retained. A judged "none" means no supplied section fits: search elsewhere. Missing required answers or controls remain unjudged, without an invented probability. Typed diagnostics name parsing/window limits and native actions; primary and control provenance remain distinct. The reading guide defines every mark once.
13
+
14
+ The decision to call is discretionary: ${USE_POLICY}
15
+
16
+ ${RECOVERY_POLICY}`;
@@ -6,8 +6,8 @@ export function runEndMessage(findings: readonly DocsFinding[]): string {
6
6
  "jev_check_diff (docs) flagged documentation that may no longer match your changes:",
7
7
  ...findings.map(
8
8
  (finding) =>
9
- `- ${finding.section.path} § ${finding.section.heading} — ${JSON.stringify(finding.sentence?.text)} no longer true after ${finding.units.map((unit) => `${unit.name} in ${unit.file}`).join(", ")}`,
9
+ `- ${finding.section.path} § ${finding.section.heading} — ${JSON.stringify(finding.sentence?.text)} may no longer match ${finding.units.map((unit) => `${unit.name} in ${unit.file}`).join(", ")}`,
10
10
  ),
11
- "Update them, or say why they are still correct.",
11
+ "Inspect each flagged sentence against the change; update it or explain with evidence why it remains correct. This limited automatic check does not certify documentation completeness and does not require another Jev call.",
12
12
  ].join("\n");
13
13
  }
@@ -1,3 +1,6 @@
1
1
  import { SELECT_FILE_SHARE, SELECT_MIN } from "../constants.ts";
2
+ import { RECOVERY_POLICY, USE_POLICY } from "./instructions.ts";
2
3
 
3
- export const SELECT_TESTS_DESCRIPTION = `Select existing tests affected by a diff and return runner commands; this tool never runs or collects tests. Use after editing code, before choosing a test subset; use native read/grep/bash for exact names and execution. base defaults to HEAD; paths are candidate test files or globs, not diff paths. Static discovery reads tracked files, literal project configuration and imports, without evaluating third-party config. Touched tests are selected for free; import closure includes unchanged intermediates and applicable pytest conftest fixtures. Remaining scenarios receive a changed-unit pointer; selected when 1-p(none) >= ${SELECT_MIN}. Missing answers are selected; unavailable Jev falls back to all. Residual exported-unit checks report changed, run by no discovered test only within the discovered inventory, never global coverage or an obligation to add a scenario. Unsupported, unknown, calculated or unresolved runners keep that conclusion unsure and name the next manual action. Runtime and type tests are separate. Commands remain separate by cwd, config, project and framework; project options are preserved. At least ${SELECT_FILE_SHARE * 100}% selected, uncertain names or unknown counts run the entire file. max_calls bounds Jev calls; unjudged tests remain selected. witnesses controls residual coverage checks (auto by default); pointers have no witnesses. This does not detect that a new test scenario is required.`;
4
+ export const SELECT_TESTS_DESCRIPTION =
5
+ `Select existing tests affected by a diff and return runner commands; this tool never runs or collects tests. Use when selecting existing affected tests can usefully change the execution plan; use native read/grep/bash for exact names and execution. base defaults to HEAD; paths are candidate test files or globs, not diff paths. Static discovery reads tracked files, literal project configuration and imports, without evaluating third-party config. Touched tests are selected for free; import closure includes unchanged intermediates and applicable pytest conftest fixtures. Remaining scenarios receive a changed-unit pointer; selected when 1-p(none) >= ${SELECT_MIN}. Missing answers are selected; unavailable Jev falls back to all. Residual exported-unit checks report changed, run by no discovered test only within the discovered inventory, never global coverage or an obligation to add a scenario. Unsupported, unknown, calculated or unresolved runners keep that conclusion unsure and name the next manual action. Runtime and type tests are separate. Commands remain separate by cwd, config, project and framework; project options are preserved. At least ${SELECT_FILE_SHARE * 100}% selected, uncertain names or unknown counts run the entire file. max_calls bounds Jev calls; unjudged tests remain selected. witnesses controls residual coverage checks (auto by default); pointers have no witnesses. This does not detect that a new test scenario is required.` +
6
+ `\n\n${USE_POLICY}\n\n${RECOVERY_POLICY}`;
@@ -3,8 +3,13 @@ import { resolve } from "node:path";
3
3
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
4
  import { type Static, Type } from "@sinclair/typebox";
5
5
  import { collectAskFiles } from "../adapters/ask-files.ts";
6
+ import {
7
+ resolveEvidenceContext,
8
+ withEvidenceContext,
9
+ } from "../adapters/evidence-context.ts";
6
10
  import { shareGitInventory } from "../adapters/git-inventory.ts";
7
11
  import { hostUsage } from "../adapters/usage.ts";
12
+ import { STATE_MAX_CHARS } from "../constants.ts";
8
13
  import { compileAsks, readAsks, reverseQuestions } from "../core/asks.ts";
9
14
  import { checkIntegrity } from "../core/integrity.ts";
10
15
  import type {
@@ -14,10 +19,22 @@ import type {
14
19
  EnvelopeInput,
15
20
  } from "../core/output.ts";
16
21
  import { buildEnvelope } from "../core/output.ts";
22
+ import {
23
+ type Action,
24
+ answerItemFromInput,
25
+ buildResultReport,
26
+ type Cause,
27
+ contextFromEvidence,
28
+ type Diagnostic,
29
+ type Item,
30
+ known,
31
+ type ResultReportV1,
32
+ unknown,
33
+ } from "../core/result-report.ts";
17
34
  import { interpolate } from "../describe.ts";
18
35
  import type { GuideContext } from "../guide.ts";
19
36
  import type { Answer, Judgment, JudgmentMetadata } from "../jev/types.ts";
20
- import { renderEnvelope } from "../render.ts";
37
+ import { renderResultReport } from "../render.ts";
21
38
  import type { ToolDependencies } from "../runtime.ts";
22
39
  import { ASK_FILES_DESCRIPTION } from "../texts/ask-files.ts";
23
40
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
@@ -25,6 +42,7 @@ import { asksParameter } from "./ask-schema.ts";
25
42
 
26
43
  export const askFilesParameters = Type.Object(
27
44
  {
45
+ root: Type.Optional(Type.String()),
28
46
  paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
29
47
  asks: asksParameter("files"),
30
48
  max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -41,6 +59,7 @@ export interface AskFilesDetails {
41
59
  files: FileJudgment[];
42
60
  skipped: string[];
43
61
  envelope: Envelope;
62
+ result: ResultReportV1;
44
63
  }
45
64
  export function createAskFilesTool(dependencies: ToolDependencies) {
46
65
  const { host, runtime, exec: execute } = dependencies;
@@ -67,8 +86,74 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
67
86
  ctx: { cwd: string } & GuideContext,
68
87
  ) {
69
88
  const client = dependencies.client;
89
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
90
+ exec: execute,
91
+ signal,
92
+ origin: dependencies.evidenceOrigin,
93
+ });
94
+ const evidenceContext = evidence.context;
95
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
70
96
  const started = performance.now();
71
97
  const exec = shareGitInventory(execute);
98
+ const plan = compileAsks(args.asks, { surface: "files" });
99
+ let refusalCause: Cause = "invalid_arguments";
100
+ let collectedPaths: string[] | undefined;
101
+ let costKnown = true;
102
+ const reportItems: Item[] = [];
103
+ const diagnostics: Diagnostic[] = [];
104
+ const actions: Action[] = [];
105
+ const auxiliary = {
106
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
107
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
108
+ };
109
+ const diagnose = (
110
+ cause: Cause,
111
+ fact: string,
112
+ scope: Diagnostic["scope"],
113
+ target?: string,
114
+ ) => {
115
+ const id = `diagnostic:${diagnostics.length}`,
116
+ actionId = `action:${actions.length}`;
117
+ actions.push({
118
+ id: actionId,
119
+ code:
120
+ cause === "invalid_root"
121
+ ? "correct_context"
122
+ : cause === "not_configured"
123
+ ? "configure_client"
124
+ : "inspect_native",
125
+ target:
126
+ target === undefined
127
+ ? unknown("target not established")
128
+ : known(target),
129
+ scope,
130
+ condition:
131
+ "Only after correcting context or supplying materially new evidence",
132
+ instruction:
133
+ "Read the admitted files natively; do not repeat the unchanged Jev call.",
134
+ repeatUnchanged: false,
135
+ });
136
+ diagnostics.push({
137
+ id,
138
+ cause,
139
+ fact,
140
+ target:
141
+ target === undefined
142
+ ? unknown("target not established")
143
+ : known(target),
144
+ origin:
145
+ cause === "call_budget" || cause === "session_budget"
146
+ ? "budget"
147
+ : "collection",
148
+ scope,
149
+ effect: "blocking",
150
+ material: true,
151
+ omittedMembers: [],
152
+ memberCount: known(0),
153
+ actionIds: [actionId],
154
+ });
155
+ return { diagnosticIds: [id], actionIds: [actionId] };
156
+ };
72
157
  const totals = {
73
158
  calls: 0,
74
159
  questions: 0,
@@ -86,35 +171,156 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
86
171
  });
87
172
  runtime.session.record(envelope);
88
173
  runtime.guide.deliver(ctx);
174
+ if (!reportItems.length && !diagnostics.length)
175
+ diagnose(
176
+ input.refusal ? refusalCause : "collection_empty",
177
+ input.refusal ?? "No file-question results collected",
178
+ { kind: "call" },
179
+ );
180
+ if (!reportItems.length && collectedPaths && plan.ok)
181
+ for (const path of collectedPaths)
182
+ for (const reading of plan.readings)
183
+ reportItems.push({
184
+ id: `${path}:${reading.id}`,
185
+ kind: "file_question",
186
+ label: `${path} ${reading.label}`,
187
+ groupId: `${path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`,
188
+ evidence: [
189
+ {
190
+ target: path,
191
+ canonicalPath: known(resolve(cwd, path)),
192
+ aliases: [],
193
+ side: "current",
194
+ revision: unknown("working tree evidence"),
195
+ },
196
+ ],
197
+ diagnosticIds: diagnostics.map((diagnostic) => diagnostic.id),
198
+ actionIds: actions.map((action) => action.id),
199
+ treatment: "not_judged",
200
+ source: "none",
201
+ });
202
+ for (const limitation of input.limitations ?? []) {
203
+ diagnose(
204
+ "evidence_limit",
205
+ limitation.fact,
206
+ { kind: "call" },
207
+ limitation.path,
208
+ );
209
+ const diagnostic = diagnostics.at(-1);
210
+ if (diagnostic) diagnostic.effect = "reservation";
211
+ }
212
+ const report = buildResultReport({
213
+ tool: "jev_ask_files",
214
+ context: contextFromEvidence(evidenceContext, {
215
+ inventories: collectedPaths
216
+ ? [
217
+ {
218
+ id: "files",
219
+ kind: "repository",
220
+ rules: [
221
+ "Requested paths expanded with admission and pruning",
222
+ ],
223
+ restrictions: args.paths,
224
+ discovered: unknown(
225
+ "collector does not expose pre-pruning candidate count",
226
+ ),
227
+ considered: known(collectedPaths.length),
228
+ scopeRestricted: true,
229
+ criteria: [],
230
+ },
231
+ ]
232
+ : [],
233
+ }),
234
+ items: reportItems,
235
+ diagnostics,
236
+ actions,
237
+ metrics: {
238
+ ...totals,
239
+ costUsd: costKnown && collectedPaths ? totals.costUsd : undefined,
240
+ elapsedMs: performance.now() - started,
241
+ },
242
+ auxiliary,
243
+ total:
244
+ collectedPaths && plan.ok
245
+ ? known(collectedPaths.length * plan.readings.length)
246
+ : unknown("file-question inventory not collected"),
247
+ refused: !!input.refusal && refusalCause !== "not_configured",
248
+ });
89
249
  const usage = hostUsage(host.isOmp, {
90
250
  inputTokens: totals.inputTokens,
91
251
  costUsd: totals.costUsd,
92
252
  });
93
253
  return {
94
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
95
- details: { files: records, skipped, envelope },
254
+ content: [
255
+ {
256
+ type: "text" as const,
257
+ text: renderResultReport(report, { details: envelope }),
258
+ },
259
+ ],
260
+ details: {
261
+ files: records,
262
+ skipped,
263
+ envelope,
264
+ evidenceContext,
265
+ result: report,
266
+ },
96
267
  ...usage,
97
268
  };
98
269
  };
99
- if (!client) return finish({ refusal: NOT_CONFIGURED });
100
- const plan = compileAsks(args.asks, { surface: "files" });
270
+ if (!evidence.ok) {
271
+ refusalCause = evidence.cause ?? "invalid_root";
272
+ return finish({ refusal: evidence.error });
273
+ }
101
274
  if (!plan.ok) return finish({ refusal: plan.error });
102
- const collected = await collectAskFiles(
103
- ctx.cwd,
104
- args.paths,
105
- signal,
106
- exec,
107
- );
108
- if (!collected.ok) return finish({ refusal: collected.error });
275
+ if (!client) {
276
+ refusalCause = "not_configured";
277
+ return finish({ refusal: NOT_CONFIGURED });
278
+ }
279
+ const collected = await collectAskFiles(cwd, args.paths, signal, exec);
280
+ if (!collected.ok) {
281
+ refusalCause = collected.cause ?? "file_unavailable";
282
+ return finish({ refusal: collected.error });
283
+ }
284
+ collectedPaths = collected.files.map((file) => file.path);
109
285
  skipped = collected.skipped;
286
+ for (const skippedPath of skipped)
287
+ diagnose(
288
+ collected.secret.includes(skippedPath)
289
+ ? "secret_pattern"
290
+ : "collection_omitted",
291
+ skippedPath,
292
+ { kind: "inventory", inventoryIds: ["files"] },
293
+ );
110
294
  if (!collected.files.length && skipped.length)
111
295
  return finish({
112
296
  refusal: `No readable evidence: ${skipped.join("; ")}`,
113
297
  });
298
+ const states = collected.files.map((file) => ({
299
+ file,
300
+ state: withEvidenceContext(
301
+ { path: file.path, content: file.content },
302
+ evidenceContext,
303
+ ),
304
+ }));
305
+ if (
306
+ states.some(
307
+ ({ state }) => JSON.stringify(state).length > STATE_MAX_CHARS,
308
+ )
309
+ ) {
310
+ refusalCause = "evidence_too_large";
311
+ return finish({
312
+ refusal: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
313
+ });
314
+ }
114
315
  let sent = 0;
115
316
  let budget: BudgetRefusal | undefined;
116
317
  const options = {
117
318
  signal,
319
+ ...runtime.session.requestGate(),
320
+ admissionCause: () =>
321
+ budget?.kind === "session"
322
+ ? ("session_budget" as const)
323
+ : ("call_budget" as const),
118
324
  beforeRequest: (count: number) => {
119
325
  if (args.max_calls !== undefined && sent >= args.max_calls) {
120
326
  budget = {
@@ -141,17 +347,17 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
141
347
  totals.cacheRequests += result.cacheRequests ?? 0;
142
348
  totals.inputTokens += result.usage?.inputTokens ?? 0;
143
349
  totals.costUsd += result.usage?.costUsd ?? 0;
350
+ if (!result.usage && result.calls !== 0) costKnown = false;
144
351
  };
145
352
  const rows = await Promise.all(
146
- collected.files.map(async (file) => {
147
- const state = { path: file.path, content: file.content };
353
+ states.map(async ({ file, state }) => {
148
354
  const integrity = checkIntegrity(state, plan.asks, [
149
355
  {
150
356
  ...file.identity,
151
- insertedPath: resolve(ctx.cwd, state.path),
152
- content: state.content,
357
+ insertedPath: resolve(cwd, file.path),
358
+ content: file.content,
153
359
  insertedSha256: createHash("sha256")
154
- .update(state.content)
360
+ .update(file.content)
155
361
  .digest("hex"),
156
362
  },
157
363
  ]);
@@ -193,6 +399,93 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
193
399
  const unchecked: string[] = [];
194
400
  const unjudged: NonNullable<EnvelopeInput["unjudged"]>[number][] = [];
195
401
  for (const row of rows) {
402
+ const readings = plan.readings;
403
+ for (const [index, reading] of readings.entries()) {
404
+ const groupId = `${row.file.path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`;
405
+ const common = {
406
+ id: `${row.file.path}:${reading.id}`,
407
+ kind: "file_question" as const,
408
+ label: `${row.file.path} ${reading.label}`,
409
+ groupId,
410
+ evidence: [
411
+ {
412
+ target: row.file.path,
413
+ canonicalPath: known(resolve(cwd, row.file.path)),
414
+ aliases: [],
415
+ side: "current" as const,
416
+ revision: unknown("working tree evidence"),
417
+ },
418
+ ],
419
+ diagnosticIds: [] as string[],
420
+ actionIds: [] as string[],
421
+ };
422
+ const answer = row.answers[index];
423
+ if (
424
+ answer &&
425
+ !answer.unjudged &&
426
+ (answer.source || answer.answer?.source)
427
+ )
428
+ reportItems.push(
429
+ answerItemFromInput(
430
+ common,
431
+ row.integrity.controls.length
432
+ ? {
433
+ ...answer,
434
+ band: "unsure",
435
+ reason: row.integrity.controls
436
+ .map((control) => control.fact)
437
+ .join("; "),
438
+ }
439
+ : answer,
440
+ ),
441
+ );
442
+ else {
443
+ const raw = row.initial.ok
444
+ ? row.initial.answers[reading.id]
445
+ : undefined;
446
+ const cause =
447
+ raw?.type === "unjudged" ? raw.cause : row.initial.failureCause;
448
+ const links = diagnose(
449
+ cause ??
450
+ (budget
451
+ ? budget.kind === "session"
452
+ ? "session_budget"
453
+ : "call_budget"
454
+ : "control_failure"),
455
+ answer?.reason ??
456
+ (row.initial.ok
457
+ ? "Required group incomplete or response provenance not established"
458
+ : row.initial.error),
459
+ { kind: "group", groupIds: [groupId] },
460
+ row.file.path,
461
+ );
462
+ reportItems.push({
463
+ ...common,
464
+ ...links,
465
+ treatment: "not_judged",
466
+ source: "none",
467
+ });
468
+ }
469
+ for (const id of [
470
+ reading.twin,
471
+ ...Object.values(reading.controls ?? {}),
472
+ ].filter((id): id is string => !!id)) {
473
+ const control = row.initial.ok
474
+ ? row.initial.answers[id]
475
+ : undefined;
476
+ if (control && control.type !== "unjudged" && control.source)
477
+ auxiliary.controls[control.source]++;
478
+ else auxiliary.controls.notJudged++;
479
+ }
480
+ const reverse = row.reversed?.ok
481
+ ? row.reversed.answers[reading.id]
482
+ : undefined;
483
+ if (row.reversed) {
484
+ if (reverse && reverse.type !== "unjudged" && reverse.source)
485
+ auxiliary.controls[reverse.source]++;
486
+ else auxiliary.controls.notJudged++;
487
+ }
488
+ }
196
489
  records.push({
197
490
  path: row.file.path,
198
491
  initial: row.initial,
@@ -203,7 +496,7 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
203
496
  unjudged.push({
204
497
  label: row.file.path,
205
498
  reason: row.initial.error,
206
- next: "read the file or retry",
499
+ next: "read the supplied file natively; a new Jev call needs materially changed evidence or recovered service",
207
500
  });
208
501
  continue;
209
502
  }