jev-agent-tools 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
package/src/jev/client.ts CHANGED
@@ -232,10 +232,19 @@ export function createJevClient(
232
232
  options.onUsage?.(next.usage);
233
233
  }
234
234
  };
235
- const missing = (ids: readonly string[], reason: string) => {
235
+ let stoppedCause: Extract<Answer, { type: "unjudged" }>["cause"];
236
+ const missing = (
237
+ ids: readonly string[],
238
+ reason: string,
239
+ cause: Extract<Answer, { type: "unjudged" }>["cause"] = stoppedCause,
240
+ ) => {
236
241
  const batchAnswers = Object.fromEntries(
237
- ids.map((id) => [id, { type: "unjudged" as const, reason }]),
242
+ ids.map((id) => [
243
+ id,
244
+ { type: "unjudged" as const, reason, ...(cause ? { cause } : {}) },
245
+ ]),
238
246
  );
247
+ if (cause) meta.failureCause ??= cause;
239
248
  for (const [id, answer] of Object.entries(batchAnswers))
240
249
  if (!witnessSet.has(id) || !answers[id]) answers[id] = answer;
241
250
  meta.batches?.push({ questionIds: [...ids], answers: batchAnswers });
@@ -281,6 +290,7 @@ export function createJevClient(
281
290
  // must not replace the token refusal with a budget reason.
282
291
  if (diagnostic) return;
283
292
  stopped = admission.error;
293
+ stoppedCause = options.admissionCause?.();
284
294
  missing(ids, stopped);
285
295
  return;
286
296
  }
@@ -322,12 +332,14 @@ export function createJevClient(
322
332
  if (diagnostic) {
323
333
  stopped =
324
334
  "Jev max_tokens_exceeded: state too large for a single question; remaining questions not judged.";
335
+ stoppedCause = "provider_context_refusal";
325
336
  return;
326
337
  }
327
338
  if (ids.length === 1) {
328
339
  missing(
329
340
  ids,
330
341
  "Jev max_tokens_exceeded: state too large for a single question; question not judged.",
342
+ "provider_context_refusal",
331
343
  );
332
344
  return;
333
345
  }
@@ -357,7 +369,7 @@ export function createJevClient(
357
369
  const split = splitGroups(groups);
358
370
  if (split.ok)
359
371
  await Promise.all(split.halves.map((half) => send(half)));
360
- else missing(ids, split.error);
372
+ else missing(ids, split.error, "provider_context_refusal");
361
373
  return;
362
374
  }
363
375
  if (response.ok && isRecord(body)) {
@@ -366,7 +378,12 @@ export function createJevClient(
366
378
  const batchAnswers = Object.fromEntries(
367
379
  ids.map((id) => [
368
380
  id,
369
- normalize(raw[id], selected[id] as Question),
381
+ (() => {
382
+ const answer = normalize(raw[id], selected[id] as Question);
383
+ return answer.type === "unjudged"
384
+ ? { ...answer, cause: "invalid_response" as const }
385
+ : { ...answer, source: "fresh" as const };
386
+ })(),
370
387
  ]),
371
388
  );
372
389
  meta.batches?.push({ questionIds: ids, answers: batchAnswers });
@@ -416,7 +433,7 @@ export function createJevClient(
416
433
  ) {
417
434
  const reason = `Jev HTTP 429, retry after ${retryAfter} s`;
418
435
  failure ??= reason;
419
- missing(ids, reason);
436
+ missing(ids, reason, "service_unavailable");
420
437
  return;
421
438
  }
422
439
  const reason = response.ok
@@ -433,7 +450,11 @@ export function createJevClient(
433
450
  failure ??= reason;
434
451
  if (response.status === 401 || response.status === 403)
435
452
  stopped = reason;
436
- missing(ids, reason);
453
+ missing(
454
+ ids,
455
+ reason,
456
+ response.ok ? "invalid_response" : "service_unavailable",
457
+ );
437
458
  return;
438
459
  }
439
460
  if (Number.isFinite(retryAfter) && retryAfter >= 0)
@@ -445,7 +466,11 @@ export function createJevClient(
445
466
  const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
446
467
  if (options.signal?.aborted || attempt === REQUEST_ATTEMPTS - 1) {
447
468
  failure ??= reason;
448
- missing(ids, reason);
469
+ missing(
470
+ ids,
471
+ reason,
472
+ options.signal?.aborted ? "cancelled" : "transport_failure",
473
+ );
449
474
  return;
450
475
  }
451
476
  } finally {
@@ -458,7 +483,11 @@ export function createJevClient(
458
483
  } catch (error) {
459
484
  const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
460
485
  failure ??= reason;
461
- missing(ids, reason);
486
+ missing(
487
+ ids,
488
+ reason,
489
+ options.signal?.aborted ? "cancelled" : "transport_failure",
490
+ );
462
491
  return;
463
492
  }
464
493
  }
@@ -473,13 +502,19 @@ export function createJevClient(
473
502
  )
474
503
  return true;
475
504
  for (const id of group) {
476
- answers[id] = structuredClone(
477
- cache.get(keys.get(id) as string) as Answer,
478
- );
505
+ answers[id] = {
506
+ ...structuredClone(cache.get(keys.get(id) as string) as Answer),
507
+ source: "cache",
508
+ };
479
509
  meta.cacheHits = (meta.cacheHits ?? 0) + 1;
480
510
  }
481
- const observedWitnesses = structuredClone(
482
- witnessCache.get(witnessKey(group)) ?? {},
511
+ const observedWitnesses = Object.fromEntries(
512
+ Object.entries(
513
+ structuredClone(witnessCache.get(witnessKey(group)) ?? {}),
514
+ ).map(([id, answer]) => [
515
+ id,
516
+ { ...answer, source: "cache" as const },
517
+ ]),
483
518
  );
484
519
  for (const [id, answer] of Object.entries(observedWitnesses)) {
485
520
  if (!answers[id]) {
@@ -495,9 +530,12 @@ export function createJevClient(
495
530
  id,
496
531
  witnessSet.has(id)
497
532
  ? (observedWitnesses[id] as Answer)
498
- : structuredClone(
499
- cache.get(keys.get(id) as string) as Answer,
500
- ),
533
+ : {
534
+ ...structuredClone(
535
+ cache.get(keys.get(id) as string) as Answer,
536
+ ),
537
+ source: "cache" as const,
538
+ },
501
539
  ]),
502
540
  ),
503
541
  });
package/src/jev/types.ts CHANGED
@@ -15,8 +15,19 @@ export type Question =
15
15
  }
16
16
  | { type: "choice"; instructions: string; criteria: Record<string, string> }
17
17
  | { type: "score"; instructions: string; criteria: string[] };
18
- export type Answer =
19
- | { type: "unjudged"; reason: string }
18
+ export type Answer = { source?: "fresh" | "cache" } & (
19
+ | {
20
+ type: "unjudged";
21
+ reason: string;
22
+ cause?:
23
+ | "call_budget"
24
+ | "session_budget"
25
+ | "provider_context_refusal"
26
+ | "invalid_response"
27
+ | "service_unavailable"
28
+ | "transport_failure"
29
+ | "cancelled";
30
+ }
20
31
  | { type: "bool"; p: number }
21
32
  | {
22
33
  type: "choice";
@@ -30,8 +41,10 @@ export type Answer =
30
41
  confidence: number;
31
42
  legend: Json;
32
43
  probabilities: Record<string, number>;
33
- };
44
+ }
45
+ );
34
46
  export interface JudgmentMetadata {
47
+ failureCause?: Extract<Answer, { type: "unjudged" }>["cause"];
35
48
  usage?: { inputTokens: number; costUsd: number };
36
49
  model?: string;
37
50
  calls?: number;
@@ -48,6 +61,8 @@ export interface JudgmentOptions {
48
61
  witnesses?: readonly string[];
49
62
  cache?: boolean;
50
63
  beforeRequest?: (questionCount: number) => Result<object>;
64
+ /** Structured cause of a denied beforeRequest admission, when established. */
65
+ admissionCause?: () => "call_budget" | "session_budget";
51
66
  /**
52
67
  * Awaited before each admission check. Resolves to a release function when
53
68
  * it reserved a slot (a session under a USD limit), which the client calls
@@ -24,6 +24,9 @@ export const INVALID_PARAMS = -32602;
24
24
  export const INTERNAL_ERROR = -32603;
25
25
  export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
26
26
 
27
+ /** Input-schema failure, distinct from semantic tool refusals and server faults. */
28
+ export class McpInvalidParams extends Error {}
29
+
27
30
  export interface McpContent {
28
31
  type: "text";
29
32
  text: string;
@@ -31,12 +34,14 @@ export interface McpContent {
31
34
  export interface McpCallResult {
32
35
  content: McpContent[];
33
36
  isError?: boolean;
37
+ structuredContent?: Record<string, unknown>;
34
38
  }
35
39
  export interface McpTool {
36
40
  name: string;
37
41
  title?: string;
38
42
  description: string;
39
43
  inputSchema: { type: "object"; [key: string]: unknown };
44
+ outputSchema?: { type: "object"; [key: string]: unknown };
40
45
  annotations?: {
41
46
  title?: string;
42
47
  readOnlyHint?: boolean;
@@ -78,6 +83,7 @@ export class McpServer {
78
83
  private readonly tools: Map<string, McpTool>;
79
84
  private readonly info: McpServerInfo;
80
85
  private readonly inFlight = new Map<Id, AbortController>();
86
+ private negotiatedVersion: string = LATEST_INITIALIZE_VERSION;
81
87
 
82
88
  constructor(info: McpServerInfo, tools: readonly McpTool[]) {
83
89
  this.info = info;
@@ -134,41 +140,60 @@ export class McpServer {
134
140
  requested,
135
141
  },
136
142
  );
143
+ const version =
144
+ typeof requested === "string"
145
+ ? requested
146
+ : method === "server/discover"
147
+ ? "2026-07-28"
148
+ : this.negotiatedVersion;
137
149
  try {
138
150
  switch (method) {
139
- case "initialize":
140
- return this.result(id, this.initialize(params));
151
+ case "initialize": {
152
+ const initialized = this.initialize(params);
153
+ return this.result(id, initialized, this.negotiatedVersion);
154
+ }
141
155
  case "server/discover":
142
- return this.result(id, {
143
- supportedVersions: [...SUPPORTED_VERSIONS],
144
- capabilities: { tools: { listChanged: false } },
145
- _meta: {
146
- "io.modelcontextprotocol/serverInfo": {
147
- name: this.info.name,
148
- version: this.info.version,
156
+ return this.result(
157
+ id,
158
+ {
159
+ supportedVersions: [...SUPPORTED_VERSIONS],
160
+ capabilities: { tools: { listChanged: false } },
161
+ _meta: {
162
+ "io.modelcontextprotocol/serverInfo": {
163
+ name: this.info.name,
164
+ version: this.info.version,
165
+ },
149
166
  },
167
+ ...(this.info.instructions
168
+ ? { instructions: this.info.instructions }
169
+ : {}),
170
+ ttlMs: 0,
171
+ cacheScope: "private",
150
172
  },
151
- ...(this.info.instructions
152
- ? { instructions: this.info.instructions }
153
- : {}),
154
- ttlMs: 0,
155
- cacheScope: "private",
156
- });
173
+ version,
174
+ );
157
175
  case "ping":
158
- return this.result(id, {});
176
+ return this.result(id, {}, version);
159
177
  case "tools/list":
160
- return this.result(id, {
161
- tools: [...this.tools.values()].map(
162
- ({ call: _call, ...tool }) => tool,
163
- ),
164
- // CacheableResult requires these from 2026-07-28; 0/private is
165
- // conservative (immediately stale, same authorization context)
166
- // and ignored by earlier clients via the open result shape.
167
- ttlMs: 0,
168
- cacheScope: "private",
169
- });
178
+ return this.result(
179
+ id,
180
+ {
181
+ tools: [...this.tools.values()].map(
182
+ ({ call: _call, outputSchema, ...tool }) => ({
183
+ ...tool,
184
+ ...(supportsStructuredResults(version) && outputSchema
185
+ ? { outputSchema }
186
+ : {}),
187
+ }),
188
+ ),
189
+ ...(version === "2026-07-28"
190
+ ? { ttlMs: 0, cacheScope: "private" }
191
+ : {}),
192
+ },
193
+ version,
194
+ );
170
195
  case "tools/call":
171
- return await this.callTool(id, params);
196
+ return await this.callTool(id, params, version);
172
197
  default:
173
198
  return this.error(
174
199
  id,
@@ -194,6 +219,7 @@ export class McpServer {
194
219
  (SUPPORTED_VERSIONS as readonly string[]).includes(requested)
195
220
  ? requested
196
221
  : LATEST_INITIALIZE_VERSION;
222
+ this.negotiatedVersion = protocolVersion;
197
223
  return {
198
224
  protocolVersion,
199
225
  capabilities: { tools: { listChanged: false } },
@@ -215,6 +241,7 @@ export class McpServer {
215
241
  private async callTool(
216
242
  id: Id,
217
243
  params: Record<string, unknown>,
244
+ version: string,
218
245
  ): Promise<JsonRpcResponse | undefined> {
219
246
  const tool =
220
247
  typeof params.name === "string" ? this.tools.get(params.name) : undefined;
@@ -240,30 +267,45 @@ export class McpServer {
240
267
  try {
241
268
  const result = await tool.call(params.arguments ?? {}, controller.signal);
242
269
  if (cancelled()) return undefined;
243
- return this.result(id, { ...result });
270
+ const { structuredContent, ...content } = result;
271
+ return this.result(
272
+ id,
273
+ {
274
+ ...content,
275
+ ...(supportsStructuredResults(version) && structuredContent
276
+ ? { structuredContent }
277
+ : {}),
278
+ },
279
+ version,
280
+ );
244
281
  } catch (error) {
245
282
  if (cancelled()) return undefined;
246
- // Tool execution failures are results the model can read, not protocol errors.
247
- return this.result(id, {
248
- content: [
249
- {
250
- type: "text",
251
- text: `${tool.name} failed: ${error instanceof Error ? error.message : String(error)}`,
252
- },
253
- ],
254
- isError: true,
255
- });
283
+ if (error instanceof McpInvalidParams)
284
+ return this.error(id, INVALID_PARAMS, error.message);
285
+ // Expected tool refusals already carry a report. An unexpected server
286
+ // fault has no trustworthy report/accounting; do not fabricate one.
287
+ return this.error(
288
+ id,
289
+ INTERNAL_ERROR,
290
+ `${tool.name} failed: ${error instanceof Error ? error.message : String(error)}`,
291
+ );
256
292
  } finally {
257
293
  this.inFlight.delete(id);
258
294
  }
259
295
  }
260
296
 
261
- private result(id: Id, result: Record<string, unknown>): JsonRpcResponse {
262
- // resultType is required from 2026-07-28 and ignored by earlier clients.
297
+ private result(
298
+ id: Id,
299
+ result: Record<string, unknown>,
300
+ version: string,
301
+ ): JsonRpcResponse {
263
302
  return {
264
303
  jsonrpc: "2.0",
265
304
  id,
266
- result: { resultType: "complete", ...result },
305
+ result: {
306
+ ...(version === "2026-07-28" ? { resultType: "complete" } : {}),
307
+ ...result,
308
+ },
267
309
  };
268
310
  }
269
311
 
@@ -280,3 +322,11 @@ export class McpServer {
280
322
  };
281
323
  }
282
324
  }
325
+
326
+ function supportsStructuredResults(version: string): boolean {
327
+ return (
328
+ version === "2025-06-18" ||
329
+ version === "2025-11-25" ||
330
+ version === "2026-07-28"
331
+ );
332
+ }
package/src/mcp/tools.ts CHANGED
@@ -4,18 +4,21 @@ import { spawnExec } from "../adapters/exec.ts";
4
4
  import { ConfigController } from "../configuration.ts";
5
5
  import { MCP_VALIDATION_MAX_ERRORS } from "../constants.ts";
6
6
  import type { GitExec } from "../core/git.ts";
7
+ import { type ResultReportV1, resultIsError } from "../core/result-report.ts";
7
8
  import { Guide } from "../guide.ts";
8
9
  import { mcpHost } from "../host.ts";
9
10
  import type { JevClient } from "../jev/types.ts";
11
+ import { mcpStructuredResultSchema } from "../report-schema.ts";
10
12
  import type { ToolDependencies } from "../runtime.ts";
11
13
  import { readSessionLimits, Session } from "../session.ts";
14
+ import { renderAgentInstructions } from "../texts/instructions.ts";
12
15
  import { createAskTool } from "../tools/ask.ts";
13
16
  import { createAskFilesTool } from "../tools/ask-files.ts";
14
17
  import { createCheckDiffTool } from "../tools/check-diff.ts";
15
18
  import { createFindFilesTool } from "../tools/find.ts";
16
19
  import { createLocateTool } from "../tools/locate.ts";
17
20
  import { createSelectTestsTool } from "../tools/select-tests.ts";
18
- import type { McpTool } from "./protocol.ts";
21
+ import { McpInvalidParams, type McpTool } from "./protocol.ts";
19
22
 
20
23
  /** The shape every tool factory already returns for pi/omp. */
21
24
  interface HarnessTool {
@@ -30,7 +33,10 @@ interface HarnessTool {
30
33
  signal: AbortSignal | undefined,
31
34
  update: unknown,
32
35
  ctx: { cwd: string },
33
- ): Promise<{ content: { type: "text"; text: string }[] }>;
36
+ ): Promise<{
37
+ content: { type: "text"; text: string }[];
38
+ details: { result: ResultReportV1 };
39
+ }>;
34
40
  }
35
41
 
36
42
  export interface McpToolOptions {
@@ -63,22 +69,26 @@ function validationError(schema: TSchema, value: unknown): string | undefined {
63
69
  export async function loadMcpClient(
64
70
  env: NodeJS.ProcessEnv,
65
71
  configDirectory?: string,
66
- ): Promise<{ client?: JevClient; warning?: string }> {
72
+ ): Promise<{ client?: JevClient; apiKey?: string; warning?: string }> {
67
73
  try {
68
74
  const controller = new ConfigController({
69
75
  env,
70
76
  ...(configDirectory ? { directory: configDirectory } : {}),
71
77
  });
78
+ const configured = () =>
79
+ controller.client
80
+ ? { client: controller.client, apiKey: controller.values().apiKey }
81
+ : {};
72
82
  try {
73
83
  await controller.initialize({});
74
84
  } catch (error) {
75
85
  // Saved storage unusable: environment configuration (if any) still applies.
76
86
  return {
77
- ...(controller.client ? { client: controller.client } : {}),
87
+ ...configured(),
78
88
  warning: error instanceof Error ? error.message : String(error),
79
89
  };
80
90
  }
81
- return controller.client ? { client: controller.client } : {};
91
+ return configured();
82
92
  } catch (error) {
83
93
  return { warning: error instanceof Error ? error.message : String(error) };
84
94
  }
@@ -97,12 +107,14 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
97
107
  const env = options.env ?? process.env;
98
108
  const host = mcpHost();
99
109
  const loaded = options.client
100
- ? { client: options.client }
110
+ ? { client: options.client, apiKey: env.JEV_TOOLS_API_KEY }
101
111
  : await loadMcpClient(env, options.configDirectory);
102
112
  const client = loaded.client;
103
113
  const dependencies: ToolDependencies = {
104
114
  client,
115
+ apiKey: loaded.apiKey,
105
116
  host,
117
+ evidenceOrigin: "server",
106
118
  runtime: {
107
119
  session: new Session(readSessionLimits(env)),
108
120
  guide: new Guide(host),
@@ -138,10 +150,10 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
138
150
  // Evidence is sent to the configured judgment endpoint.
139
151
  openWorldHint: true,
140
152
  },
153
+ outputSchema: mcpStructuredResultSchema as McpTool["inputSchema"],
141
154
  async call(args, signal) {
142
155
  const invalid = validationError(tool.parameters, args);
143
- if (invalid)
144
- return { content: [{ type: "text", text: invalid }], isError: true };
156
+ if (invalid) throw new McpInvalidParams(invalid);
145
157
  const result = await tool.execute(
146
158
  `mcp-${++id}`,
147
159
  args as never,
@@ -149,14 +161,15 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
149
161
  undefined,
150
162
  { cwd: options.root },
151
163
  );
152
- return { content: result.content };
164
+ return {
165
+ content: result.content,
166
+ structuredContent: { result: result.details.result },
167
+ ...(resultIsError(result.details.result) ? { isError: true } : {}),
168
+ };
153
169
  },
154
170
  };
155
171
  });
156
- const guidelines = harness.flatMap((tool) => tool.promptGuidelines ?? []);
157
- const instructions = [dependencies.runtime.guide.text, ...guidelines].join(
158
- "\n\n",
159
- );
172
+ const instructions = renderAgentInstructions("mcp", host.names);
160
173
  return {
161
174
  tools,
162
175
  instructions,
package/src/render.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Envelope, OutputLine } from "./core/output.ts";
2
+ import type { Knowledge, ResultReportV1, Scope } from "./core/result-report.ts";
2
3
 
3
4
  function renderLine(line: OutputLine): string {
4
5
  switch (line.type) {
@@ -43,3 +44,111 @@ function renderLine(line: OutputLine): string {
43
44
  export function renderEnvelope(envelope: Envelope): string {
44
45
  return envelope.lines.map(renderLine).join("\n");
45
46
  }
47
+
48
+ function knowledge<T>(
49
+ value: Knowledge<T>,
50
+ format: (value: T) => string = String,
51
+ ): string {
52
+ return value.status === "known"
53
+ ? format(value.value)
54
+ : `${value.status} (${value.reason})`;
55
+ }
56
+ function scopeLabel(scope: Scope): string {
57
+ return scope.kind === "call"
58
+ ? "call"
59
+ : scope.kind === "item"
60
+ ? `items ${scope.itemIds.join(", ")}`
61
+ : scope.kind === "group"
62
+ ? `groups ${scope.groupIds.join(", ")}`
63
+ : `inventory ${scope.inventoryIds.join(", ")}`;
64
+ }
65
+
66
+ /** Human projection uses the same typed facts as structured transport, never prose inference. */
67
+ export function renderResultReport(
68
+ report: ResultReportV1,
69
+ options: { details?: Envelope } = {},
70
+ ): string {
71
+ const { context: c, accounting: a } = report;
72
+ const counts = a.requestedResults;
73
+ const mode =
74
+ counts.fresh + counts.cache
75
+ ? `${counts.fresh} fresh / ${counts.cache} cache Jev results`
76
+ : counts.static
77
+ ? "static selection; no Jev judgment"
78
+ : report.items.some((item) => item.selection?.selected)
79
+ ? "conservative fallback selection; no Jev judgment"
80
+ : "no admissible judgment";
81
+ const lines = [
82
+ `${report.execution} — ${mode}${counts.notJudged ? `; ${counts.notJudged} not judged` : ""}`,
83
+ `context: authority ${knowledge(c.authority, (v) => `${v.path} (${v.origin})`)} · requested root ${knowledge(c.requestedRoot)} · effective root ${knowledge(c.effectiveRoot, (v) => `${v.path} (${v.origin}); common-dir ${knowledge(v.commonDir)}`)} · base ${knowledge(c.requestedBase)} → ${knowledge(c.resolvedBase)}`,
84
+ ];
85
+ for (const inv of c.inventories) {
86
+ lines.push(
87
+ `inventory ${inv.id} (${inv.kind}): discovered ${knowledge(inv.discovered)} · considered ${knowledge(inv.considered)} · scopeRestricted ${inv.scopeRestricted} · rules ${inv.rules.join("; ")} · restrictions ${inv.restrictions.join("; ") || "none"}`,
88
+ );
89
+ for (const criterion of inv.criteria)
90
+ lines.push(
91
+ `criterion ${JSON.stringify(criterion.criterion)}: ${criterion.outcome}; matches ${knowledge(criterion.matches)}`,
92
+ );
93
+ }
94
+ if (c.command.execution !== "not_requested")
95
+ lines.push(
96
+ `command: ${c.command.execution} · cwd ${knowledge(c.command.cwd)} · exit ${knowledge(c.command.exitCode)} · timedOut ${knowledge(c.command.timedOut)}`,
97
+ );
98
+ for (const item of report.items) {
99
+ if (item.treatment === "judged") {
100
+ const j = item.judgment;
101
+ lines.push(
102
+ `${j.band === "verdict" ? "" : `${j.band} `}${j.uncalibrated ? "uncalibrated " : ""}${item.label} = ${String(j.result)} (${j.measure.kind} ${knowledge(j.measure.value)}) [${item.source}]${j.reason ? ` — ${j.reason}` : ""}`,
103
+ );
104
+ for (const raw of j.rawValues)
105
+ lines.push(
106
+ ` raw ${raw.label} = ${String(raw.value)}; probability ${knowledge(raw.probability)}`,
107
+ );
108
+ for (const control of j.controls) {
109
+ lines.push(
110
+ ` control ${control.id} (${control.kind}, ${control.source}): ${control.outcome}`,
111
+ );
112
+ for (const raw of control.rawValues)
113
+ lines.push(
114
+ ` raw ${raw.label} = ${String(raw.value)}; probability ${knowledge(raw.probability)}`,
115
+ );
116
+ }
117
+ } else
118
+ lines.push(
119
+ `${item.treatment === "static" ? "static" : "unjudged"} ${item.label}${item.treatment === "static" ? ` — ${item.staticReason}` : ""}`,
120
+ );
121
+ for (const evidence of item.evidence)
122
+ lines.push(
123
+ ` evidence ${evidence.target} (${evidence.side}): ${knowledge(evidence.canonicalPath)} · aliases ${evidence.aliases.join(", ") || "none"} · revision ${knowledge(evidence.revision)}`,
124
+ );
125
+ if (item.selection)
126
+ lines.push(
127
+ ` selection: ${item.selection.selected ? "selected" : "not selected"} — ${item.selection.reason}`,
128
+ );
129
+ if (item.diagnosticIds.length)
130
+ lines.push(` diagnostics ${item.diagnosticIds.join(", ")}`);
131
+ if (item.actionIds.length)
132
+ lines.push(` actions ${item.actionIds.join(", ")}`);
133
+ }
134
+ // Complete diagnostic lists keep legacy text-only clients self-contained.
135
+ for (const d of report.diagnostics)
136
+ lines.push(
137
+ `${d.material ? "material " : ""}${d.effect} ${d.id} — ${d.cause}: ${d.fact} · target ${knowledge(d.target)} · origin ${d.origin} · scope ${scopeLabel(d.scope)} · members ${knowledge(d.memberCount)}${d.omittedMembers.length ? ` · omitted ${d.omittedMembers.join(", ")}` : ""} · actions ${d.actionIds.join(", ") || "none"}`,
138
+ );
139
+ for (const action of report.actions)
140
+ lines.push(
141
+ `next ${action.id} (${action.code}; ${scopeLabel(action.scope)}; target ${knowledge(action.target)}): ${action.condition} — ${action.instruction}`,
142
+ );
143
+ if (options.details) {
144
+ const detailLines = options.details.lines.filter(
145
+ (line) => line.type === "command" || line.type === "list",
146
+ );
147
+ if (detailLines.length)
148
+ lines.push("details:", renderEnvelope({ lines: detailLines }));
149
+ }
150
+ lines.push(
151
+ `HTTP attempts ${a.httpAttempts} · questions sent ${a.questionsSent} · requested results total ${knowledge(counts.total)} / fresh ${counts.fresh} / cache ${counts.cache} / not judged ${counts.notJudged} / static ${counts.static} · cache probes ${a.cacheHits}/${a.cacheRequests} · auxiliary controls fresh ${a.auxiliary.controls.fresh} / cache ${a.auxiliary.controls.cache} / not judged ${a.auxiliary.controls.notJudged} / static ${a.auxiliary.controls.static} · passages fresh ${a.auxiliary.passages.fresh} / cache ${a.auxiliary.passages.cache} / not judged ${a.auxiliary.passages.notJudged} · current cost USD ${knowledge(a.costUsd)} · elapsed ${a.elapsedMs} ms`,
152
+ );
153
+ return lines.join("\n");
154
+ }