jev-agent-tools 0.1.3 → 0.2.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 (135) hide show
  1. package/CHANGELOG.md +88 -3
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +64 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -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 +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +294 -0
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host-tui.d.ts +14 -0
  117. package/src/host.ts +11 -0
  118. package/src/index.ts +13 -5
  119. package/src/jev/client.ts +12 -0
  120. package/src/jev/types.ts +6 -0
  121. package/src/mcp/main.ts +135 -0
  122. package/src/mcp/protocol.ts +282 -0
  123. package/src/mcp/tools.ts +166 -0
  124. package/src/secret-input.ts +222 -0
  125. package/src/session.ts +59 -0
  126. package/src/setup.ts +170 -0
  127. package/src/texts/configuration.ts +1 -1
  128. package/src/tools/ask-files.ts +8 -13
  129. package/src/tools/ask.ts +29 -28
  130. package/src/tools/check-diff.ts +11 -11
  131. package/src/tools/docs-check.ts +1 -0
  132. package/src/tools/find.ts +8 -8
  133. package/src/tools/locate.ts +8 -9
  134. package/src/tools/select-tests.ts +10 -10
  135. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,686 @@
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 { collectFiles } from "../adapters/files.js";
10
+ import { shareGitInventory } from "../adapters/git-inventory.js";
11
+ import { hostUsage } from "../adapters/usage.js";
12
+ 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";
13
+ import { buildAskClosure } from "../core/ask-closure.js";
14
+ import { blockedAskReadings, resolveAskReferences, } from "../core/ask-references.js";
15
+ import { compileAsks, readAsks, reverseQuestions } from "../core/asks.js";
16
+ import { outputChunks, selectOutput } from "../core/command-output.js";
17
+ import { createImportGraphBuilder, } from "../core/imports.js";
18
+ import { checkIntegrity } from "../core/integrity.js";
19
+ import { buildEnvelope } from "../core/output.js";
20
+ import { assembleState } from "../core/state.js";
21
+ import { discoverTests } from "../core/test-discovery.js";
22
+ import { describeAsk } from "../describe.js";
23
+ import { renderEnvelope } from "../render.js";
24
+ import { isRecord } from "../result.js";
25
+ import { COMMAND_LIMIT_NOTICE } from "../texts/ask.js";
26
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
27
+ import { asksParameter } from "./ask-schema.js";
28
+ export const askParameters = Type.Object({
29
+ state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
30
+ paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { maxItems: ASK_MAX_FILES })),
31
+ base: Type.Optional(Type.String({ minLength: 1 })),
32
+ command: Type.Optional(Type.String({ minLength: 1 })),
33
+ timeout_s: Type.Optional(Type.Number({
34
+ minimum: 1,
35
+ maximum: ASK_TIMEOUT_MAX_S,
36
+ default: ASK_TIMEOUT_S,
37
+ })),
38
+ asks: asksParameter("ask"),
39
+ max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
40
+ }, { additionalProperties: false });
41
+ export function createAskTool(dependencies) {
42
+ const { host, runtime, exec: execute } = dependencies;
43
+ const { isOmp, names } = host;
44
+ const allowCommand = process.env.JEV_TOOLS_ALLOW_COMMAND !== "0";
45
+ const parameters = allowCommand
46
+ ? askParameters
47
+ : Type.Omit(askParameters, ["command", "timeout_s"]);
48
+ return {
49
+ name: "jev_ask",
50
+ label: "Jev ask",
51
+ description: `${describeAsk(names)}\n\n${COMMAND_LIMIT_NOTICE}`,
52
+ parameters,
53
+ ...(isOmp
54
+ ? {
55
+ approval: (args) => args.command ? "exec" : "read",
56
+ loadMode: "essential",
57
+ }
58
+ : {
59
+ promptSnippet: "Typed answers about one situation built from a note, files and a command's output",
60
+ promptGuidelines: [
61
+ "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",
62
+ ],
63
+ }),
64
+ async execute(_id, args, signal, _update, ctx) {
65
+ const client = dependencies.client;
66
+ const cwd = await canonicalPath(ctx.cwd);
67
+ const started = performance.now();
68
+ const exec = shareGitInventory(execute);
69
+ const finish = (result, input) => {
70
+ const envelope = buildEnvelope({
71
+ ...input,
72
+ yield: {
73
+ calls: result.calls ?? 0,
74
+ questions: result.questions ?? 0,
75
+ costUsd: result.usage?.costUsd,
76
+ cacheHits: result.cacheHits ?? 0,
77
+ cacheRequests: result.cacheRequests ?? 0,
78
+ elapsedMs: performance.now() - started,
79
+ },
80
+ });
81
+ runtime.session.record(envelope);
82
+ runtime.guide.deliver(ctx);
83
+ return {
84
+ content: [{ type: "text", text: renderEnvelope(envelope) }],
85
+ details: result,
86
+ };
87
+ };
88
+ const textResult = (text) => finish({ ok: false, error: text }, { refusal: text });
89
+ if (!client)
90
+ return textResult(NOT_CONFIGURED);
91
+ if (args.command && !allowCommand)
92
+ return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
93
+ if (args.timeout_s !== undefined &&
94
+ (!Number.isFinite(args.timeout_s) ||
95
+ args.timeout_s < 1 ||
96
+ args.timeout_s > ASK_TIMEOUT_MAX_S))
97
+ return textResult(`timeout_s must be between 1 and ${ASK_TIMEOUT_MAX_S}`);
98
+ if (args.state === undefined && !args.paths?.length && !args.command)
99
+ return textResult("Provide state, command or at least one path.");
100
+ const asks = compileAsks(args.asks, { surface: "ask" });
101
+ if (!asks.ok)
102
+ return textResult(asks.error);
103
+ const files = await collectFiles(cwd, args.paths ?? [], signal, {
104
+ exec,
105
+ allowEmpty: true,
106
+ skipInvalidUtf8: true,
107
+ });
108
+ if (!files.ok)
109
+ return textResult(files.error);
110
+ if (!Object.keys(files.files).length &&
111
+ args.state === undefined &&
112
+ !args.command &&
113
+ files.skipped.length)
114
+ return textResult(`No readable evidence: ${files.skipped.join("; ")}`);
115
+ const proofLimitations = files.skipped.map((fact) => ({
116
+ fact,
117
+ next: "Provide this file as UTF-8 text if the judgment needs it.",
118
+ }));
119
+ let repository = await collectAskRepository(exec, cwd, args.base, signal);
120
+ if (!repository.ok && args.base)
121
+ return textResult(repository.error);
122
+ if (!repository.ok)
123
+ proofLimitations.push({
124
+ fact: `closure unavailable: ${repository.error}`,
125
+ next: "pass dependencies explicitly in paths",
126
+ });
127
+ const command = args.command
128
+ ? await captureCommand(exec, repository.ok ? repository.root : cwd, args.command, args.timeout_s, signal)
129
+ : undefined;
130
+ if (command && !command.ok)
131
+ return textResult(command.error);
132
+ if (command?.ok) {
133
+ repository = await collectAskRepository(exec, cwd, args.base, signal);
134
+ if (!repository.ok && args.base)
135
+ return textResult(repository.error);
136
+ }
137
+ const initialState = assembleState(args.state, files.files);
138
+ if (!initialState.ok)
139
+ return textResult(initialState.error);
140
+ if (command?.ok && Object.hasOwn(initialState.state, "output"))
141
+ return textResult("state contains reserved key output when command is supplied; rename it in your note");
142
+ let outputNotIdentifiable = false;
143
+ if (command?.ok && repository.ok) {
144
+ for (const target of command.targets) {
145
+ if (target.path.startsWith(`${repository.root}/`))
146
+ target.path = target.path.slice(repository.root.length + 1);
147
+ else if (isAbsolute(target.path)) {
148
+ // Windows reports C:/... or short 8.3 forms; compare canonically.
149
+ const local = relative(repository.root, await canonicalPath(target.path))
150
+ .split("\\")
151
+ .join("/");
152
+ if (local && !local.startsWith("../") && !isAbsolute(local))
153
+ target.path = local;
154
+ }
155
+ let matches = [...repository.known].filter((path) => path === target.path || path.endsWith(`/${target.path}`));
156
+ if (target.testName) {
157
+ const matched = [];
158
+ for (const candidate of matches) {
159
+ const source = await collectFiles(repository.root, [candidate], signal, { exec, allowEmpty: true, inventory: repository.known });
160
+ if (source.ok &&
161
+ discoverTests([
162
+ { path: candidate, text: source.files[candidate] ?? "" },
163
+ ]).entries.some((entry) => entry.scenarios.some((scenario) => scenario.name === target.testName)))
164
+ matched.push(candidate);
165
+ }
166
+ matches = matched;
167
+ }
168
+ if (matches.length !== 1) {
169
+ outputNotIdentifiable ||= command.assertion && target.assertion;
170
+ continue;
171
+ }
172
+ const path = matches[0];
173
+ if (!path)
174
+ continue;
175
+ const local = repositoryPath(cwd, repository.root, path);
176
+ if (!Object.hasOwn(files.files, local)) {
177
+ const added = await collectFiles(repository.root, [path], signal, {
178
+ exec,
179
+ allowEmpty: true,
180
+ inventory: repository.known,
181
+ });
182
+ if (added.ok && Object.keys(files.files).length < ASK_MAX_FILES) {
183
+ files.files[local] = added.files[path] ?? "";
184
+ files.identities.push(...added.identities.map((identity) => ({
185
+ ...identity,
186
+ requestedPath: local,
187
+ })));
188
+ }
189
+ else {
190
+ outputNotIdentifiable ||= command.assertion && target.assertion;
191
+ continue;
192
+ }
193
+ }
194
+ if (target.assertion && target.nodeId?.includes("::")) {
195
+ const discovery = discoverTests([
196
+ { path, text: files.files[local] ?? "" },
197
+ ]);
198
+ const name = target.nodeId
199
+ .split("::")
200
+ .slice(1)
201
+ .join("::")
202
+ .replace(/\[[\s\S]*\]$/, "");
203
+ if (!discovery.entries.some((entry) => entry.scenarios.some((scenario) => scenario.id === name ||
204
+ scenario.name === name.split("::").at(-1))))
205
+ outputNotIdentifiable ||= command.assertion;
206
+ }
207
+ }
208
+ }
209
+ if (command?.ok &&
210
+ command.assertion &&
211
+ (!command.targets.length || !repository.ok) &&
212
+ !discoverTests(Object.entries(files.files).map(([path, text]) => ({ path, text }))).entries.length)
213
+ outputNotIdentifiable = true;
214
+ const references = resolveAskReferences(asks, args.state, files.files, repository?.ok ? [...repository.known] : []);
215
+ const unresolved = [...references.unresolved];
216
+ if (repository?.ok) {
217
+ const root = repository.root;
218
+ const additions = await Promise.all(references.additions.map(async (addition) => ({
219
+ addition,
220
+ added: await collectFiles(root, [addition.path], signal, {
221
+ allowEmpty: true,
222
+ exec,
223
+ inventory: repository.known,
224
+ }),
225
+ })));
226
+ for (const { addition, added } of additions) {
227
+ if (added.ok) {
228
+ files.files[addition.reference] = added.files[addition.path] ?? "";
229
+ files.identities.push(...added.identities.map((identity) => ({
230
+ ...identity,
231
+ requestedPath: addition.reference,
232
+ })));
233
+ }
234
+ else
235
+ unresolved.push({
236
+ reference: addition.reference,
237
+ reason: added.error,
238
+ });
239
+ }
240
+ }
241
+ const state = assembleState(args.state, files.files);
242
+ if (!state.ok)
243
+ return textResult(state.error);
244
+ if (command?.ok && Object.hasOwn(state.state, "output"))
245
+ return textResult("state contains reserved key output when command is supplied; rename it in your note");
246
+ if (command?.ok)
247
+ state.state.output = { ...command.output };
248
+ if (repository?.ok) {
249
+ const canonical = Object.fromEntries(Object.entries(files.files).map(([path, text]) => [
250
+ repositoryPath(repository.root, cwd, path),
251
+ text,
252
+ ]));
253
+ for (const addition of references.additions)
254
+ canonical[addition.path] = files.files[addition.reference] ?? "";
255
+ const sources = Object.entries(canonical).map(([path, text]) => ({ path, text }));
256
+ const before = {};
257
+ if (args.base) {
258
+ for (const path of Object.keys(canonical)) {
259
+ const old = await repository.readBefore(path);
260
+ if (!old.ok)
261
+ return textResult(old.error);
262
+ before[path] = old.text;
263
+ }
264
+ state.state.base_sha = repository.baseSha ?? "";
265
+ state.state.files_before = Object.fromEntries(Object.keys(files.files).map((path) => {
266
+ const canonicalPath = references.additions.find((addition) => addition.reference === path)?.path ?? repositoryPath(repository.root, cwd, path);
267
+ return [path, before[canonicalPath] ?? null];
268
+ }));
269
+ const stateSize = JSON.stringify(state.state).length;
270
+ if (stateSize > STATE_MAX_CHARS) {
271
+ const partSizes = Object.entries(state.state)
272
+ .map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
273
+ .join(", ");
274
+ return textResult(`serialized state including files_before exceeds ${STATE_MAX_CHARS} chars (${stateSize}); serialized parts: ${partSizes}; split the situation into smaller states.`);
275
+ }
276
+ }
277
+ const configuration = [];
278
+ for (const path of repository.known) {
279
+ if (!/(^|\/)(?:(tsconfig|jsconfig)(?:\.[^/]+)?|package)\.json$/.test(path))
280
+ continue;
281
+ const source = await repository.read(path);
282
+ if (source.ok)
283
+ configuration.push(source);
284
+ }
285
+ const analysis = await createAnalysisContext();
286
+ const buildGraph = createImportGraphBuilder(configuration, repository.known, analysis);
287
+ const edges = new Map();
288
+ const limits = [];
289
+ const declarations = [];
290
+ const bindings = [];
291
+ const syntaxLimits = [];
292
+ const uses = new Map();
293
+ const reached = new Set(sources.map((source) => source.path));
294
+ for (const path of Object.keys(canonical)) {
295
+ if (!/(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path))
296
+ continue;
297
+ let parent = path.slice(0, path.lastIndexOf("/") + 1);
298
+ while (true) {
299
+ const fixture = `${parent}conftest.py`;
300
+ if (repository.known.has(fixture) && !reached.has(fixture)) {
301
+ reached.add(fixture);
302
+ const loaded = await repository.read(fixture);
303
+ if (loaded.ok)
304
+ sources.push(loaded);
305
+ }
306
+ if (!parent)
307
+ break;
308
+ parent = parent
309
+ .slice(0, -1)
310
+ .slice(0, parent.slice(0, -1).lastIndexOf("/") + 1);
311
+ }
312
+ }
313
+ for (let index = 0; index < sources.length; index++) {
314
+ const source = sources[index];
315
+ if (!source)
316
+ continue;
317
+ if (args.base && !Object.hasOwn(before, source.path)) {
318
+ const old = await repository.readBefore(source.path);
319
+ if (old.ok)
320
+ before[source.path] = old.text;
321
+ else
322
+ proofLimitations.push({
323
+ fact: old.error,
324
+ next: `provide before evidence for ${source.path}`,
325
+ });
326
+ }
327
+ const parsed = await collectProofSyntax([source], args.base ? before : undefined, analysis.parser);
328
+ declarations.push(...parsed.declarations);
329
+ bindings.push(...parsed.bindings);
330
+ syntaxLimits.push(...parsed.limits);
331
+ for (const [path, names] of parsed.uses ?? [])
332
+ uses.set(path, names);
333
+ const graph = buildGraph([source]);
334
+ for (const [path, targets] of graph.edges)
335
+ edges.set(path, targets);
336
+ limits.push(...graph.limits);
337
+ if (!Object.hasOwn(canonical, source.path) &&
338
+ !source.path.endsWith("conftest.py") &&
339
+ !parsed.bindings.some((binding) => binding.reexport))
340
+ continue;
341
+ for (const path of graph.edges.get(source.path) ?? []) {
342
+ if (reached.has(path))
343
+ continue;
344
+ reached.add(path);
345
+ const loaded = await repository.read(path);
346
+ if (loaded.ok)
347
+ sources.push(loaded);
348
+ else
349
+ proofLimitations.push({
350
+ fact: loaded.error,
351
+ next: `provide readable evidence for ${path}`,
352
+ });
353
+ }
354
+ }
355
+ const syntax = {
356
+ declarations,
357
+ bindings,
358
+ limits: syntaxLimits,
359
+ uses,
360
+ sources,
361
+ ...(args.base ? { before } : {}),
362
+ };
363
+ const closure = buildAskClosure({
364
+ graph: { edges, limits },
365
+ syntax,
366
+ paths: Object.keys(canonical),
367
+ intentions: JSON.stringify(asks.asks),
368
+ state: state.state,
369
+ });
370
+ state.state = closure.state;
371
+ proofLimitations.push(...closure.limitations.filter((limit) => !limit.fact.startsWith("Import closure only,")));
372
+ proofLimitations.push({
373
+ 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`,
374
+ next: "pass unrelated config, docs and other flows in paths when the answer depends on them",
375
+ });
376
+ }
377
+ else
378
+ proofLimitations.push({
379
+ fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
380
+ next: "pass dependencies explicitly in paths",
381
+ });
382
+ const blocked = blockedAskReadings(asks, state.state, unresolved, args.state);
383
+ const inserted = isRecord(state.state.files) ? state.state.files : {};
384
+ const identities = files.identities.map((file) => {
385
+ const content = inserted[file.requestedPath];
386
+ const insertedPath = Object.keys(inserted).find((path) => resolve(cwd, path) === file.resolvedPath);
387
+ return {
388
+ ...file,
389
+ insertedPath: insertedPath ? resolve(cwd, insertedPath) : "",
390
+ content: typeof content === "string" ? content : "",
391
+ insertedSha256: createHash("sha256")
392
+ .update(typeof content === "string" ? content : "")
393
+ .digest("hex"),
394
+ };
395
+ });
396
+ const blockedMembers = new Set(asks.groups
397
+ .filter((group) => group.some((id) => blocked.has(id)))
398
+ .flat());
399
+ const questions = Object.fromEntries(Object.entries(asks.questions).filter(([id]) => !blockedMembers.has(id)));
400
+ const integrity = checkIntegrity(state.state, asks.asks, identities.filter((identity) => identity.content.trim().length > 0));
401
+ let sent = 0;
402
+ let refusal;
403
+ const findResults = [];
404
+ const options = {
405
+ signal,
406
+ cache: !args.command,
407
+ ...runtime.session.requestGate(),
408
+ beforeRequest: (questionCount) => {
409
+ if (args.max_calls !== undefined && sent >= args.max_calls) {
410
+ refusal = {
411
+ kind: "max_calls",
412
+ message: `max_calls=${args.max_calls} reached`,
413
+ };
414
+ return { ok: false, error: refusal.message };
415
+ }
416
+ const admitted = runtime.session.admit(questionCount);
417
+ if (!admitted.ok) {
418
+ refusal = { kind: "session", message: admitted.error };
419
+ return admitted;
420
+ }
421
+ sent++;
422
+ return admitted;
423
+ },
424
+ onUsage: (usage) => runtime.session.recordUsage(usage),
425
+ };
426
+ if (command?.ok) {
427
+ const output = command.output;
428
+ const text = output.stdout + output.stderr;
429
+ const size = JSON.stringify({ ...state.state, output }).length;
430
+ if (size > STATE_MAX_CHARS) {
431
+ const stdoutChunks = outputChunks(output.stdout);
432
+ const stderrChunks = outputChunks(output.stderr);
433
+ const chunks = [...stdoutChunks, ...stderrChunks];
434
+ if (command.compressedChars >
435
+ OUTPUT_CHUNK_CHARS * OUTPUT_FIND_MAX_CALLS ||
436
+ chunks.length > OUTPUT_FIND_MAX_CALLS)
437
+ 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)`);
438
+ const found = await Promise.all(chunks.map(async (chunk) => {
439
+ const judgment = await client.judge({ output: chunk.text }, {
440
+ find: {
441
+ type: "bool",
442
+ 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?",
443
+ },
444
+ }, options);
445
+ findResults.push(judgment);
446
+ return judgment.ok && judgment.answers.find?.type === "bool"
447
+ ? judgment.answers.find.p
448
+ : 0;
449
+ }));
450
+ if (findResults.some((judgment) => !judgment.ok || judgment.answers.find?.type !== "bool"))
451
+ integrity.controls.push({
452
+ fact: "output find incomplete: some chunks were not judged",
453
+ next: "narrow command or raise max_calls",
454
+ });
455
+ const emptySize = JSON.stringify({
456
+ ...state.state,
457
+ output: { ...output, stdout: "", stderr: "", truncated: true },
458
+ }).length;
459
+ const budget = STATE_MAX_CHARS - emptySize - 16;
460
+ if (budget < 100)
461
+ return textResult("No space for command output; reduce paths or state.");
462
+ const stderrSize = JSON.stringify(output.stderr).length;
463
+ const stdoutSize = JSON.stringify(output.stdout).length;
464
+ const stdoutBudget = !output.stderr
465
+ ? budget
466
+ : stderrSize < budget / 2
467
+ ? budget - stderrSize
468
+ : stdoutSize < budget / 2
469
+ ? stdoutSize
470
+ : Math.floor((budget * output.stdout.length) / Math.max(1, text.length));
471
+ output.stdout = output.stdout
472
+ ? selectOutput(output.stdout, stdoutChunks, found.slice(0, stdoutChunks.length), stdoutBudget)
473
+ : "";
474
+ output.stderr = output.stderr
475
+ ? selectOutput(output.stderr, stderrChunks, found.slice(stdoutChunks.length), budget - stdoutBudget)
476
+ : "";
477
+ if (chunks.some((chunk, index) => (found[index] ?? 0) >= 0.5 &&
478
+ !(index < stdoutChunks.length ? output.stdout : output.stderr).includes(chunk.text)))
479
+ integrity.controls.push({
480
+ fact: "failing output passages omitted: insufficient state budget",
481
+ next: "narrow command or reduce paths",
482
+ });
483
+ output.truncated = true;
484
+ command.selectedPassages =
485
+ output.stdout.length + output.stderr.length < text.length;
486
+ }
487
+ state.state.output = { ...output };
488
+ const checked = checkIntegrity(state.state, asks.asks, [], {
489
+ text: output.stdout + output.stderr,
490
+ truncated: output.truncated,
491
+ omittedChars: Math.max(0, command.lineOmittedChars +
492
+ text.length -
493
+ output.stdout.length -
494
+ output.stderr.length),
495
+ });
496
+ integrity.controls.push(...checked.controls);
497
+ integrity.notes.push(...checked.notes);
498
+ if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
499
+ return textResult("serialized command output exceeds state budget; narrow command or reduce paths");
500
+ if (outputNotIdentifiable)
501
+ integrity.controls.push({
502
+ fact: "not identifiable: the output shows an assertion failure and the failing test is absent; a wrong test and a bug look alike",
503
+ next: "pass the failing test and its code in paths",
504
+ });
505
+ }
506
+ let result = Object.keys(questions).length
507
+ ? await client.judge(state.state, questions, {
508
+ ...options,
509
+ groups: asks.groups.filter((group) => !group.some((id) => blockedMembers.has(id))),
510
+ })
511
+ : { ok: true, answers: {}, calls: 0, questions: 0 };
512
+ if (result.ok) {
513
+ for (const id of blockedMembers)
514
+ result.answers[id] = {
515
+ type: "unjudged",
516
+ reason: blocked.get(id) ?? "required evidence is absent or empty",
517
+ };
518
+ }
519
+ let reversed;
520
+ if (result.ok) {
521
+ const reverse = reverseQuestions(asks, result.answers);
522
+ if (Object.keys(reverse).length) {
523
+ reversed = await client.judge(state.state, reverse, options);
524
+ if (!reversed.ok)
525
+ reversed = {
526
+ ...reversed,
527
+ ok: true,
528
+ answers: Object.fromEntries(Object.keys(reverse).map((id) => [
529
+ id,
530
+ {
531
+ type: "unjudged",
532
+ reason: reversed && !reversed.ok
533
+ ? reversed.error
534
+ : "reverse control failed",
535
+ },
536
+ ])),
537
+ };
538
+ const first = result;
539
+ result = {
540
+ ...first,
541
+ calls: (first.calls ?? 0) + (reversed.calls ?? 0),
542
+ questions: (first.questions ?? 0) + (reversed.questions ?? 0),
543
+ cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
544
+ cacheRequests: (first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
545
+ usage: {
546
+ inputTokens: (first.usage?.inputTokens ?? 0) +
547
+ (reversed.usage?.inputTokens ?? 0),
548
+ costUsd: (first.usage?.costUsd ?? 0) + (reversed.usage?.costUsd ?? 0),
549
+ },
550
+ };
551
+ }
552
+ }
553
+ for (const found of findResults)
554
+ result = {
555
+ ...result,
556
+ calls: (result.calls ?? 0) + (found.calls ?? 0),
557
+ questions: (result.questions ?? 0) + (found.questions ?? 0),
558
+ usage: {
559
+ inputTokens: (result.usage?.inputTokens ?? 0) +
560
+ (found.usage?.inputTokens ?? 0),
561
+ costUsd: (result.usage?.costUsd ?? 0) + (found.usage?.costUsd ?? 0),
562
+ },
563
+ };
564
+ const attribution = [];
565
+ if (result.ok) {
566
+ const initial = readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined);
567
+ for (const reading of asks.readings.filter((reading) => reading.attributable)) {
568
+ const selected = initial.find((answer) => answer.label === reading.label)?.value?.head;
569
+ if (typeof selected !== "string" ||
570
+ !reading.among?.includes(selected)) {
571
+ attribution.push({
572
+ fact: `attribution ${reading.label}: no designated candidate`,
573
+ next: "provide decisive candidate evidence before acting",
574
+ });
575
+ continue;
576
+ }
577
+ if (!Object.hasOwn(inserted, selected)) {
578
+ attribution.push({
579
+ fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
580
+ next: `add ${selected} to paths before acting`,
581
+ });
582
+ continue;
583
+ }
584
+ const selectedPath = repository?.ok
585
+ ? (references.additions.find((addition) => addition.reference === selected)?.path ?? repositoryPath(repository.root, cwd, selected))
586
+ : selected;
587
+ const retained = (path) => {
588
+ if (!repository?.ok)
589
+ return path !== selected;
590
+ return ((references.additions.find((addition) => addition.reference === path)?.path ?? repositoryPath(repository.root, cwd, path)) !==
591
+ selectedPath);
592
+ };
593
+ const ablated = {
594
+ ...state.state,
595
+ files: Object.fromEntries(Object.entries(inserted).filter(([path]) => retained(path))),
596
+ ...(isRecord(state.state.files_before)
597
+ ? {
598
+ files_before: Object.fromEntries(Object.entries(state.state.files_before).filter(([path]) => retained(path))),
599
+ }
600
+ : {}),
601
+ ...(isRecord(state.state.closure)
602
+ ? {
603
+ closure: Object.fromEntries(Object.entries(state.state.closure).filter(([piece]) => !piece.startsWith(`${selectedPath} (`))),
604
+ }
605
+ : {}),
606
+ };
607
+ const question = questions[reading.id];
608
+ if (!question)
609
+ continue;
610
+ const control = await client.judge(ablated, { [reading.id]: question }, options);
611
+ const answer = control.ok ? control.answers[reading.id] : undefined;
612
+ attribution.push(answer?.type === "choice"
613
+ ? {
614
+ fact: `attribution ${reading.label}: ${answer.choice === selected ? "does not rest on it" : "attributed"} (${selected})`,
615
+ next: "read the designated evidence before acting",
616
+ }
617
+ : {
618
+ fact: `attribution ${reading.label}: unjudged${!control.ok ? `: ${control.error}` : answer?.type === "unjudged" ? `: ${answer.reason}` : ""}`,
619
+ next: `rerun attribution for ${selected} before acting`,
620
+ });
621
+ result = {
622
+ ...result,
623
+ calls: (result.calls ?? 0) + (control.calls ?? 0),
624
+ questions: (result.questions ?? 0) + (control.questions ?? 0),
625
+ cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
626
+ cacheRequests: (result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
627
+ usage: {
628
+ inputTokens: (result.usage?.inputTokens ?? 0) +
629
+ (control.usage?.inputTokens ?? 0),
630
+ costUsd: (result.usage?.costUsd ?? 0) + (control.usage?.costUsd ?? 0),
631
+ },
632
+ };
633
+ }
634
+ }
635
+ const unchecked = asks.readings
636
+ .filter((reading) => !result.ok ||
637
+ [
638
+ reading.id,
639
+ ...(reading.twin ? [reading.twin] : []),
640
+ ...Object.values(reading.controls ?? {}),
641
+ ].some((id) => !result.answers[id] || result.answers[id]?.type === "unjudged") ||
642
+ (reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"))
643
+ .map((reading) => reading.label);
644
+ const output = finish(result, {
645
+ ...(result.ok
646
+ ? {
647
+ answers: readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined),
648
+ }
649
+ : !refusal
650
+ ? { refusal: result.error }
651
+ : {}),
652
+ ...(refusal ? { budget: refusal, unchecked } : {}),
653
+ controls: integrity.controls,
654
+ state: {
655
+ files: Object.keys(inserted).length,
656
+ integrity: integrity.controls.length ? "failed" : "ok",
657
+ warnings: asks.warnings.map((w) => w.fact),
658
+ },
659
+ limitationPriorityPaths: [],
660
+ limitations: [
661
+ ...(command?.ok
662
+ ? [
663
+ {
664
+ 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" : ""})`,
665
+ next: "read command output with bash when exact text is needed",
666
+ },
667
+ ]
668
+ : []),
669
+ ...(command?.ok && command.shapeLimitExceeded
670
+ ? [
671
+ {
672
+ fact: `output shape limit exceeded at ${OUTPUT_SHAPE_MAX_COUNT}; rarity grouping disabled`,
673
+ next: "narrow command or reduce verbosity",
674
+ },
675
+ ]
676
+ : []),
677
+ ...asks.warnings,
678
+ ...integrity.notes,
679
+ ...proofLimitations,
680
+ ...attribution,
681
+ ],
682
+ });
683
+ return { ...output, ...hostUsage(isOmp, result.usage) };
684
+ },
685
+ };
686
+ }