jev-agent-tools 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/CONTRIBUTING.md +43 -0
  3. package/README.md +58 -17
  4. package/SECURITY.md +43 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +198 -0
  7. package/dist/adapters/ask-proof.js +200 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +234 -0
  11. package/dist/adapters/docs.js +192 -0
  12. package/dist/adapters/evidence-context.js +119 -0
  13. package/dist/adapters/exec.js +207 -0
  14. package/dist/adapters/files.js +418 -0
  15. package/dist/adapters/find.js +150 -0
  16. package/dist/adapters/git-base.js +32 -0
  17. package/dist/adapters/git-inventory.js +71 -0
  18. package/dist/adapters/git.js +483 -0
  19. package/dist/adapters/locate-file.js +197 -0
  20. package/dist/adapters/output-lines.js +46 -0
  21. package/dist/adapters/private-storage.js +106 -0
  22. package/dist/adapters/risk-callers.js +429 -0
  23. package/dist/adapters/runner-version.js +78 -0
  24. package/dist/adapters/shell.js +92 -0
  25. package/dist/adapters/syntax.js +187 -0
  26. package/dist/adapters/test-inventory.js +139 -0
  27. package/dist/adapters/usage.js +20 -0
  28. package/dist/adapters/utf8.js +47 -0
  29. package/dist/configuration.js +267 -0
  30. package/dist/constants.js +140 -0
  31. package/dist/core/ask-closure.js +282 -0
  32. package/dist/core/ask-proof.js +1 -0
  33. package/dist/core/ask-references.js +278 -0
  34. package/dist/core/asks.js +507 -0
  35. package/dist/core/batches.js +65 -0
  36. package/dist/core/command-output.js +224 -0
  37. package/dist/core/diff.js +178 -0
  38. package/dist/core/docs.js +302 -0
  39. package/dist/core/find.js +108 -0
  40. package/dist/core/git.js +1 -0
  41. package/dist/core/imports.js +550 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/lexical.js +132 -0
  44. package/dist/core/locate.js +169 -0
  45. package/dist/core/output.js +137 -0
  46. package/dist/core/pointer.js +29 -0
  47. package/dist/core/result-report.js +302 -0
  48. package/dist/core/risk-callers.js +851 -0
  49. package/dist/core/runner-version.js +45 -0
  50. package/dist/core/secret-path.js +34 -0
  51. package/dist/core/sections.js +230 -0
  52. package/dist/core/state.js +51 -0
  53. package/dist/core/syntax.js +1 -0
  54. package/dist/core/test-commands.js +334 -0
  55. package/dist/core/test-coverage.js +74 -0
  56. package/dist/core/test-discovery.js +1382 -0
  57. package/dist/core/test-evidence.js +527 -0
  58. package/dist/core/test-state.js +81 -0
  59. package/dist/core/truncate.js +12 -0
  60. package/dist/core/units.js +349 -0
  61. package/dist/describe.js +23 -0
  62. package/dist/guide.js +33 -0
  63. package/dist/host.js +24 -0
  64. package/dist/jev/client.js +456 -0
  65. package/dist/jev/pool.js +54 -0
  66. package/dist/jev/types.js +1 -0
  67. package/dist/mcp/main.js +124 -0
  68. package/dist/mcp/protocol.js +210 -0
  69. package/dist/mcp/tools.js +129 -0
  70. package/dist/presets/docs.js +62 -0
  71. package/dist/presets/risk.js +179 -0
  72. package/dist/presets/spec.js +81 -0
  73. package/dist/presets/witnesses.js +249 -0
  74. package/dist/render.js +114 -0
  75. package/dist/report-schema.js +1356 -0
  76. package/dist/result-types.js +1 -0
  77. package/dist/result.js +3 -0
  78. package/dist/runtime.js +1 -0
  79. package/dist/session.js +147 -0
  80. package/dist/texts/ask-files.js +3 -0
  81. package/dist/texts/ask.js +4 -0
  82. package/dist/texts/check-diff.js +20 -0
  83. package/dist/texts/configuration.js +1 -0
  84. package/dist/texts/find.js +19 -0
  85. package/dist/texts/guide.js +3 -0
  86. package/dist/texts/instructions.js +72 -0
  87. package/dist/texts/locate.js +15 -0
  88. package/dist/texts/select-tests.js +4 -0
  89. package/dist/tools/ask-files.js +450 -0
  90. package/dist/tools/ask-schema.js +70 -0
  91. package/dist/tools/ask.js +1147 -0
  92. package/dist/tools/check-diff.js +594 -0
  93. package/dist/tools/docs-check.js +408 -0
  94. package/dist/tools/find.js +682 -0
  95. package/dist/tools/locate.js +602 -0
  96. package/dist/tools/review-report.js +230 -0
  97. package/dist/tools/select-tests.js +821 -0
  98. package/dist/tools/spec-check.js +263 -0
  99. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  100. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  101. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  102. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  103. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  104. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  105. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  106. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  107. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  108. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  109. package/docs/agent-instructions.md +120 -0
  110. package/docs/design.md +16 -4
  111. package/docs/mcp.md +233 -0
  112. package/docs/tools/jev_ask.md +8 -5
  113. package/docs/tools/jev_ask_files.md +2 -1
  114. package/docs/tools/jev_check_diff.md +4 -1
  115. package/docs/tools/jev_find_files.md +2 -1
  116. package/docs/tools/jev_locate_in_file.md +5 -0
  117. package/docs/tools/jev_select_tests.md +4 -1
  118. package/package.json +19 -4
  119. package/rules/jev-ask.md +22 -1
  120. package/server.json +57 -0
  121. package/src/adapters/ask-files.ts +11 -3
  122. package/src/adapters/ask-proof.ts +69 -11
  123. package/src/adapters/canonical-path.ts +18 -0
  124. package/src/adapters/command.ts +102 -36
  125. package/src/adapters/docs.ts +33 -14
  126. package/src/adapters/evidence-context.ts +169 -0
  127. package/src/adapters/exec.ts +226 -0
  128. package/src/adapters/files.ts +146 -16
  129. package/src/adapters/find.ts +37 -7
  130. package/src/adapters/git-base.ts +7 -1
  131. package/src/adapters/git.ts +61 -8
  132. package/src/adapters/locate-file.ts +51 -9
  133. package/src/adapters/private-storage.ts +155 -0
  134. package/src/adapters/risk-callers.ts +7 -2
  135. package/src/adapters/shell.ts +113 -0
  136. package/src/adapters/test-inventory.ts +12 -4
  137. package/src/configuration.ts +55 -14
  138. package/src/constants.ts +37 -5
  139. package/src/core/ask-references.ts +262 -146
  140. package/src/core/asks.ts +79 -7
  141. package/src/core/command-output.ts +17 -1
  142. package/src/core/import-boundaries.ts +8 -3
  143. package/src/core/locate.ts +8 -5
  144. package/src/core/output.ts +34 -0
  145. package/src/core/result-report.ts +410 -0
  146. package/src/core/secret-path.ts +37 -0
  147. package/src/core/state.ts +8 -1
  148. package/src/core/units.ts +3 -2
  149. package/src/host.ts +11 -0
  150. package/src/index.ts +3 -0
  151. package/src/jev/client.ts +66 -16
  152. package/src/jev/types.ts +24 -3
  153. package/src/mcp/main.ts +135 -0
  154. package/src/mcp/protocol.ts +332 -0
  155. package/src/mcp/tools.ts +179 -0
  156. package/src/render.ts +109 -0
  157. package/src/report-schema.ts +1380 -0
  158. package/src/result-types.ts +234 -0
  159. package/src/result.ts +4 -1
  160. package/src/runtime.ts +6 -0
  161. package/src/session.ts +59 -0
  162. package/src/setup.ts +13 -5
  163. package/src/texts/ask-files.ts +4 -1
  164. package/src/texts/ask.ts +8 -1
  165. package/src/texts/check-diff.ts +7 -4
  166. package/src/texts/find.ts +8 -2
  167. package/src/texts/guide.ts +8 -16
  168. package/src/texts/instructions.ts +98 -0
  169. package/src/texts/locate.ts +8 -2
  170. package/src/texts/run-end.ts +2 -2
  171. package/src/texts/select-tests.ts +4 -1
  172. package/src/tools/ask-files.ts +311 -18
  173. package/src/tools/ask.ts +722 -95
  174. package/src/tools/check-diff.ts +337 -31
  175. package/src/tools/docs-check.ts +241 -38
  176. package/src/tools/find.ts +389 -29
  177. package/src/tools/locate.ts +387 -25
  178. package/src/tools/review-report.ts +308 -0
  179. package/src/tools/select-tests.ts +484 -23
  180. package/src/tools/spec-check.ts +194 -19
@@ -0,0 +1,1147 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { createAnalysisContext } from "../adapters/analysis-context.js";
5
+ import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.js";
6
+ import { collectProofSyntax } from "../adapters/ask-syntax.js";
7
+ import { canonicalPath } from "../adapters/canonical-path.js";
8
+ import { captureCommand } from "../adapters/command.js";
9
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
10
+ import { collectFiles } from "../adapters/files.js";
11
+ import { shareGitInventory } from "../adapters/git-inventory.js";
12
+ import { hostUsage } from "../adapters/usage.js";
13
+ import { ASK_MAX_FILES, ASK_NOTE_MAX_CHARS, ASK_TIMEOUT_MAX_S, ASK_TIMEOUT_S, OUTPUT_CHUNK_CHARS, OUTPUT_FIND_MAX_CALLS, OUTPUT_SHAPE_MAX_COUNT, STATE_MAX_CHARS, } from "../constants.js";
14
+ import { buildAskClosure } from "../core/ask-closure.js";
15
+ import { blockedAskReadings, resolveAskReferences, } from "../core/ask-references.js";
16
+ import { compileAsks, readAsks, reverseQuestions } from "../core/asks.js";
17
+ import { outputChunks, selectOutput } from "../core/command-output.js";
18
+ import { createImportGraphBuilder, } from "../core/imports.js";
19
+ import { checkIntegrity } from "../core/integrity.js";
20
+ import { buildEnvelope } from "../core/output.js";
21
+ import { answerItemFromInput, buildResultReport, contextFromEvidence, known, notApplicable, unknown, } from "../core/result-report.js";
22
+ import { assembleState } from "../core/state.js";
23
+ import { discoverTests } from "../core/test-discovery.js";
24
+ import { describeAsk } from "../describe.js";
25
+ import { renderResultReport } from "../render.js";
26
+ import { isRecord } from "../result.js";
27
+ import { COMMAND_LIMIT_NOTICE } from "../texts/ask.js";
28
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
29
+ import { ASK_GUIDELINE } from "../texts/instructions.js";
30
+ import { asksParameter } from "./ask-schema.js";
31
+ export const askParameters = Type.Object({
32
+ root: Type.Optional(Type.String()),
33
+ state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
34
+ paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
35
+ base: Type.Optional(Type.String({ minLength: 1 })),
36
+ command: Type.Optional(Type.String({ minLength: 1 })),
37
+ timeout_s: Type.Optional(Type.Number({
38
+ minimum: 1,
39
+ maximum: ASK_TIMEOUT_MAX_S,
40
+ default: ASK_TIMEOUT_S,
41
+ })),
42
+ asks: asksParameter("ask"),
43
+ max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
44
+ }, { additionalProperties: false });
45
+ export function createAskTool(dependencies) {
46
+ const { host, runtime, exec: execute } = dependencies;
47
+ const { isOmp, names } = host;
48
+ const allowCommand = process.env.JEV_TOOLS_ALLOW_COMMAND !== "0";
49
+ const parameters = allowCommand
50
+ ? askParameters
51
+ : Type.Omit(askParameters, ["command", "timeout_s"]);
52
+ return {
53
+ name: "jev_ask",
54
+ label: "Jev ask",
55
+ description: `${describeAsk(names)}\n\n${COMMAND_LIMIT_NOTICE}`,
56
+ parameters,
57
+ ...(isOmp
58
+ ? {
59
+ approval: (args) => args.command ? "exec" : "read",
60
+ loadMode: "essential",
61
+ }
62
+ : {
63
+ promptSnippet: "Typed answers about one situation built from a note, files and a command's output",
64
+ promptGuidelines: [ASK_GUIDELINE],
65
+ }),
66
+ async execute(_id, args, signal, _update, ctx) {
67
+ const client = dependencies.client;
68
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
69
+ exec: execute,
70
+ signal,
71
+ origin: dependencies.evidenceOrigin,
72
+ });
73
+ const evidenceContext = evidence.context;
74
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
75
+ const started = performance.now();
76
+ const evidenceFacts = {
77
+ omissions: [],
78
+ blockedGroups: [],
79
+ inventory: [],
80
+ };
81
+ evidenceContext.requestedBase = args.base;
82
+ const asks = compileAsks(args.asks, { surface: "ask" });
83
+ let commandContext = args.command
84
+ ? {
85
+ execution: "not_started",
86
+ cwd: evidence.ok
87
+ ? known(cwd)
88
+ : unknown("effective root not established"),
89
+ exitCode: unknown("command not started"),
90
+ timedOut: unknown("command not started"),
91
+ }
92
+ : {
93
+ execution: "not_requested",
94
+ cwd: notApplicable("no command"),
95
+ exitCode: notApplicable("no command"),
96
+ timedOut: notApplicable("no command"),
97
+ };
98
+ let refusalCause = "invalid_arguments";
99
+ let inventoryCollected = false;
100
+ const auxiliary = {
101
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
102
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
103
+ };
104
+ const attributionAnswers = {};
105
+ const attributionMissing = new Set();
106
+ const findResults = [];
107
+ const exec = shareGitInventory(execute);
108
+ const finish = (result, input) => {
109
+ const envelope = buildEnvelope({
110
+ ...input,
111
+ yield: {
112
+ calls: result.calls ?? 0,
113
+ questions: result.questions ?? 0,
114
+ costUsd: result.usage?.costUsd,
115
+ cacheHits: result.cacheHits ?? 0,
116
+ cacheRequests: result.cacheRequests ?? 0,
117
+ elapsedMs: performance.now() - started,
118
+ },
119
+ });
120
+ runtime.session.record(envelope);
121
+ const diagnostics = [];
122
+ const actions = [];
123
+ const globalMissing = new Map();
124
+ const diagnose = (cause, fact, scope, target, origin, effect = "blocking", next) => {
125
+ const id = `diagnostic:${diagnostics.length}`;
126
+ const actionId = `action:${actions.length}`;
127
+ const code = cause === "invalid_root" || cause === "invalid_base"
128
+ ? "correct_context"
129
+ : cause === "forbidden_path"
130
+ ? "correct_reference"
131
+ : cause === "missing_required" || cause === "empty_required"
132
+ ? "provide_evidence"
133
+ : cause === "not_configured"
134
+ ? "configure_client"
135
+ : "inspect_native";
136
+ actions.push({
137
+ id: actionId,
138
+ code,
139
+ target: target === undefined
140
+ ? unknown("target not established")
141
+ : known(target),
142
+ scope,
143
+ condition: "Only if materially new evidence or corrected context is available",
144
+ instruction: next ??
145
+ (code === "provide_evidence"
146
+ ? `Provide the actually missing evidence${target ? `: ${target}` : ""}; do not repeat an already captured command.`
147
+ : "Inspect the supplied evidence natively; correct the named cause before any useful new Jev call."),
148
+ repeatUnchanged: false,
149
+ });
150
+ diagnostics.push({
151
+ id,
152
+ cause,
153
+ fact,
154
+ target: target === undefined
155
+ ? unknown("target not established")
156
+ : known(target),
157
+ origin,
158
+ scope,
159
+ effect,
160
+ material: true,
161
+ omittedMembers: [],
162
+ memberCount: known(0),
163
+ actionIds: [actionId],
164
+ });
165
+ return { diagnosticIds: [id], actionIds: [actionId] };
166
+ };
167
+ const items = asks.ok
168
+ ? asks.readings.map((reading, index) => {
169
+ const groupId = `group:${asks.groups.findIndex((group) => group.includes(reading.id))}`;
170
+ const target = asks.asks[reading.askIndex]?.about ?? "state";
171
+ const pieces = [
172
+ {
173
+ target,
174
+ canonicalPath: unknown("selector does not establish a canonical file identity"),
175
+ aliases: [],
176
+ side: target === "output" ? "output" : "state",
177
+ revision: notApplicable("non-file evidence"),
178
+ },
179
+ ];
180
+ for (const [alias, path] of Object.entries(evidenceFacts.references?.aliases ?? {})) {
181
+ const before = evidenceFacts.references?.additions.some((addition) => addition.reference === alias &&
182
+ addition.side === "files_before") || target.includes("files_before");
183
+ pieces.push({
184
+ target: alias,
185
+ canonicalPath: evidenceContext.effectiveRoot
186
+ ? known(resolve(evidenceContext.effectiveRoot.path, path))
187
+ : unknown("effective root unavailable"),
188
+ aliases: [alias],
189
+ side: before ? "before" : "current",
190
+ revision: before && evidenceContext.resolvedBase
191
+ ? known(evidenceContext.resolvedBase)
192
+ : unknown("working tree evidence"),
193
+ });
194
+ }
195
+ const common = {
196
+ id: reading.id,
197
+ kind: reading.kind === "verify"
198
+ ? "claim"
199
+ : "question",
200
+ label: reading.label,
201
+ groupId,
202
+ evidence: pieces,
203
+ diagnosticIds: [],
204
+ actionIds: [],
205
+ };
206
+ const originalAnswer = input.answers?.[index];
207
+ const attributionControls = attributionAnswers[reading.id];
208
+ const answer = originalAnswer && attributionMissing.has(reading.id)
209
+ ? {
210
+ ...originalAnswer,
211
+ unjudged: true,
212
+ reason: "Required attribution control is incomplete",
213
+ }
214
+ : originalAnswer && attributionControls
215
+ ? {
216
+ ...originalAnswer,
217
+ reportControls: [
218
+ ...(originalAnswer.reportControls ?? []),
219
+ ...attributionControls,
220
+ ],
221
+ }
222
+ : originalAnswer;
223
+ if (answer &&
224
+ !answer.unjudged &&
225
+ (answer.source || answer.answer?.source))
226
+ return answerItemFromInput(common, input.controls?.length
227
+ ? {
228
+ ...answer,
229
+ band: "unsure",
230
+ reason: input.controls
231
+ .map((control) => control.fact)
232
+ .join("; "),
233
+ }
234
+ : answer);
235
+ const blocked = evidenceFacts.blockedGroups.find((group) => group.ids.includes(reading.id));
236
+ const omission = evidenceFacts.omissions.find((item) => item.required &&
237
+ (item.origin === "note" || item.scope === reading.id));
238
+ const groupFailure = result.ok
239
+ ? (asks.groups.find((group) => group.includes(reading.id)) ??
240
+ [])
241
+ .map((id) => result.answers[id])
242
+ .find((answer) => answer?.type === "unjudged" && answer.cause)
243
+ : undefined;
244
+ const cause = blocked
245
+ ? (blocked.cause ?? "missing_required")
246
+ : groupFailure?.type === "unjudged" && groupFailure.cause
247
+ ? groupFailure.cause
248
+ : input.budget
249
+ ? input.budget.kind === "session"
250
+ ? "session_budget"
251
+ : "call_budget"
252
+ : (result.failureCause ??
253
+ (input.refusal ? refusalCause : "control_failure"));
254
+ const globalKey = omission?.origin === "note" ? omission.reference : undefined;
255
+ const existingLinks = globalKey
256
+ ? globalMissing.get(globalKey)
257
+ : undefined;
258
+ const links = existingLinks ??
259
+ diagnose(cause, blocked?.reason ??
260
+ answer?.reason ??
261
+ (result.ok
262
+ ? "Required judgment group incomplete or response provenance not established"
263
+ : result.error), omission?.origin === "note"
264
+ ? { kind: "call" }
265
+ : { kind: "group", groupIds: [groupId] }, omission?.reference, omission?.origin === "note"
266
+ ? "note"
267
+ : blocked
268
+ ? "ask"
269
+ : input.budget
270
+ ? "budget"
271
+ : "provider");
272
+ if (globalKey)
273
+ globalMissing.set(globalKey, links);
274
+ return {
275
+ ...common,
276
+ ...links,
277
+ treatment: "not_judged",
278
+ source: "none",
279
+ };
280
+ })
281
+ : [];
282
+ if (!items.length)
283
+ diagnose(input.refusal ? refusalCause : "collection_empty", input.refusal ?? "No requested results collected", { kind: "call" }, undefined, "input");
284
+ for (const control of input.controls ?? [])
285
+ diagnose("control_failure", control.fact, { kind: "call" }, undefined, "control", "reservation", control.next);
286
+ for (const limitation of input.limitations ?? [])
287
+ diagnose("evidence_limit", limitation.fact, { kind: "call" }, limitation.path, "closure", "reservation", limitation.next);
288
+ const report = buildResultReport({
289
+ tool: "jev_ask",
290
+ context: contextFromEvidence(evidenceContext, {
291
+ command: commandContext,
292
+ inventories: inventoryCollected
293
+ ? [
294
+ {
295
+ id: "repository",
296
+ kind: "repository",
297
+ rules: ["Git tracked and admitted repository evidence"],
298
+ restrictions: args.paths ?? [],
299
+ discovered: known(evidenceFacts.inventory.length),
300
+ considered: known(evidenceFacts.inventory.length),
301
+ scopeRestricted: false,
302
+ criteria: [],
303
+ },
304
+ ]
305
+ : [],
306
+ }),
307
+ items,
308
+ diagnostics,
309
+ actions,
310
+ metrics: {
311
+ calls: result.calls ?? 0,
312
+ questions: result.questions ?? 0,
313
+ cacheHits: result.cacheHits ?? 0,
314
+ cacheRequests: result.cacheRequests ?? 0,
315
+ costUsd: result.usage?.costUsd,
316
+ elapsedMs: performance.now() - started,
317
+ },
318
+ auxiliary,
319
+ total: known(items.length),
320
+ refused: !!input.refusal &&
321
+ refusalCause !== "not_configured" &&
322
+ !result.failureCause,
323
+ });
324
+ runtime.guide.deliver(ctx);
325
+ return {
326
+ content: [
327
+ {
328
+ type: "text",
329
+ text: renderResultReport(report, { details: envelope }),
330
+ },
331
+ ],
332
+ details: {
333
+ ...result,
334
+ result: report,
335
+ evidenceContext,
336
+ evidenceFacts,
337
+ },
338
+ };
339
+ };
340
+ const textResult = (text, cause = "invalid_arguments") => {
341
+ refusalCause = cause;
342
+ const technical = findResults.reduce((totals, judgment) => ({
343
+ calls: totals.calls + (judgment.calls ?? 0),
344
+ questions: totals.questions + (judgment.questions ?? 0),
345
+ cacheHits: totals.cacheHits + (judgment.cacheHits ?? 0),
346
+ cacheRequests: totals.cacheRequests + (judgment.cacheRequests ?? 0),
347
+ }), { calls: 0, questions: 0, cacheHits: 0, cacheRequests: 0 });
348
+ return finish({ ok: false, error: text, ...technical }, { refusal: text });
349
+ };
350
+ if (!evidence.ok)
351
+ return textResult(evidence.error, evidence.cause ?? "invalid_root");
352
+ if (!asks.ok)
353
+ return textResult(asks.error);
354
+ const preliminary = resolveAskReferences(asks, args.state, {}, []);
355
+ if (preliminary.invalid.length) {
356
+ evidenceFacts.references = preliminary;
357
+ return textResult(preliminary.invalid.map((item) => item.reason).join("; "), "forbidden_path");
358
+ }
359
+ if (!client)
360
+ return textResult(NOT_CONFIGURED, "not_configured");
361
+ if (args.command && !allowCommand)
362
+ return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
363
+ if (args.timeout_s !== undefined &&
364
+ (!Number.isFinite(args.timeout_s) ||
365
+ args.timeout_s < 1 ||
366
+ args.timeout_s > ASK_TIMEOUT_MAX_S))
367
+ return textResult(`timeout_s must be between 1 and ${ASK_TIMEOUT_MAX_S}`);
368
+ if (args.state === undefined && !args.paths?.length && !args.command)
369
+ return textResult("Provide state, command or at least one path.");
370
+ const files = await collectFiles(cwd, args.paths ?? [], signal, {
371
+ exec,
372
+ allowEmpty: true,
373
+ skipInvalidUtf8: true,
374
+ });
375
+ if (!files.ok)
376
+ return textResult(files.error, files.cause ?? "file_unavailable");
377
+ if (!Object.keys(files.files).length &&
378
+ args.state === undefined &&
379
+ !args.command &&
380
+ files.skipped.length)
381
+ return textResult(`No readable evidence: ${files.skipped.join("; ")}`, "collection_empty");
382
+ const proofLimitations = files.skipped.map((fact) => ({
383
+ fact,
384
+ next: "Provide this file as UTF-8 text if the judgment needs it.",
385
+ }));
386
+ let repository = await collectAskRepository(exec, cwd, args.base, signal);
387
+ inventoryCollected = repository.ok;
388
+ if (repository.ok) {
389
+ evidenceFacts.inventory = [...repository.known];
390
+ evidenceContext.resolvedBase = repository.baseSha;
391
+ }
392
+ if (!repository.ok && args.base)
393
+ return textResult(repository.error, "invalid_base");
394
+ if (!repository.ok)
395
+ proofLimitations.push({
396
+ fact: `closure unavailable: ${repository.error}`,
397
+ next: "pass dependencies explicitly in paths",
398
+ });
399
+ if (args.command)
400
+ commandContext = {
401
+ ...commandContext,
402
+ execution: "started",
403
+ cwd: known(repository.ok ? repository.root : cwd),
404
+ };
405
+ const command = args.command
406
+ ? await captureCommand(exec, repository.ok ? repository.root : cwd, args.command, args.timeout_s, signal, dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY)
407
+ : undefined;
408
+ if (command?.ok)
409
+ commandContext = {
410
+ execution: "finished",
411
+ cwd: known(repository.ok ? repository.root : cwd),
412
+ exitCode: known(command.output.exit_code),
413
+ timedOut: known(command.output.timed_out),
414
+ };
415
+ if (command && !command.ok) {
416
+ commandContext = {
417
+ // Reported execution states are not_requested, not_started, started
418
+ // and finished. A spawn whose completion was not observed (adapter
419
+ // "unknown") did start, so it reports "started" with unknown exit
420
+ // code and timeout.
421
+ execution: command.commandExecution === "unknown"
422
+ ? "started"
423
+ : command.commandExecution,
424
+ cwd: known(repository.ok ? repository.root : cwd),
425
+ exitCode: command.commandExitCode !== undefined
426
+ ? known(command.commandExitCode)
427
+ : unknown("command completion not observed"),
428
+ timedOut: command.commandTimedOut !== undefined
429
+ ? known(command.commandTimedOut)
430
+ : unknown("command completion not observed"),
431
+ };
432
+ return textResult(command.error, command.cause ?? "file_unavailable");
433
+ }
434
+ if (command?.ok) {
435
+ repository = await collectAskRepository(exec, cwd, args.base, signal);
436
+ if (!repository.ok && args.base)
437
+ return textResult(repository.error, "invalid_base");
438
+ }
439
+ const initialState = assembleState(args.state, files.files);
440
+ if (!initialState.ok)
441
+ return textResult(initialState.error);
442
+ if (command?.ok && Object.hasOwn(initialState.state, "output"))
443
+ return textResult("state contains reserved key output when command is supplied; rename it in your note", "reserved_key");
444
+ let outputNotIdentifiable = false;
445
+ if (command?.ok && repository.ok) {
446
+ for (const target of command.targets) {
447
+ if (target.path.startsWith(`${repository.root}/`))
448
+ target.path = target.path.slice(repository.root.length + 1);
449
+ else if (isAbsolute(target.path)) {
450
+ // Windows reports C:/... or short 8.3 forms; compare canonically.
451
+ const local = relative(repository.root, await canonicalPath(target.path))
452
+ .split("\\")
453
+ .join("/");
454
+ if (local && !local.startsWith("../") && !isAbsolute(local))
455
+ target.path = local;
456
+ }
457
+ let matches = [...repository.known].filter((path) => path === target.path || path.endsWith(`/${target.path}`));
458
+ if (target.testName) {
459
+ const matched = [];
460
+ for (const candidate of matches) {
461
+ const source = await collectFiles(repository.root, [candidate], signal, { exec, allowEmpty: true, inventory: repository.known });
462
+ if (source.ok &&
463
+ discoverTests([
464
+ { path: candidate, text: source.files[candidate] ?? "" },
465
+ ]).entries.some((entry) => entry.scenarios.some((scenario) => scenario.name === target.testName)))
466
+ matched.push(candidate);
467
+ }
468
+ matches = matched;
469
+ }
470
+ if (matches.length !== 1) {
471
+ outputNotIdentifiable ||= command.assertion && target.assertion;
472
+ continue;
473
+ }
474
+ const path = matches[0];
475
+ if (!path)
476
+ continue;
477
+ const local = repositoryPath(cwd, repository.root, path);
478
+ if (!Object.hasOwn(files.files, local)) {
479
+ const added = await collectFiles(repository.root, [path], signal, {
480
+ exec,
481
+ allowEmpty: true,
482
+ inventory: repository.known,
483
+ });
484
+ if (added.ok && Object.keys(files.files).length < ASK_MAX_FILES) {
485
+ files.files[local] = added.files[path] ?? "";
486
+ files.identities.push(...added.identities.map((identity) => ({
487
+ ...identity,
488
+ requestedPath: local,
489
+ })));
490
+ }
491
+ else {
492
+ outputNotIdentifiable ||= command.assertion && target.assertion;
493
+ continue;
494
+ }
495
+ }
496
+ if (target.assertion && target.nodeId?.includes("::")) {
497
+ const discovery = discoverTests([
498
+ { path, text: files.files[local] ?? "" },
499
+ ]);
500
+ const name = target.nodeId
501
+ .split("::")
502
+ .slice(1)
503
+ .join("::")
504
+ .replace(/\[[\s\S]*\]$/, "");
505
+ if (!discovery.entries.some((entry) => entry.scenarios.some((scenario) => scenario.id === name ||
506
+ scenario.name === name.split("::").at(-1))))
507
+ outputNotIdentifiable ||= command.assertion;
508
+ }
509
+ }
510
+ }
511
+ if (command?.ok &&
512
+ command.assertion &&
513
+ (!command.targets.length || !repository.ok) &&
514
+ !discoverTests(Object.entries(files.files).map(([path, text]) => ({ path, text }))).entries.length)
515
+ outputNotIdentifiable = true;
516
+ const references = resolveAskReferences(asks, args.state, repository.ok
517
+ ? {
518
+ files: Object.fromEntries(Object.entries(files.files).map(([path, text]) => [
519
+ repositoryPath(repository.root, cwd, path),
520
+ text,
521
+ ])),
522
+ beforeInventory: args.base ? [...repository.beforeKnown] : [],
523
+ }
524
+ : files.files, repository?.ok ? [...repository.known] : []);
525
+ evidenceFacts.references = references;
526
+ evidenceFacts.inventory = repository.ok ? [...repository.known] : [];
527
+ if (references.invalid.length)
528
+ return textResult(references.invalid.map((item) => item.reason).join("; "), "forbidden_path");
529
+ const unresolved = [...references.unresolved];
530
+ const historical = {};
531
+ if (repository.ok && args.base) {
532
+ for (const reference of references.additions.filter((reference) => reference.side === "files_before" && reference.required)) {
533
+ const before = await repository.readBefore(reference.path);
534
+ if (before.ok)
535
+ historical[reference.path] = before.text;
536
+ else {
537
+ if (before.cause === "forbidden_path")
538
+ return textResult(before.error, before.cause);
539
+ unresolved.push({
540
+ ...reference,
541
+ reason: before.error,
542
+ ...(before.cause ? { cause: before.cause } : {}),
543
+ });
544
+ }
545
+ }
546
+ }
547
+ if (repository?.ok) {
548
+ const root = repository.root;
549
+ const additions = await Promise.all(references.additions
550
+ .filter((addition) => addition.side !== "files_before")
551
+ .map(async (addition) => ({
552
+ addition,
553
+ added: await collectFiles(root, [addition.path], signal, {
554
+ allowEmpty: true,
555
+ exec,
556
+ inventory: repository.known,
557
+ }),
558
+ })));
559
+ for (const { addition, added } of additions) {
560
+ if (added.ok) {
561
+ const local = repositoryPath(cwd, root, addition.path);
562
+ files.files[local] = added.files[addition.path] ?? "";
563
+ files.identities.push(...added.identities.map((identity) => ({
564
+ ...identity,
565
+ requestedPath: local,
566
+ })));
567
+ }
568
+ else
569
+ unresolved.push({
570
+ ...addition,
571
+ reason: added.error,
572
+ ...(added.cause ? { cause: added.cause } : {}),
573
+ });
574
+ }
575
+ }
576
+ const state = assembleState(args.state, files.files);
577
+ if (!state.ok)
578
+ return textResult(state.error);
579
+ if (command?.ok && Object.hasOwn(state.state, "output"))
580
+ return textResult("state contains reserved key output when command is supplied; rename it in your note", "reserved_key");
581
+ if (command?.ok)
582
+ state.state.output = { ...command.output };
583
+ if (repository?.ok) {
584
+ if (evidenceContext.effectiveRoot)
585
+ evidenceContext.effectiveRoot.path = repository.root;
586
+ evidenceContext.requestedBase = args.base;
587
+ evidenceContext.resolvedBase = repository.baseSha;
588
+ const canonical = Object.fromEntries(Object.entries(files.files).map(([path, text]) => [
589
+ repositoryPath(repository.root, cwd, path),
590
+ text,
591
+ ]));
592
+ state.state.files = canonical;
593
+ state.state.evidence = {
594
+ context: evidenceContext,
595
+ aliases: Object.fromEntries(Object.entries(references.aliases)
596
+ .map(([alias, path]) => [
597
+ alias,
598
+ repository.known.has(path) || repository.beforeKnown.has(path)
599
+ ? path
600
+ : repositoryPath(repository.root, cwd, path),
601
+ ])
602
+ .sort(([a], [b]) => a.localeCompare(b))),
603
+ };
604
+ // Canonical repository paths are the sole serialized content keys.
605
+ const sources = Object.entries(canonical).map(([path, text]) => ({ path, text }));
606
+ const before = { ...historical };
607
+ if (args.base) {
608
+ for (const path of Object.keys(canonical)) {
609
+ const old = await repository.readBefore(path);
610
+ if (!old.ok)
611
+ return textResult(old.error, old.cause ?? "file_unavailable");
612
+ before[path] = old.text;
613
+ }
614
+ state.state.base_sha = repository.baseSha ?? "";
615
+ state.state.files_before = before;
616
+ const stateSize = JSON.stringify(command?.ok ? { ...state.state, output: undefined } : state.state).length;
617
+ if (stateSize > STATE_MAX_CHARS) {
618
+ const partSizes = Object.entries(state.state)
619
+ .map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
620
+ .join(", ");
621
+ return textResult(`serialized state including files_before exceeds ${STATE_MAX_CHARS} chars (${stateSize}); serialized parts: ${partSizes}; split the situation into smaller states.`, "evidence_too_large");
622
+ }
623
+ }
624
+ const configuration = [];
625
+ for (const path of repository.known) {
626
+ if (!/(^|\/)(?:(tsconfig|jsconfig)(?:\.[^/]+)?|package)\.json$/.test(path))
627
+ continue;
628
+ const source = await repository.read(path);
629
+ if (source.ok)
630
+ configuration.push(source);
631
+ }
632
+ const analysis = await createAnalysisContext();
633
+ const buildGraph = createImportGraphBuilder(configuration, repository.known, analysis);
634
+ const edges = new Map();
635
+ const limits = [];
636
+ const declarations = [];
637
+ const bindings = [];
638
+ const syntaxLimits = [];
639
+ const uses = new Map();
640
+ const reached = new Set(sources.map((source) => source.path));
641
+ for (const path of Object.keys(canonical)) {
642
+ if (!/(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path))
643
+ continue;
644
+ let parent = path.slice(0, path.lastIndexOf("/") + 1);
645
+ while (true) {
646
+ const fixture = `${parent}conftest.py`;
647
+ if (repository.known.has(fixture) && !reached.has(fixture)) {
648
+ reached.add(fixture);
649
+ const loaded = await repository.read(fixture);
650
+ if (loaded.ok)
651
+ sources.push(loaded);
652
+ }
653
+ if (!parent)
654
+ break;
655
+ parent = parent
656
+ .slice(0, -1)
657
+ .slice(0, parent.slice(0, -1).lastIndexOf("/") + 1);
658
+ }
659
+ }
660
+ for (let index = 0; index < sources.length; index++) {
661
+ const source = sources[index];
662
+ if (!source)
663
+ continue;
664
+ if (args.base && !Object.hasOwn(before, source.path)) {
665
+ const old = await repository.readBefore(source.path);
666
+ if (old.ok)
667
+ before[source.path] = old.text;
668
+ else
669
+ proofLimitations.push({
670
+ fact: old.error,
671
+ next: `provide before evidence for ${source.path}`,
672
+ });
673
+ }
674
+ const parsed = await collectProofSyntax([source], args.base ? before : undefined, analysis.parser);
675
+ declarations.push(...parsed.declarations);
676
+ bindings.push(...parsed.bindings);
677
+ syntaxLimits.push(...parsed.limits);
678
+ for (const [path, names] of parsed.uses ?? [])
679
+ uses.set(path, names);
680
+ const graph = buildGraph([source]);
681
+ for (const [path, targets] of graph.edges)
682
+ edges.set(path, targets);
683
+ limits.push(...graph.limits);
684
+ if (!Object.hasOwn(canonical, source.path) &&
685
+ !source.path.endsWith("conftest.py") &&
686
+ !parsed.bindings.some((binding) => binding.reexport))
687
+ continue;
688
+ for (const path of graph.edges.get(source.path) ?? []) {
689
+ if (reached.has(path))
690
+ continue;
691
+ reached.add(path);
692
+ const loaded = await repository.read(path);
693
+ if (loaded.ok)
694
+ sources.push(loaded);
695
+ else
696
+ proofLimitations.push({
697
+ fact: loaded.error,
698
+ next: `provide readable evidence for ${path}`,
699
+ });
700
+ }
701
+ }
702
+ const syntax = {
703
+ declarations,
704
+ bindings,
705
+ limits: syntaxLimits,
706
+ uses,
707
+ sources,
708
+ ...(args.base ? { before } : {}),
709
+ };
710
+ const closureState = { ...state.state };
711
+ if (command?.ok)
712
+ delete closureState.output;
713
+ const closure = buildAskClosure({
714
+ graph: { edges, limits },
715
+ syntax,
716
+ paths: Object.keys(canonical),
717
+ intentions: JSON.stringify(asks.asks),
718
+ state: closureState,
719
+ });
720
+ state.state = closure.state;
721
+ if (command?.ok)
722
+ state.state.output = { ...command.output };
723
+ proofLimitations.push(...closure.limitations.filter((limit) => !limit.fact.startsWith("Import closure only,")));
724
+ proofLimitations.push({
725
+ fact: `closure: +${closure.added.length}${closure.added.length ? ` (${closure.added.map((piece) => `${piece.path} ${piece.name}, ${piece.chars} chars`).join("; ")})` : ""}; imports only, depth 1; files with no import link are not checked`,
726
+ next: "pass unrelated config, docs and other flows in paths when the answer depends on them",
727
+ });
728
+ }
729
+ else
730
+ proofLimitations.push({
731
+ fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
732
+ next: "pass dependencies explicitly in paths",
733
+ });
734
+ if (!repository.ok)
735
+ state.state.evidence = {
736
+ context: evidenceContext,
737
+ aliases: references.aliases,
738
+ };
739
+ // Command output has its own passage-selection budget below. Only immutable
740
+ // file/note evidence can refuse here, before selection has had a chance.
741
+ const serializedSize = JSON.stringify(command?.ok ? { ...state.state, output: undefined } : state.state).length;
742
+ if (serializedSize > STATE_MAX_CHARS)
743
+ return textResult(`serialized state exceeds ${STATE_MAX_CHARS} chars (${serializedSize}); files and versions are never truncated`, "evidence_too_large");
744
+ evidenceFacts.omissions = unresolved;
745
+ const blocked = blockedAskReadings(asks, state.state, unresolved, args.state);
746
+ const inserted = isRecord(state.state.files) ? state.state.files : {};
747
+ const identities = files.identities.map((file) => {
748
+ const insertedPath = repository.ok
749
+ ? repositoryPath(repository.root, cwd, file.requestedPath)
750
+ : file.requestedPath;
751
+ const content = inserted[insertedPath];
752
+ return {
753
+ ...file,
754
+ insertedPath: resolve(repository.ok ? repository.root : cwd, insertedPath),
755
+ content: typeof content === "string" ? content : "",
756
+ insertedSha256: createHash("sha256")
757
+ .update(typeof content === "string" ? content : "")
758
+ .digest("hex"),
759
+ };
760
+ });
761
+ const blockedMembers = new Set(asks.groups
762
+ .filter((group) => group.some((id) => blocked.has(id)))
763
+ .flat());
764
+ evidenceFacts.blockedGroups = asks.groups
765
+ .filter((group) => group.some((id) => blocked.has(id)))
766
+ .map((ids) => {
767
+ const reason = ids
768
+ .map((id) => blocked.get(id))
769
+ .find((reason) => reason !== undefined) ??
770
+ "Required evidence unavailable";
771
+ // A secret refusal stays named as such instead of missing_required.
772
+ const secret = unresolved.some((item) => item.reason === reason && item.cause === "secret_pattern");
773
+ return {
774
+ ids,
775
+ reason,
776
+ ...(secret ? { cause: "secret_pattern" } : {}),
777
+ };
778
+ });
779
+ const questions = Object.fromEntries(Object.entries(asks.questions).filter(([id]) => !blockedMembers.has(id)));
780
+ const integrity = checkIntegrity(state.state, asks.asks, identities.filter((identity) => identity.content.trim().length > 0));
781
+ let sent = 0;
782
+ let refusal;
783
+ const options = {
784
+ signal,
785
+ cache: !args.command,
786
+ ...runtime.session.requestGate(),
787
+ admissionCause: () => refusal?.kind === "session"
788
+ ? "session_budget"
789
+ : "call_budget",
790
+ beforeRequest: (questionCount) => {
791
+ if (args.max_calls !== undefined && sent >= args.max_calls) {
792
+ refusal = {
793
+ kind: "max_calls",
794
+ message: `max_calls=${args.max_calls} reached`,
795
+ };
796
+ return { ok: false, error: refusal.message };
797
+ }
798
+ const admitted = runtime.session.admit(questionCount);
799
+ if (!admitted.ok) {
800
+ refusal = { kind: "session", message: admitted.error };
801
+ return admitted;
802
+ }
803
+ sent++;
804
+ return admitted;
805
+ },
806
+ onUsage: (usage) => runtime.session.recordUsage(usage),
807
+ };
808
+ if (command?.ok) {
809
+ const output = command.output;
810
+ const text = output.stdout + output.stderr;
811
+ const size = JSON.stringify({ ...state.state, output }).length;
812
+ if (size > STATE_MAX_CHARS) {
813
+ const stdoutChunks = outputChunks(output.stdout);
814
+ const stderrChunks = outputChunks(output.stderr);
815
+ const chunks = [...stdoutChunks, ...stderrChunks];
816
+ if (command.compressedChars >
817
+ OUTPUT_CHUNK_CHARS * OUTPUT_FIND_MAX_CALLS ||
818
+ chunks.length > OUTPUT_FIND_MAX_CALLS)
819
+ return textResult(`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)`, "evidence_too_large");
820
+ const chunkStates = chunks.map((chunk) => withEvidenceContext({ output: chunk.text }, evidenceContext));
821
+ if (chunkStates.some((chunkState) => JSON.stringify(chunkState).length > STATE_MAX_CHARS))
822
+ return textResult(`serialized output chunk including evidence context exceeds ${STATE_MAX_CHARS} chars; narrow command`, "evidence_too_large");
823
+ const found = await Promise.all(chunkStates.map(async (chunkState) => {
824
+ const judgment = await client.judge(chunkState, {
825
+ find: {
826
+ type: "bool",
827
+ instructions: "Does this excerpt of a command's output show something failing (a failed test or assertion, a compiler error, a crash, an error message) with its details?",
828
+ },
829
+ }, options);
830
+ findResults.push(judgment);
831
+ return judgment.ok && judgment.answers.find?.type === "bool"
832
+ ? judgment.answers.find.p
833
+ : 0;
834
+ }));
835
+ if (findResults.some((judgment) => !judgment.ok || judgment.answers.find?.type !== "bool"))
836
+ integrity.controls.push({
837
+ fact: "output find incomplete: some chunks were not judged",
838
+ next: "narrow command or raise max_calls",
839
+ });
840
+ const emptySize = JSON.stringify({
841
+ ...state.state,
842
+ output: { ...output, stdout: "", stderr: "", truncated: true },
843
+ }).length;
844
+ const budget = STATE_MAX_CHARS - emptySize - 16;
845
+ if (budget < 100)
846
+ return textResult("No space for command output; reduce paths or state.", "evidence_too_large");
847
+ const stderrSize = JSON.stringify(output.stderr).length;
848
+ const stdoutSize = JSON.stringify(output.stdout).length;
849
+ const stdoutBudget = !output.stderr
850
+ ? budget
851
+ : stderrSize < budget / 2
852
+ ? budget - stderrSize
853
+ : stdoutSize < budget / 2
854
+ ? stdoutSize
855
+ : Math.floor((budget * output.stdout.length) / Math.max(1, text.length));
856
+ output.stdout = output.stdout
857
+ ? selectOutput(output.stdout, stdoutChunks, found.slice(0, stdoutChunks.length), stdoutBudget)
858
+ : "";
859
+ output.stderr = output.stderr
860
+ ? selectOutput(output.stderr, stderrChunks, found.slice(stdoutChunks.length), budget - stdoutBudget)
861
+ : "";
862
+ if (chunks.some((chunk, index) => (found[index] ?? 0) >= 0.5 &&
863
+ !(index < stdoutChunks.length ? output.stdout : output.stderr).includes(chunk.text)))
864
+ integrity.controls.push({
865
+ fact: "failing output passages omitted: insufficient state budget",
866
+ next: "narrow command or reduce paths",
867
+ });
868
+ output.truncated = true;
869
+ command.selectedPassages =
870
+ output.stdout.length + output.stderr.length < text.length;
871
+ }
872
+ state.state.output = { ...output };
873
+ const checked = checkIntegrity(state.state, asks.asks, [], {
874
+ text: output.stdout + output.stderr,
875
+ truncated: output.truncated,
876
+ omittedChars: Math.max(0, command.lineOmittedChars +
877
+ text.length -
878
+ output.stdout.length -
879
+ output.stderr.length),
880
+ });
881
+ integrity.controls.push(...checked.controls);
882
+ integrity.notes.push(...checked.notes);
883
+ if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
884
+ return textResult("serialized command output exceeds state budget; narrow command or reduce paths", "evidence_too_large");
885
+ if (outputNotIdentifiable)
886
+ integrity.controls.push({
887
+ fact: "not identifiable: the output shows an assertion failure and the failing test is absent; a wrong test and a bug look alike",
888
+ next: "pass the failing test and its code in paths",
889
+ });
890
+ }
891
+ let result = Object.keys(questions).length
892
+ ? await client.judge(state.state, questions, {
893
+ ...options,
894
+ groups: asks.groups.filter((group) => !group.some((id) => blockedMembers.has(id))),
895
+ })
896
+ : { ok: true, answers: {}, calls: 0, questions: 0 };
897
+ if (result.ok) {
898
+ for (const id of blockedMembers)
899
+ result.answers[id] = {
900
+ type: "unjudged",
901
+ reason: blocked.get(id) ?? "required evidence is absent or empty",
902
+ };
903
+ }
904
+ let reversed;
905
+ if (result.ok) {
906
+ const reverse = reverseQuestions(asks, result.answers);
907
+ if (Object.keys(reverse).length) {
908
+ reversed = await client.judge(state.state, reverse, options);
909
+ if (!reversed.ok)
910
+ reversed = {
911
+ ...reversed,
912
+ ok: true,
913
+ answers: Object.fromEntries(Object.keys(reverse).map((id) => [
914
+ id,
915
+ {
916
+ type: "unjudged",
917
+ reason: reversed && !reversed.ok
918
+ ? reversed.error
919
+ : "reverse control failed",
920
+ },
921
+ ])),
922
+ };
923
+ const first = result;
924
+ result = {
925
+ ...first,
926
+ calls: (first.calls ?? 0) + (reversed.calls ?? 0),
927
+ questions: (first.questions ?? 0) + (reversed.questions ?? 0),
928
+ cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
929
+ cacheRequests: (first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
930
+ usage: first.usage && reversed.usage
931
+ ? {
932
+ inputTokens: first.usage.inputTokens + reversed.usage.inputTokens,
933
+ costUsd: first.usage.costUsd + reversed.usage.costUsd,
934
+ }
935
+ : undefined,
936
+ };
937
+ }
938
+ }
939
+ for (const found of findResults)
940
+ result = {
941
+ ...result,
942
+ calls: (result.calls ?? 0) + (found.calls ?? 0),
943
+ questions: (result.questions ?? 0) + (found.questions ?? 0),
944
+ cacheHits: (result.cacheHits ?? 0) + (found.cacheHits ?? 0),
945
+ cacheRequests: (result.cacheRequests ?? 0) + (found.cacheRequests ?? 0),
946
+ usage: result.usage && found.usage
947
+ ? {
948
+ inputTokens: result.usage.inputTokens + found.usage.inputTokens,
949
+ costUsd: result.usage.costUsd + found.usage.costUsd,
950
+ }
951
+ : undefined,
952
+ };
953
+ const attribution = [];
954
+ if (result.ok) {
955
+ const initial = readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined);
956
+ for (const reading of asks.readings.filter((reading) => reading.attributable)) {
957
+ const selected = initial.find((answer) => answer.label === reading.label)?.value?.head;
958
+ if (typeof selected !== "string" ||
959
+ !reading.among?.includes(selected)) {
960
+ attribution.push({
961
+ fact: `attribution ${reading.label}: no designated candidate`,
962
+ next: "provide decisive candidate evidence before acting",
963
+ });
964
+ attributionMissing.add(reading.id);
965
+ auxiliary.controls.notJudged++;
966
+ continue;
967
+ }
968
+ if (!Object.hasOwn(inserted, selected)) {
969
+ attribution.push({
970
+ fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
971
+ next: `add ${selected} to paths before acting`,
972
+ });
973
+ attributionMissing.add(reading.id);
974
+ auxiliary.controls.notJudged++;
975
+ continue;
976
+ }
977
+ const selectedPath = repository?.ok
978
+ ? (references.additions.find((addition) => addition.reference === selected)?.path ?? repositoryPath(repository.root, cwd, selected))
979
+ : selected;
980
+ const retained = (path) => {
981
+ if (!repository?.ok)
982
+ return path !== selected;
983
+ return ((references.additions.find((addition) => addition.reference === path)?.path ?? repositoryPath(repository.root, cwd, path)) !==
984
+ selectedPath);
985
+ };
986
+ const ablated = {
987
+ ...state.state,
988
+ files: Object.fromEntries(Object.entries(inserted).filter(([path]) => retained(path))),
989
+ ...(isRecord(state.state.files_before)
990
+ ? {
991
+ files_before: Object.fromEntries(Object.entries(state.state.files_before).filter(([path]) => retained(path))),
992
+ }
993
+ : {}),
994
+ ...(isRecord(state.state.closure)
995
+ ? {
996
+ closure: Object.fromEntries(Object.entries(state.state.closure).filter(([piece]) => !piece.startsWith(`${selectedPath} (`))),
997
+ }
998
+ : {}),
999
+ };
1000
+ const question = questions[reading.id];
1001
+ if (!question)
1002
+ continue;
1003
+ const control = await client.judge(ablated, { [reading.id]: question }, options);
1004
+ const answer = control.ok ? control.answers[reading.id] : undefined;
1005
+ if (answer && answer.type !== "unjudged" && answer.source)
1006
+ attributionAnswers[reading.id] = [
1007
+ {
1008
+ id: `${reading.id}:attribution`,
1009
+ kind: "attribution",
1010
+ source: answer.source,
1011
+ outcome: answer.type === "choice"
1012
+ ? answer.choice === selected
1013
+ ? "does not rest on it"
1014
+ : "attributed"
1015
+ : "invalid attribution response",
1016
+ rawValues: answer.type === "bool"
1017
+ ? [
1018
+ {
1019
+ label: "attribution",
1020
+ value: answer.p >= 0.5,
1021
+ probability: known(answer.p),
1022
+ },
1023
+ ]
1024
+ : Object.entries(answer.probabilities).map(([label, value]) => ({
1025
+ label,
1026
+ value: label,
1027
+ probability: known(value),
1028
+ })),
1029
+ },
1030
+ ];
1031
+ if (answer?.type !== "choice")
1032
+ attributionMissing.add(reading.id);
1033
+ if (answer && answer.type !== "unjudged" && answer.source)
1034
+ auxiliary.controls[answer.source]++;
1035
+ else
1036
+ auxiliary.controls.notJudged++;
1037
+ attribution.push(answer?.type === "choice"
1038
+ ? {
1039
+ fact: `attribution ${reading.label}: ${answer.choice === selected ? "does not rest on it" : "attributed"} (${selected})`,
1040
+ next: "read the designated evidence before acting",
1041
+ }
1042
+ : {
1043
+ fact: `attribution ${reading.label}: unjudged${!control.ok ? `: ${control.error}` : answer?.type === "unjudged" ? `: ${answer.reason}` : ""}`,
1044
+ next: `rerun attribution for ${selected} before acting`,
1045
+ });
1046
+ result = {
1047
+ ...result,
1048
+ calls: (result.calls ?? 0) + (control.calls ?? 0),
1049
+ questions: (result.questions ?? 0) + (control.questions ?? 0),
1050
+ cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
1051
+ cacheRequests: (result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
1052
+ usage: result.usage && control.usage
1053
+ ? {
1054
+ inputTokens: result.usage.inputTokens + control.usage.inputTokens,
1055
+ costUsd: result.usage.costUsd + control.usage.costUsd,
1056
+ }
1057
+ : undefined,
1058
+ };
1059
+ }
1060
+ }
1061
+ const unchecked = asks.readings
1062
+ .filter((reading) => !result.ok ||
1063
+ [
1064
+ reading.id,
1065
+ ...(reading.twin ? [reading.twin] : []),
1066
+ ...Object.values(reading.controls ?? {}),
1067
+ ].some((id) => !result.answers[id] || result.answers[id]?.type === "unjudged") ||
1068
+ (reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"))
1069
+ .map((reading) => reading.label);
1070
+ for (const reading of asks.readings) {
1071
+ for (const id of [
1072
+ reading.twin,
1073
+ ...Object.values(reading.controls ?? {}),
1074
+ ].filter((id) => !!id)) {
1075
+ const answer = result.ok ? result.answers[id] : undefined;
1076
+ if (answer && answer.type !== "unjudged" && answer.source)
1077
+ auxiliary.controls[answer.source]++;
1078
+ else
1079
+ auxiliary.controls.notJudged++;
1080
+ }
1081
+ if (reversed &&
1082
+ Object.hasOwn(reversed.ok ? reversed.answers : {}, reading.id)) {
1083
+ const answer = reversed.ok ? reversed.answers[reading.id] : undefined;
1084
+ if (answer && answer.type !== "unjudged" && answer.source)
1085
+ auxiliary.controls[answer.source]++;
1086
+ else
1087
+ auxiliary.controls.notJudged++;
1088
+ }
1089
+ }
1090
+ for (const found of findResults) {
1091
+ const answer = found.ok ? found.answers.find : undefined;
1092
+ if (answer && answer.type !== "unjudged" && answer.source)
1093
+ auxiliary.passages[answer.source]++;
1094
+ else
1095
+ auxiliary.passages.notJudged++;
1096
+ }
1097
+ const output = finish(result, {
1098
+ ...(result.ok
1099
+ ? {
1100
+ answers: readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined),
1101
+ }
1102
+ : !refusal
1103
+ ? { refusal: result.error }
1104
+ : {}),
1105
+ ...(refusal ? { budget: refusal, unchecked } : {}),
1106
+ controls: integrity.controls,
1107
+ state: {
1108
+ files: Object.keys(inserted).length,
1109
+ integrity: integrity.controls.length ? "failed" : "ok",
1110
+ warnings: asks.warnings.map((w) => w.fact),
1111
+ },
1112
+ limitationPriorityPaths: [],
1113
+ limitations: [
1114
+ ...(command?.ok
1115
+ ? [
1116
+ {
1117
+ fact: `output: ${command.output.command} → ${command.output.timed_out ? "timed out" : `exit ${command.output.exit_code ?? "unavailable"}`} (${command.originalBytes} bytes → ${command.compressedChars} chars, repetitive lines collapsed${command.lineOmittedChars ? `, ${command.lineOmittedChars} line chars omitted` : ""}${command.selectedPassages ? ", selected passages" : ""})`,
1118
+ next: "read command output with bash when exact text is needed",
1119
+ },
1120
+ ]
1121
+ : []),
1122
+ ...(command?.ok && command.redactions
1123
+ ? [
1124
+ {
1125
+ fact: `output: ${command.redactions} occurrence${command.redactions === 1 ? "" : "s"} of the configured Jev API key replaced with [redacted]`,
1126
+ next: "remove the key from the command's output; it is never sent to Jev",
1127
+ },
1128
+ ]
1129
+ : []),
1130
+ ...(command?.ok && command.shapeLimitExceeded
1131
+ ? [
1132
+ {
1133
+ fact: `output shape limit exceeded at ${OUTPUT_SHAPE_MAX_COUNT}; rarity grouping disabled`,
1134
+ next: "narrow command or reduce verbosity",
1135
+ },
1136
+ ]
1137
+ : []),
1138
+ ...asks.warnings,
1139
+ ...integrity.notes,
1140
+ ...proofLimitations,
1141
+ ...attribution,
1142
+ ],
1143
+ });
1144
+ return { ...output, ...hostUsage(isOmp, result.usage) };
1145
+ },
1146
+ };
1147
+ }