@bobobowis/jev-kit 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +311 -2
- package/dist/capabilities.d.ts +33 -0
- package/dist/capabilities.js +81 -0
- package/dist/cli/ask.d.ts +9 -0
- package/dist/cli/ask.js +210 -0
- package/dist/cli/common.d.ts +38 -0
- package/dist/cli/common.js +40 -0
- package/dist/cli/hook.d.ts +3 -0
- package/dist/cli/hook.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +41 -0
- package/dist/cli/mcp.d.ts +3 -0
- package/dist/cli/mcp.js +47 -0
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +84 -0
- package/dist/compact/compact-on-demand.d.ts +22 -0
- package/dist/compact/compact-on-demand.js +29 -0
- package/dist/compact/evaluate.d.ts +36 -0
- package/dist/compact/evaluate.js +31 -0
- package/dist/compact/index.d.ts +7 -0
- package/dist/compact/index.js +7 -0
- package/dist/compact/pick-cut-point.d.ts +18 -0
- package/dist/compact/pick-cut-point.js +29 -0
- package/dist/compact/should-compact.d.ts +57 -0
- package/dist/compact/should-compact.js +87 -0
- package/dist/core/client.d.ts +93 -0
- package/dist/core/client.js +295 -0
- package/dist/core/helpers.d.ts +14 -0
- package/dist/core/helpers.js +9 -0
- package/dist/core/mock.d.ts +20 -0
- package/dist/core/mock.js +132 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/core/types.js +82 -0
- package/dist/events/index.d.ts +44 -0
- package/dist/events/index.js +77 -0
- package/dist/files/ask-file-bool.d.ts +12 -0
- package/dist/files/ask-file-bool.js +15 -0
- package/dist/files/ask-file-choice.d.ts +10 -0
- package/dist/files/ask-file-choice.js +17 -0
- package/dist/files/ask-file-score.d.ts +12 -0
- package/dist/files/ask-file-score.js +15 -0
- package/dist/files/ask-files.d.ts +23 -0
- package/dist/files/ask-files.js +56 -0
- package/dist/files/ask.d.ts +12 -0
- package/dist/files/ask.js +28 -0
- package/dist/files/assemble.d.ts +52 -0
- package/dist/files/assemble.js +129 -0
- package/dist/files/decide.d.ts +10 -0
- package/dist/files/decide.js +1 -0
- package/dist/files/index.d.ts +14 -0
- package/dist/files/index.js +14 -0
- package/dist/files/pick-first.d.ts +16 -0
- package/dist/files/pick-first.js +24 -0
- package/dist/files/prune.d.ts +13 -0
- package/dist/files/prune.js +87 -0
- package/dist/files/read-state.d.ts +17 -0
- package/dist/files/read-state.js +47 -0
- package/dist/guards/bash-gate.d.ts +65 -0
- package/dist/guards/bash-gate.js +78 -0
- package/dist/guards/index.d.ts +7 -0
- package/dist/guards/index.js +7 -0
- package/dist/guards/mcp-gate.d.ts +23 -0
- package/dist/guards/mcp-gate.js +52 -0
- package/dist/guards/result-screen.d.ts +20 -0
- package/dist/guards/result-screen.js +31 -0
- package/dist/guards/write-gate.d.ts +18 -0
- package/dist/guards/write-gate.js +44 -0
- package/dist/hooks/handle.d.ts +76 -0
- package/dist/hooks/handle.js +61 -0
- package/dist/hooks/harness.d.ts +45 -0
- package/dist/hooks/harness.js +124 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/state.d.ts +8 -0
- package/dist/hooks/state.js +25 -0
- package/dist/hooks/transcript.d.ts +8 -0
- package/dist/hooks/transcript.js +52 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/ledger.d.ts +22 -0
- package/dist/ledger.js +26 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.js +124 -0
- package/dist/policy/index.d.ts +17 -0
- package/dist/policy/index.js +30 -0
- package/dist/setup/index.d.ts +76 -0
- package/dist/setup/index.js +221 -0
- package/dist/tools/ask-jev.d.ts +7 -0
- package/dist/tools/ask-jev.js +42 -0
- package/dist/tools/compact-tool.d.ts +2 -0
- package/dist/tools/compact-tool.js +33 -0
- package/dist/tools/description.d.ts +5 -0
- package/dist/tools/description.js +20 -0
- package/dist/tools/file-tools.d.ts +7 -0
- package/dist/tools/file-tools.js +127 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/run-command.d.ts +7 -0
- package/dist/tools/run-command.js +27 -0
- package/dist/tools/sets.d.ts +9 -0
- package/dist/tools/sets.js +32 -0
- package/dist/tools/types.d.ts +51 -0
- package/dist/tools/types.js +4 -0
- package/package.json +96 -3
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10, option C: the spend ledger.
|
|
3
|
+
* Every ask_jev call is counted against the agent's own turn cost, so the session can say what the habit cost and what it replaced. Numbers in code: nothing here asks Jev anything.
|
|
4
|
+
*/
|
|
5
|
+
export interface JevUsage {
|
|
6
|
+
input_tokens: number;
|
|
7
|
+
output_tokens: number;
|
|
8
|
+
cost?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Jev's list price per input token when the endpoint reports no cost. Output is free. */
|
|
11
|
+
export declare const JEV_INPUT_USD_PER_M = 0.042;
|
|
12
|
+
export interface Ledger {
|
|
13
|
+
calls: number;
|
|
14
|
+
questions: number;
|
|
15
|
+
inputTokens: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
usd: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const emptyLedger: () => Ledger;
|
|
20
|
+
export declare function record(ledger: Ledger, usage: JevUsage | undefined, questionCount: number): Ledger;
|
|
21
|
+
/** One line the agent, or the window, can print at the end of a task. */
|
|
22
|
+
export declare function summarize(ledger: Ledger, agentUsd: number): string;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10, option C: the spend ledger.
|
|
3
|
+
* Every ask_jev call is counted against the agent's own turn cost, so the session can say what the habit cost and what it replaced. Numbers in code: nothing here asks Jev anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Jev's list price per input token when the endpoint reports no cost. Output is free. */
|
|
6
|
+
export const JEV_INPUT_USD_PER_M = 0.042;
|
|
7
|
+
export const emptyLedger = () => ({ calls: 0, questions: 0, inputTokens: 0, outputTokens: 0, usd: 0 });
|
|
8
|
+
export function record(ledger, usage, questionCount) {
|
|
9
|
+
const input = usage?.input_tokens ?? 0;
|
|
10
|
+
const output = usage?.output_tokens ?? 0;
|
|
11
|
+
return {
|
|
12
|
+
calls: ledger.calls + 1,
|
|
13
|
+
questions: ledger.questions + questionCount,
|
|
14
|
+
inputTokens: ledger.inputTokens + input,
|
|
15
|
+
outputTokens: ledger.outputTokens + output,
|
|
16
|
+
usd: ledger.usd + (usage?.cost ?? (input * JEV_INPUT_USD_PER_M) / 1e6),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** One line the agent, or the window, can print at the end of a task. */
|
|
20
|
+
export function summarize(ledger, agentUsd) {
|
|
21
|
+
if (ledger.calls === 0)
|
|
22
|
+
return "No Jev calls this session.";
|
|
23
|
+
const ratio = ledger.usd > 0 ? Math.round(agentUsd / ledger.usd) : 0;
|
|
24
|
+
return `${ledger.calls} Jev call${ledger.calls === 1 ? "" : "s"}, ${ledger.questions} question${ledger.questions === 1 ? "" : "s"}, $${ledger.usd.toFixed(6)}` +
|
|
25
|
+
(ratio ? `, the agent's own spend was ${ratio}x that` : "");
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Decider } from "../events/index.ts";
|
|
2
|
+
import type { CompactionSource, JevTool } from "../tools/types.ts";
|
|
3
|
+
/** Protocol revisions whose tools subset this server speaks. A client's revision is echoed when known. */
|
|
4
|
+
export declare const PROTOCOL_VERSIONS: string[];
|
|
5
|
+
export interface ServerOptions {
|
|
6
|
+
tools: JevTool[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
decide: Decider;
|
|
9
|
+
report(kind: string, payload: Record<string, unknown>): void;
|
|
10
|
+
version?: string;
|
|
11
|
+
}
|
|
12
|
+
type Message = {
|
|
13
|
+
jsonrpc?: string;
|
|
14
|
+
id?: string | number | null;
|
|
15
|
+
method?: string;
|
|
16
|
+
params?: any;
|
|
17
|
+
};
|
|
18
|
+
type Reply = Record<string, unknown>;
|
|
19
|
+
/** The Level 7 hooks' notes for the most recent session in the project, for should_i_compact. */
|
|
20
|
+
export declare function latestSessionSource(cwd: string): CompactionSource;
|
|
21
|
+
export declare function instructionsFor(tools: JevTool[]): string;
|
|
22
|
+
/** Handle one message. Returns the reply to send, or null for notifications. */
|
|
23
|
+
export declare function createHandler(opts: ServerOptions): (msg: Message) => Promise<Reply | null>;
|
|
24
|
+
/** Serve until stdin closes. Calls run concurrently; replies go out as each one finishes. */
|
|
25
|
+
export declare function serve(opts: ServerOptions, input?: NodeJS.ReadableStream, output?: {
|
|
26
|
+
write(s: string): unknown;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jev tools over MCP: newline-delimited JSON-RPC 2.0 on stdio, the subset a tool server needs.
|
|
3
|
+
* initialize, notifications/initialized, tools/list, tools/call, ping. Unknown methods get -32601,
|
|
4
|
+
* unknown tools -32602, and a failing tool is an `isError` result, never a crash.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { emptyLedger, record, summarize } from "../ledger.js";
|
|
10
|
+
import { HOOK_LINES } from "../hooks/harness.js";
|
|
11
|
+
import { readState, stateDir } from "../hooks/state.js";
|
|
12
|
+
import { ASK_JEV_NUDGE } from "../tools/ask-jev.js";
|
|
13
|
+
/** Protocol revisions whose tools subset this server speaks. A client's revision is echoed when known. */
|
|
14
|
+
export const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
15
|
+
/** The Level 7 hooks' notes for the most recent session in the project, for should_i_compact. */
|
|
16
|
+
export function latestSessionSource(cwd) {
|
|
17
|
+
return {
|
|
18
|
+
action: "ask_user",
|
|
19
|
+
input: () => {
|
|
20
|
+
const dir = stateDir(cwd);
|
|
21
|
+
let newest = null;
|
|
22
|
+
try {
|
|
23
|
+
for (const name of readdirSync(dir)) {
|
|
24
|
+
const m = name.match(/^session-(.+)\.json$/);
|
|
25
|
+
if (!m)
|
|
26
|
+
continue;
|
|
27
|
+
const at = statSync(join(dir, name)).mtimeMs;
|
|
28
|
+
if (!newest || at > newest.at)
|
|
29
|
+
newest = { name: m[1], at };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!newest)
|
|
36
|
+
return null;
|
|
37
|
+
const s = readState(dir, newest.name);
|
|
38
|
+
if (!s.requests.length)
|
|
39
|
+
return null;
|
|
40
|
+
return { requests: s.requests, recentTurn: "", toolsThisTurn: [], usage: null, compactable: true, lines: HOOK_LINES };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function instructionsFor(tools) {
|
|
45
|
+
const lines = ["Jev answers typed questions (yes or no, pick one, a score) about files, a command's output, or your own state, in about 300 ms for a fraction of a cent. Prefer it to reasoning a judgment call out yourself."];
|
|
46
|
+
if (tools.some((t) => t.name === "ask_jev"))
|
|
47
|
+
lines.push(ASK_JEV_NUDGE);
|
|
48
|
+
if (tools.some((t) => t.name.startsWith("ask_jev_file")))
|
|
49
|
+
lines.push("The ask_jev_file tools judge one file without loading it into your context; use read when you need the code itself.");
|
|
50
|
+
return lines.join("\n\n");
|
|
51
|
+
}
|
|
52
|
+
/** Handle one message. Returns the reply to send, or null for notifications. */
|
|
53
|
+
export function createHandler(opts) {
|
|
54
|
+
let ledger = emptyLedger();
|
|
55
|
+
const ctx = {
|
|
56
|
+
cwd: opts.cwd, harness: "mcp", decide: opts.decide, report: opts.report,
|
|
57
|
+
compaction: latestSessionSource(opts.cwd),
|
|
58
|
+
};
|
|
59
|
+
const ok = (id, result) => ({ jsonrpc: "2.0", id: id ?? null, result });
|
|
60
|
+
const err = (id, code, message) => ({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
61
|
+
return async function handle(msg) {
|
|
62
|
+
const isRequest = msg.id !== undefined && msg.id !== null;
|
|
63
|
+
switch (msg.method) {
|
|
64
|
+
case "initialize": {
|
|
65
|
+
const asked = String(msg.params?.protocolVersion ?? "");
|
|
66
|
+
return ok(msg.id, {
|
|
67
|
+
protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
68
|
+
capabilities: { tools: { listChanged: false } },
|
|
69
|
+
serverInfo: { name: "jev", version: opts.version ?? "0.0.0" },
|
|
70
|
+
instructions: instructionsFor(opts.tools),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
case "ping":
|
|
74
|
+
return isRequest ? ok(msg.id, {}) : null;
|
|
75
|
+
case "tools/list":
|
|
76
|
+
return ok(msg.id, { tools: opts.tools.map((t) => ({ name: t.name, title: t.label, description: t.description, inputSchema: t.parameters })) });
|
|
77
|
+
case "tools/call": {
|
|
78
|
+
const name = String(msg.params?.name ?? "");
|
|
79
|
+
const tool = opts.tools.find((t) => t.name === name);
|
|
80
|
+
if (!tool)
|
|
81
|
+
return err(msg.id, -32602, `Unknown tool: ${name}. This server has ${opts.tools.map((t) => t.name).join(", ")}.`);
|
|
82
|
+
const r = await tool.execute(msg.params?.arguments ?? {}, ctx);
|
|
83
|
+
if (tool.name === "ask_jev" && !r.isError) {
|
|
84
|
+
const p = r.payload;
|
|
85
|
+
ledger = record(ledger, p.usage, Object.keys(p.answers).length);
|
|
86
|
+
opts.report("ledger", { ...ledger, summary: summarize(ledger, 0) });
|
|
87
|
+
}
|
|
88
|
+
const result = { content: [{ type: "text", text: r.text }], isError: r.isError ?? false };
|
|
89
|
+
if (!r.isError && r.payload && typeof r.payload === "object" && !Array.isArray(r.payload))
|
|
90
|
+
result.structuredContent = r.payload;
|
|
91
|
+
return ok(msg.id, result);
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
if (msg.method?.startsWith("notifications/") || !isRequest)
|
|
95
|
+
return null;
|
|
96
|
+
return err(msg.id, -32601, `Method not found: ${msg.method}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Serve until stdin closes. Calls run concurrently; replies go out as each one finishes. */
|
|
101
|
+
export async function serve(opts, input = process.stdin, output = process.stdout) {
|
|
102
|
+
const handle = createHandler(opts);
|
|
103
|
+
const send = (reply) => { if (reply)
|
|
104
|
+
output.write(JSON.stringify(reply) + "\n"); };
|
|
105
|
+
const pending = new Set();
|
|
106
|
+
for await (const line of createInterface({ input, crlfDelay: Infinity })) {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
continue;
|
|
109
|
+
let msg;
|
|
110
|
+
try {
|
|
111
|
+
msg = JSON.parse(line);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const p = handle(msg)
|
|
118
|
+
.then(send)
|
|
119
|
+
.catch((e) => send(msg.id !== undefined ? { jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e?.message ?? String(e) } } : null))
|
|
120
|
+
.finally(() => pending.delete(p));
|
|
121
|
+
pending.add(p);
|
|
122
|
+
}
|
|
123
|
+
await Promise.all(pending);
|
|
124
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy helpers: the numbers in code that turn answers into decisions.
|
|
3
|
+
* Jev never sees these. Tuning means changing a number, not a prompt.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScoreAnswer } from "../core/types.ts";
|
|
6
|
+
/** Normalize a score answer to 0-1 against its own top level, so scales of different lengths compare. */
|
|
7
|
+
export declare function normalized(answer: ScoreAnswer): number;
|
|
8
|
+
/** Weighted sum of 0-1 parts. Weights are divided by their total, so they need not add up to 1. */
|
|
9
|
+
export declare function weighted<K extends string>(parts: Record<K, number>, weights: Record<K, number>): number;
|
|
10
|
+
/** The two confidence thresholds a gate reads. */
|
|
11
|
+
export declare const CONFIDENCE: {
|
|
12
|
+
readonly REVIEW_FLOOR: 0.5;
|
|
13
|
+
readonly DESTRUCTIVE_BAR: 0.9;
|
|
14
|
+
};
|
|
15
|
+
export type ConfidenceBand = "review" | "confirm" | "act";
|
|
16
|
+
/** The answer says what, confidence says whether: below the floor a human decides, above the bar it runs, in between it asks. */
|
|
17
|
+
export declare function confidenceBand(confidence: number, floor?: number, bar?: number): ConfidenceBand;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Normalize a score answer to 0-1 against its own top level, so scales of different lengths compare. */
|
|
2
|
+
export function normalized(answer) {
|
|
3
|
+
const topLevel = Object.keys(answer.legend).length - 1;
|
|
4
|
+
return topLevel === 0 ? 0 : answer.score / topLevel;
|
|
5
|
+
}
|
|
6
|
+
/** Weighted sum of 0-1 parts. Weights are divided by their total, so they need not add up to 1. */
|
|
7
|
+
export function weighted(parts, weights) {
|
|
8
|
+
const keys = Object.keys(weights);
|
|
9
|
+
const total = keys.reduce((n, k) => n + weights[k], 0);
|
|
10
|
+
if (total <= 0)
|
|
11
|
+
throw new RangeError("weights must add up to more than 0");
|
|
12
|
+
return keys.reduce((n, k) => {
|
|
13
|
+
if (typeof parts[k] !== "number")
|
|
14
|
+
throw new RangeError(`missing part "${k}"`);
|
|
15
|
+
return n + (weights[k] / total) * parts[k];
|
|
16
|
+
}, 0);
|
|
17
|
+
}
|
|
18
|
+
/** The two confidence thresholds a gate reads. */
|
|
19
|
+
export const CONFIDENCE = {
|
|
20
|
+
REVIEW_FLOOR: 0.5, // below this: don't act, ask a human
|
|
21
|
+
DESTRUCTIVE_BAR: 0.9, // above this: act without confirmation
|
|
22
|
+
};
|
|
23
|
+
/** The answer says what, confidence says whether: below the floor a human decides, above the bar it runs, in between it asks. */
|
|
24
|
+
export function confidenceBand(confidence, floor = CONFIDENCE.REVIEW_FLOOR, bar = CONFIDENCE.DESTRUCTIVE_BAR) {
|
|
25
|
+
if (confidence < floor)
|
|
26
|
+
return "review";
|
|
27
|
+
if (confidence > bar)
|
|
28
|
+
return "act";
|
|
29
|
+
return "confirm";
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Guard } from "../hooks/handle.ts";
|
|
2
|
+
export type SetupHarness = "claude" | "codex";
|
|
3
|
+
export interface SetupOptions {
|
|
4
|
+
harness: SetupHarness;
|
|
5
|
+
/** The project directory. */
|
|
6
|
+
dir: string;
|
|
7
|
+
guards?: Guard[];
|
|
8
|
+
tools?: string[];
|
|
9
|
+
/** Run the Level 7 advisor hooks. */
|
|
10
|
+
compact?: boolean;
|
|
11
|
+
/** The bash and MCP gates ask the user when Jev is unsure or leans destructive. Claude Code only. */
|
|
12
|
+
ask?: boolean;
|
|
13
|
+
/** The bash and MCP gates approve what Jev is sure only reads. Claude Code only. */
|
|
14
|
+
allowReadOnly?: boolean;
|
|
15
|
+
/** A lab level: 6 means the guards, 7 the advisor and should_i_compact, 8 to 10 their tools. */
|
|
16
|
+
level?: number;
|
|
17
|
+
/** How the harness runs this package's bin. Defaults to node and its absolute path. */
|
|
18
|
+
command?: string[];
|
|
19
|
+
/** Extra `jev hook` flags, for example --lines or --window. */
|
|
20
|
+
hookArgs?: string[];
|
|
21
|
+
/** Hook timeout in seconds, as the harness counts it. */
|
|
22
|
+
timeoutSec?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface FileChange {
|
|
25
|
+
path: string;
|
|
26
|
+
before: string | null;
|
|
27
|
+
after: string;
|
|
28
|
+
changed: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare class SetupError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The bin this module ships with: dist/cli/index.js next to dist/setup/. Run through npx or
|
|
34
|
+
* pnpm dlx, it is `npx -y` on this exact version instead, which outlives the cache.
|
|
35
|
+
*/
|
|
36
|
+
export declare const defaultCommand: (bin?: string) => string[];
|
|
37
|
+
export declare const isJevHookCommand: (command: unknown) => boolean;
|
|
38
|
+
export interface Resolved {
|
|
39
|
+
guards: Guard[];
|
|
40
|
+
tools: string[];
|
|
41
|
+
compact: boolean;
|
|
42
|
+
ask?: boolean;
|
|
43
|
+
allowReadOnly?: boolean;
|
|
44
|
+
}
|
|
45
|
+
type Selection = Pick<SetupOptions, "guards" | "tools" | "compact" | "level" | "ask" | "allowReadOnly"> & {
|
|
46
|
+
harness?: SetupHarness;
|
|
47
|
+
};
|
|
48
|
+
export declare function resolveSelection(o: Selection): Resolved;
|
|
49
|
+
interface HookGroup {
|
|
50
|
+
matcher?: string;
|
|
51
|
+
hooks: {
|
|
52
|
+
type: "command";
|
|
53
|
+
command: string;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/** The hook entries, by event, that a selection needs. Same shape for Claude Code and Codex. */
|
|
58
|
+
export declare function hookGroups(harness: SetupHarness, sel: Resolved, command: string[], extra?: string[], timeoutSec?: number): Record<string, HookGroup[]>;
|
|
59
|
+
/** Drop every hook this tool wrote before, then add the new groups. Unrelated hooks stay in place. */
|
|
60
|
+
export declare function mergeHooks(existing: Record<string, HookGroup[]> | undefined, ours: Record<string, HookGroup[]>): Record<string, HookGroup[]>;
|
|
61
|
+
/** Claude Code: `.mcp.json` for the tools, `.claude/settings.json` for the hooks. */
|
|
62
|
+
export declare function planClaude(o: SetupOptions): FileChange[];
|
|
63
|
+
/** What the MCP server needs from the agent's environment. */
|
|
64
|
+
export declare const MCP_ENV_VARS: string[];
|
|
65
|
+
/** Our block of `.codex/config.toml`: the hooks as arrays of tables, the server as one table. */
|
|
66
|
+
export declare function codexBlock(sel: Resolved, command: string[], extra?: string[], timeoutSec?: number): string;
|
|
67
|
+
/**
|
|
68
|
+
* Codex: `.codex/config.toml`, loaded once the project is trusted. There is no TOML parser here, so
|
|
69
|
+
* the file is not rewritten: our entries live in one marked block at the end, replaced on every run.
|
|
70
|
+
*/
|
|
71
|
+
export declare function planCodex(o: SetupOptions): FileChange[];
|
|
72
|
+
/** One line for people: what the selection turns on. */
|
|
73
|
+
export declare function describeSelection(o: Selection): string;
|
|
74
|
+
export declare function plan(o: SetupOptions): FileChange[];
|
|
75
|
+
export declare function apply(changes: FileChange[]): void;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire Jev into a project's agent config: the MCP server for the tools, hook entries for the guards
|
|
3
|
+
* and the compaction advisor. Merges into what is there. Entries it wrote before are recognized by
|
|
4
|
+
* their marker (the `jev` server name, hook commands running `hook --harness <name>`) and replaced;
|
|
5
|
+
* everything else is left as it was. A file that is not valid JSON is refused, never overwritten.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { ALL_GUARDS, LEVEL6_GUARDS } from "../hooks/handle.js";
|
|
11
|
+
import { LEVEL_TOOLS, selectTools } from "../tools/sets.js";
|
|
12
|
+
export class SetupError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/** npx and pnpm dlx unpack into a cache they may clear, so a path into one would stop working. */
|
|
15
|
+
const EPHEMERAL = /[\\/](_npx|dlx)[\\/]/;
|
|
16
|
+
/**
|
|
17
|
+
* The bin this module ships with: dist/cli/index.js next to dist/setup/. Run through npx or
|
|
18
|
+
* pnpm dlx, it is `npx -y` on this exact version instead, which outlives the cache.
|
|
19
|
+
*/
|
|
20
|
+
export const defaultCommand = (bin = fileURLToPath(new URL(`../cli/index.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, import.meta.url))) => {
|
|
21
|
+
if (!EPHEMERAL.test(bin))
|
|
22
|
+
return [process.execPath, bin];
|
|
23
|
+
const pkg = JSON.parse(readFileSync(resolve(dirname(bin), "../../package.json"), "utf8"));
|
|
24
|
+
return ["npx", "-y", `${pkg.name}@${pkg.version}`];
|
|
25
|
+
};
|
|
26
|
+
const quote = (s) => (/^[A-Za-z0-9_./:@=,+-]+$/.test(s) ? s : JSON.stringify(s));
|
|
27
|
+
const MARKER = /\shook --harness (claude|codex)(\s|$)/;
|
|
28
|
+
export const isJevHookCommand = (command) => typeof command === "string" && MARKER.test(command);
|
|
29
|
+
export function resolveSelection(o) {
|
|
30
|
+
const guards = new Set(o.guards ?? []);
|
|
31
|
+
const tools = new Set(o.tools ?? []);
|
|
32
|
+
let compact = o.compact ?? false;
|
|
33
|
+
if (o.level !== undefined) {
|
|
34
|
+
if (o.level === 6)
|
|
35
|
+
LEVEL6_GUARDS.forEach((g) => guards.add(g));
|
|
36
|
+
else if (LEVEL_TOOLS[o.level]) {
|
|
37
|
+
LEVEL_TOOLS[o.level].forEach((t) => tools.add(t));
|
|
38
|
+
if (o.level === 7)
|
|
39
|
+
compact = true;
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
throw new SetupError(`level ${o.level} has nothing to set up; levels 6 to 10 do`);
|
|
43
|
+
}
|
|
44
|
+
for (const g of guards)
|
|
45
|
+
if (!ALL_GUARDS.includes(g))
|
|
46
|
+
throw new SetupError(`unknown guard ${g}; known: ${ALL_GUARDS.join(", ")}`);
|
|
47
|
+
try {
|
|
48
|
+
selectTools({ names: [...tools] });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
throw new SetupError(err.message);
|
|
52
|
+
}
|
|
53
|
+
if (!guards.size && !tools.size && !compact)
|
|
54
|
+
throw new SetupError("nothing to set up: pass --guards, --tools, --compact, or --level");
|
|
55
|
+
const ladder = o.ask || o.allowReadOnly;
|
|
56
|
+
if (ladder && !guards.has("bash") && !guards.has("mcp"))
|
|
57
|
+
throw new SetupError("--ask and --allow-read-only apply to the bash and mcp guards; add one of them to --guards");
|
|
58
|
+
if (o.harness === "codex" && (ladder || guards.has("mcp"))) {
|
|
59
|
+
throw new SetupError("Codex hooks can only deny, so the mcp guard, --ask, and --allow-read-only are Claude Code only");
|
|
60
|
+
}
|
|
61
|
+
return { guards: [...guards], tools: [...tools], compact, ...(o.ask ? { ask: true } : {}), ...(o.allowReadOnly ? { allowReadOnly: true } : {}) };
|
|
62
|
+
}
|
|
63
|
+
/** The hook entries, by event, that a selection needs. Same shape for Claude Code and Codex. */
|
|
64
|
+
export function hookGroups(harness, sel, command, extra = [], timeoutSec = 10) {
|
|
65
|
+
const run = (event, flags) => ({ type: "command", command: [...command, "hook", "--harness", harness, "--event", event, ...flags, ...extra].map(quote).join(" "), timeout: timeoutSec });
|
|
66
|
+
const out = {};
|
|
67
|
+
const guardFlag = (gs) => ["--guards", gs.join(",")];
|
|
68
|
+
const ladder = [...(sel.ask ? ["--ask"] : []), ...(sel.allowReadOnly ? ["--allow-read-only"] : [])];
|
|
69
|
+
const pre = [];
|
|
70
|
+
if (sel.guards.includes("bash"))
|
|
71
|
+
pre.push("Bash");
|
|
72
|
+
if (sel.guards.includes("write"))
|
|
73
|
+
pre.push(...(harness === "codex" ? ["apply_patch"] : ["Write", "Edit", "MultiEdit", "NotebookEdit"]));
|
|
74
|
+
const preGuards = sel.guards.filter((g) => g === "bash" || g === "write");
|
|
75
|
+
if (pre.length)
|
|
76
|
+
out.PreToolUse = [{ matcher: pre.join("|"), hooks: [run("PreToolUse", [...guardFlag(preGuards), ...(sel.guards.includes("bash") ? ladder : [])])] }];
|
|
77
|
+
if (sel.guards.includes("mcp"))
|
|
78
|
+
(out.PreToolUse ??= []).push({ matcher: "mcp__.*", hooks: [run("PreToolUse", [...guardFlag(["mcp"]), ...ladder])] });
|
|
79
|
+
if (sel.guards.includes("result"))
|
|
80
|
+
out.PostToolUse = [{ matcher: harness === "codex" ? "Bash" : "Read|Bash", hooks: [run("PostToolUse", guardFlag(["result"]))] }];
|
|
81
|
+
if (sel.compact) {
|
|
82
|
+
out.UserPromptSubmit = [{ hooks: [run("UserPromptSubmit", ["--compact"])] }];
|
|
83
|
+
out.Stop = [{ hooks: [run("Stop", ["--compact"])] }];
|
|
84
|
+
out.PreCompact = [{ hooks: [run("PreCompact", [])] }];
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Drop every hook this tool wrote before, then add the new groups. Unrelated hooks stay in place. */
|
|
89
|
+
export function mergeHooks(existing, ours) {
|
|
90
|
+
const merged = {};
|
|
91
|
+
for (const [event, groups] of Object.entries(existing ?? {})) {
|
|
92
|
+
const kept = (Array.isArray(groups) ? groups : [])
|
|
93
|
+
.map((g) => ({ ...g, hooks: (g?.hooks ?? []).filter((h) => !isJevHookCommand(h?.command)) }))
|
|
94
|
+
.filter((g) => g.hooks.length);
|
|
95
|
+
if (kept.length)
|
|
96
|
+
merged[event] = kept;
|
|
97
|
+
}
|
|
98
|
+
for (const [event, groups] of Object.entries(ours))
|
|
99
|
+
merged[event] = [...(merged[event] ?? []), ...groups];
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
function readJson(path) {
|
|
103
|
+
if (!existsSync(path))
|
|
104
|
+
return { text: null, value: {} };
|
|
105
|
+
const text = readFileSync(path, "utf8");
|
|
106
|
+
if (!text.trim())
|
|
107
|
+
return { text, value: {} };
|
|
108
|
+
try {
|
|
109
|
+
const value = JSON.parse(text);
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
111
|
+
throw new Error("not a JSON object");
|
|
112
|
+
return { text, value };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new SetupError(`${path} is not valid JSON (${err.message}); fix or move it, then run jev setup again. Nothing was written.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const change = (path, before, value) => {
|
|
119
|
+
const after = JSON.stringify(value, null, 2) + "\n";
|
|
120
|
+
return { path, before, after, changed: before === null ? true : JSON.stringify(JSON.parse(before)) !== JSON.stringify(value) };
|
|
121
|
+
};
|
|
122
|
+
/** Claude Code: `.mcp.json` for the tools, `.claude/settings.json` for the hooks. */
|
|
123
|
+
export function planClaude(o) {
|
|
124
|
+
const sel = resolveSelection(o);
|
|
125
|
+
const command = o.command ?? defaultCommand();
|
|
126
|
+
const dir = resolve(o.dir);
|
|
127
|
+
const out = [];
|
|
128
|
+
const mcpPath = join(dir, ".mcp.json");
|
|
129
|
+
const mcp = readJson(mcpPath);
|
|
130
|
+
const servers = { ...(mcp.value.mcpServers ?? {}) };
|
|
131
|
+
if (sel.tools.length)
|
|
132
|
+
servers.jev = { command: command[0], args: [...command.slice(1), "mcp", "--tools", sel.tools.join(",")] };
|
|
133
|
+
else
|
|
134
|
+
delete servers.jev;
|
|
135
|
+
if (sel.tools.length || mcp.text !== null)
|
|
136
|
+
out.push(change(mcpPath, mcp.text, { ...mcp.value, mcpServers: servers }));
|
|
137
|
+
const settingsPath = join(dir, ".claude", "settings.json");
|
|
138
|
+
const settings = readJson(settingsPath);
|
|
139
|
+
const hooks = mergeHooks(settings.value.hooks, hookGroups("claude", sel, command, o.hookArgs, o.timeoutSec));
|
|
140
|
+
const next = { ...settings.value };
|
|
141
|
+
if (Object.keys(hooks).length)
|
|
142
|
+
next.hooks = hooks;
|
|
143
|
+
else
|
|
144
|
+
delete next.hooks;
|
|
145
|
+
if (Object.keys(hooks).length || settings.text !== null)
|
|
146
|
+
out.push(change(settingsPath, settings.text, next));
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
const BLOCK_START = "# >>> jev setup: managed by `jev setup`; this block is replaced on every run";
|
|
150
|
+
const BLOCK_END = "# <<< jev setup";
|
|
151
|
+
/** What the MCP server needs from the agent's environment. */
|
|
152
|
+
export const MCP_ENV_VARS = ["TYPESAFE_API_KEY", "OPENROUTER_API_KEY", "JEV_EVENT_DIR", "JEV_STATE_DIR"];
|
|
153
|
+
const tomlString = (s) => JSON.stringify(s); // JSON escapes are valid TOML basic-string escapes
|
|
154
|
+
const tomlArray = (xs) => `[${xs.map(tomlString).join(", ")}]`;
|
|
155
|
+
/** Our block of `.codex/config.toml`: the hooks as arrays of tables, the server as one table. */
|
|
156
|
+
export function codexBlock(sel, command, extra = [], timeoutSec = 10) {
|
|
157
|
+
const lines = [BLOCK_START];
|
|
158
|
+
for (const [event, groups] of Object.entries(hookGroups("codex", sel, command, extra, timeoutSec))) {
|
|
159
|
+
for (const g of groups) {
|
|
160
|
+
lines.push("", `[[hooks.${event}]]`);
|
|
161
|
+
if (g.matcher)
|
|
162
|
+
lines.push(`matcher = ${tomlString(g.matcher)}`);
|
|
163
|
+
for (const h of g.hooks)
|
|
164
|
+
lines.push(`[[hooks.${event}.hooks]]`, `type = "command"`, `command = ${tomlString(h.command)}`, `timeout = ${h.timeout ?? timeoutSec}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (sel.tools.length) {
|
|
168
|
+
lines.push("", "[mcp_servers.jev]", `command = ${tomlString(command[0])}`, `args = ${tomlArray([...command.slice(1), "mcp", "--tools", sel.tools.join(",")])}`, `default_tools_approval_mode = "approve"`,
|
|
169
|
+
// Codex starts MCP servers with a clean environment; hooks inherit it, servers do not.
|
|
170
|
+
`env_vars = ${tomlArray(MCP_ENV_VARS)}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push(BLOCK_END);
|
|
173
|
+
return lines.join("\n") + "\n";
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Codex: `.codex/config.toml`, loaded once the project is trusted. There is no TOML parser here, so
|
|
177
|
+
* the file is not rewritten: our entries live in one marked block at the end, replaced on every run.
|
|
178
|
+
*/
|
|
179
|
+
export function planCodex(o) {
|
|
180
|
+
const sel = resolveSelection(o);
|
|
181
|
+
const command = o.command ?? defaultCommand();
|
|
182
|
+
const path = join(resolve(o.dir), ".codex", "config.toml");
|
|
183
|
+
const before = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
184
|
+
const text = before ?? "";
|
|
185
|
+
const start = text.indexOf(BLOCK_START);
|
|
186
|
+
const end = text.indexOf(BLOCK_END);
|
|
187
|
+
if ((start === -1) !== (end === -1) || end < start) {
|
|
188
|
+
throw new SetupError(`${path} has a broken jev setup block (a start or end marker is missing); fix or remove it, then run jev setup again. Nothing was written.`);
|
|
189
|
+
}
|
|
190
|
+
const outside = start === -1 ? text : text.slice(0, start) + text.slice(end + BLOCK_END.length);
|
|
191
|
+
if (/^\s*\[\s*mcp_servers\.jev\s*\]/m.test(outside)) {
|
|
192
|
+
throw new SetupError(`${path} already defines [mcp_servers.jev] outside the jev setup block; remove it or rename it, then run jev setup again. Nothing was written.`);
|
|
193
|
+
}
|
|
194
|
+
const block = codexBlock(sel, command, o.hookArgs, o.timeoutSec);
|
|
195
|
+
const rest = outside.replace(/\s+$/, "");
|
|
196
|
+
const after = (rest ? rest + "\n\n" : "") + block;
|
|
197
|
+
return [{ path, before, after, changed: before !== after }];
|
|
198
|
+
}
|
|
199
|
+
/** One line for people: what the selection turns on. */
|
|
200
|
+
export function describeSelection(o) {
|
|
201
|
+
const sel = resolveSelection(o);
|
|
202
|
+
const parts = [
|
|
203
|
+
sel.guards.length ? `guards ${sel.guards.join(",")}` : "",
|
|
204
|
+
sel.ask ? "ask when unsure" : "",
|
|
205
|
+
sel.allowReadOnly ? "approve read only" : "",
|
|
206
|
+
sel.compact ? "compaction advisor" : "",
|
|
207
|
+
sel.tools.length ? `tools ${sel.tools.join(",")}` : "",
|
|
208
|
+
].filter(Boolean);
|
|
209
|
+
return parts.join("; ");
|
|
210
|
+
}
|
|
211
|
+
export function plan(o) {
|
|
212
|
+
return o.harness === "claude" ? planClaude(o) : planCodex(o);
|
|
213
|
+
}
|
|
214
|
+
export function apply(changes) {
|
|
215
|
+
for (const c of changes) {
|
|
216
|
+
if (!c.changed)
|
|
217
|
+
continue;
|
|
218
|
+
mkdirSync(dirname(c.path), { recursive: true });
|
|
219
|
+
writeFileSync(c.path, c.after);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RunCommand } from "../files/assemble.ts";
|
|
2
|
+
import { type JevTool } from "./types.ts";
|
|
3
|
+
/** The usage guidance: a system prompt line under pi, server instructions under MCP. */
|
|
4
|
+
export declare const ASK_JEV_NUDGE: string;
|
|
5
|
+
/** `run` is the unguarded runner the gate wraps; tests pass a fake. */
|
|
6
|
+
export declare function makeAskJev(run?: RunCommand): JevTool;
|
|
7
|
+
export declare const askJev: JevTool;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10: ask_jev on any situation. The agent passes its own state, paths for code to read,
|
|
3
|
+
* a command for code to run, and its question block. Code assembles one state and makes one call.
|
|
4
|
+
* A command runs through the bash gate first. Over the budget, the error names the parts and a split.
|
|
5
|
+
*/
|
|
6
|
+
import { parseQuestions } from "../files/ask-files.js";
|
|
7
|
+
import { AskStateError, assembleState } from "../files/assemble.js";
|
|
8
|
+
import { ASK_JEV_DESCRIPTION } from "./description.js";
|
|
9
|
+
import { gatedRunner } from "./run-command.js";
|
|
10
|
+
import { fail, object, ok, str } from "./types.js";
|
|
11
|
+
/** The usage guidance: a system prompt line under pi, server instructions under MCP. */
|
|
12
|
+
export const ASK_JEV_NUDGE = "You have ask_jev. When you need a classification, a risk score, or a yes or no with a confidence, " +
|
|
13
|
+
"prefer it over reasoning it out yourself. Give it paths or a command instead of pasting content. " +
|
|
14
|
+
"It answers in 300 ms and costs almost nothing.";
|
|
15
|
+
/** `run` is the unguarded runner the gate wraps; tests pass a fake. */
|
|
16
|
+
export function makeAskJev(run) {
|
|
17
|
+
return {
|
|
18
|
+
name: "ask_jev",
|
|
19
|
+
label: "Ask Jev",
|
|
20
|
+
description: ASK_JEV_DESCRIPTION,
|
|
21
|
+
parameters: object({
|
|
22
|
+
questions_json: str("The question block, a JSON object keyed by question id"),
|
|
23
|
+
state: str("Your own state: plain text, or a JSON object as a string. Short. Not for pasting files or output."),
|
|
24
|
+
paths: { type: "array", items: { type: "string" }, description: "Files or globs for code to read into files[\"path\"]. Up to 20 files." },
|
|
25
|
+
command: str("A command for code to run in the repo; its result goes into output. Runs through the bash gate."),
|
|
26
|
+
}, ["questions_json"]),
|
|
27
|
+
async execute(p, ctx) {
|
|
28
|
+
try {
|
|
29
|
+
const questions = parseQuestions(p.questions_json);
|
|
30
|
+
const { state, summary } = await assembleState({ state: p.state, paths: p.paths, command: p.command }, ctx.cwd, gatedRunner(ctx, run));
|
|
31
|
+
const result = await ctx.decide("ask_jev", state, questions, { summary });
|
|
32
|
+
return ok({ answers: result.answers, state_summary: summary, usage: result.usage, model: result.model });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const text = err instanceof AskStateError ? err.message : `ask_jev error: ${err?.message ?? err}`;
|
|
36
|
+
ctx.report("error", { hook: "ask_jev", message: text });
|
|
37
|
+
return fail(text);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const askJev = makeAskJev();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 7, option B: should_i_compact on demand. The same four questions and tiers as the turn-end
|
|
3
|
+
* hook, but the agent asks. The harness supplies the session view; without one the tool says so.
|
|
4
|
+
*/
|
|
5
|
+
import { compactVerdict } from "../compact/compact-on-demand.js";
|
|
6
|
+
import { evaluateCompaction } from "../compact/evaluate.js";
|
|
7
|
+
import { fail, ok } from "./types.js";
|
|
8
|
+
const NO_SIGNALS = {
|
|
9
|
+
switched_gears: { type: "noul", noul: 0 }, at_boundary: { type: "noul", noul: 0 },
|
|
10
|
+
needs_history: { type: "score", score: 2 }, mid_operation: { type: "noul", noul: 0 },
|
|
11
|
+
};
|
|
12
|
+
export const shouldICompact = {
|
|
13
|
+
name: "should_i_compact",
|
|
14
|
+
label: "Should I compact",
|
|
15
|
+
description: "Ask whether now is a good moment to compact the conversation. Returns should_compact, a tier " +
|
|
16
|
+
"(silent, notice, recommend, request), the reason, and the current context percent. Cheap, call it freely.",
|
|
17
|
+
parameters: { type: "object", properties: {} },
|
|
18
|
+
async execute(_p, ctx) {
|
|
19
|
+
try {
|
|
20
|
+
const source = ctx.compaction;
|
|
21
|
+
const input = source ? await source.input() : null;
|
|
22
|
+
if (!source || !input) {
|
|
23
|
+
return fail("should_i_compact error: no session history is available to this tool. Enable the Level 7 hooks (jev setup --level 7) so the session is recorded.");
|
|
24
|
+
}
|
|
25
|
+
const r = await evaluateCompaction(input, "should_i_compact", ctx.decide, ctx.report, source.action);
|
|
26
|
+
source.onEvaluation?.(r);
|
|
27
|
+
return ok(compactVerdict(r.decision, r.answers ?? NO_SIGNALS, r.usage, input.lines, source.action));
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return fail(`should_i_compact error: ${err?.message ?? err}`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10, option B: the tool description is the teaching.
|
|
3
|
+
* The prompt says what to ask. The description says how: what Jev is, the three question types, when a typed answer beats reasoning, and when it does not.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ASK_JEV_DESCRIPTION: string;
|