jev-agent-tools 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
@@ -3,9 +3,13 @@ import { resolve } from "node:path";
3
3
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
4
  import { type Static, Type } from "@sinclair/typebox";
5
5
  import { collectAskFiles } from "../adapters/ask-files.ts";
6
- import { canonicalPath } from "../adapters/canonical-path.ts";
6
+ import {
7
+ resolveEvidenceContext,
8
+ withEvidenceContext,
9
+ } from "../adapters/evidence-context.ts";
7
10
  import { shareGitInventory } from "../adapters/git-inventory.ts";
8
11
  import { hostUsage } from "../adapters/usage.ts";
12
+ import { STATE_MAX_CHARS } from "../constants.ts";
9
13
  import { compileAsks, readAsks, reverseQuestions } from "../core/asks.ts";
10
14
  import { checkIntegrity } from "../core/integrity.ts";
11
15
  import type {
@@ -15,10 +19,22 @@ import type {
15
19
  EnvelopeInput,
16
20
  } from "../core/output.ts";
17
21
  import { buildEnvelope } from "../core/output.ts";
22
+ import {
23
+ type Action,
24
+ answerItemFromInput,
25
+ buildResultReport,
26
+ type Cause,
27
+ contextFromEvidence,
28
+ type Diagnostic,
29
+ type Item,
30
+ known,
31
+ type ResultReportV1,
32
+ unknown,
33
+ } from "../core/result-report.ts";
18
34
  import { interpolate } from "../describe.ts";
19
35
  import type { GuideContext } from "../guide.ts";
20
36
  import type { Answer, Judgment, JudgmentMetadata } from "../jev/types.ts";
21
- import { renderEnvelope } from "../render.ts";
37
+ import { renderResultReport } from "../render.ts";
22
38
  import type { ToolDependencies } from "../runtime.ts";
23
39
  import { ASK_FILES_DESCRIPTION } from "../texts/ask-files.ts";
24
40
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
@@ -26,6 +42,7 @@ import { asksParameter } from "./ask-schema.ts";
26
42
 
27
43
  export const askFilesParameters = Type.Object(
28
44
  {
45
+ root: Type.Optional(Type.String()),
29
46
  paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
30
47
  asks: asksParameter("files"),
31
48
  max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -42,6 +59,7 @@ export interface AskFilesDetails {
42
59
  files: FileJudgment[];
43
60
  skipped: string[];
44
61
  envelope: Envelope;
62
+ result: ResultReportV1;
45
63
  }
46
64
  export function createAskFilesTool(dependencies: ToolDependencies) {
47
65
  const { host, runtime, exec: execute } = dependencies;
@@ -68,9 +86,74 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
68
86
  ctx: { cwd: string } & GuideContext,
69
87
  ) {
70
88
  const client = dependencies.client;
71
- const cwd = await canonicalPath(ctx.cwd);
89
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
90
+ exec: execute,
91
+ signal,
92
+ origin: dependencies.evidenceOrigin,
93
+ });
94
+ const evidenceContext = evidence.context;
95
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
72
96
  const started = performance.now();
73
97
  const exec = shareGitInventory(execute);
98
+ const plan = compileAsks(args.asks, { surface: "files" });
99
+ let refusalCause: Cause = "invalid_arguments";
100
+ let collectedPaths: string[] | undefined;
101
+ let costKnown = true;
102
+ const reportItems: Item[] = [];
103
+ const diagnostics: Diagnostic[] = [];
104
+ const actions: Action[] = [];
105
+ const auxiliary = {
106
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
107
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
108
+ };
109
+ const diagnose = (
110
+ cause: Cause,
111
+ fact: string,
112
+ scope: Diagnostic["scope"],
113
+ target?: string,
114
+ ) => {
115
+ const id = `diagnostic:${diagnostics.length}`,
116
+ actionId = `action:${actions.length}`;
117
+ actions.push({
118
+ id: actionId,
119
+ code:
120
+ cause === "invalid_root"
121
+ ? "correct_context"
122
+ : cause === "not_configured"
123
+ ? "configure_client"
124
+ : "inspect_native",
125
+ target:
126
+ target === undefined
127
+ ? unknown("target not established")
128
+ : known(target),
129
+ scope,
130
+ condition:
131
+ "Only after correcting context or supplying materially new evidence",
132
+ instruction:
133
+ "Read the admitted files natively; do not repeat the unchanged Jev call.",
134
+ repeatUnchanged: false,
135
+ });
136
+ diagnostics.push({
137
+ id,
138
+ cause,
139
+ fact,
140
+ target:
141
+ target === undefined
142
+ ? unknown("target not established")
143
+ : known(target),
144
+ origin:
145
+ cause === "call_budget" || cause === "session_budget"
146
+ ? "budget"
147
+ : "collection",
148
+ scope,
149
+ effect: "blocking",
150
+ material: true,
151
+ omittedMembers: [],
152
+ memberCount: known(0),
153
+ actionIds: [actionId],
154
+ });
155
+ return { diagnosticIds: [id], actionIds: [actionId] };
156
+ };
74
157
  const totals = {
75
158
  calls: 0,
76
159
  questions: 0,
@@ -88,31 +171,156 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
88
171
  });
89
172
  runtime.session.record(envelope);
90
173
  runtime.guide.deliver(ctx);
174
+ if (!reportItems.length && !diagnostics.length)
175
+ diagnose(
176
+ input.refusal ? refusalCause : "collection_empty",
177
+ input.refusal ?? "No file-question results collected",
178
+ { kind: "call" },
179
+ );
180
+ if (!reportItems.length && collectedPaths && plan.ok)
181
+ for (const path of collectedPaths)
182
+ for (const reading of plan.readings)
183
+ reportItems.push({
184
+ id: `${path}:${reading.id}`,
185
+ kind: "file_question",
186
+ label: `${path} ${reading.label}`,
187
+ groupId: `${path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`,
188
+ evidence: [
189
+ {
190
+ target: path,
191
+ canonicalPath: known(resolve(cwd, path)),
192
+ aliases: [],
193
+ side: "current",
194
+ revision: unknown("working tree evidence"),
195
+ },
196
+ ],
197
+ diagnosticIds: diagnostics.map((diagnostic) => diagnostic.id),
198
+ actionIds: actions.map((action) => action.id),
199
+ treatment: "not_judged",
200
+ source: "none",
201
+ });
202
+ for (const limitation of input.limitations ?? []) {
203
+ diagnose(
204
+ "evidence_limit",
205
+ limitation.fact,
206
+ { kind: "call" },
207
+ limitation.path,
208
+ );
209
+ const diagnostic = diagnostics.at(-1);
210
+ if (diagnostic) diagnostic.effect = "reservation";
211
+ }
212
+ const report = buildResultReport({
213
+ tool: "jev_ask_files",
214
+ context: contextFromEvidence(evidenceContext, {
215
+ inventories: collectedPaths
216
+ ? [
217
+ {
218
+ id: "files",
219
+ kind: "repository",
220
+ rules: [
221
+ "Requested paths expanded with admission and pruning",
222
+ ],
223
+ restrictions: args.paths,
224
+ discovered: unknown(
225
+ "collector does not expose pre-pruning candidate count",
226
+ ),
227
+ considered: known(collectedPaths.length),
228
+ scopeRestricted: true,
229
+ criteria: [],
230
+ },
231
+ ]
232
+ : [],
233
+ }),
234
+ items: reportItems,
235
+ diagnostics,
236
+ actions,
237
+ metrics: {
238
+ ...totals,
239
+ costUsd: costKnown && collectedPaths ? totals.costUsd : undefined,
240
+ elapsedMs: performance.now() - started,
241
+ },
242
+ auxiliary,
243
+ total:
244
+ collectedPaths && plan.ok
245
+ ? known(collectedPaths.length * plan.readings.length)
246
+ : unknown("file-question inventory not collected"),
247
+ refused: !!input.refusal && refusalCause !== "not_configured",
248
+ });
91
249
  const usage = hostUsage(host.isOmp, {
92
250
  inputTokens: totals.inputTokens,
93
251
  costUsd: totals.costUsd,
94
252
  });
95
253
  return {
96
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
97
- details: { files: records, skipped, envelope },
254
+ content: [
255
+ {
256
+ type: "text" as const,
257
+ text: renderResultReport(report, { details: envelope }),
258
+ },
259
+ ],
260
+ details: {
261
+ files: records,
262
+ skipped,
263
+ envelope,
264
+ evidenceContext,
265
+ result: report,
266
+ },
98
267
  ...usage,
99
268
  };
100
269
  };
101
- if (!client) return finish({ refusal: NOT_CONFIGURED });
102
- const plan = compileAsks(args.asks, { surface: "files" });
270
+ if (!evidence.ok) {
271
+ refusalCause = evidence.cause ?? "invalid_root";
272
+ return finish({ refusal: evidence.error });
273
+ }
103
274
  if (!plan.ok) return finish({ refusal: plan.error });
275
+ if (!client) {
276
+ refusalCause = "not_configured";
277
+ return finish({ refusal: NOT_CONFIGURED });
278
+ }
104
279
  const collected = await collectAskFiles(cwd, args.paths, signal, exec);
105
- if (!collected.ok) return finish({ refusal: collected.error });
280
+ if (!collected.ok) {
281
+ refusalCause = collected.cause ?? "file_unavailable";
282
+ return finish({ refusal: collected.error });
283
+ }
284
+ collectedPaths = collected.files.map((file) => file.path);
106
285
  skipped = collected.skipped;
286
+ for (const skippedPath of skipped)
287
+ diagnose(
288
+ collected.secret.includes(skippedPath)
289
+ ? "secret_pattern"
290
+ : "collection_omitted",
291
+ skippedPath,
292
+ { kind: "inventory", inventoryIds: ["files"] },
293
+ );
107
294
  if (!collected.files.length && skipped.length)
108
295
  return finish({
109
296
  refusal: `No readable evidence: ${skipped.join("; ")}`,
110
297
  });
298
+ const states = collected.files.map((file) => ({
299
+ file,
300
+ state: withEvidenceContext(
301
+ { path: file.path, content: file.content },
302
+ evidenceContext,
303
+ ),
304
+ }));
305
+ if (
306
+ states.some(
307
+ ({ state }) => JSON.stringify(state).length > STATE_MAX_CHARS,
308
+ )
309
+ ) {
310
+ refusalCause = "evidence_too_large";
311
+ return finish({
312
+ refusal: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
313
+ });
314
+ }
111
315
  let sent = 0;
112
316
  let budget: BudgetRefusal | undefined;
113
317
  const options = {
114
318
  signal,
115
319
  ...runtime.session.requestGate(),
320
+ admissionCause: () =>
321
+ budget?.kind === "session"
322
+ ? ("session_budget" as const)
323
+ : ("call_budget" as const),
116
324
  beforeRequest: (count: number) => {
117
325
  if (args.max_calls !== undefined && sent >= args.max_calls) {
118
326
  budget = {
@@ -139,17 +347,17 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
139
347
  totals.cacheRequests += result.cacheRequests ?? 0;
140
348
  totals.inputTokens += result.usage?.inputTokens ?? 0;
141
349
  totals.costUsd += result.usage?.costUsd ?? 0;
350
+ if (!result.usage && result.calls !== 0) costKnown = false;
142
351
  };
143
352
  const rows = await Promise.all(
144
- collected.files.map(async (file) => {
145
- const state = { path: file.path, content: file.content };
353
+ states.map(async ({ file, state }) => {
146
354
  const integrity = checkIntegrity(state, plan.asks, [
147
355
  {
148
356
  ...file.identity,
149
- insertedPath: resolve(cwd, state.path),
150
- content: state.content,
357
+ insertedPath: resolve(cwd, file.path),
358
+ content: file.content,
151
359
  insertedSha256: createHash("sha256")
152
- .update(state.content)
360
+ .update(file.content)
153
361
  .digest("hex"),
154
362
  },
155
363
  ]);
@@ -191,6 +399,93 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
191
399
  const unchecked: string[] = [];
192
400
  const unjudged: NonNullable<EnvelopeInput["unjudged"]>[number][] = [];
193
401
  for (const row of rows) {
402
+ const readings = plan.readings;
403
+ for (const [index, reading] of readings.entries()) {
404
+ const groupId = `${row.file.path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`;
405
+ const common = {
406
+ id: `${row.file.path}:${reading.id}`,
407
+ kind: "file_question" as const,
408
+ label: `${row.file.path} ${reading.label}`,
409
+ groupId,
410
+ evidence: [
411
+ {
412
+ target: row.file.path,
413
+ canonicalPath: known(resolve(cwd, row.file.path)),
414
+ aliases: [],
415
+ side: "current" as const,
416
+ revision: unknown("working tree evidence"),
417
+ },
418
+ ],
419
+ diagnosticIds: [] as string[],
420
+ actionIds: [] as string[],
421
+ };
422
+ const answer = row.answers[index];
423
+ if (
424
+ answer &&
425
+ !answer.unjudged &&
426
+ (answer.source || answer.answer?.source)
427
+ )
428
+ reportItems.push(
429
+ answerItemFromInput(
430
+ common,
431
+ row.integrity.controls.length
432
+ ? {
433
+ ...answer,
434
+ band: "unsure",
435
+ reason: row.integrity.controls
436
+ .map((control) => control.fact)
437
+ .join("; "),
438
+ }
439
+ : answer,
440
+ ),
441
+ );
442
+ else {
443
+ const raw = row.initial.ok
444
+ ? row.initial.answers[reading.id]
445
+ : undefined;
446
+ const cause =
447
+ raw?.type === "unjudged" ? raw.cause : row.initial.failureCause;
448
+ const links = diagnose(
449
+ cause ??
450
+ (budget
451
+ ? budget.kind === "session"
452
+ ? "session_budget"
453
+ : "call_budget"
454
+ : "control_failure"),
455
+ answer?.reason ??
456
+ (row.initial.ok
457
+ ? "Required group incomplete or response provenance not established"
458
+ : row.initial.error),
459
+ { kind: "group", groupIds: [groupId] },
460
+ row.file.path,
461
+ );
462
+ reportItems.push({
463
+ ...common,
464
+ ...links,
465
+ treatment: "not_judged",
466
+ source: "none",
467
+ });
468
+ }
469
+ for (const id of [
470
+ reading.twin,
471
+ ...Object.values(reading.controls ?? {}),
472
+ ].filter((id): id is string => !!id)) {
473
+ const control = row.initial.ok
474
+ ? row.initial.answers[id]
475
+ : undefined;
476
+ if (control && control.type !== "unjudged" && control.source)
477
+ auxiliary.controls[control.source]++;
478
+ else auxiliary.controls.notJudged++;
479
+ }
480
+ const reverse = row.reversed?.ok
481
+ ? row.reversed.answers[reading.id]
482
+ : undefined;
483
+ if (row.reversed) {
484
+ if (reverse && reverse.type !== "unjudged" && reverse.source)
485
+ auxiliary.controls[reverse.source]++;
486
+ else auxiliary.controls.notJudged++;
487
+ }
488
+ }
194
489
  records.push({
195
490
  path: row.file.path,
196
491
  initial: row.initial,
@@ -201,7 +496,7 @@ export function createAskFilesTool(dependencies: ToolDependencies) {
201
496
  unjudged.push({
202
497
  label: row.file.path,
203
498
  reason: row.initial.error,
204
- next: "read the file or retry",
499
+ next: "read the supplied file natively; a new Jev call needs materially changed evidence or recovered service",
205
500
  });
206
501
  continue;
207
502
  }