jev-agent-tools 0.1.4 → 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 (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -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 +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,303 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { canonicalPath } from "../adapters/canonical-path.js";
3
+ import { shareGitInventory } from "../adapters/git-inventory.js";
4
+ import { readLocateFile, scanRange } from "../adapters/locate-file.js";
5
+ import { loadSyntaxParser } from "../adapters/syntax.js";
6
+ import { hostUsage } from "../adapters/usage.js";
7
+ import { CHOICE_MAX_OPTIONS, LOCATE_GRAY_MIN, LOCATE_MIN_KB, LOCATE_SECTION_MAX_LINES, LOCATE_SHRINK_TOP, LOCATE_VERDICT_MIN, STATE_MAX_CHARS, } from "../constants.js";
8
+ import { locateDisplay, outlineEvidence, sectionOutline, sectionState, sectionStateFits, } from "../core/locate.js";
9
+ import { buildEnvelope, } from "../core/output.js";
10
+ import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.js";
11
+ import { sectionFile } from "../core/sections.js";
12
+ import { interpolate } from "../describe.js";
13
+ import { renderEnvelope } from "../render.js";
14
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
15
+ import { LOCATE_DESCRIPTION } from "../texts/locate.js";
16
+ const parameters = Type.Object({ path: Type.String({ minLength: 1 }), goal: Type.String({ minLength: 1 }) }, { additionalProperties: false });
17
+ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
18
+ const { host, runtime, exec: execute } = dependencies;
19
+ return {
20
+ name: "jev_locate_in_file",
21
+ label: "Jev locate in file",
22
+ description: interpolate(LOCATE_DESCRIPTION.replace("{{ompOutline}}", host.isOmp
23
+ ? " read already outlines code files; use this when the outline does not tell you which range to open."
24
+ : ""), host.names),
25
+ parameters,
26
+ ...(host.isOmp
27
+ ? { approval: "read", loadMode: "essential" }
28
+ : {
29
+ promptSnippet: "Find which line range of one large file serves a goal",
30
+ }),
31
+ async execute(_id, args, signal, _update, ctx) {
32
+ const client = dependencies.client;
33
+ const cwd = await canonicalPath(ctx.cwd);
34
+ const started = performance.now();
35
+ const exec = shareGitInventory(execute);
36
+ const results = [];
37
+ const limitations = [];
38
+ const finish = (input) => {
39
+ const usage = results.reduce((sum, result) => ({
40
+ inputTokens: sum.inputTokens + (result.usage?.inputTokens ?? 0),
41
+ costUsd: sum.costUsd + (result.usage?.costUsd ?? 0),
42
+ }), { inputTokens: 0, costUsd: 0 });
43
+ const metrics = {
44
+ calls: 0,
45
+ questions: 0,
46
+ cacheHits: 0,
47
+ cacheRequests: 0,
48
+ };
49
+ for (const result of results) {
50
+ metrics.calls += result.calls ?? 0;
51
+ metrics.questions += result.questions ?? 0;
52
+ metrics.cacheHits += result.cacheHits ?? 0;
53
+ metrics.cacheRequests += result.cacheRequests ?? 0;
54
+ }
55
+ const envelope = buildEnvelope({
56
+ ...input,
57
+ limitations: [...limitations, ...(input.limitations ?? [])],
58
+ yield: {
59
+ ...metrics,
60
+ costUsd: usage.costUsd,
61
+ elapsedMs: performance.now() - started,
62
+ },
63
+ });
64
+ runtime.session.record(envelope);
65
+ runtime.guide.deliver(ctx);
66
+ return {
67
+ content: [{ type: "text", text: renderEnvelope(envelope) }],
68
+ details: { results },
69
+ ...hostUsage(host.isOmp, results.some((r) => r.usage) ? usage : undefined),
70
+ };
71
+ };
72
+ const file = await readLocateFile(cwd, args.path, signal, exec);
73
+ if (!file.ok)
74
+ return finish({ refusal: file.error });
75
+ if (file.bytes < LOCATE_MIN_KB * 1000)
76
+ return finish({
77
+ refusal: `small file (${file.lines} lines): read it directly`,
78
+ });
79
+ if (!client)
80
+ return finish({ refusal: NOT_CONFIGURED });
81
+ let sections;
82
+ if (file.kind === "whole") {
83
+ const parser = await loadParser();
84
+ const declarations = parser?.supports(args.path)
85
+ ? parser.declarations(args.path, file.text, {
86
+ descendAbove: LOCATE_SECTION_MAX_LINES,
87
+ })
88
+ : undefined;
89
+ if (args.path.endsWith(".py") && !parser?.supports(args.path))
90
+ limitations.push({
91
+ fact: "Python: unsupported language (plugin missing), heuristic sections",
92
+ next: "install @ast-grep/lang-python",
93
+ });
94
+ for (const limit of declarations?.limits ?? [])
95
+ limitations.push({
96
+ fact: limit.kind === "parse_failed"
97
+ ? "syntax parse failed: heuristic sections"
98
+ : `parse_partial: ${args.path}${limit.declaration ? `: ${limit.declaration}` : ""}`,
99
+ next: limit.kind === "parse_failed"
100
+ ? "read the selected range directly"
101
+ : "read the complete source and check its syntax",
102
+ });
103
+ sections = sectionFile(args.path, file.text, declarations?.limits.some((limit) => limit.kind === "parse_failed")
104
+ ? undefined
105
+ : declarations?.declarations).sections;
106
+ }
107
+ else {
108
+ sections = file.windows.map((window, index) => ({
109
+ id: `S${index + 1}`,
110
+ ...window,
111
+ }));
112
+ limitations.push({
113
+ fact: "large file: window outline, not declarations",
114
+ next: "read the selected range directly",
115
+ });
116
+ }
117
+ const sectionCount = sections.length;
118
+ const judge = async (candidates, outline = false) => {
119
+ const state = sectionState(args.path, args.goal, candidates, outline);
120
+ const ask = async (reverse) => {
121
+ const result = await client.judge(state, {
122
+ pointer: pointerQuestion({
123
+ instructions: `Which section serves this goal: ${args.goal}? Choose none if no section fits. Judge the supplied text, not the label alone.`,
124
+ noneLabel: "No section serves the goal",
125
+ candidates,
126
+ reverse,
127
+ }),
128
+ }, {
129
+ signal,
130
+ ...runtime.session.requestGate(),
131
+ beforeRequest: (n) => runtime.session.admit(n),
132
+ onUsage: (usage) => runtime.session.recordUsage(usage),
133
+ });
134
+ results.push(result);
135
+ if (!result.ok)
136
+ return { ok: false, error: result.error };
137
+ const answer = result.answers.pointer;
138
+ return answer?.type === "choice"
139
+ ? { ok: true, probabilities: answer.probabilities }
140
+ : {
141
+ ok: false,
142
+ error: answer?.type === "unjudged"
143
+ ? answer.reason
144
+ : "Pointer answer missing.",
145
+ };
146
+ };
147
+ const first = await ask(false);
148
+ if (!first.ok)
149
+ return first;
150
+ let pointer = readPointer(first.probabilities);
151
+ if (needsReverse(first.probabilities, candidates.length)) {
152
+ const second = await ask(true);
153
+ if (!second.ok)
154
+ return second;
155
+ pointer = readPointer(first.probabilities, second.probabilities);
156
+ }
157
+ return { ok: true, ...pointer };
158
+ };
159
+ const fits = (candidates) => sectionStateFits(args.path, args.goal, candidates);
160
+ const twoStage = file.kind === "outline" || !fits(sections);
161
+ let planUnsure = false;
162
+ if (twoStage) {
163
+ const outline = sectionOutline(args.path, args.goal, sections);
164
+ if (outline.length < 2 || outline.length >= CHOICE_MAX_OPTIONS)
165
+ return finish({
166
+ refusal: "No usable multi-section plan fits the budget; use grep or read directly.",
167
+ });
168
+ const evidence = outlineEvidence(args.path, args.goal, outline);
169
+ if (!sectionStateFits(args.path, args.goal, evidence))
170
+ return finish({
171
+ refusal: "Block plan exceeds the state budget; use grep.",
172
+ });
173
+ if (outline.length < sections.length)
174
+ limitations.push({
175
+ fact: "adjacent sections grouped; a class may span outline blocks",
176
+ next: "read neighboring blocks when needed",
177
+ });
178
+ const plan = await judge(evidence);
179
+ if (!plan.ok)
180
+ return finish({ refusal: plan.error });
181
+ const head = plan.ranked[0];
182
+ if (!head)
183
+ return finish({ refusal: "Pointer probabilities missing." });
184
+ if (head.id === "none") {
185
+ const display = locateDisplay(args.path, outline, plan.ranked, false);
186
+ if (!display)
187
+ return finish({ refusal: "Pointer probabilities missing." });
188
+ return finish({
189
+ answers: [
190
+ {
191
+ ...display.primary,
192
+ band: display.band,
193
+ candidates: display.candidates,
194
+ orderDependent: plan.orderDependent,
195
+ },
196
+ ],
197
+ limitations: [
198
+ {
199
+ fact: "no block serves the goal",
200
+ next: display.band === "unsure"
201
+ ? "read the alternative block or search elsewhere"
202
+ : "search elsewhere",
203
+ },
204
+ ],
205
+ });
206
+ }
207
+ const block = outline.find((s) => s.id === head.id);
208
+ if (!block)
209
+ return finish({ refusal: "Pointer named an unknown block." });
210
+ planUnsure = head.p < LOCATE_VERDICT_MIN;
211
+ if (planUnsure) {
212
+ const alternative = outline.find((s) => s.id === plan.ranked[1]?.id);
213
+ limitations.push({
214
+ fact: `plan unsure: refined only the top block; also consider ${alternative ? `${args.path}:${alternative.start}-${alternative.end}` : "none"}`,
215
+ next: "read the other block if needed",
216
+ });
217
+ }
218
+ if (file.kind === "outline") {
219
+ const selected = await scanRange(cwd, args.path, block, signal, exec);
220
+ if (!selected.ok)
221
+ return finish({ refusal: selected.error });
222
+ const lines = selected.text.split("\n");
223
+ sections = sections
224
+ .filter((s) => s.start >= block.start && s.end <= block.end)
225
+ .map((section) => ({
226
+ ...section,
227
+ label: `lines ${section.start}-${section.end}`,
228
+ text: lines
229
+ .slice(section.start - block.start, section.end - block.start + 1)
230
+ .join("\n"),
231
+ }));
232
+ }
233
+ else
234
+ sections = sections.filter((s) => s.start >= block.start && s.end <= block.end);
235
+ if (sections.length < 2)
236
+ return finish({
237
+ refusal: "Selected block has fewer than two sections; use grep or read directly.",
238
+ });
239
+ }
240
+ if (!fits(sections))
241
+ return finish({
242
+ refusal: `Selected block exceeds ${STATE_MAX_CHARS} chars or the choice limit; read ${args.path}:${sections[0]?.start}-${sections.at(-1)?.end} directly.`,
243
+ });
244
+ let pointer = await judge(sections);
245
+ if (!pointer.ok)
246
+ return finish({ refusal: pointer.error });
247
+ let shrunk = false;
248
+ if ((pointer.ranked[0]?.p ?? 0) < LOCATE_GRAY_MIN) {
249
+ const ids = pointer.ranked
250
+ .filter((s) => s.id !== "none")
251
+ .slice(0, LOCATE_SHRINK_TOP)
252
+ .map((s) => s.id);
253
+ sections = ids.flatMap((id) => sections.filter((s) => s.id === id));
254
+ pointer = await judge(sections);
255
+ if (!pointer.ok)
256
+ return finish({ refusal: pointer.error });
257
+ shrunk = true;
258
+ if (twoStage)
259
+ limitations.push({
260
+ fact: "two-stage pointer: shrinking re-ask not measured for outlines",
261
+ next: "read the selected evidence",
262
+ });
263
+ limitations.push({
264
+ fact: "pointer below gray threshold: narrowed to top three plus none",
265
+ next: "read both candidates or search elsewhere",
266
+ });
267
+ }
268
+ const head = pointer.ranked[0];
269
+ if (!head)
270
+ return finish({ refusal: "Pointer probabilities missing." });
271
+ const displayed = locateDisplay(args.path, sections, pointer.ranked, shrunk || planUnsure);
272
+ if (!displayed)
273
+ return finish({ refusal: "Pointer probabilities missing." });
274
+ const { primary, band, candidates } = displayed;
275
+ if (band === "unsure" || head.id === "none")
276
+ limitations.push({
277
+ fact: head.id === "none"
278
+ ? "no section fits"
279
+ : "two sections share the mass",
280
+ next: head.id === "none" ? "search elsewhere" : "read both",
281
+ });
282
+ return finish({
283
+ answers: [
284
+ {
285
+ ...primary,
286
+ band,
287
+ orderDependent: pointer.orderDependent,
288
+ candidates,
289
+ },
290
+ ],
291
+ lines: head.id !== "none"
292
+ ? [{ type: "list", title: "read", items: [primary.label] }]
293
+ : [],
294
+ limitations: [
295
+ {
296
+ fact: `${file.lines} lines, ${sectionCount} sections`,
297
+ next: "open the cited line range",
298
+ },
299
+ ],
300
+ });
301
+ },
302
+ };
303
+ }