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.
- package/CHANGELOG.md +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { askBand } from "./output.js";
|
|
2
|
+
export const known = (value) => ({
|
|
3
|
+
status: "known",
|
|
4
|
+
value,
|
|
5
|
+
});
|
|
6
|
+
export const unknown = (reason) => ({
|
|
7
|
+
status: "unknown",
|
|
8
|
+
reason,
|
|
9
|
+
});
|
|
10
|
+
export const notApplicable = (reason) => ({
|
|
11
|
+
status: "not_applicable",
|
|
12
|
+
reason,
|
|
13
|
+
});
|
|
14
|
+
export const notCollected = (reason) => ({
|
|
15
|
+
status: "not_collected",
|
|
16
|
+
reason,
|
|
17
|
+
});
|
|
18
|
+
export function uncollectedContext(input) {
|
|
19
|
+
return {
|
|
20
|
+
authority: input.authority,
|
|
21
|
+
requestedRoot: input.requestedRoot ?? notApplicable("no override requested"),
|
|
22
|
+
effectiveRoot: unknown("effective root not established"),
|
|
23
|
+
requestedBase: input.requestedBase ?? notApplicable("no base requested"),
|
|
24
|
+
resolvedBase: input.requestedBase
|
|
25
|
+
? notCollected("base resolution not performed")
|
|
26
|
+
: notApplicable("no base requested"),
|
|
27
|
+
inventories: [],
|
|
28
|
+
command: input.command ?? {
|
|
29
|
+
execution: "not_requested",
|
|
30
|
+
cwd: notApplicable("no command"),
|
|
31
|
+
exitCode: notApplicable("no command"),
|
|
32
|
+
timedOut: notApplicable("no command"),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function contextFromEvidence(evidence, options = {}) {
|
|
37
|
+
const context = uncollectedContext({
|
|
38
|
+
authority: evidence?.authorityReason
|
|
39
|
+
? unknown(evidence.authorityReason)
|
|
40
|
+
: evidence?.authority
|
|
41
|
+
? known(evidence.authority)
|
|
42
|
+
: unknown("canonical authority not established"),
|
|
43
|
+
...(evidence?.requestedRoot !== undefined
|
|
44
|
+
? { requestedRoot: known(evidence.requestedRoot) }
|
|
45
|
+
: {}),
|
|
46
|
+
...(evidence?.requestedBase !== undefined
|
|
47
|
+
? { requestedBase: known(evidence.requestedBase) }
|
|
48
|
+
: {}),
|
|
49
|
+
...(options.command ? { command: options.command } : {}),
|
|
50
|
+
});
|
|
51
|
+
if (evidence?.effectiveRoot)
|
|
52
|
+
context.effectiveRoot = known({
|
|
53
|
+
path: evidence.effectiveRoot.path,
|
|
54
|
+
origin: evidence.effectiveRoot.origin,
|
|
55
|
+
commonDir: evidence.effectiveRoot.commonDir === undefined
|
|
56
|
+
? unknown("Git common directory not collected")
|
|
57
|
+
: known(evidence.effectiveRoot.commonDir),
|
|
58
|
+
});
|
|
59
|
+
if (evidence?.resolvedBase !== undefined)
|
|
60
|
+
context.resolvedBase = known(evidence.resolvedBase);
|
|
61
|
+
else if (evidence?.requestedBase !== undefined)
|
|
62
|
+
context.resolvedBase = unknown("requested base not resolved");
|
|
63
|
+
context.inventories = options.inventories ?? [];
|
|
64
|
+
return context;
|
|
65
|
+
}
|
|
66
|
+
export function buildResultReport(input) {
|
|
67
|
+
const counts = { fresh: 0, cache: 0, notJudged: 0, static: 0 };
|
|
68
|
+
for (const item of input.items) {
|
|
69
|
+
if (item.treatment === "judged")
|
|
70
|
+
counts[item.source]++;
|
|
71
|
+
else if (item.treatment === "static")
|
|
72
|
+
counts.static++;
|
|
73
|
+
else
|
|
74
|
+
counts.notJudged++;
|
|
75
|
+
}
|
|
76
|
+
const admissible = counts.fresh + counts.cache + counts.static;
|
|
77
|
+
const usefulFallback = input.items.some((item) => item.treatment === "not_judged" && item.selection?.selected);
|
|
78
|
+
const missing = input.missingWork ||
|
|
79
|
+
counts.notJudged > 0 ||
|
|
80
|
+
input.diagnostics.some((d) => d.cause === "criteria_no_match" || d.effect === "blocking");
|
|
81
|
+
const report = {
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
tool: input.tool,
|
|
84
|
+
execution: !admissible && !usefulFallback
|
|
85
|
+
? input.refused
|
|
86
|
+
? "refused"
|
|
87
|
+
: "not_judged"
|
|
88
|
+
: missing
|
|
89
|
+
? "partial"
|
|
90
|
+
: "complete",
|
|
91
|
+
context: input.context,
|
|
92
|
+
items: input.items,
|
|
93
|
+
diagnostics: input.diagnostics,
|
|
94
|
+
actions: input.actions,
|
|
95
|
+
accounting: {
|
|
96
|
+
toolInvocations: 1,
|
|
97
|
+
httpAttempts: input.metrics.calls,
|
|
98
|
+
questionsSent: input.metrics.questions,
|
|
99
|
+
cacheHits: input.metrics.cacheHits,
|
|
100
|
+
cacheRequests: input.metrics.cacheRequests,
|
|
101
|
+
requestedResults: { total: input.total, ...counts },
|
|
102
|
+
auxiliary: input.auxiliary,
|
|
103
|
+
costUsd: input.metrics.costUsd === undefined
|
|
104
|
+
? unknown("current request cost not reported")
|
|
105
|
+
: known(input.metrics.costUsd),
|
|
106
|
+
elapsedMs: input.metrics.elapsedMs,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const errors = validateResultReport(report);
|
|
110
|
+
if (errors.length)
|
|
111
|
+
throw new Error(`Invalid ResultReportV1: ${errors.join("; ")}`);
|
|
112
|
+
return report;
|
|
113
|
+
}
|
|
114
|
+
/** Semantic checks complement, rather than replace, the public JSON Schema. */
|
|
115
|
+
export function validateResultReport(report) {
|
|
116
|
+
const errors = [];
|
|
117
|
+
const counter = (value, label) => {
|
|
118
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
119
|
+
errors.push(`invalid counter ${label}`);
|
|
120
|
+
};
|
|
121
|
+
const measurement = (value, label, probability = false) => {
|
|
122
|
+
if (value.status === "known" &&
|
|
123
|
+
(!Number.isFinite(value.value) ||
|
|
124
|
+
value.value < 0 ||
|
|
125
|
+
(probability && value.value > 1)))
|
|
126
|
+
errors.push(`invalid measure ${label}`);
|
|
127
|
+
};
|
|
128
|
+
const accounting = report.accounting;
|
|
129
|
+
for (const key of [
|
|
130
|
+
"httpAttempts",
|
|
131
|
+
"questionsSent",
|
|
132
|
+
"cacheHits",
|
|
133
|
+
"cacheRequests",
|
|
134
|
+
])
|
|
135
|
+
counter(accounting[key], key);
|
|
136
|
+
if (!Number.isFinite(accounting.elapsedMs) || accounting.elapsedMs < 0)
|
|
137
|
+
errors.push("invalid elapsedMs");
|
|
138
|
+
measurement(accounting.costUsd, "costUsd");
|
|
139
|
+
for (const [kind, values] of Object.entries(accounting.auxiliary))
|
|
140
|
+
for (const [source, value] of Object.entries(values))
|
|
141
|
+
counter(value, `${kind}.${source}`);
|
|
142
|
+
for (const inv of report.context.inventories) {
|
|
143
|
+
for (const [name, value] of [
|
|
144
|
+
["discovered", inv.discovered],
|
|
145
|
+
["considered", inv.considered],
|
|
146
|
+
])
|
|
147
|
+
if (value.status === "known")
|
|
148
|
+
counter(value.value, `${inv.id}.${name}`);
|
|
149
|
+
for (const criterion of inv.criteria)
|
|
150
|
+
if (criterion.matches.status === "known")
|
|
151
|
+
counter(criterion.matches.value, `${inv.id}.matches`);
|
|
152
|
+
}
|
|
153
|
+
const ids = (values, label) => {
|
|
154
|
+
const set = new Set(values.map((v) => v.id));
|
|
155
|
+
if (set.size !== values.length)
|
|
156
|
+
errors.push(`duplicate ${label} IDs`);
|
|
157
|
+
return set;
|
|
158
|
+
};
|
|
159
|
+
const items = ids(report.items, "item");
|
|
160
|
+
const diagnostics = ids(report.diagnostics, "diagnostic");
|
|
161
|
+
const actions = ids(report.actions, "action");
|
|
162
|
+
const inventories = ids(report.context.inventories, "inventory");
|
|
163
|
+
const groups = new Set(report.items.map((i) => i.groupId));
|
|
164
|
+
const refs = (values, set, label) => {
|
|
165
|
+
for (const id of values)
|
|
166
|
+
if (!set.has(id))
|
|
167
|
+
errors.push(`unknown ${label} ID ${id}`);
|
|
168
|
+
};
|
|
169
|
+
const scope = (s) => {
|
|
170
|
+
if (s.kind === "item")
|
|
171
|
+
refs(s.itemIds, items, "item");
|
|
172
|
+
else if (s.kind === "group")
|
|
173
|
+
refs(s.groupIds, groups, "group");
|
|
174
|
+
else if (s.kind === "inventory")
|
|
175
|
+
refs(s.inventoryIds, inventories, "inventory");
|
|
176
|
+
};
|
|
177
|
+
for (const item of report.items) {
|
|
178
|
+
refs(item.diagnosticIds, diagnostics, "diagnostic");
|
|
179
|
+
refs(item.actionIds, actions, "action");
|
|
180
|
+
if (item.treatment === "judged") {
|
|
181
|
+
ids(item.judgment.controls, `control in ${item.id}`);
|
|
182
|
+
measurement(item.judgment.measure.value, `${item.id}.measure`, true);
|
|
183
|
+
for (const raw of [
|
|
184
|
+
...item.judgment.rawValues,
|
|
185
|
+
...item.judgment.controls.flatMap((c) => c.rawValues),
|
|
186
|
+
])
|
|
187
|
+
measurement(raw.probability, `${item.id}.${raw.label}`, true);
|
|
188
|
+
if (typeof item.judgment.result === "number" &&
|
|
189
|
+
!Number.isFinite(item.judgment.result))
|
|
190
|
+
errors.push(`invalid result ${item.id}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const d of report.diagnostics) {
|
|
194
|
+
scope(d.scope);
|
|
195
|
+
refs(d.actionIds, actions, "action");
|
|
196
|
+
}
|
|
197
|
+
for (const a of report.actions)
|
|
198
|
+
scope(a.scope);
|
|
199
|
+
for (const inv of report.context.inventories)
|
|
200
|
+
for (const criterion of inv.criteria)
|
|
201
|
+
refs(criterion.diagnosticIds, diagnostics, "diagnostic");
|
|
202
|
+
const c = report.accounting.requestedResults;
|
|
203
|
+
for (const key of ["fresh", "cache", "static", "notJudged"])
|
|
204
|
+
counter(c[key], `requestedResults.${key}`);
|
|
205
|
+
if (c.total.status === "known")
|
|
206
|
+
counter(c.total.value, "requestedResults.total");
|
|
207
|
+
const actual = { fresh: 0, cache: 0, static: 0, notJudged: 0 };
|
|
208
|
+
for (const i of report.items) {
|
|
209
|
+
if (i.treatment === "judged")
|
|
210
|
+
actual[i.source]++;
|
|
211
|
+
else if (i.treatment === "static")
|
|
212
|
+
actual.static++;
|
|
213
|
+
else
|
|
214
|
+
actual.notJudged++;
|
|
215
|
+
}
|
|
216
|
+
for (const key of ["fresh", "cache", "static", "notJudged"])
|
|
217
|
+
if (c[key] !== actual[key])
|
|
218
|
+
errors.push(`requestedResults.${key} does not match items`);
|
|
219
|
+
if (c.total.status === "known" && c.total.value !== report.items.length)
|
|
220
|
+
errors.push("known requested total does not match items");
|
|
221
|
+
const admissible = actual.fresh + actual.cache + actual.static;
|
|
222
|
+
const usefulFallback = report.items.some((item) => item.treatment === "not_judged" && item.selection?.selected);
|
|
223
|
+
if ((report.execution === "refused" || report.execution === "not_judged") &&
|
|
224
|
+
(admissible || usefulFallback))
|
|
225
|
+
errors.push("non-result execution has admissible items or useful fallback");
|
|
226
|
+
if ((report.execution === "complete" || report.execution === "partial") &&
|
|
227
|
+
!admissible &&
|
|
228
|
+
!usefulFallback)
|
|
229
|
+
errors.push("result execution has no admissible items or useful fallback");
|
|
230
|
+
if (report.execution === "complete" &&
|
|
231
|
+
(actual.notJudged ||
|
|
232
|
+
report.diagnostics.some((d) => d.effect === "blocking" || d.cause === "criteria_no_match")))
|
|
233
|
+
errors.push("complete execution omits requested work");
|
|
234
|
+
if (!admissible && !report.diagnostics.length)
|
|
235
|
+
errors.push("absent judgment needs an explicit cause");
|
|
236
|
+
return errors;
|
|
237
|
+
}
|
|
238
|
+
export function resultIsError(report) {
|
|
239
|
+
if (report.execution === "refused")
|
|
240
|
+
return true;
|
|
241
|
+
if (report.items.some((i) => i.treatment !== "not_judged" || i.selection?.selected))
|
|
242
|
+
return false;
|
|
243
|
+
return report.diagnostics.some((d) => d.effect === "blocking" &&
|
|
244
|
+
[
|
|
245
|
+
"provider_context_refusal",
|
|
246
|
+
"not_configured",
|
|
247
|
+
"service_unavailable",
|
|
248
|
+
"transport_failure",
|
|
249
|
+
"invalid_response",
|
|
250
|
+
"internal_error",
|
|
251
|
+
].includes(d.cause));
|
|
252
|
+
}
|
|
253
|
+
/** Source must be established by the client, never guessed from footer totals. */
|
|
254
|
+
export function answerItemFromInput(common, input) {
|
|
255
|
+
if (input.unjudged)
|
|
256
|
+
return { ...common, treatment: "not_judged", source: "none" };
|
|
257
|
+
const source = input.source ?? input.answer?.source;
|
|
258
|
+
if (source !== "fresh" && source !== "cache")
|
|
259
|
+
throw new Error(`Answer source not established: ${common.id}`);
|
|
260
|
+
const value = input.value;
|
|
261
|
+
const answer = input.answer;
|
|
262
|
+
if (!value && !answer)
|
|
263
|
+
throw new Error(`Answer value not established: ${common.id}`);
|
|
264
|
+
const result = input.publicResult ??
|
|
265
|
+
(answer?.type === "bool"
|
|
266
|
+
? answer.p >= 0.5
|
|
267
|
+
: (value?.head ??
|
|
268
|
+
(answer?.type === "choice" ? answer.choice : answer?.score)));
|
|
269
|
+
if (result === undefined)
|
|
270
|
+
throw new Error(`Answer result not established: ${common.id}`);
|
|
271
|
+
const p = value?.p ??
|
|
272
|
+
(answer?.type === "bool"
|
|
273
|
+
? answer.p
|
|
274
|
+
: answer
|
|
275
|
+
? Math.max(...Object.values(answer.probabilities))
|
|
276
|
+
: undefined);
|
|
277
|
+
return {
|
|
278
|
+
...common,
|
|
279
|
+
treatment: "judged",
|
|
280
|
+
source,
|
|
281
|
+
judgment: {
|
|
282
|
+
band: input.band ??
|
|
283
|
+
(p === undefined
|
|
284
|
+
? "unsure"
|
|
285
|
+
: askBand({
|
|
286
|
+
head: typeof result === "boolean" ? String(result) : result,
|
|
287
|
+
p,
|
|
288
|
+
}, input)),
|
|
289
|
+
result,
|
|
290
|
+
measure: {
|
|
291
|
+
kind: "probability",
|
|
292
|
+
value: p === undefined
|
|
293
|
+
? unknown("leading probability not collected")
|
|
294
|
+
: known(p),
|
|
295
|
+
},
|
|
296
|
+
reason: input.reason ?? "",
|
|
297
|
+
uncalibrated: input.uncalibrated ?? false,
|
|
298
|
+
rawValues: input.rawValues ?? [],
|
|
299
|
+
controls: input.reportControls ?? [],
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Base-name globs of likely secret files, matched case-insensitively. */
|
|
2
|
+
export const SECRET_NAME_GLOBS = [
|
|
3
|
+
".env",
|
|
4
|
+
".env.*",
|
|
5
|
+
"*.pem",
|
|
6
|
+
"id_rsa*",
|
|
7
|
+
"*.p12",
|
|
8
|
+
"credentials*",
|
|
9
|
+
"secrets*",
|
|
10
|
+
];
|
|
11
|
+
/** Value-free templates that stay admitted despite the `.env.*` rule. */
|
|
12
|
+
const templates = [".env.example", ".env.sample", ".env.template"];
|
|
13
|
+
/**
|
|
14
|
+
* True when a path's base name matches SECRET_NAME_GLOBS (any depth,
|
|
15
|
+
* case-insensitive) and is not a value-free `.env` template. Such files are
|
|
16
|
+
* refused before their content is read, on every evidence admission path;
|
|
17
|
+
* there is no per-call override.
|
|
18
|
+
*/
|
|
19
|
+
export function isSecretPath(path) {
|
|
20
|
+
const name = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
|
|
21
|
+
if (templates.includes(name))
|
|
22
|
+
return false;
|
|
23
|
+
return (name === ".env" ||
|
|
24
|
+
name.startsWith(".env.") ||
|
|
25
|
+
name.endsWith(".pem") ||
|
|
26
|
+
name.startsWith("id_rsa") ||
|
|
27
|
+
name.endsWith(".p12") ||
|
|
28
|
+
name.startsWith("credentials") ||
|
|
29
|
+
name.startsWith("secrets"));
|
|
30
|
+
}
|
|
31
|
+
/** Refusal text for a secret-named path: names the path, never its content. */
|
|
32
|
+
export function secretRefusal(path) {
|
|
33
|
+
return `${path}: secret file name (secret_pattern): content not read, not sent to Jev`;
|
|
34
|
+
}
|
package/dist/core/state.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ASK_NOTE_MAX_CHARS, STATE_MAX_CHARS } from "../constants.js";
|
|
2
2
|
import { isRecord } from "../result.js";
|
|
3
|
-
const reservedKeys = [
|
|
3
|
+
const reservedKeys = [
|
|
4
|
+
"files",
|
|
5
|
+
"files_before",
|
|
6
|
+
"base_sha",
|
|
7
|
+
"closure",
|
|
8
|
+
"evidence",
|
|
9
|
+
"output",
|
|
10
|
+
];
|
|
4
11
|
export function assembleState(note, files) {
|
|
5
12
|
if (note !== undefined && note.length > ASK_NOTE_MAX_CHARS)
|
|
6
13
|
return {
|
package/dist/core/units.js
CHANGED
|
@@ -75,7 +75,7 @@ export function buildUnits(files, parser) {
|
|
|
75
75
|
const units = [];
|
|
76
76
|
const limits = [];
|
|
77
77
|
for (const file of files) {
|
|
78
|
-
if (isTestFile(file.path))
|
|
78
|
+
if (isTestFile(file.path) && file.limitation !== "secret_pattern")
|
|
79
79
|
continue;
|
|
80
80
|
if (file.binary) {
|
|
81
81
|
limits.push({ kind: "binary_ignored", file: file.path });
|
package/dist/jev/client.js
CHANGED
|
@@ -191,8 +191,14 @@ export function createJevClient(config, injected) {
|
|
|
191
191
|
options.onUsage?.(next.usage);
|
|
192
192
|
}
|
|
193
193
|
};
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
let stoppedCause;
|
|
195
|
+
const missing = (ids, reason, cause = stoppedCause) => {
|
|
196
|
+
const batchAnswers = Object.fromEntries(ids.map((id) => [
|
|
197
|
+
id,
|
|
198
|
+
{ type: "unjudged", reason, ...(cause ? { cause } : {}) },
|
|
199
|
+
]));
|
|
200
|
+
if (cause)
|
|
201
|
+
meta.failureCause ??= cause;
|
|
196
202
|
for (const [id, answer] of Object.entries(batchAnswers))
|
|
197
203
|
if (!witnessSet.has(id) || !answers[id])
|
|
198
204
|
answers[id] = answer;
|
|
@@ -235,6 +241,7 @@ export function createJevClient(config, injected) {
|
|
|
235
241
|
if (diagnostic)
|
|
236
242
|
return;
|
|
237
243
|
stopped = admission.error;
|
|
244
|
+
stoppedCause = options.admissionCause?.();
|
|
238
245
|
missing(ids, stopped);
|
|
239
246
|
return;
|
|
240
247
|
}
|
|
@@ -272,10 +279,11 @@ export function createJevClient(config, injected) {
|
|
|
272
279
|
if (diagnostic) {
|
|
273
280
|
stopped =
|
|
274
281
|
"Jev max_tokens_exceeded: state too large for a single question; remaining questions not judged.";
|
|
282
|
+
stoppedCause = "provider_context_refusal";
|
|
275
283
|
return;
|
|
276
284
|
}
|
|
277
285
|
if (ids.length === 1) {
|
|
278
|
-
missing(ids, "Jev max_tokens_exceeded: state too large for a single question; question not judged.");
|
|
286
|
+
missing(ids, "Jev max_tokens_exceeded: state too large for a single question; question not judged.", "provider_context_refusal");
|
|
279
287
|
return;
|
|
280
288
|
}
|
|
281
289
|
if (!stateChecked && groups.length > 1) {
|
|
@@ -303,7 +311,7 @@ export function createJevClient(config, injected) {
|
|
|
303
311
|
if (split.ok)
|
|
304
312
|
await Promise.all(split.halves.map((half) => send(half)));
|
|
305
313
|
else
|
|
306
|
-
missing(ids, split.error);
|
|
314
|
+
missing(ids, split.error, "provider_context_refusal");
|
|
307
315
|
return;
|
|
308
316
|
}
|
|
309
317
|
if (response.ok && isRecord(body)) {
|
|
@@ -312,7 +320,12 @@ export function createJevClient(config, injected) {
|
|
|
312
320
|
const raw = isRecord(body.answers) ? body.answers : {};
|
|
313
321
|
const batchAnswers = Object.fromEntries(ids.map((id) => [
|
|
314
322
|
id,
|
|
315
|
-
|
|
323
|
+
(() => {
|
|
324
|
+
const answer = normalize(raw[id], selected[id]);
|
|
325
|
+
return answer.type === "unjudged"
|
|
326
|
+
? { ...answer, cause: "invalid_response" }
|
|
327
|
+
: { ...answer, source: "fresh" };
|
|
328
|
+
})(),
|
|
316
329
|
]));
|
|
317
330
|
meta.batches?.push({ questionIds: ids, answers: batchAnswers });
|
|
318
331
|
for (const [id, answer] of Object.entries(batchAnswers)) {
|
|
@@ -347,7 +360,7 @@ export function createJevClient(config, injected) {
|
|
|
347
360
|
retryAfter * 1_000 > RETRY_MAX_MS) {
|
|
348
361
|
const reason = `Jev HTTP 429, retry after ${retryAfter} s`;
|
|
349
362
|
failure ??= reason;
|
|
350
|
-
missing(ids, reason);
|
|
363
|
+
missing(ids, reason, "service_unavailable");
|
|
351
364
|
return;
|
|
352
365
|
}
|
|
353
366
|
const reason = response.ok
|
|
@@ -360,7 +373,7 @@ export function createJevClient(config, injected) {
|
|
|
360
373
|
failure ??= reason;
|
|
361
374
|
if (response.status === 401 || response.status === 403)
|
|
362
375
|
stopped = reason;
|
|
363
|
-
missing(ids, reason);
|
|
376
|
+
missing(ids, reason, response.ok ? "invalid_response" : "service_unavailable");
|
|
364
377
|
return;
|
|
365
378
|
}
|
|
366
379
|
if (Number.isFinite(retryAfter) && retryAfter >= 0)
|
|
@@ -374,7 +387,7 @@ export function createJevClient(config, injected) {
|
|
|
374
387
|
const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
|
|
375
388
|
if (options.signal?.aborted || attempt === REQUEST_ATTEMPTS - 1) {
|
|
376
389
|
failure ??= reason;
|
|
377
|
-
missing(ids, reason);
|
|
390
|
+
missing(ids, reason, options.signal?.aborted ? "cancelled" : "transport_failure");
|
|
378
391
|
return;
|
|
379
392
|
}
|
|
380
393
|
}
|
|
@@ -389,7 +402,7 @@ export function createJevClient(config, injected) {
|
|
|
389
402
|
catch (error) {
|
|
390
403
|
const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
|
|
391
404
|
failure ??= reason;
|
|
392
|
-
missing(ids, reason);
|
|
405
|
+
missing(ids, reason, options.signal?.aborted ? "cancelled" : "transport_failure");
|
|
393
406
|
return;
|
|
394
407
|
}
|
|
395
408
|
}
|
|
@@ -401,10 +414,16 @@ export function createJevClient(config, injected) {
|
|
|
401
414
|
(witnesses.length > 0 && !witnessCache.has(witnessKey(group))))
|
|
402
415
|
return true;
|
|
403
416
|
for (const id of group) {
|
|
404
|
-
answers[id] =
|
|
417
|
+
answers[id] = {
|
|
418
|
+
...structuredClone(cache.get(keys.get(id))),
|
|
419
|
+
source: "cache",
|
|
420
|
+
};
|
|
405
421
|
meta.cacheHits = (meta.cacheHits ?? 0) + 1;
|
|
406
422
|
}
|
|
407
|
-
const observedWitnesses = structuredClone(witnessCache.get(witnessKey(group)) ?? {})
|
|
423
|
+
const observedWitnesses = Object.fromEntries(Object.entries(structuredClone(witnessCache.get(witnessKey(group)) ?? {})).map(([id, answer]) => [
|
|
424
|
+
id,
|
|
425
|
+
{ ...answer, source: "cache" },
|
|
426
|
+
]));
|
|
408
427
|
for (const [id, answer] of Object.entries(observedWitnesses)) {
|
|
409
428
|
if (!answers[id]) {
|
|
410
429
|
answers[id] = answer;
|
|
@@ -418,7 +437,10 @@ export function createJevClient(config, injected) {
|
|
|
418
437
|
id,
|
|
419
438
|
witnessSet.has(id)
|
|
420
439
|
? observedWitnesses[id]
|
|
421
|
-
:
|
|
440
|
+
: {
|
|
441
|
+
...structuredClone(cache.get(keys.get(id))),
|
|
442
|
+
source: "cache",
|
|
443
|
+
},
|
|
422
444
|
])),
|
|
423
445
|
});
|
|
424
446
|
return false;
|
package/dist/mcp/protocol.js
CHANGED
|
@@ -21,6 +21,9 @@ export const METHOD_NOT_FOUND = -32601;
|
|
|
21
21
|
export const INVALID_PARAMS = -32602;
|
|
22
22
|
export const INTERNAL_ERROR = -32603;
|
|
23
23
|
export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
24
|
+
/** Input-schema failure, distinct from semantic tool refusals and server faults. */
|
|
25
|
+
export class McpInvalidParams extends Error {
|
|
26
|
+
}
|
|
24
27
|
function isRecord(value) {
|
|
25
28
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
29
|
}
|
|
@@ -32,6 +35,7 @@ export class McpServer {
|
|
|
32
35
|
tools;
|
|
33
36
|
info;
|
|
34
37
|
inFlight = new Map();
|
|
38
|
+
negotiatedVersion = LATEST_INITIALIZE_VERSION;
|
|
35
39
|
constructor(info, tools) {
|
|
36
40
|
this.info = info;
|
|
37
41
|
this.tools = new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -67,10 +71,17 @@ export class McpServer {
|
|
|
67
71
|
supported: [...SUPPORTED_VERSIONS],
|
|
68
72
|
requested,
|
|
69
73
|
});
|
|
74
|
+
const version = typeof requested === "string"
|
|
75
|
+
? requested
|
|
76
|
+
: method === "server/discover"
|
|
77
|
+
? "2026-07-28"
|
|
78
|
+
: this.negotiatedVersion;
|
|
70
79
|
try {
|
|
71
80
|
switch (method) {
|
|
72
|
-
case "initialize":
|
|
73
|
-
|
|
81
|
+
case "initialize": {
|
|
82
|
+
const initialized = this.initialize(params);
|
|
83
|
+
return this.result(id, initialized, this.negotiatedVersion);
|
|
84
|
+
}
|
|
74
85
|
case "server/discover":
|
|
75
86
|
return this.result(id, {
|
|
76
87
|
supportedVersions: [...SUPPORTED_VERSIONS],
|
|
@@ -86,20 +97,23 @@ export class McpServer {
|
|
|
86
97
|
: {}),
|
|
87
98
|
ttlMs: 0,
|
|
88
99
|
cacheScope: "private",
|
|
89
|
-
});
|
|
100
|
+
}, version);
|
|
90
101
|
case "ping":
|
|
91
|
-
return this.result(id, {});
|
|
102
|
+
return this.result(id, {}, version);
|
|
92
103
|
case "tools/list":
|
|
93
104
|
return this.result(id, {
|
|
94
|
-
tools: [...this.tools.values()].map(({ call: _call, ...tool }) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
tools: [...this.tools.values()].map(({ call: _call, outputSchema, ...tool }) => ({
|
|
106
|
+
...tool,
|
|
107
|
+
...(supportsStructuredResults(version) && outputSchema
|
|
108
|
+
? { outputSchema }
|
|
109
|
+
: {}),
|
|
110
|
+
})),
|
|
111
|
+
...(version === "2026-07-28"
|
|
112
|
+
? { ttlMs: 0, cacheScope: "private" }
|
|
113
|
+
: {}),
|
|
114
|
+
}, version);
|
|
101
115
|
case "tools/call":
|
|
102
|
-
return await this.callTool(id, params);
|
|
116
|
+
return await this.callTool(id, params, version);
|
|
103
117
|
default:
|
|
104
118
|
return this.error(id, METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
105
119
|
}
|
|
@@ -116,6 +130,7 @@ export class McpServer {
|
|
|
116
130
|
SUPPORTED_VERSIONS.includes(requested)
|
|
117
131
|
? requested
|
|
118
132
|
: LATEST_INITIALIZE_VERSION;
|
|
133
|
+
this.negotiatedVersion = protocolVersion;
|
|
119
134
|
return {
|
|
120
135
|
protocolVersion,
|
|
121
136
|
capabilities: { tools: { listChanged: false } },
|
|
@@ -132,7 +147,7 @@ export class McpServer {
|
|
|
132
147
|
}
|
|
133
148
|
// notifications/initialized and unknown notifications need no action.
|
|
134
149
|
}
|
|
135
|
-
async callTool(id, params) {
|
|
150
|
+
async callTool(id, params, version) {
|
|
136
151
|
const tool = typeof params.name === "string" ? this.tools.get(params.name) : undefined;
|
|
137
152
|
if (!tool)
|
|
138
153
|
return this.error(id, INVALID_PARAMS, `Unknown tool: ${String(params.name)}`);
|
|
@@ -149,32 +164,35 @@ export class McpServer {
|
|
|
149
164
|
const result = await tool.call(params.arguments ?? {}, controller.signal);
|
|
150
165
|
if (cancelled())
|
|
151
166
|
return undefined;
|
|
152
|
-
|
|
167
|
+
const { structuredContent, ...content } = result;
|
|
168
|
+
return this.result(id, {
|
|
169
|
+
...content,
|
|
170
|
+
...(supportsStructuredResults(version) && structuredContent
|
|
171
|
+
? { structuredContent }
|
|
172
|
+
: {}),
|
|
173
|
+
}, version);
|
|
153
174
|
}
|
|
154
175
|
catch (error) {
|
|
155
176
|
if (cancelled())
|
|
156
177
|
return undefined;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
text: `${tool.name} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
isError: true,
|
|
166
|
-
});
|
|
178
|
+
if (error instanceof McpInvalidParams)
|
|
179
|
+
return this.error(id, INVALID_PARAMS, error.message);
|
|
180
|
+
// Expected tool refusals already carry a report. An unexpected server
|
|
181
|
+
// fault has no trustworthy report/accounting; do not fabricate one.
|
|
182
|
+
return this.error(id, INTERNAL_ERROR, `${tool.name} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
183
|
}
|
|
168
184
|
finally {
|
|
169
185
|
this.inFlight.delete(id);
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
|
-
result(id, result) {
|
|
173
|
-
// resultType is required from 2026-07-28 and ignored by earlier clients.
|
|
188
|
+
result(id, result, version) {
|
|
174
189
|
return {
|
|
175
190
|
jsonrpc: "2.0",
|
|
176
191
|
id,
|
|
177
|
-
result: {
|
|
192
|
+
result: {
|
|
193
|
+
...(version === "2026-07-28" ? { resultType: "complete" } : {}),
|
|
194
|
+
...result,
|
|
195
|
+
},
|
|
178
196
|
};
|
|
179
197
|
}
|
|
180
198
|
error(id, code, message, data) {
|
|
@@ -185,3 +203,8 @@ export class McpServer {
|
|
|
185
203
|
};
|
|
186
204
|
}
|
|
187
205
|
}
|
|
206
|
+
function supportsStructuredResults(version) {
|
|
207
|
+
return (version === "2025-06-18" ||
|
|
208
|
+
version === "2025-11-25" ||
|
|
209
|
+
version === "2026-07-28");
|
|
210
|
+
}
|