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/ask.ts CHANGED
@@ -7,6 +7,11 @@ import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.ts";
7
7
  import { collectProofSyntax } from "../adapters/ask-syntax.ts";
8
8
  import { canonicalPath } from "../adapters/canonical-path.ts";
9
9
  import { captureCommand } from "../adapters/command.ts";
10
+ import {
11
+ type EvidenceContext,
12
+ resolveEvidenceContext,
13
+ withEvidenceContext,
14
+ } from "../adapters/evidence-context.ts";
10
15
  import { collectFiles } from "../adapters/files.ts";
11
16
  import { shareGitInventory } from "../adapters/git-inventory.ts";
12
17
  import { hostUsage } from "../adapters/usage.ts";
@@ -21,6 +26,7 @@ import {
21
26
  STATE_MAX_CHARS,
22
27
  } from "../constants.ts";
23
28
  import { buildAskClosure } from "../core/ask-closure.ts";
29
+ import type { ReferenceResolution } from "../core/ask-references.ts";
24
30
  import {
25
31
  blockedAskReadings,
26
32
  resolveAskReferences,
@@ -32,26 +38,57 @@ import {
32
38
  type ImportSource,
33
39
  } from "../core/imports.ts";
34
40
  import { checkIntegrity } from "../core/integrity.ts";
35
- import type { BudgetRefusal, EnvelopeInput } from "../core/output.ts";
41
+ import type {
42
+ AnswerInput,
43
+ BudgetRefusal,
44
+ EnvelopeInput,
45
+ } from "../core/output.ts";
36
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";
37
61
  import { assembleState } from "../core/state.ts";
38
62
  import { discoverTests } from "../core/test-discovery.ts";
39
63
  import { describeAsk } from "../describe.ts";
40
64
  import type { GuideContext } from "../guide.ts";
41
65
  import type { Judgment } from "../jev/types.ts";
42
- import { renderEnvelope } from "../render.ts";
66
+ import { renderResultReport } from "../render.ts";
43
67
  import { isRecord } from "../result.ts";
44
68
  import type { ToolDependencies } from "../runtime.ts";
45
69
  import { COMMAND_LIMIT_NOTICE } from "../texts/ask.ts";
46
70
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
71
+ import { ASK_GUIDELINE } from "../texts/instructions.ts";
47
72
  import { asksParameter } from "./ask-schema.ts";
48
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
+
49
87
  export const askParameters = Type.Object(
50
88
  {
89
+ root: Type.Optional(Type.String()),
51
90
  state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
52
- paths: Type.Optional(
53
- Type.Array(Type.String({ minLength: 1 }), { maxItems: ASK_MAX_FILES }),
54
- ),
91
+ paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
55
92
  base: Type.Optional(Type.String({ minLength: 1 })),
56
93
  command: Type.Optional(Type.String({ minLength: 1 })),
57
94
  timeout_s: Type.Optional(
@@ -89,9 +126,7 @@ export function createAskTool(dependencies: ToolDependencies) {
89
126
  : {
90
127
  promptSnippet:
91
128
  "Typed answers about one situation built from a note, files and a command's output",
92
- promptGuidelines: [
93
- "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",
94
- ],
129
+ promptGuidelines: [ASK_GUIDELINE],
95
130
  }),
96
131
  async execute(
97
132
  _id: string,
@@ -101,7 +136,11 @@ export function createAskTool(dependencies: ToolDependencies) {
101
136
  ctx: { cwd: string } & GuideContext,
102
137
  ): Promise<{
103
138
  content: { type: "text"; text: string }[];
104
- details: Judgment;
139
+ details: Judgment & {
140
+ result: ResultReportV1;
141
+ evidenceContext: EvidenceContext;
142
+ evidenceFacts: AskEvidenceFacts;
143
+ };
105
144
  usage?: {
106
145
  input: number;
107
146
  output: number;
@@ -118,8 +157,46 @@ export function createAskTool(dependencies: ToolDependencies) {
118
157
  };
119
158
  }> {
120
159
  const client = dependencies.client;
121
- const cwd = await canonicalPath(ctx.cwd);
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;
122
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[] = [];
123
200
  const exec = shareGitInventory(execute);
124
201
  const finish = (
125
202
  result: Judgment,
@@ -137,15 +214,324 @@ export function createAskTool(dependencies: ToolDependencies) {
137
214
  },
138
215
  });
139
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
+ });
140
491
  runtime.guide.deliver(ctx);
141
492
  return {
142
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
143
- 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
+ },
144
505
  };
145
506
  };
146
- const textResult = (text: string) =>
147
- finish({ ok: false, error: text }, { refusal: text });
148
- 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");
149
535
  if (args.command && !allowCommand)
150
536
  return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
151
537
  if (
@@ -159,21 +545,23 @@ export function createAskTool(dependencies: ToolDependencies) {
159
545
  );
160
546
  if (args.state === undefined && !args.paths?.length && !args.command)
161
547
  return textResult("Provide state, command or at least one path.");
162
- const asks = compileAsks(args.asks, { surface: "ask" });
163
- if (!asks.ok) return textResult(asks.error);
164
548
  const files = await collectFiles(cwd, args.paths ?? [], signal, {
165
549
  exec,
166
550
  allowEmpty: true,
167
551
  skipInvalidUtf8: true,
168
552
  });
169
- if (!files.ok) return textResult(files.error);
553
+ if (!files.ok)
554
+ return textResult(files.error, files.cause ?? "file_unavailable");
170
555
  if (
171
556
  !Object.keys(files.files).length &&
172
557
  args.state === undefined &&
173
558
  !args.command &&
174
559
  files.skipped.length
175
560
  )
176
- return textResult(`No readable evidence: ${files.skipped.join("; ")}`);
561
+ return textResult(
562
+ `No readable evidence: ${files.skipped.join("; ")}`,
563
+ "collection_empty",
564
+ );
177
565
  const proofLimitations: Array<
178
566
  NonNullable<EnvelopeInput["limitations"]>[number]
179
567
  > = files.skipped.map((fact) => ({
@@ -181,12 +569,24 @@ export function createAskTool(dependencies: ToolDependencies) {
181
569
  next: "Provide this file as UTF-8 text if the judgment needs it.",
182
570
  }));
183
571
  let repository = await collectAskRepository(exec, cwd, args.base, signal);
184
- if (!repository.ok && args.base) return textResult(repository.error);
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");
185
579
  if (!repository.ok)
186
580
  proofLimitations.push({
187
581
  fact: `closure unavailable: ${repository.error}`,
188
582
  next: "pass dependencies explicitly in paths",
189
583
  });
584
+ if (args.command)
585
+ commandContext = {
586
+ ...commandContext,
587
+ execution: "started",
588
+ cwd: known(repository.ok ? repository.root : cwd),
589
+ };
190
590
  const command = args.command
191
591
  ? await captureCommand(
192
592
  exec,
@@ -194,18 +594,49 @@ export function createAskTool(dependencies: ToolDependencies) {
194
594
  args.command,
195
595
  args.timeout_s,
196
596
  signal,
597
+ dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY,
197
598
  )
198
599
  : undefined;
199
- 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
+ }
200
629
  if (command?.ok) {
201
630
  repository = await collectAskRepository(exec, cwd, args.base, signal);
202
- if (!repository.ok && args.base) return textResult(repository.error);
631
+ if (!repository.ok && args.base)
632
+ return textResult(repository.error, "invalid_base");
203
633
  }
204
634
  const initialState = assembleState(args.state, files.files);
205
635
  if (!initialState.ok) return textResult(initialState.error);
206
636
  if (command?.ok && Object.hasOwn(initialState.state, "output"))
207
637
  return textResult(
208
638
  "state contains reserved key output when command is supplied; rename it in your note",
639
+ "reserved_key",
209
640
  );
210
641
  let outputNotIdentifiable = false;
211
642
  if (command?.ok && repository.ok) {
@@ -309,35 +740,75 @@ export function createAskTool(dependencies: ToolDependencies) {
309
740
  const references = resolveAskReferences(
310
741
  asks,
311
742
  args.state,
312
- 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,
313
754
  repository?.ok ? [...repository.known] : [],
314
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
+ );
315
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
+ }
316
783
  if (repository?.ok) {
317
784
  const root = repository.root;
318
785
  const additions = await Promise.all(
319
- references.additions.map(async (addition) => ({
320
- addition,
321
- added: await collectFiles(root, [addition.path], signal, {
322
- allowEmpty: true,
323
- exec,
324
- inventory: repository.known,
325
- }),
326
- })),
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
+ })),
327
796
  );
328
797
  for (const { addition, added } of additions) {
329
798
  if (added.ok) {
330
- files.files[addition.reference] = added.files[addition.path] ?? "";
799
+ const local = repositoryPath(cwd, root, addition.path);
800
+ files.files[local] = added.files[addition.path] ?? "";
331
801
  files.identities.push(
332
802
  ...added.identities.map((identity) => ({
333
803
  ...identity,
334
- requestedPath: addition.reference,
804
+ requestedPath: local,
335
805
  })),
336
806
  );
337
807
  } else
338
808
  unresolved.push({
339
- reference: addition.reference,
809
+ ...addition,
340
810
  reason: added.error,
811
+ ...(added.cause ? { cause: added.cause } : {}),
341
812
  });
342
813
  }
343
814
  }
@@ -346,44 +817,58 @@ export function createAskTool(dependencies: ToolDependencies) {
346
817
  if (command?.ok && Object.hasOwn(state.state, "output"))
347
818
  return textResult(
348
819
  "state contains reserved key output when command is supplied; rename it in your note",
820
+ "reserved_key",
349
821
  );
350
822
  if (command?.ok) state.state.output = { ...command.output };
351
823
  if (repository?.ok) {
824
+ if (evidenceContext.effectiveRoot)
825
+ evidenceContext.effectiveRoot.path = repository.root;
826
+ evidenceContext.requestedBase = args.base;
827
+ evidenceContext.resolvedBase = repository.baseSha;
352
828
  const canonical = Object.fromEntries(
353
829
  Object.entries(files.files).map(([path, text]) => [
354
830
  repositoryPath(repository.root, cwd, path),
355
831
  text,
356
832
  ]),
357
833
  );
358
- for (const addition of references.additions)
359
- 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.
360
849
  const sources: ImportSource[] = Object.entries(canonical).map(
361
850
  ([path, text]) => ({ path, text }),
362
851
  );
363
- const before: Record<string, string | null> = {};
852
+ const before: Record<string, string | null> = { ...historical };
364
853
  if (args.base) {
365
854
  for (const path of Object.keys(canonical)) {
366
855
  const old = await repository.readBefore(path);
367
- if (!old.ok) return textResult(old.error);
856
+ if (!old.ok)
857
+ return textResult(old.error, old.cause ?? "file_unavailable");
368
858
  before[path] = old.text;
369
859
  }
370
860
  state.state.base_sha = repository.baseSha ?? "";
371
- state.state.files_before = Object.fromEntries(
372
- Object.keys(files.files).map((path) => {
373
- const canonicalPath =
374
- references.additions.find(
375
- (addition) => addition.reference === path,
376
- )?.path ?? repositoryPath(repository.root, cwd, path);
377
- return [path, before[canonicalPath] ?? null];
378
- }),
379
- );
380
- 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;
381
865
  if (stateSize > STATE_MAX_CHARS) {
382
866
  const partSizes = Object.entries(state.state)
383
867
  .map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
384
868
  .join(", ");
385
869
  return textResult(
386
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",
387
872
  );
388
873
  }
389
874
  }
@@ -483,14 +968,17 @@ export function createAskTool(dependencies: ToolDependencies) {
483
968
  sources,
484
969
  ...(args.base ? { before } : {}),
485
970
  };
971
+ const closureState = { ...state.state };
972
+ if (command?.ok) delete closureState.output;
486
973
  const closure = buildAskClosure({
487
974
  graph: { edges, limits },
488
975
  syntax,
489
976
  paths: Object.keys(canonical),
490
977
  intentions: JSON.stringify(asks.asks),
491
- state: state.state,
978
+ state: closureState,
492
979
  });
493
980
  state.state = closure.state;
981
+ if (command?.ok) state.state.output = { ...command.output };
494
982
  proofLimitations.push(
495
983
  ...closure.limitations.filter(
496
984
  (limit) => !limit.fact.startsWith("Import closure only,"),
@@ -505,6 +993,22 @@ export function createAskTool(dependencies: ToolDependencies) {
505
993
  fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
506
994
  next: "pass dependencies explicitly in paths",
507
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;
508
1012
  const blocked = blockedAskReadings(
509
1013
  asks,
510
1014
  state.state,
@@ -513,13 +1017,16 @@ export function createAskTool(dependencies: ToolDependencies) {
513
1017
  );
514
1018
  const inserted = isRecord(state.state.files) ? state.state.files : {};
515
1019
  const identities = files.identities.map((file) => {
516
- const content = inserted[file.requestedPath];
517
- const insertedPath = Object.keys(inserted).find(
518
- (path) => resolve(cwd, path) === file.resolvedPath,
519
- );
1020
+ const insertedPath = repository.ok
1021
+ ? repositoryPath(repository.root, cwd, file.requestedPath)
1022
+ : file.requestedPath;
1023
+ const content = inserted[insertedPath];
520
1024
  return {
521
1025
  ...file,
522
- insertedPath: insertedPath ? resolve(cwd, insertedPath) : "",
1026
+ insertedPath: resolve(
1027
+ repository.ok ? repository.root : cwd,
1028
+ insertedPath,
1029
+ ),
523
1030
  content: typeof content === "string" ? content : "",
524
1031
  insertedSha256: createHash("sha256")
525
1032
  .update(typeof content === "string" ? content : "")
@@ -531,6 +1038,24 @@ export function createAskTool(dependencies: ToolDependencies) {
531
1038
  .filter((group) => group.some((id) => blocked.has(id)))
532
1039
  .flat(),
533
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
+ });
534
1059
  const questions = Object.fromEntries(
535
1060
  Object.entries(asks.questions).filter(
536
1061
  ([id]) => !blockedMembers.has(id),
@@ -543,11 +1068,14 @@ export function createAskTool(dependencies: ToolDependencies) {
543
1068
  );
544
1069
  let sent = 0;
545
1070
  let refusal: BudgetRefusal | undefined;
546
- const findResults: Judgment[] = [];
547
1071
  const options = {
548
1072
  signal,
549
1073
  cache: !args.command,
550
1074
  ...runtime.session.requestGate(),
1075
+ admissionCause: () =>
1076
+ refusal?.kind === "session"
1077
+ ? ("session_budget" as const)
1078
+ : ("call_budget" as const),
551
1079
  beforeRequest: (questionCount: number) => {
552
1080
  if (args.max_calls !== undefined && sent >= args.max_calls) {
553
1081
  refusal = {
@@ -582,11 +1110,25 @@ export function createAskTool(dependencies: ToolDependencies) {
582
1110
  )
583
1111
  return textResult(
584
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",
585
1127
  );
586
1128
  const found = await Promise.all(
587
- chunks.map(async (chunk) => {
1129
+ chunkStates.map(async (chunkState) => {
588
1130
  const judgment = await client.judge(
589
- { output: chunk.text },
1131
+ chunkState,
590
1132
  {
591
1133
  find: {
592
1134
  type: "bool",
@@ -620,6 +1162,7 @@ export function createAskTool(dependencies: ToolDependencies) {
620
1162
  if (budget < 100)
621
1163
  return textResult(
622
1164
  "No space for command output; reduce paths or state.",
1165
+ "evidence_too_large",
623
1166
  );
624
1167
  const stderrSize = JSON.stringify(output.stderr).length;
625
1168
  const stdoutSize = JSON.stringify(output.stdout).length;
@@ -682,6 +1225,7 @@ export function createAskTool(dependencies: ToolDependencies) {
682
1225
  if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
683
1226
  return textResult(
684
1227
  "serialized command output exceeds state budget; narrow command or reduce paths",
1228
+ "evidence_too_large",
685
1229
  );
686
1230
  if (outputNotIdentifiable)
687
1231
  integrity.controls.push({
@@ -734,13 +1278,14 @@ export function createAskTool(dependencies: ToolDependencies) {
734
1278
  cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
735
1279
  cacheRequests:
736
1280
  (first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
737
- usage: {
738
- inputTokens:
739
- (first.usage?.inputTokens ?? 0) +
740
- (reversed.usage?.inputTokens ?? 0),
741
- costUsd:
742
- (first.usage?.costUsd ?? 0) + (reversed.usage?.costUsd ?? 0),
743
- },
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,
744
1289
  };
745
1290
  }
746
1291
  }
@@ -749,12 +1294,17 @@ export function createAskTool(dependencies: ToolDependencies) {
749
1294
  ...result,
750
1295
  calls: (result.calls ?? 0) + (found.calls ?? 0),
751
1296
  questions: (result.questions ?? 0) + (found.questions ?? 0),
752
- usage: {
753
- inputTokens:
754
- (result.usage?.inputTokens ?? 0) +
755
- (found.usage?.inputTokens ?? 0),
756
- costUsd: (result.usage?.costUsd ?? 0) + (found.usage?.costUsd ?? 0),
757
- },
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,
758
1308
  };
759
1309
  const attribution: Array<
760
1310
  NonNullable<EnvelopeInput["limitations"]>[number]
@@ -779,6 +1329,8 @@ export function createAskTool(dependencies: ToolDependencies) {
779
1329
  fact: `attribution ${reading.label}: no designated candidate`,
780
1330
  next: "provide decisive candidate evidence before acting",
781
1331
  });
1332
+ attributionMissing.add(reading.id);
1333
+ auxiliary.controls.notJudged++;
782
1334
  continue;
783
1335
  }
784
1336
  if (!Object.hasOwn(inserted, selected)) {
@@ -786,6 +1338,8 @@ export function createAskTool(dependencies: ToolDependencies) {
786
1338
  fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
787
1339
  next: `add ${selected} to paths before acting`,
788
1340
  });
1341
+ attributionMissing.add(reading.id);
1342
+ auxiliary.controls.notJudged++;
789
1343
  continue;
790
1344
  }
791
1345
  const selectedPath = repository?.ok
@@ -834,6 +1388,40 @@ export function createAskTool(dependencies: ToolDependencies) {
834
1388
  options,
835
1389
  );
836
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++;
837
1425
  attribution.push(
838
1426
  answer?.type === "choice"
839
1427
  ? {
@@ -852,13 +1440,14 @@ export function createAskTool(dependencies: ToolDependencies) {
852
1440
  cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
853
1441
  cacheRequests:
854
1442
  (result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
855
- usage: {
856
- inputTokens:
857
- (result.usage?.inputTokens ?? 0) +
858
- (control.usage?.inputTokens ?? 0),
859
- costUsd:
860
- (result.usage?.costUsd ?? 0) + (control.usage?.costUsd ?? 0),
861
- },
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,
862
1451
  };
863
1452
  }
864
1453
  }
@@ -877,6 +1466,32 @@ export function createAskTool(dependencies: ToolDependencies) {
877
1466
  (reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"),
878
1467
  )
879
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
+ }
880
1495
  const output = finish(result, {
881
1496
  ...(result.ok
882
1497
  ? {
@@ -906,6 +1521,14 @@ export function createAskTool(dependencies: ToolDependencies) {
906
1521
  },
907
1522
  ]
908
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
+ : []),
909
1532
  ...(command?.ok && command.shapeLimitExceeded
910
1533
  ? [
911
1534
  {