@bobobowis/jev-kit 0.0.0-stage → 0.1.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 +240 -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 +82 -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 +79 -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 +29 -0
- package/dist/guards/bash-gate.js +35 -0
- package/dist/guards/index.d.ts +6 -0
- package/dist/guards/index.js +6 -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 +62 -0
- package/dist/hooks/handle.js +48 -0
- package/dist/hooks/harness.d.ts +40 -0
- package/dist/hooks/harness.js +116 -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 +67 -0
- package/dist/setup/index.js +209 -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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every subcommand shares: the client, the reporter, list flags, stdin, and a deadline.
|
|
3
|
+
*/
|
|
4
|
+
import { JevClient, sharedJev } from "../core/client.js";
|
|
5
|
+
import { createDecider, createReporter } from "../events/index.js";
|
|
6
|
+
export const processIo = () => ({ stdout: process.stdout, stderr: process.stderr, stdin: process.stdin });
|
|
7
|
+
/**
|
|
8
|
+
* `--mock` selects the offline mock explicitly; otherwise the client picks TypeSafe or OpenRouter
|
|
9
|
+
* from the keys. A deadline gets its own client, so a call cut off by it is aborted, not left running.
|
|
10
|
+
*/
|
|
11
|
+
export function clientFor(mock, timeoutMs) {
|
|
12
|
+
if (!mock && !timeoutMs)
|
|
13
|
+
return sharedJev;
|
|
14
|
+
let client;
|
|
15
|
+
return () => (client ??= new JevClient(mock ? { provider: "mock" } : { timeoutMs }));
|
|
16
|
+
}
|
|
17
|
+
export function deciderFor(harness, opts) {
|
|
18
|
+
const reporter = createReporter({ harness, quiet: opts.quiet, stderr: opts.stderr });
|
|
19
|
+
return { reporter, decide: createDecider(reporter, opts.client ?? clientFor(opts.mock, opts.timeoutMs)) };
|
|
20
|
+
}
|
|
21
|
+
export const list = (raw) => (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
22
|
+
export async function readAll(stdin) {
|
|
23
|
+
let text = "";
|
|
24
|
+
for await (const chunk of stdin)
|
|
25
|
+
text += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
/** Resolve with the work, or with `fallback` once `ms` pass. The work keeps running but nobody waits. */
|
|
29
|
+
export async function withDeadline(work, ms, fallback) {
|
|
30
|
+
let timer;
|
|
31
|
+
const late = new Promise((resolve) => { timer = setTimeout(() => resolve(fallback()), ms); });
|
|
32
|
+
try {
|
|
33
|
+
return await Promise.race([work, late]);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class UsageError extends Error {
|
|
40
|
+
}
|
package/dist/cli/hook.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jev hook: run as a Claude Code or Codex hook. Reads the hook payload on stdin, writes the
|
|
3
|
+
* harness's response on stdout. Fails open: a Jev error, a timeout, or a bad payload allows the
|
|
4
|
+
* action. Never exits 2, which both harnesses read as "block".
|
|
5
|
+
*/
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { ALL_GUARDS, runHarnessHook, HOOK_LINES } from "../hooks/index.js";
|
|
8
|
+
import { deciderFor, list, readAll, withDeadline } from "./common.js";
|
|
9
|
+
export const HOOK_HELP = `Usage: jev hook --harness claude|codex --event <HookEvent> [options]
|
|
10
|
+
|
|
11
|
+
Reads the hook payload on stdin and answers on stdout. Exit 0 always, except 1 for bad options.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--harness <name> claude or codex
|
|
15
|
+
--event <name> PreToolUse, PostToolUse, UserPromptSubmit, Stop, PreCompact
|
|
16
|
+
--guards <list> bash,write,result (default: all)
|
|
17
|
+
--compact run the Level 7 advisor on UserPromptSubmit and Stop
|
|
18
|
+
--lines <n,n,n> notice, recommend, request lines in context tokens (default ${HOOK_LINES.notice},${HOOK_LINES.recommend},${HOOK_LINES.request})
|
|
19
|
+
--window <tokens> the model's context window, for percentages
|
|
20
|
+
--timeout-ms <ms> give up and allow after this long (default 8000)
|
|
21
|
+
--mock use the offline mock instead of a Jev key
|
|
22
|
+
`;
|
|
23
|
+
export async function hook(argv, io) {
|
|
24
|
+
let values;
|
|
25
|
+
try {
|
|
26
|
+
({ values } = parseArgs({
|
|
27
|
+
args: argv, strict: true,
|
|
28
|
+
options: {
|
|
29
|
+
harness: { type: "string" }, event: { type: "string" }, guards: { type: "string" }, compact: { type: "boolean" },
|
|
30
|
+
lines: { type: "string" }, window: { type: "string" }, "timeout-ms": { type: "string" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
io.stderr.write(`jev hook: ${err.message}\n\n${HOOK_HELP}`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (values.help) {
|
|
39
|
+
io.stdout.write(HOOK_HELP);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
const harness = values.harness;
|
|
43
|
+
if (harness !== "claude" && harness !== "codex") {
|
|
44
|
+
io.stderr.write(`jev hook: --harness must be claude or codex\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (!values.event) {
|
|
48
|
+
io.stderr.write(`jev hook: --event is required\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const guards = values.guards ? list(values.guards) : ALL_GUARDS;
|
|
52
|
+
const unknown = guards.filter((g) => !ALL_GUARDS.includes(g));
|
|
53
|
+
if (unknown.length) {
|
|
54
|
+
io.stderr.write(`jev hook: unknown guard ${unknown.join(", ")}; known: ${ALL_GUARDS.join(", ")}\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
let lines = HOOK_LINES;
|
|
58
|
+
if (values.lines) {
|
|
59
|
+
const [notice, recommend, request] = list(values.lines).map(Number);
|
|
60
|
+
if (![notice, recommend, request].every((n) => Number.isFinite(n) && n > 0) || !(notice < recommend && recommend < request)) {
|
|
61
|
+
io.stderr.write(`jev hook: --lines needs three increasing token counts, for example 50000,100000,150000\n`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
lines = { notice, recommend, request };
|
|
65
|
+
}
|
|
66
|
+
const window = values.window ? Number(values.window) : null;
|
|
67
|
+
const timeoutMs = values["timeout-ms"] ? Number(values["timeout-ms"]) : 8000;
|
|
68
|
+
const { reporter, decide } = deciderFor(harness, { mock: values.mock, stderr: io.stderr, timeoutMs, client: io.client });
|
|
69
|
+
let payload;
|
|
70
|
+
try {
|
|
71
|
+
payload = JSON.parse(await readAll(io.stdin));
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
reporter.report("error", { hook: values.event, message: `unreadable hook payload: ${err?.message ?? err}` });
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const response = await withDeadline(runHarnessHook(values.event, payload, { harness, guards, compact: values.compact, lines, window, decide, report: (k, p) => reporter.report(k, p) })
|
|
78
|
+
.catch((err) => { reporter.report("error", { hook: values.event, message: err?.message ?? String(err) }); return null; }), timeoutMs, () => { reporter.report("error", { hook: values.event, message: `timed out after ${timeoutMs} ms; allowed` }); return null; });
|
|
79
|
+
if (response)
|
|
80
|
+
io.stdout.write(JSON.stringify(response));
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `jev` bin. Everything lives in main.ts, so tests can call it without a process.
|
|
4
|
+
*/
|
|
5
|
+
import { main } from "./main.js";
|
|
6
|
+
import { processIo } from "./common.js";
|
|
7
|
+
// A reader that stops early (`jev files ... | head -1`) closes the pipe: that ends the output, not an error.
|
|
8
|
+
process.stdout.on("error", (err) => {
|
|
9
|
+
if (err.code === "EPIPE")
|
|
10
|
+
process.exit(process.exitCode ?? 0);
|
|
11
|
+
throw err;
|
|
12
|
+
});
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
main(argv, processIo()).then((code) => {
|
|
15
|
+
// A hook must exit as soon as it has answered: a Jev call abandoned at the deadline would
|
|
16
|
+
// otherwise hold the process, and the harness waits for it. Exit once stdout has flushed.
|
|
17
|
+
if (argv[0] === "hook")
|
|
18
|
+
process.stdout.write("", () => process.exit(code));
|
|
19
|
+
else
|
|
20
|
+
process.exitCode = code;
|
|
21
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `jev` command: one bin, one subcommand per delivery surface.
|
|
3
|
+
*/
|
|
4
|
+
import { HOOK_HELP } from "./hook.ts";
|
|
5
|
+
import type { Io } from "./common.ts";
|
|
6
|
+
export { HOOK_HELP };
|
|
7
|
+
export declare function helpText(): string;
|
|
8
|
+
export declare function main(argv: string[], io: Io): Promise<number>;
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `jev` command: one bin, one subcommand per delivery surface.
|
|
3
|
+
*/
|
|
4
|
+
import { hook, HOOK_HELP } from "./hook.js";
|
|
5
|
+
import { setup } from "./setup.js";
|
|
6
|
+
import { mcp } from "./mcp.js";
|
|
7
|
+
import { ask, files, gate, guard } from "./ask.js";
|
|
8
|
+
const SUBCOMMANDS = [
|
|
9
|
+
["ask", "Ask a question block about a state, files, or a command's output; JSON on stdout", ask],
|
|
10
|
+
["files", "Ask the same question block about each file, in parallel", files],
|
|
11
|
+
["gate", "One yes or no question; exit 0 yes, 1 no, 2 usage, 3 Jev error", gate],
|
|
12
|
+
["guard", "Check a shell command or a file write with the Level 6 gates; exit 0 allow, 1 block", guard],
|
|
13
|
+
["setup", "Write or merge the Jev hooks and MCP server into a project's Claude Code or Codex config", setup],
|
|
14
|
+
["hook", "Run as a Claude Code or Codex hook: guards and the compaction advisor", hook],
|
|
15
|
+
["mcp", "Serve the Jev tools over stdio MCP", mcp],
|
|
16
|
+
];
|
|
17
|
+
export { HOOK_HELP };
|
|
18
|
+
export function helpText() {
|
|
19
|
+
const width = Math.max(...SUBCOMMANDS.map(([n]) => n.length));
|
|
20
|
+
return [
|
|
21
|
+
"Usage: jev <command> [options]",
|
|
22
|
+
"",
|
|
23
|
+
"Commands:",
|
|
24
|
+
...SUBCOMMANDS.map(([n, s]) => ` ${n.padEnd(width)} ${s}`),
|
|
25
|
+
"",
|
|
26
|
+
"Run `jev <command> --help` for a command's options.",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
export async function main(argv, io) {
|
|
30
|
+
const [command, ...rest] = argv;
|
|
31
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
32
|
+
io.stdout.write(helpText() + "\n");
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const entry = SUBCOMMANDS.find(([n]) => n === command);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
io.stderr.write(`jev: unknown command "${command}".\n\n${helpText()}\n`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
return entry[2](rest, io);
|
|
41
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Io } from "./common.ts";
|
|
2
|
+
export declare const MCP_HELP = "Usage: jev mcp [--level 7..10 | --tools a,b] [--cwd dir] [--mock]\n\nServes the Jev tools over stdio MCP. Paths resolve against --cwd (default: where it was started).\nWith neither --level nor --tools it serves all of them.\n";
|
|
3
|
+
export declare function mcp(argv: string[], io: Io): Promise<number>;
|
package/dist/cli/mcp.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jev mcp: serve the Jev tools over stdio MCP, by level or by tool name.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { serve } from "../mcp/server.js";
|
|
8
|
+
import { selectTools } from "../tools/sets.js";
|
|
9
|
+
import { deciderFor, list } from "./common.js";
|
|
10
|
+
export const MCP_HELP = `Usage: jev mcp [--level 7..10 | --tools a,b] [--cwd dir] [--mock]
|
|
11
|
+
|
|
12
|
+
Serves the Jev tools over stdio MCP. Paths resolve against --cwd (default: where it was started).
|
|
13
|
+
With neither --level nor --tools it serves all of them.
|
|
14
|
+
`;
|
|
15
|
+
const version = () => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export async function mcp(argv, io) {
|
|
24
|
+
let values;
|
|
25
|
+
try {
|
|
26
|
+
({ values } = parseArgs({ args: argv, strict: true, options: { level: { type: "string" }, tools: { type: "string" }, cwd: { type: "string" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" } } }));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
io.stderr.write(`jev mcp: ${err.message}\n\n${MCP_HELP}`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
if (values.help) {
|
|
33
|
+
io.stdout.write(MCP_HELP);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
let tools;
|
|
37
|
+
try {
|
|
38
|
+
tools = selectTools({ level: values.level !== undefined ? Number(values.level) : undefined, names: values.tools ? list(values.tools) : undefined });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
io.stderr.write(`jev mcp: ${err.message}\n`);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
const { reporter, decide } = deciderFor("mcp", { mock: values.mock, stderr: io.stderr, client: io.client });
|
|
45
|
+
await serve({ tools, cwd: resolve(values.cwd ?? process.cwd()), decide, report: (k, p) => reporter.report(k, p), version: version() });
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Io } from "./common.ts";
|
|
2
|
+
export declare const SETUP_HELP = "Usage: jev setup --harness claude|codex [options]\n\nWrites or merges the Jev entries into the project's agent config, leaving everything else alone.\nClaude Code: .mcp.json and .claude/settings.json. Codex: a marked block in .codex/config.toml.\nEach run sets the whole selection: running it again replaces only what it set up before.\n\nOptions:\n --harness <name> claude or codex\n --guards <list> bash,write,result: the Level 6 hooks\n --tools <list> Jev tools to serve over MCP, for example ask_jev,ask_jev_files\n --compact the Level 7 advisor hooks\n --level <n> a lab level: 6 guards, 7 advisor, 8 to 10 their tools\n --out <dir> the project directory (default: the current one)\n --command <cmd> how to run jev in the hooks and the MCP entry (default: node and this package's bin)\n --hook-args <args> extra jev hook flags, for example \"--lines 60000,120000,180000\"\n --dry-run print what would change, write nothing\n";
|
|
3
|
+
export declare function setup(argv: string[], io: Io): Promise<number>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jev setup: wire the Jev hooks and MCP server into a project's Claude Code or Codex config.
|
|
3
|
+
*/
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { relative } from "node:path";
|
|
6
|
+
import { toPosix } from "../files/read-state.js";
|
|
7
|
+
import { apply, describeSelection, plan, SetupError } from "../setup/index.js";
|
|
8
|
+
import { list } from "./common.js";
|
|
9
|
+
export const SETUP_HELP = `Usage: jev setup --harness claude|codex [options]
|
|
10
|
+
|
|
11
|
+
Writes or merges the Jev entries into the project's agent config, leaving everything else alone.
|
|
12
|
+
Claude Code: .mcp.json and .claude/settings.json. Codex: a marked block in .codex/config.toml.
|
|
13
|
+
Each run sets the whole selection: running it again replaces only what it set up before.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--harness <name> claude or codex
|
|
17
|
+
--guards <list> bash,write,result: the Level 6 hooks
|
|
18
|
+
--tools <list> Jev tools to serve over MCP, for example ask_jev,ask_jev_files
|
|
19
|
+
--compact the Level 7 advisor hooks
|
|
20
|
+
--level <n> a lab level: 6 guards, 7 advisor, 8 to 10 their tools
|
|
21
|
+
--out <dir> the project directory (default: the current one)
|
|
22
|
+
--command <cmd> how to run jev in the hooks and the MCP entry (default: node and this package's bin)
|
|
23
|
+
--hook-args <args> extra jev hook flags, for example "--lines 60000,120000,180000"
|
|
24
|
+
--dry-run print what would change, write nothing
|
|
25
|
+
`;
|
|
26
|
+
export async function setup(argv, io) {
|
|
27
|
+
let values;
|
|
28
|
+
try {
|
|
29
|
+
({ values } = parseArgs({
|
|
30
|
+
args: argv, strict: true,
|
|
31
|
+
options: {
|
|
32
|
+
harness: { type: "string" }, guards: { type: "string" }, tools: { type: "string" }, compact: { type: "boolean" },
|
|
33
|
+
level: { type: "string" }, out: { type: "string" }, command: { type: "string" }, "hook-args": { type: "string" },
|
|
34
|
+
"dry-run": { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
io.stderr.write(`jev setup: ${err.message}\n\n${SETUP_HELP}`);
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
if (values.help) {
|
|
43
|
+
io.stdout.write(SETUP_HELP);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
const harness = values.harness;
|
|
47
|
+
if (harness !== "claude" && harness !== "codex") {
|
|
48
|
+
io.stderr.write("jev setup: --harness must be claude or codex\n");
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const dir = values.out ?? process.cwd();
|
|
52
|
+
try {
|
|
53
|
+
const selection = {
|
|
54
|
+
guards: values.guards ? list(values.guards) : undefined,
|
|
55
|
+
tools: values.tools ? list(values.tools) : undefined,
|
|
56
|
+
compact: values.compact,
|
|
57
|
+
level: values.level !== undefined ? Number(values.level) : undefined,
|
|
58
|
+
};
|
|
59
|
+
const changes = plan({
|
|
60
|
+
harness, dir, ...selection,
|
|
61
|
+
command: values.command ? values.command.split(/\s+/).filter(Boolean) : undefined,
|
|
62
|
+
hookArgs: values["hook-args"] ? values["hook-args"].split(/\s+/).filter(Boolean) : undefined,
|
|
63
|
+
});
|
|
64
|
+
const changed = changes.filter((c) => c.changed);
|
|
65
|
+
if (values["dry-run"]) {
|
|
66
|
+
for (const c of changes)
|
|
67
|
+
io.stdout.write(`--- ${toPosix(relative(dir, c.path) || c.path)}${c.changed ? "" : " (unchanged)"}\n${c.after}`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
apply(changes);
|
|
71
|
+
io.stdout.write(changed.length ? `jev setup: wrote ${changed.map((c) => toPosix(relative(dir, c.path))).join(", ")}\n` : "jev setup: no changes\n");
|
|
72
|
+
io.stdout.write(`jev setup: on: ${describeSelection(selection)}. Running it again replaces this selection.\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
io.stderr.write(`jev setup: ${err instanceof SetupError ? err.message : err?.message ?? err}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 7, option B: should_i_compact on demand.
|
|
3
|
+
* The same four questions and the same tiers, but the agent asks. The tool returns a typed verdict it can act on instead of a nudge it might miss.
|
|
4
|
+
*/
|
|
5
|
+
import type { CompactAction, CompactAnswers, CompactDecision, Lines, Usage } from "./should-compact.ts";
|
|
6
|
+
export interface CompactVerdict {
|
|
7
|
+
should_compact: boolean;
|
|
8
|
+
tier: CompactDecision["tier"];
|
|
9
|
+
reason: string;
|
|
10
|
+
context_tokens: number | null;
|
|
11
|
+
context_percent: number | null;
|
|
12
|
+
lines: Lines;
|
|
13
|
+
signals: {
|
|
14
|
+
switched_gears: number;
|
|
15
|
+
at_boundary: number;
|
|
16
|
+
needs_history: number;
|
|
17
|
+
mid_operation: number;
|
|
18
|
+
};
|
|
19
|
+
next_step: string;
|
|
20
|
+
}
|
|
21
|
+
/** The tool result the agent reads. Booleans and numbers first, prose last. */
|
|
22
|
+
export declare function compactVerdict(d: CompactDecision, a: CompactAnswers, usage: Usage | null, lines: Lines, action?: CompactAction): CompactVerdict;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The tool result the agent reads. Booleans and numbers first, prose last. */
|
|
2
|
+
export function compactVerdict(d, a, usage, lines, action = "compact_now") {
|
|
3
|
+
const should = d.tier === "recommend" || d.tier === "request";
|
|
4
|
+
return {
|
|
5
|
+
should_compact: should,
|
|
6
|
+
tier: d.tier,
|
|
7
|
+
reason: d.reason,
|
|
8
|
+
context_tokens: usage ? usage.tokens : null,
|
|
9
|
+
context_percent: usage && usage.pct !== null ? Number(usage.pct.toFixed(1)) : null,
|
|
10
|
+
lines,
|
|
11
|
+
signals: {
|
|
12
|
+
switched_gears: a.switched_gears.noul,
|
|
13
|
+
at_boundary: a.at_boundary.noul,
|
|
14
|
+
needs_history: a.needs_history.score,
|
|
15
|
+
mid_operation: a.mid_operation.noul,
|
|
16
|
+
},
|
|
17
|
+
next_step: action === "ask_user"
|
|
18
|
+
? should
|
|
19
|
+
? "Ask the user to run /compact with a one line note of what to keep, then continue the current request."
|
|
20
|
+
: d.tier === "notice"
|
|
21
|
+
? "Optional. Continue, or suggest /compact to the user at a clean point."
|
|
22
|
+
: "Continue. Nothing to do."
|
|
23
|
+
: should
|
|
24
|
+
? "Call compact_now with a short note to yourself, then continue the current request."
|
|
25
|
+
: d.tier === "notice"
|
|
26
|
+
? "Optional. Continue, or call compact_now if you are at a clean point."
|
|
27
|
+
: "Continue. Nothing to do.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One evaluation of the Level 7 decision from a harness-neutral view of the session: the user
|
|
3
|
+
* requests so far, the last turn, and context usage when the harness reports it. pi builds the
|
|
4
|
+
* input from its session; the Claude and Codex hooks build it from the transcript.
|
|
5
|
+
*/
|
|
6
|
+
import type { Decider } from "../events/index.ts";
|
|
7
|
+
import { type CompactAction, type CompactAnswers, type CompactDecision, type CompactState, type Lines, type Usage } from "./should-compact.ts";
|
|
8
|
+
export interface TurnInput {
|
|
9
|
+
/** Every user request so far, oldest first. */
|
|
10
|
+
requests: string[];
|
|
11
|
+
/** The last compaction summary, when there was one. */
|
|
12
|
+
summary?: string;
|
|
13
|
+
/** The assistant's last text, empty when the turn was tool calls only. */
|
|
14
|
+
recentTurn: string;
|
|
15
|
+
toolsThisTurn: string[];
|
|
16
|
+
/** Null when the harness gives no reading. */
|
|
17
|
+
usage: Usage | null;
|
|
18
|
+
/** Whether there is anything the harness could compact. */
|
|
19
|
+
compactable: boolean;
|
|
20
|
+
lines: Lines;
|
|
21
|
+
turn?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Evaluation {
|
|
24
|
+
decision: CompactDecision;
|
|
25
|
+
message: string | null;
|
|
26
|
+
usage: Usage | null;
|
|
27
|
+
answers: CompactAnswers | null;
|
|
28
|
+
state: CompactState;
|
|
29
|
+
}
|
|
30
|
+
/** The state Jev sees. Built in code from the session, no LLM involved. */
|
|
31
|
+
export declare function buildCompactState(input: TurnInput): CompactState;
|
|
32
|
+
/**
|
|
33
|
+
* Ask the four questions and decide the tier, reporting a `hook` event either way. The first
|
|
34
|
+
* request is silent with no call: there is nothing to move on from yet.
|
|
35
|
+
*/
|
|
36
|
+
export declare function evaluateCompaction(input: TurnInput, source: string, decide: Decider, report: (kind: string, payload: Record<string, unknown>) => void, action?: CompactAction): Promise<Evaluation>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { COMPACT_QUESTIONS, decideTier, tierMessage, } from "./should-compact.js";
|
|
2
|
+
const clip = (s, n) => (s.length > n ? s.slice(0, n - 1) + "…" : s);
|
|
3
|
+
/** The state Jev sees. Built in code from the session, no LLM involved. */
|
|
4
|
+
export function buildCompactState(input) {
|
|
5
|
+
const previous = [...input.requests.slice(0, -1).map((m) => clip(m, 200)), input.summary ? `Summary so far: ${clip(input.summary, 400)}` : ""].filter(Boolean).join("\n");
|
|
6
|
+
return {
|
|
7
|
+
current_request: clip(input.requests.at(-1) ?? "", 600),
|
|
8
|
+
previous_work: previous || "(nothing before this request)",
|
|
9
|
+
recent_turn: clip(input.recentTurn.trim() || `(tool calls only: ${input.toolsThisTurn.join(", ") || "none"})`, 600),
|
|
10
|
+
tools_this_turn: input.toolsThisTurn,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Ask the four questions and decide the tier, reporting a `hook` event either way. The first
|
|
15
|
+
* request is silent with no call: there is nothing to move on from yet.
|
|
16
|
+
*/
|
|
17
|
+
export async function evaluateCompaction(input, source, decide, report, action = "compact_now") {
|
|
18
|
+
const state = buildCompactState(input);
|
|
19
|
+
const base = { hook: source, turn: input.turn, usage: input.usage, usage_known: input.usage !== null, lines: input.lines, compactable: input.compactable };
|
|
20
|
+
if (input.requests.length < 2) {
|
|
21
|
+
const decision = { tier: "silent", reason: "first request, nothing to move on from" };
|
|
22
|
+
report("hook", { ...base, tier: decision.tier, reason: decision.reason, message: null, skipped: true });
|
|
23
|
+
return { decision, message: null, usage: input.usage, answers: null, state };
|
|
24
|
+
}
|
|
25
|
+
const { answers } = await decide(source, state, COMPACT_QUESTIONS, { turn: input.turn, context: input.usage, lines: input.lines });
|
|
26
|
+
const a = answers;
|
|
27
|
+
const decision = decideTier(a, input.usage, input.compactable, input.lines, state);
|
|
28
|
+
const message = tierMessage(decision, input.usage, action);
|
|
29
|
+
report("hook", { ...base, tier: decision.tier, reason: decision.reason, message });
|
|
30
|
+
return { decision, message, usage: input.usage, answers: a, state };
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction: the Level 7 decisions. Four questions after a turn, a tier in code, the verdict for an on-demand tool, and the cut point.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./should-compact.ts";
|
|
5
|
+
export * from "./compact-on-demand.ts";
|
|
6
|
+
export * from "./pick-cut-point.ts";
|
|
7
|
+
export * from "./evaluate.ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction: the Level 7 decisions. Four questions after a turn, a tier in code, the verdict for an on-demand tool, and the cut point.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./should-compact.js";
|
|
5
|
+
export * from "./compact-on-demand.js";
|
|
6
|
+
export * from "./pick-cut-point.js";
|
|
7
|
+
export * from "./evaluate.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChoiceAnswer } from "../core/types.ts";
|
|
2
|
+
export interface TurnSummary {
|
|
3
|
+
/** Zero based index in the session. */
|
|
4
|
+
index: number;
|
|
5
|
+
/** The user message that started the turn, trimmed. */
|
|
6
|
+
request: string;
|
|
7
|
+
}
|
|
8
|
+
/** One Choice over the turns. Keys are the indices, so the pick is always a real turn. */
|
|
9
|
+
export declare function cutPointQuestion(turns: TurnSummary[]): {
|
|
10
|
+
live_from: import("../core/types.ts").ChoiceQuestion;
|
|
11
|
+
};
|
|
12
|
+
export interface CutPoint {
|
|
13
|
+
liveFrom: number | null;
|
|
14
|
+
confidence: number;
|
|
15
|
+
instructions: string;
|
|
16
|
+
}
|
|
17
|
+
/** The pick as compaction instructions. Low confidence falls back to pi's default cut. */
|
|
18
|
+
export declare function cutPointInstructions(turns: TurnSummary[], answer: ChoiceAnswer, floor?: number): CutPoint;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 7, option C: pick the cut point.
|
|
3
|
+
* When compaction runs, which turn starts the work that is still live? A Choice over the session's turns, each described by the user message that started it. Code turns the pick into the compaction instructions.
|
|
4
|
+
*/
|
|
5
|
+
import { choice } from "../core/helpers.js";
|
|
6
|
+
/** One Choice over the turns. Keys are the indices, so the pick is always a real turn. */
|
|
7
|
+
export function cutPointQuestion(turns) {
|
|
8
|
+
const criteria = {};
|
|
9
|
+
for (const t of turns)
|
|
10
|
+
criteria[String(t.index)] = t.request;
|
|
11
|
+
criteria.none = "Every turn is still live; keep the most recent context only";
|
|
12
|
+
return {
|
|
13
|
+
live_from: choice("Which turn in `turns` starts the work that is still live? Earlier turns can be summarized briefly.", criteria),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The pick as compaction instructions. Low confidence falls back to pi's default cut. */
|
|
17
|
+
export function cutPointInstructions(turns, answer, floor = 0.6) {
|
|
18
|
+
if (answer.choice === "none" || answer.confidence < floor) {
|
|
19
|
+
return { liveFrom: null, confidence: answer.confidence, instructions: "Summarize the earlier work briefly and keep the most recent turns in detail." };
|
|
20
|
+
}
|
|
21
|
+
const idx = Number(answer.choice);
|
|
22
|
+
const turn = turns.find((t) => t.index === idx);
|
|
23
|
+
const from = turn ? `"${turn.request}"` : `turn ${idx}`;
|
|
24
|
+
return {
|
|
25
|
+
liveFrom: idx,
|
|
26
|
+
confidence: answer.confidence,
|
|
27
|
+
instructions: `The live work starts at ${from}. Summarize everything before it in a few lines. Keep the decisions, file paths, and open questions from ${from} onward in full detail.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { NoulAnswer, ScoreAnswer } from "../core/types.ts";
|
|
2
|
+
/** What the hook sends. Built in code from the session, no LLM involved. */
|
|
3
|
+
export type CompactState = {
|
|
4
|
+
/** The latest user message, trimmed. */
|
|
5
|
+
current_request: string;
|
|
6
|
+
/** Earlier user messages plus the last compaction summary, trimmed. */
|
|
7
|
+
previous_work: string;
|
|
8
|
+
/** The assistant's last text plus the tools it called, trimmed. */
|
|
9
|
+
recent_turn: string;
|
|
10
|
+
tools_this_turn: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare const COMPACT_QUESTIONS: {
|
|
13
|
+
switched_gears: import("../core/types.ts").NoulQuestion;
|
|
14
|
+
at_boundary: import("../core/types.ts").NoulQuestion;
|
|
15
|
+
needs_history: import("../core/types.ts").ScoreQuestion;
|
|
16
|
+
mid_operation: import("../core/types.ts").NoulQuestion;
|
|
17
|
+
};
|
|
18
|
+
export interface CompactAnswers {
|
|
19
|
+
switched_gears: NoulAnswer;
|
|
20
|
+
at_boundary: NoulAnswer;
|
|
21
|
+
needs_history: ScoreAnswer;
|
|
22
|
+
mid_operation: NoulAnswer;
|
|
23
|
+
}
|
|
24
|
+
export type Tier = "silent" | "notice" | "recommend" | "request";
|
|
25
|
+
/**
|
|
26
|
+
* Context tokens, not percent: a 1M window makes percent useless for a short session. Low on
|
|
27
|
+
* purpose so a few file reads cross them. pi's keepRecentTokens must sit below the notice line.
|
|
28
|
+
*/
|
|
29
|
+
export interface Lines {
|
|
30
|
+
notice: number;
|
|
31
|
+
recommend: number;
|
|
32
|
+
request: number;
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_LINES: Lines;
|
|
35
|
+
/** What the gauge reads: tokens in context, and the same as a percent of the model's window (null when the window is unknown). */
|
|
36
|
+
export interface Usage {
|
|
37
|
+
tokens: number;
|
|
38
|
+
pct: number | null;
|
|
39
|
+
}
|
|
40
|
+
/** A gauge reading in words: "12k tokens, 6.0% of the window", or just the tokens. */
|
|
41
|
+
export declare const usageText: (u: Usage) => string;
|
|
42
|
+
export interface CompactDecision {
|
|
43
|
+
tier: Tier;
|
|
44
|
+
reason: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The decision, numbers in code. Silent when there is nothing to compact, when the agent is mid
|
|
48
|
+
* operation, or when the work has not moved on. Otherwise the tier follows usage against the lines.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decideTier(a: CompactAnswers, usage: Usage | null, compactable: boolean, lines: Lines, state?: CompactState): CompactDecision;
|
|
51
|
+
/**
|
|
52
|
+
* How the agent can act on a tier: call compact_now itself (pi), or ask the user to run /compact
|
|
53
|
+
* (Claude Code, Codex, where a tool cannot start compaction).
|
|
54
|
+
*/
|
|
55
|
+
export type CompactAction = "compact_now" | "ask_user";
|
|
56
|
+
/** The exact text the agent sees on its next call. Silent returns null: nothing is injected. */
|
|
57
|
+
export declare function tierMessage(d: CompactDecision, usage: Usage | null, action?: CompactAction): string | null;
|