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
package/src/tools/ask.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
- import { resolve } from "node:path";
2
+ import { isAbsolute, relative, 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 { createAnalysisContext } from "../adapters/analysis-context.ts";
6
6
  import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.ts";
7
7
  import { collectProofSyntax } from "../adapters/ask-syntax.ts";
8
+ import { canonicalPath } from "../adapters/canonical-path.ts";
8
9
  import { captureCommand } from "../adapters/command.ts";
10
+ import {
11
+ type EvidenceContext,
12
+ resolveEvidenceContext,
13
+ withEvidenceContext,
14
+ } from "../adapters/evidence-context.ts";
9
15
  import { collectFiles } from "../adapters/files.ts";
10
16
  import { shareGitInventory } from "../adapters/git-inventory.ts";
11
17
  import { hostUsage } from "../adapters/usage.ts";
@@ -20,6 +26,7 @@ import {
20
26
  STATE_MAX_CHARS,
21
27
  } from "../constants.ts";
22
28
  import { buildAskClosure } from "../core/ask-closure.ts";
29
+ import type { ReferenceResolution } from "../core/ask-references.ts";
23
30
  import {
24
31
  blockedAskReadings,
25
32
  resolveAskReferences,
@@ -31,26 +38,57 @@ import {
31
38
  type ImportSource,
32
39
  } from "../core/imports.ts";
33
40
  import { checkIntegrity } from "../core/integrity.ts";
34
- import type { BudgetRefusal, EnvelopeInput } from "../core/output.ts";
41
+ import type {
42
+ AnswerInput,
43
+ BudgetRefusal,
44
+ EnvelopeInput,
45
+ } from "../core/output.ts";
35
46
  import { buildEnvelope } from "../core/output.ts";
47
+ import {
48
+ type Action,
49
+ answerItemFromInput,
50
+ buildResultReport,
51
+ type Cause,
52
+ type Context,
53
+ contextFromEvidence,
54
+ type Diagnostic,
55
+ type Item,
56
+ known,
57
+ notApplicable,
58
+ type ResultReportV1,
59
+ unknown,
60
+ } from "../core/result-report.ts";
36
61
  import { assembleState } from "../core/state.ts";
37
62
  import { discoverTests } from "../core/test-discovery.ts";
38
63
  import { describeAsk } from "../describe.ts";
39
64
  import type { GuideContext } from "../guide.ts";
40
65
  import type { Judgment } from "../jev/types.ts";
41
- import { renderEnvelope } from "../render.ts";
66
+ import { renderResultReport } from "../render.ts";
42
67
  import { isRecord } from "../result.ts";
43
68
  import type { ToolDependencies } from "../runtime.ts";
44
69
  import { COMMAND_LIMIT_NOTICE } from "../texts/ask.ts";
45
70
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
71
+ import { ASK_GUIDELINE } from "../texts/instructions.ts";
46
72
  import { asksParameter } from "./ask-schema.ts";
47
73
 
74
+ interface AskEvidenceFacts {
75
+ references?: ReferenceResolution;
76
+ omissions: {
77
+ reference: string;
78
+ reason: string;
79
+ origin?: string;
80
+ scope?: string;
81
+ required?: boolean;
82
+ }[];
83
+ blockedGroups: { ids: string[]; reason: string; cause?: Cause }[];
84
+ inventory: string[];
85
+ }
86
+
48
87
  export const askParameters = Type.Object(
49
88
  {
89
+ root: Type.Optional(Type.String()),
50
90
  state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
51
- paths: Type.Optional(
52
- Type.Array(Type.String({ minLength: 1 }), { maxItems: ASK_MAX_FILES }),
53
- ),
91
+ paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
54
92
  base: Type.Optional(Type.String({ minLength: 1 })),
55
93
  command: Type.Optional(Type.String({ minLength: 1 })),
56
94
  timeout_s: Type.Optional(
@@ -88,9 +126,7 @@ export function createAskTool(dependencies: ToolDependencies) {
88
126
  : {
89
127
  promptSnippet:
90
128
  "Typed answers about one situation built from a note, files and a command's output",
91
- promptGuidelines: [
92
- "jev_ask: before you conclude that a failure is a bug in the code, a wrong test or the environment, or that a plan matches the docs, pass the files to jev_ask and weigh its answer against your own reading",
93
- ],
129
+ promptGuidelines: [ASK_GUIDELINE],
94
130
  }),
95
131
  async execute(
96
132
  _id: string,
@@ -100,7 +136,11 @@ export function createAskTool(dependencies: ToolDependencies) {
100
136
  ctx: { cwd: string } & GuideContext,
101
137
  ): Promise<{
102
138
  content: { type: "text"; text: string }[];
103
- details: Judgment;
139
+ details: Judgment & {
140
+ result: ResultReportV1;
141
+ evidenceContext: EvidenceContext;
142
+ evidenceFacts: AskEvidenceFacts;
143
+ };
104
144
  usage?: {
105
145
  input: number;
106
146
  output: number;
@@ -117,7 +157,46 @@ export function createAskTool(dependencies: ToolDependencies) {
117
157
  };
118
158
  }> {
119
159
  const client = dependencies.client;
160
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
161
+ exec: execute,
162
+ signal,
163
+ origin: dependencies.evidenceOrigin,
164
+ });
165
+ const evidenceContext = evidence.context;
166
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
120
167
  const started = performance.now();
168
+ const evidenceFacts: AskEvidenceFacts = {
169
+ omissions: [],
170
+ blockedGroups: [],
171
+ inventory: [],
172
+ };
173
+ evidenceContext.requestedBase = args.base;
174
+ const asks = compileAsks(args.asks, { surface: "ask" });
175
+ let commandContext: Context["command"] = args.command
176
+ ? {
177
+ execution: "not_started",
178
+ cwd: evidence.ok
179
+ ? known(cwd)
180
+ : unknown("effective root not established"),
181
+ exitCode: unknown("command not started"),
182
+ timedOut: unknown("command not started"),
183
+ }
184
+ : {
185
+ execution: "not_requested",
186
+ cwd: notApplicable("no command"),
187
+ exitCode: notApplicable("no command"),
188
+ timedOut: notApplicable("no command"),
189
+ };
190
+ let refusalCause: Cause = "invalid_arguments";
191
+ let inventoryCollected = false;
192
+ const auxiliary = {
193
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
194
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
195
+ };
196
+ const attributionAnswers: Record<string, AnswerInput["reportControls"]> =
197
+ {};
198
+ const attributionMissing = new Set<string>();
199
+ const findResults: Judgment[] = [];
121
200
  const exec = shareGitInventory(execute);
122
201
  const finish = (
123
202
  result: Judgment,
@@ -135,15 +214,324 @@ export function createAskTool(dependencies: ToolDependencies) {
135
214
  },
136
215
  });
137
216
  runtime.session.record(envelope);
217
+ const diagnostics: Diagnostic[] = [];
218
+ const actions: Action[] = [];
219
+ const globalMissing = new Map<
220
+ string,
221
+ { diagnosticIds: string[]; actionIds: string[] }
222
+ >();
223
+ const diagnose = (
224
+ cause: Cause,
225
+ fact: string,
226
+ scope: Diagnostic["scope"],
227
+ target: string | undefined,
228
+ origin: Diagnostic["origin"],
229
+ effect: Diagnostic["effect"] = "blocking",
230
+ next?: string,
231
+ ) => {
232
+ const id = `diagnostic:${diagnostics.length}`;
233
+ const actionId = `action:${actions.length}`;
234
+ const code =
235
+ cause === "invalid_root" || cause === "invalid_base"
236
+ ? "correct_context"
237
+ : cause === "forbidden_path"
238
+ ? "correct_reference"
239
+ : cause === "missing_required" || cause === "empty_required"
240
+ ? "provide_evidence"
241
+ : cause === "not_configured"
242
+ ? "configure_client"
243
+ : "inspect_native";
244
+ actions.push({
245
+ id: actionId,
246
+ code,
247
+ target:
248
+ target === undefined
249
+ ? unknown("target not established")
250
+ : known(target),
251
+ scope,
252
+ condition:
253
+ "Only if materially new evidence or corrected context is available",
254
+ instruction:
255
+ next ??
256
+ (code === "provide_evidence"
257
+ ? `Provide the actually missing evidence${target ? `: ${target}` : ""}; do not repeat an already captured command.`
258
+ : "Inspect the supplied evidence natively; correct the named cause before any useful new Jev call."),
259
+ repeatUnchanged: false,
260
+ });
261
+ diagnostics.push({
262
+ id,
263
+ cause,
264
+ fact,
265
+ target:
266
+ target === undefined
267
+ ? unknown("target not established")
268
+ : known(target),
269
+ origin,
270
+ scope,
271
+ effect,
272
+ material: true,
273
+ omittedMembers: [],
274
+ memberCount: known(0),
275
+ actionIds: [actionId],
276
+ });
277
+ return { diagnosticIds: [id], actionIds: [actionId] };
278
+ };
279
+ const items: Item[] = asks.ok
280
+ ? asks.readings.map((reading, index) => {
281
+ const groupId = `group:${asks.groups.findIndex((group) => group.includes(reading.id))}`;
282
+ const target = asks.asks[reading.askIndex]?.about ?? "state";
283
+ const pieces: Item["evidence"] = [
284
+ {
285
+ target,
286
+ canonicalPath: unknown(
287
+ "selector does not establish a canonical file identity",
288
+ ),
289
+ aliases: [],
290
+ side: target === "output" ? "output" : "state",
291
+ revision: notApplicable("non-file evidence"),
292
+ },
293
+ ];
294
+ for (const [alias, path] of Object.entries(
295
+ evidenceFacts.references?.aliases ?? {},
296
+ )) {
297
+ const before =
298
+ evidenceFacts.references?.additions.some(
299
+ (addition) =>
300
+ addition.reference === alias &&
301
+ addition.side === "files_before",
302
+ ) || target.includes("files_before");
303
+ pieces.push({
304
+ target: alias,
305
+ canonicalPath: evidenceContext.effectiveRoot
306
+ ? known(resolve(evidenceContext.effectiveRoot.path, path))
307
+ : unknown("effective root unavailable"),
308
+ aliases: [alias],
309
+ side: before ? "before" : "current",
310
+ revision:
311
+ before && evidenceContext.resolvedBase
312
+ ? known(evidenceContext.resolvedBase)
313
+ : unknown("working tree evidence"),
314
+ });
315
+ }
316
+ const common = {
317
+ id: reading.id,
318
+ kind:
319
+ reading.kind === "verify"
320
+ ? ("claim" as const)
321
+ : ("question" as const),
322
+ label: reading.label,
323
+ groupId,
324
+ evidence: pieces,
325
+ diagnosticIds: [] as string[],
326
+ actionIds: [] as string[],
327
+ };
328
+ const originalAnswer = input.answers?.[index];
329
+ const attributionControls = attributionAnswers[reading.id];
330
+ const answer =
331
+ originalAnswer && attributionMissing.has(reading.id)
332
+ ? {
333
+ ...originalAnswer,
334
+ unjudged: true,
335
+ reason: "Required attribution control is incomplete",
336
+ }
337
+ : originalAnswer && attributionControls
338
+ ? {
339
+ ...originalAnswer,
340
+ reportControls: [
341
+ ...(originalAnswer.reportControls ?? []),
342
+ ...attributionControls,
343
+ ],
344
+ }
345
+ : originalAnswer;
346
+ if (
347
+ answer &&
348
+ !answer.unjudged &&
349
+ (answer.source || answer.answer?.source)
350
+ )
351
+ return answerItemFromInput(
352
+ common,
353
+ input.controls?.length
354
+ ? {
355
+ ...answer,
356
+ band: "unsure",
357
+ reason: input.controls
358
+ .map((control) => control.fact)
359
+ .join("; "),
360
+ }
361
+ : answer,
362
+ );
363
+ const blocked = evidenceFacts.blockedGroups.find((group) =>
364
+ group.ids.includes(reading.id),
365
+ );
366
+ const omission = evidenceFacts.omissions.find(
367
+ (item) =>
368
+ item.required &&
369
+ (item.origin === "note" || item.scope === reading.id),
370
+ );
371
+ const groupFailure = result.ok
372
+ ? (
373
+ asks.groups.find((group) => group.includes(reading.id)) ??
374
+ []
375
+ )
376
+ .map((id) => result.answers[id])
377
+ .find(
378
+ (answer) => answer?.type === "unjudged" && answer.cause,
379
+ )
380
+ : undefined;
381
+ const cause = blocked
382
+ ? (blocked.cause ?? "missing_required")
383
+ : groupFailure?.type === "unjudged" && groupFailure.cause
384
+ ? groupFailure.cause
385
+ : input.budget
386
+ ? input.budget.kind === "session"
387
+ ? "session_budget"
388
+ : "call_budget"
389
+ : (result.failureCause ??
390
+ (input.refusal ? refusalCause : "control_failure"));
391
+ const globalKey =
392
+ omission?.origin === "note" ? omission.reference : undefined;
393
+ const existingLinks = globalKey
394
+ ? globalMissing.get(globalKey)
395
+ : undefined;
396
+ const links =
397
+ existingLinks ??
398
+ diagnose(
399
+ cause,
400
+ blocked?.reason ??
401
+ answer?.reason ??
402
+ (result.ok
403
+ ? "Required judgment group incomplete or response provenance not established"
404
+ : result.error),
405
+ omission?.origin === "note"
406
+ ? { kind: "call" }
407
+ : { kind: "group", groupIds: [groupId] },
408
+ omission?.reference,
409
+ omission?.origin === "note"
410
+ ? "note"
411
+ : blocked
412
+ ? "ask"
413
+ : input.budget
414
+ ? "budget"
415
+ : "provider",
416
+ );
417
+ if (globalKey) globalMissing.set(globalKey, links);
418
+ return {
419
+ ...common,
420
+ ...links,
421
+ treatment: "not_judged",
422
+ source: "none",
423
+ };
424
+ })
425
+ : [];
426
+ if (!items.length)
427
+ diagnose(
428
+ input.refusal ? refusalCause : "collection_empty",
429
+ input.refusal ?? "No requested results collected",
430
+ { kind: "call" },
431
+ undefined,
432
+ "input",
433
+ );
434
+ for (const control of input.controls ?? [])
435
+ diagnose(
436
+ "control_failure",
437
+ control.fact,
438
+ { kind: "call" },
439
+ undefined,
440
+ "control",
441
+ "reservation",
442
+ control.next,
443
+ );
444
+ for (const limitation of input.limitations ?? [])
445
+ diagnose(
446
+ "evidence_limit",
447
+ limitation.fact,
448
+ { kind: "call" },
449
+ limitation.path,
450
+ "closure",
451
+ "reservation",
452
+ limitation.next,
453
+ );
454
+ const report = buildResultReport({
455
+ tool: "jev_ask",
456
+ context: contextFromEvidence(evidenceContext, {
457
+ command: commandContext,
458
+ inventories: inventoryCollected
459
+ ? [
460
+ {
461
+ id: "repository",
462
+ kind: "repository",
463
+ rules: ["Git tracked and admitted repository evidence"],
464
+ restrictions: args.paths ?? [],
465
+ discovered: known(evidenceFacts.inventory.length),
466
+ considered: known(evidenceFacts.inventory.length),
467
+ scopeRestricted: false,
468
+ criteria: [],
469
+ },
470
+ ]
471
+ : [],
472
+ }),
473
+ items,
474
+ diagnostics,
475
+ actions,
476
+ metrics: {
477
+ calls: result.calls ?? 0,
478
+ questions: result.questions ?? 0,
479
+ cacheHits: result.cacheHits ?? 0,
480
+ cacheRequests: result.cacheRequests ?? 0,
481
+ costUsd: result.usage?.costUsd,
482
+ elapsedMs: performance.now() - started,
483
+ },
484
+ auxiliary,
485
+ total: known(items.length),
486
+ refused:
487
+ !!input.refusal &&
488
+ refusalCause !== "not_configured" &&
489
+ !result.failureCause,
490
+ });
138
491
  runtime.guide.deliver(ctx);
139
492
  return {
140
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
141
- details: result,
493
+ content: [
494
+ {
495
+ type: "text" as const,
496
+ text: renderResultReport(report, { details: envelope }),
497
+ },
498
+ ],
499
+ details: {
500
+ ...result,
501
+ result: report,
502
+ evidenceContext,
503
+ evidenceFacts,
504
+ },
142
505
  };
143
506
  };
144
- const textResult = (text: string) =>
145
- finish({ ok: false, error: text }, { refusal: text });
146
- if (!client) return textResult(NOT_CONFIGURED);
507
+ const textResult = (text: string, cause: Cause = "invalid_arguments") => {
508
+ refusalCause = cause;
509
+ const technical = findResults.reduce(
510
+ (totals, judgment) => ({
511
+ calls: totals.calls + (judgment.calls ?? 0),
512
+ questions: totals.questions + (judgment.questions ?? 0),
513
+ cacheHits: totals.cacheHits + (judgment.cacheHits ?? 0),
514
+ cacheRequests: totals.cacheRequests + (judgment.cacheRequests ?? 0),
515
+ }),
516
+ { calls: 0, questions: 0, cacheHits: 0, cacheRequests: 0 },
517
+ );
518
+ return finish(
519
+ { ok: false, error: text, ...technical },
520
+ { refusal: text },
521
+ );
522
+ };
523
+ if (!evidence.ok)
524
+ return textResult(evidence.error, evidence.cause ?? "invalid_root");
525
+ if (!asks.ok) return textResult(asks.error);
526
+ const preliminary = resolveAskReferences(asks, args.state, {}, []);
527
+ if (preliminary.invalid.length) {
528
+ evidenceFacts.references = preliminary;
529
+ return textResult(
530
+ preliminary.invalid.map((item) => item.reason).join("; "),
531
+ "forbidden_path",
532
+ );
533
+ }
534
+ if (!client) return textResult(NOT_CONFIGURED, "not_configured");
147
535
  if (args.command && !allowCommand)
148
536
  return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
149
537
  if (
@@ -157,69 +545,115 @@ export function createAskTool(dependencies: ToolDependencies) {
157
545
  );
158
546
  if (args.state === undefined && !args.paths?.length && !args.command)
159
547
  return textResult("Provide state, command or at least one path.");
160
- const asks = compileAsks(args.asks, { surface: "ask" });
161
- if (!asks.ok) return textResult(asks.error);
162
- const files = await collectFiles(ctx.cwd, args.paths ?? [], signal, {
548
+ const files = await collectFiles(cwd, args.paths ?? [], signal, {
163
549
  exec,
164
550
  allowEmpty: true,
165
551
  skipInvalidUtf8: true,
166
552
  });
167
- if (!files.ok) return textResult(files.error);
553
+ if (!files.ok)
554
+ return textResult(files.error, files.cause ?? "file_unavailable");
168
555
  if (
169
556
  !Object.keys(files.files).length &&
170
557
  args.state === undefined &&
171
558
  !args.command &&
172
559
  files.skipped.length
173
560
  )
174
- return textResult(`No readable evidence: ${files.skipped.join("; ")}`);
561
+ return textResult(
562
+ `No readable evidence: ${files.skipped.join("; ")}`,
563
+ "collection_empty",
564
+ );
175
565
  const proofLimitations: Array<
176
566
  NonNullable<EnvelopeInput["limitations"]>[number]
177
567
  > = files.skipped.map((fact) => ({
178
568
  fact,
179
569
  next: "Provide this file as UTF-8 text if the judgment needs it.",
180
570
  }));
181
- let repository = await collectAskRepository(
182
- exec,
183
- ctx.cwd,
184
- args.base,
185
- signal,
186
- );
187
- if (!repository.ok && args.base) return textResult(repository.error);
571
+ let repository = await collectAskRepository(exec, cwd, args.base, signal);
572
+ inventoryCollected = repository.ok;
573
+ if (repository.ok) {
574
+ evidenceFacts.inventory = [...repository.known];
575
+ evidenceContext.resolvedBase = repository.baseSha;
576
+ }
577
+ if (!repository.ok && args.base)
578
+ return textResult(repository.error, "invalid_base");
188
579
  if (!repository.ok)
189
580
  proofLimitations.push({
190
581
  fact: `closure unavailable: ${repository.error}`,
191
582
  next: "pass dependencies explicitly in paths",
192
583
  });
584
+ if (args.command)
585
+ commandContext = {
586
+ ...commandContext,
587
+ execution: "started",
588
+ cwd: known(repository.ok ? repository.root : cwd),
589
+ };
193
590
  const command = args.command
194
591
  ? await captureCommand(
195
592
  exec,
196
- repository.ok ? repository.root : ctx.cwd,
593
+ repository.ok ? repository.root : cwd,
197
594
  args.command,
198
595
  args.timeout_s,
199
596
  signal,
597
+ dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY,
200
598
  )
201
599
  : undefined;
202
- if (command && !command.ok) return textResult(command.error);
600
+ if (command?.ok)
601
+ commandContext = {
602
+ execution: "finished",
603
+ cwd: known(repository.ok ? repository.root : cwd),
604
+ exitCode: known(command.output.exit_code),
605
+ timedOut: known(command.output.timed_out),
606
+ };
607
+ if (command && !command.ok) {
608
+ commandContext = {
609
+ // Reported execution states are not_requested, not_started, started
610
+ // and finished. A spawn whose completion was not observed (adapter
611
+ // "unknown") did start, so it reports "started" with unknown exit
612
+ // code and timeout.
613
+ execution:
614
+ command.commandExecution === "unknown"
615
+ ? "started"
616
+ : command.commandExecution,
617
+ cwd: known(repository.ok ? repository.root : cwd),
618
+ exitCode:
619
+ command.commandExitCode !== undefined
620
+ ? known(command.commandExitCode)
621
+ : unknown("command completion not observed"),
622
+ timedOut:
623
+ command.commandTimedOut !== undefined
624
+ ? known(command.commandTimedOut)
625
+ : unknown("command completion not observed"),
626
+ };
627
+ return textResult(command.error, command.cause ?? "file_unavailable");
628
+ }
203
629
  if (command?.ok) {
204
- repository = await collectAskRepository(
205
- exec,
206
- ctx.cwd,
207
- args.base,
208
- signal,
209
- );
210
- if (!repository.ok && args.base) return textResult(repository.error);
630
+ repository = await collectAskRepository(exec, cwd, args.base, signal);
631
+ if (!repository.ok && args.base)
632
+ return textResult(repository.error, "invalid_base");
211
633
  }
212
634
  const initialState = assembleState(args.state, files.files);
213
635
  if (!initialState.ok) return textResult(initialState.error);
214
636
  if (command?.ok && Object.hasOwn(initialState.state, "output"))
215
637
  return textResult(
216
638
  "state contains reserved key output when command is supplied; rename it in your note",
639
+ "reserved_key",
217
640
  );
218
641
  let outputNotIdentifiable = false;
219
642
  if (command?.ok && repository.ok) {
220
643
  for (const target of command.targets) {
221
644
  if (target.path.startsWith(`${repository.root}/`))
222
645
  target.path = target.path.slice(repository.root.length + 1);
646
+ else if (isAbsolute(target.path)) {
647
+ // Windows reports C:/... or short 8.3 forms; compare canonically.
648
+ const local = relative(
649
+ repository.root,
650
+ await canonicalPath(target.path),
651
+ )
652
+ .split("\\")
653
+ .join("/");
654
+ if (local && !local.startsWith("../") && !isAbsolute(local))
655
+ target.path = local;
656
+ }
223
657
  let matches = [...repository.known].filter(
224
658
  (path) => path === target.path || path.endsWith(`/${target.path}`),
225
659
  );
@@ -252,7 +686,7 @@ export function createAskTool(dependencies: ToolDependencies) {
252
686
  }
253
687
  const path = matches[0];
254
688
  if (!path) continue;
255
- const local = repositoryPath(ctx.cwd, repository.root, path);
689
+ const local = repositoryPath(cwd, repository.root, path);
256
690
  if (!Object.hasOwn(files.files, local)) {
257
691
  const added = await collectFiles(repository.root, [path], signal, {
258
692
  exec,
@@ -306,35 +740,75 @@ export function createAskTool(dependencies: ToolDependencies) {
306
740
  const references = resolveAskReferences(
307
741
  asks,
308
742
  args.state,
309
- files.files,
743
+ repository.ok
744
+ ? {
745
+ files: Object.fromEntries(
746
+ Object.entries(files.files).map(([path, text]) => [
747
+ repositoryPath(repository.root, cwd, path),
748
+ text,
749
+ ]),
750
+ ),
751
+ beforeInventory: args.base ? [...repository.beforeKnown] : [],
752
+ }
753
+ : files.files,
310
754
  repository?.ok ? [...repository.known] : [],
311
755
  );
756
+ evidenceFacts.references = references;
757
+ evidenceFacts.inventory = repository.ok ? [...repository.known] : [];
758
+ if (references.invalid.length)
759
+ return textResult(
760
+ references.invalid.map((item) => item.reason).join("; "),
761
+ "forbidden_path",
762
+ );
312
763
  const unresolved = [...references.unresolved];
764
+ const historical: Record<string, string | null> = {};
765
+ if (repository.ok && args.base) {
766
+ for (const reference of references.additions.filter(
767
+ (reference) =>
768
+ reference.side === "files_before" && reference.required,
769
+ )) {
770
+ const before = await repository.readBefore(reference.path);
771
+ if (before.ok) historical[reference.path] = before.text;
772
+ else {
773
+ if (before.cause === "forbidden_path")
774
+ return textResult(before.error, before.cause);
775
+ unresolved.push({
776
+ ...reference,
777
+ reason: before.error,
778
+ ...(before.cause ? { cause: before.cause } : {}),
779
+ });
780
+ }
781
+ }
782
+ }
313
783
  if (repository?.ok) {
314
784
  const root = repository.root;
315
785
  const additions = await Promise.all(
316
- references.additions.map(async (addition) => ({
317
- addition,
318
- added: await collectFiles(root, [addition.path], signal, {
319
- allowEmpty: true,
320
- exec,
321
- inventory: repository.known,
322
- }),
323
- })),
786
+ references.additions
787
+ .filter((addition) => addition.side !== "files_before")
788
+ .map(async (addition) => ({
789
+ addition,
790
+ added: await collectFiles(root, [addition.path], signal, {
791
+ allowEmpty: true,
792
+ exec,
793
+ inventory: repository.known,
794
+ }),
795
+ })),
324
796
  );
325
797
  for (const { addition, added } of additions) {
326
798
  if (added.ok) {
327
- files.files[addition.reference] = added.files[addition.path] ?? "";
799
+ const local = repositoryPath(cwd, root, addition.path);
800
+ files.files[local] = added.files[addition.path] ?? "";
328
801
  files.identities.push(
329
802
  ...added.identities.map((identity) => ({
330
803
  ...identity,
331
- requestedPath: addition.reference,
804
+ requestedPath: local,
332
805
  })),
333
806
  );
334
807
  } else
335
808
  unresolved.push({
336
- reference: addition.reference,
809
+ ...addition,
337
810
  reason: added.error,
811
+ ...(added.cause ? { cause: added.cause } : {}),
338
812
  });
339
813
  }
340
814
  }
@@ -343,44 +817,58 @@ export function createAskTool(dependencies: ToolDependencies) {
343
817
  if (command?.ok && Object.hasOwn(state.state, "output"))
344
818
  return textResult(
345
819
  "state contains reserved key output when command is supplied; rename it in your note",
820
+ "reserved_key",
346
821
  );
347
822
  if (command?.ok) state.state.output = { ...command.output };
348
823
  if (repository?.ok) {
824
+ if (evidenceContext.effectiveRoot)
825
+ evidenceContext.effectiveRoot.path = repository.root;
826
+ evidenceContext.requestedBase = args.base;
827
+ evidenceContext.resolvedBase = repository.baseSha;
349
828
  const canonical = Object.fromEntries(
350
829
  Object.entries(files.files).map(([path, text]) => [
351
- repositoryPath(repository.root, ctx.cwd, path),
830
+ repositoryPath(repository.root, cwd, path),
352
831
  text,
353
832
  ]),
354
833
  );
355
- for (const addition of references.additions)
356
- canonical[addition.path] = files.files[addition.reference] ?? "";
834
+ state.state.files = canonical;
835
+ state.state.evidence = {
836
+ context: evidenceContext,
837
+ aliases: Object.fromEntries(
838
+ Object.entries(references.aliases)
839
+ .map(([alias, path]): [string, string] => [
840
+ alias,
841
+ repository.known.has(path) || repository.beforeKnown.has(path)
842
+ ? path
843
+ : repositoryPath(repository.root, cwd, path),
844
+ ])
845
+ .sort(([a], [b]) => a.localeCompare(b)),
846
+ ),
847
+ };
848
+ // Canonical repository paths are the sole serialized content keys.
357
849
  const sources: ImportSource[] = Object.entries(canonical).map(
358
850
  ([path, text]) => ({ path, text }),
359
851
  );
360
- const before: Record<string, string | null> = {};
852
+ const before: Record<string, string | null> = { ...historical };
361
853
  if (args.base) {
362
854
  for (const path of Object.keys(canonical)) {
363
855
  const old = await repository.readBefore(path);
364
- if (!old.ok) return textResult(old.error);
856
+ if (!old.ok)
857
+ return textResult(old.error, old.cause ?? "file_unavailable");
365
858
  before[path] = old.text;
366
859
  }
367
860
  state.state.base_sha = repository.baseSha ?? "";
368
- state.state.files_before = Object.fromEntries(
369
- Object.keys(files.files).map((path) => {
370
- const canonicalPath =
371
- references.additions.find(
372
- (addition) => addition.reference === path,
373
- )?.path ?? repositoryPath(repository.root, ctx.cwd, path);
374
- return [path, before[canonicalPath] ?? null];
375
- }),
376
- );
377
- const stateSize = JSON.stringify(state.state).length;
861
+ state.state.files_before = before;
862
+ const stateSize = JSON.stringify(
863
+ command?.ok ? { ...state.state, output: undefined } : state.state,
864
+ ).length;
378
865
  if (stateSize > STATE_MAX_CHARS) {
379
866
  const partSizes = Object.entries(state.state)
380
867
  .map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
381
868
  .join(", ");
382
869
  return textResult(
383
870
  `serialized state including files_before exceeds ${STATE_MAX_CHARS} chars (${stateSize}); serialized parts: ${partSizes}; split the situation into smaller states.`,
871
+ "evidence_too_large",
384
872
  );
385
873
  }
386
874
  }
@@ -480,14 +968,17 @@ export function createAskTool(dependencies: ToolDependencies) {
480
968
  sources,
481
969
  ...(args.base ? { before } : {}),
482
970
  };
971
+ const closureState = { ...state.state };
972
+ if (command?.ok) delete closureState.output;
483
973
  const closure = buildAskClosure({
484
974
  graph: { edges, limits },
485
975
  syntax,
486
976
  paths: Object.keys(canonical),
487
977
  intentions: JSON.stringify(asks.asks),
488
- state: state.state,
978
+ state: closureState,
489
979
  });
490
980
  state.state = closure.state;
981
+ if (command?.ok) state.state.output = { ...command.output };
491
982
  proofLimitations.push(
492
983
  ...closure.limitations.filter(
493
984
  (limit) => !limit.fact.startsWith("Import closure only,"),
@@ -502,6 +993,22 @@ export function createAskTool(dependencies: ToolDependencies) {
502
993
  fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
503
994
  next: "pass dependencies explicitly in paths",
504
995
  });
996
+ if (!repository.ok)
997
+ state.state.evidence = {
998
+ context: evidenceContext,
999
+ aliases: references.aliases,
1000
+ };
1001
+ // Command output has its own passage-selection budget below. Only immutable
1002
+ // file/note evidence can refuse here, before selection has had a chance.
1003
+ const serializedSize = JSON.stringify(
1004
+ command?.ok ? { ...state.state, output: undefined } : state.state,
1005
+ ).length;
1006
+ if (serializedSize > STATE_MAX_CHARS)
1007
+ return textResult(
1008
+ `serialized state exceeds ${STATE_MAX_CHARS} chars (${serializedSize}); files and versions are never truncated`,
1009
+ "evidence_too_large",
1010
+ );
1011
+ evidenceFacts.omissions = unresolved;
505
1012
  const blocked = blockedAskReadings(
506
1013
  asks,
507
1014
  state.state,
@@ -510,13 +1017,16 @@ export function createAskTool(dependencies: ToolDependencies) {
510
1017
  );
511
1018
  const inserted = isRecord(state.state.files) ? state.state.files : {};
512
1019
  const identities = files.identities.map((file) => {
513
- const content = inserted[file.requestedPath];
514
- const insertedPath = Object.keys(inserted).find(
515
- (path) => resolve(ctx.cwd, path) === file.resolvedPath,
516
- );
1020
+ const insertedPath = repository.ok
1021
+ ? repositoryPath(repository.root, cwd, file.requestedPath)
1022
+ : file.requestedPath;
1023
+ const content = inserted[insertedPath];
517
1024
  return {
518
1025
  ...file,
519
- insertedPath: insertedPath ? resolve(ctx.cwd, insertedPath) : "",
1026
+ insertedPath: resolve(
1027
+ repository.ok ? repository.root : cwd,
1028
+ insertedPath,
1029
+ ),
520
1030
  content: typeof content === "string" ? content : "",
521
1031
  insertedSha256: createHash("sha256")
522
1032
  .update(typeof content === "string" ? content : "")
@@ -528,6 +1038,24 @@ export function createAskTool(dependencies: ToolDependencies) {
528
1038
  .filter((group) => group.some((id) => blocked.has(id)))
529
1039
  .flat(),
530
1040
  );
1041
+ evidenceFacts.blockedGroups = asks.groups
1042
+ .filter((group) => group.some((id) => blocked.has(id)))
1043
+ .map((ids) => {
1044
+ const reason =
1045
+ ids
1046
+ .map((id) => blocked.get(id))
1047
+ .find((reason) => reason !== undefined) ??
1048
+ "Required evidence unavailable";
1049
+ // A secret refusal stays named as such instead of missing_required.
1050
+ const secret = unresolved.some(
1051
+ (item) => item.reason === reason && item.cause === "secret_pattern",
1052
+ );
1053
+ return {
1054
+ ids,
1055
+ reason,
1056
+ ...(secret ? { cause: "secret_pattern" as const } : {}),
1057
+ };
1058
+ });
531
1059
  const questions = Object.fromEntries(
532
1060
  Object.entries(asks.questions).filter(
533
1061
  ([id]) => !blockedMembers.has(id),
@@ -540,10 +1068,14 @@ export function createAskTool(dependencies: ToolDependencies) {
540
1068
  );
541
1069
  let sent = 0;
542
1070
  let refusal: BudgetRefusal | undefined;
543
- const findResults: Judgment[] = [];
544
1071
  const options = {
545
1072
  signal,
546
1073
  cache: !args.command,
1074
+ ...runtime.session.requestGate(),
1075
+ admissionCause: () =>
1076
+ refusal?.kind === "session"
1077
+ ? ("session_budget" as const)
1078
+ : ("call_budget" as const),
547
1079
  beforeRequest: (questionCount: number) => {
548
1080
  if (args.max_calls !== undefined && sent >= args.max_calls) {
549
1081
  refusal = {
@@ -578,11 +1110,25 @@ export function createAskTool(dependencies: ToolDependencies) {
578
1110
  )
579
1111
  return textResult(
580
1112
  `output requires more than ${OUTPUT_FIND_MAX_CALLS} find calls: ${command.originalBytes} bytes before, ${command.compressedChars} chars after compression; narrow command (remove verbosity or filter)`,
1113
+ "evidence_too_large",
1114
+ );
1115
+ const chunkStates = chunks.map((chunk) =>
1116
+ withEvidenceContext({ output: chunk.text }, evidenceContext),
1117
+ );
1118
+ if (
1119
+ chunkStates.some(
1120
+ (chunkState) =>
1121
+ JSON.stringify(chunkState).length > STATE_MAX_CHARS,
1122
+ )
1123
+ )
1124
+ return textResult(
1125
+ `serialized output chunk including evidence context exceeds ${STATE_MAX_CHARS} chars; narrow command`,
1126
+ "evidence_too_large",
581
1127
  );
582
1128
  const found = await Promise.all(
583
- chunks.map(async (chunk) => {
1129
+ chunkStates.map(async (chunkState) => {
584
1130
  const judgment = await client.judge(
585
- { output: chunk.text },
1131
+ chunkState,
586
1132
  {
587
1133
  find: {
588
1134
  type: "bool",
@@ -616,6 +1162,7 @@ export function createAskTool(dependencies: ToolDependencies) {
616
1162
  if (budget < 100)
617
1163
  return textResult(
618
1164
  "No space for command output; reduce paths or state.",
1165
+ "evidence_too_large",
619
1166
  );
620
1167
  const stderrSize = JSON.stringify(output.stderr).length;
621
1168
  const stdoutSize = JSON.stringify(output.stdout).length;
@@ -678,6 +1225,7 @@ export function createAskTool(dependencies: ToolDependencies) {
678
1225
  if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
679
1226
  return textResult(
680
1227
  "serialized command output exceeds state budget; narrow command or reduce paths",
1228
+ "evidence_too_large",
681
1229
  );
682
1230
  if (outputNotIdentifiable)
683
1231
  integrity.controls.push({
@@ -730,13 +1278,14 @@ export function createAskTool(dependencies: ToolDependencies) {
730
1278
  cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
731
1279
  cacheRequests:
732
1280
  (first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
733
- usage: {
734
- inputTokens:
735
- (first.usage?.inputTokens ?? 0) +
736
- (reversed.usage?.inputTokens ?? 0),
737
- costUsd:
738
- (first.usage?.costUsd ?? 0) + (reversed.usage?.costUsd ?? 0),
739
- },
1281
+ usage:
1282
+ first.usage && reversed.usage
1283
+ ? {
1284
+ inputTokens:
1285
+ first.usage.inputTokens + reversed.usage.inputTokens,
1286
+ costUsd: first.usage.costUsd + reversed.usage.costUsd,
1287
+ }
1288
+ : undefined,
740
1289
  };
741
1290
  }
742
1291
  }
@@ -745,12 +1294,17 @@ export function createAskTool(dependencies: ToolDependencies) {
745
1294
  ...result,
746
1295
  calls: (result.calls ?? 0) + (found.calls ?? 0),
747
1296
  questions: (result.questions ?? 0) + (found.questions ?? 0),
748
- usage: {
749
- inputTokens:
750
- (result.usage?.inputTokens ?? 0) +
751
- (found.usage?.inputTokens ?? 0),
752
- costUsd: (result.usage?.costUsd ?? 0) + (found.usage?.costUsd ?? 0),
753
- },
1297
+ cacheHits: (result.cacheHits ?? 0) + (found.cacheHits ?? 0),
1298
+ cacheRequests:
1299
+ (result.cacheRequests ?? 0) + (found.cacheRequests ?? 0),
1300
+ usage:
1301
+ result.usage && found.usage
1302
+ ? {
1303
+ inputTokens:
1304
+ result.usage.inputTokens + found.usage.inputTokens,
1305
+ costUsd: result.usage.costUsd + found.usage.costUsd,
1306
+ }
1307
+ : undefined,
754
1308
  };
755
1309
  const attribution: Array<
756
1310
  NonNullable<EnvelopeInput["limitations"]>[number]
@@ -775,6 +1329,8 @@ export function createAskTool(dependencies: ToolDependencies) {
775
1329
  fact: `attribution ${reading.label}: no designated candidate`,
776
1330
  next: "provide decisive candidate evidence before acting",
777
1331
  });
1332
+ attributionMissing.add(reading.id);
1333
+ auxiliary.controls.notJudged++;
778
1334
  continue;
779
1335
  }
780
1336
  if (!Object.hasOwn(inserted, selected)) {
@@ -782,19 +1338,21 @@ export function createAskTool(dependencies: ToolDependencies) {
782
1338
  fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
783
1339
  next: `add ${selected} to paths before acting`,
784
1340
  });
1341
+ attributionMissing.add(reading.id);
1342
+ auxiliary.controls.notJudged++;
785
1343
  continue;
786
1344
  }
787
1345
  const selectedPath = repository?.ok
788
1346
  ? (references.additions.find(
789
1347
  (addition) => addition.reference === selected,
790
- )?.path ?? repositoryPath(repository.root, ctx.cwd, selected))
1348
+ )?.path ?? repositoryPath(repository.root, cwd, selected))
791
1349
  : selected;
792
1350
  const retained = (path: string) => {
793
1351
  if (!repository?.ok) return path !== selected;
794
1352
  return (
795
1353
  (references.additions.find(
796
1354
  (addition) => addition.reference === path,
797
- )?.path ?? repositoryPath(repository.root, ctx.cwd, path)) !==
1355
+ )?.path ?? repositoryPath(repository.root, cwd, path)) !==
798
1356
  selectedPath
799
1357
  );
800
1358
  };
@@ -830,6 +1388,40 @@ export function createAskTool(dependencies: ToolDependencies) {
830
1388
  options,
831
1389
  );
832
1390
  const answer = control.ok ? control.answers[reading.id] : undefined;
1391
+ if (answer && answer.type !== "unjudged" && answer.source)
1392
+ attributionAnswers[reading.id] = [
1393
+ {
1394
+ id: `${reading.id}:attribution`,
1395
+ kind: "attribution",
1396
+ source: answer.source,
1397
+ outcome:
1398
+ answer.type === "choice"
1399
+ ? answer.choice === selected
1400
+ ? "does not rest on it"
1401
+ : "attributed"
1402
+ : "invalid attribution response",
1403
+ rawValues:
1404
+ answer.type === "bool"
1405
+ ? [
1406
+ {
1407
+ label: "attribution",
1408
+ value: answer.p >= 0.5,
1409
+ probability: known(answer.p),
1410
+ },
1411
+ ]
1412
+ : Object.entries(answer.probabilities).map(
1413
+ ([label, value]) => ({
1414
+ label,
1415
+ value: label,
1416
+ probability: known(value),
1417
+ }),
1418
+ ),
1419
+ },
1420
+ ];
1421
+ if (answer?.type !== "choice") attributionMissing.add(reading.id);
1422
+ if (answer && answer.type !== "unjudged" && answer.source)
1423
+ auxiliary.controls[answer.source]++;
1424
+ else auxiliary.controls.notJudged++;
833
1425
  attribution.push(
834
1426
  answer?.type === "choice"
835
1427
  ? {
@@ -848,13 +1440,14 @@ export function createAskTool(dependencies: ToolDependencies) {
848
1440
  cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
849
1441
  cacheRequests:
850
1442
  (result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
851
- usage: {
852
- inputTokens:
853
- (result.usage?.inputTokens ?? 0) +
854
- (control.usage?.inputTokens ?? 0),
855
- costUsd:
856
- (result.usage?.costUsd ?? 0) + (control.usage?.costUsd ?? 0),
857
- },
1443
+ usage:
1444
+ result.usage && control.usage
1445
+ ? {
1446
+ inputTokens:
1447
+ result.usage.inputTokens + control.usage.inputTokens,
1448
+ costUsd: result.usage.costUsd + control.usage.costUsd,
1449
+ }
1450
+ : undefined,
858
1451
  };
859
1452
  }
860
1453
  }
@@ -873,6 +1466,32 @@ export function createAskTool(dependencies: ToolDependencies) {
873
1466
  (reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"),
874
1467
  )
875
1468
  .map((reading) => reading.label);
1469
+ for (const reading of asks.readings) {
1470
+ for (const id of [
1471
+ reading.twin,
1472
+ ...Object.values(reading.controls ?? {}),
1473
+ ].filter((id): id is string => !!id)) {
1474
+ const answer = result.ok ? result.answers[id] : undefined;
1475
+ if (answer && answer.type !== "unjudged" && answer.source)
1476
+ auxiliary.controls[answer.source]++;
1477
+ else auxiliary.controls.notJudged++;
1478
+ }
1479
+ if (
1480
+ reversed &&
1481
+ Object.hasOwn(reversed.ok ? reversed.answers : {}, reading.id)
1482
+ ) {
1483
+ const answer = reversed.ok ? reversed.answers[reading.id] : undefined;
1484
+ if (answer && answer.type !== "unjudged" && answer.source)
1485
+ auxiliary.controls[answer.source]++;
1486
+ else auxiliary.controls.notJudged++;
1487
+ }
1488
+ }
1489
+ for (const found of findResults) {
1490
+ const answer = found.ok ? found.answers.find : undefined;
1491
+ if (answer && answer.type !== "unjudged" && answer.source)
1492
+ auxiliary.passages[answer.source]++;
1493
+ else auxiliary.passages.notJudged++;
1494
+ }
876
1495
  const output = finish(result, {
877
1496
  ...(result.ok
878
1497
  ? {
@@ -902,6 +1521,14 @@ export function createAskTool(dependencies: ToolDependencies) {
902
1521
  },
903
1522
  ]
904
1523
  : []),
1524
+ ...(command?.ok && command.redactions
1525
+ ? [
1526
+ {
1527
+ fact: `output: ${command.redactions} occurrence${command.redactions === 1 ? "" : "s"} of the configured Jev API key replaced with [redacted]`,
1528
+ next: "remove the key from the command's output; it is never sent to Jev",
1529
+ },
1530
+ ]
1531
+ : []),
905
1532
  ...(command?.ok && command.shapeLimitExceeded
906
1533
  ? [
907
1534
  {