@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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -2
  3. package/dist/capabilities.d.ts +33 -0
  4. package/dist/capabilities.js +81 -0
  5. package/dist/cli/ask.d.ts +9 -0
  6. package/dist/cli/ask.js +210 -0
  7. package/dist/cli/common.d.ts +38 -0
  8. package/dist/cli/common.js +40 -0
  9. package/dist/cli/hook.d.ts +3 -0
  10. package/dist/cli/hook.js +82 -0
  11. package/dist/cli/index.d.ts +2 -0
  12. package/dist/cli/index.js +21 -0
  13. package/dist/cli/main.d.ts +8 -0
  14. package/dist/cli/main.js +41 -0
  15. package/dist/cli/mcp.d.ts +3 -0
  16. package/dist/cli/mcp.js +47 -0
  17. package/dist/cli/setup.d.ts +3 -0
  18. package/dist/cli/setup.js +79 -0
  19. package/dist/compact/compact-on-demand.d.ts +22 -0
  20. package/dist/compact/compact-on-demand.js +29 -0
  21. package/dist/compact/evaluate.d.ts +36 -0
  22. package/dist/compact/evaluate.js +31 -0
  23. package/dist/compact/index.d.ts +7 -0
  24. package/dist/compact/index.js +7 -0
  25. package/dist/compact/pick-cut-point.d.ts +18 -0
  26. package/dist/compact/pick-cut-point.js +29 -0
  27. package/dist/compact/should-compact.d.ts +57 -0
  28. package/dist/compact/should-compact.js +87 -0
  29. package/dist/core/client.d.ts +93 -0
  30. package/dist/core/client.js +295 -0
  31. package/dist/core/helpers.d.ts +14 -0
  32. package/dist/core/helpers.js +9 -0
  33. package/dist/core/mock.d.ts +20 -0
  34. package/dist/core/mock.js +132 -0
  35. package/dist/core/types.d.ts +102 -0
  36. package/dist/core/types.js +82 -0
  37. package/dist/events/index.d.ts +44 -0
  38. package/dist/events/index.js +77 -0
  39. package/dist/files/ask-file-bool.d.ts +12 -0
  40. package/dist/files/ask-file-bool.js +15 -0
  41. package/dist/files/ask-file-choice.d.ts +10 -0
  42. package/dist/files/ask-file-choice.js +17 -0
  43. package/dist/files/ask-file-score.d.ts +12 -0
  44. package/dist/files/ask-file-score.js +15 -0
  45. package/dist/files/ask-files.d.ts +23 -0
  46. package/dist/files/ask-files.js +56 -0
  47. package/dist/files/ask.d.ts +12 -0
  48. package/dist/files/ask.js +28 -0
  49. package/dist/files/assemble.d.ts +52 -0
  50. package/dist/files/assemble.js +129 -0
  51. package/dist/files/decide.d.ts +10 -0
  52. package/dist/files/decide.js +1 -0
  53. package/dist/files/index.d.ts +14 -0
  54. package/dist/files/index.js +14 -0
  55. package/dist/files/pick-first.d.ts +16 -0
  56. package/dist/files/pick-first.js +24 -0
  57. package/dist/files/prune.d.ts +13 -0
  58. package/dist/files/prune.js +87 -0
  59. package/dist/files/read-state.d.ts +17 -0
  60. package/dist/files/read-state.js +47 -0
  61. package/dist/guards/bash-gate.d.ts +29 -0
  62. package/dist/guards/bash-gate.js +35 -0
  63. package/dist/guards/index.d.ts +6 -0
  64. package/dist/guards/index.js +6 -0
  65. package/dist/guards/result-screen.d.ts +20 -0
  66. package/dist/guards/result-screen.js +31 -0
  67. package/dist/guards/write-gate.d.ts +18 -0
  68. package/dist/guards/write-gate.js +44 -0
  69. package/dist/hooks/handle.d.ts +62 -0
  70. package/dist/hooks/handle.js +48 -0
  71. package/dist/hooks/harness.d.ts +40 -0
  72. package/dist/hooks/harness.js +116 -0
  73. package/dist/hooks/index.d.ts +7 -0
  74. package/dist/hooks/index.js +7 -0
  75. package/dist/hooks/state.d.ts +8 -0
  76. package/dist/hooks/state.js +25 -0
  77. package/dist/hooks/transcript.d.ts +8 -0
  78. package/dist/hooks/transcript.js +52 -0
  79. package/dist/index.d.ts +8 -0
  80. package/dist/index.js +8 -0
  81. package/dist/ledger.d.ts +22 -0
  82. package/dist/ledger.js +26 -0
  83. package/dist/mcp/server.d.ts +28 -0
  84. package/dist/mcp/server.js +124 -0
  85. package/dist/policy/index.d.ts +17 -0
  86. package/dist/policy/index.js +30 -0
  87. package/dist/setup/index.d.ts +67 -0
  88. package/dist/setup/index.js +209 -0
  89. package/dist/tools/ask-jev.d.ts +7 -0
  90. package/dist/tools/ask-jev.js +42 -0
  91. package/dist/tools/compact-tool.d.ts +2 -0
  92. package/dist/tools/compact-tool.js +33 -0
  93. package/dist/tools/description.d.ts +5 -0
  94. package/dist/tools/description.js +20 -0
  95. package/dist/tools/file-tools.d.ts +7 -0
  96. package/dist/tools/file-tools.js +127 -0
  97. package/dist/tools/index.d.ts +10 -0
  98. package/dist/tools/index.js +10 -0
  99. package/dist/tools/run-command.d.ts +7 -0
  100. package/dist/tools/run-command.js +27 -0
  101. package/dist/tools/sets.d.ts +9 -0
  102. package/dist/tools/sets.js +32 -0
  103. package/dist/tools/types.d.ts +51 -0
  104. package/dist/tools/types.js +4 -0
  105. package/package.json +96 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IndyDevDan / AgenticEngineer.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,241 @@
1
- # Temporary Holding Version
1
+ # @bobobowis/jev-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Jev, TypeSafe AI's System One decision model, as a typed client, guards, file reads, agent tools,
4
+ hooks, an MCP server, and a CLI. Zero runtime dependencies. Node 22.18 or later.
5
+
6
+ Jev answers typed questions about a state (yes or no, pick one of your options, a position on your
7
+ scale) in about 300 ms for a fraction of a cent. This package is the reusable part of
8
+ [10 Levels of Jev](https://github.com/bobobowis/ten-levels-of-jev): the client and the decisions the lab teaches, packaged so a
9
+ project or a coding agent can use them directly.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @bobobowis/jev-kit # or: pnpm add @bobobowis/jev-kit
15
+ npx jev --help
16
+ ```
17
+
18
+ Or run the command without installing anything:
19
+
20
+ ```bash
21
+ npx @bobobowis/jev-kit gate "Is this ticket about billing?" --state "I was charged twice this month"
22
+ npx @bobobowis/jev-kit setup --harness claude --guards bash,write --tools ask_jev
23
+ ```
24
+
25
+ Run through `npx` or `pnpm dlx`, `jev setup` writes `npx -y @bobobowis/jev-kit@<version>` into the
26
+ hooks and the MCP entry, not a path into the npx cache, so the config keeps working after the cache is cleared.
27
+ Installed in the project, it writes node and the installed bin.
28
+
29
+ ## Keys
30
+
31
+ Everything reads `TYPESAFE_API_KEY`, or else `OPENROUTER_API_KEY`, from the environment. Without one,
32
+ calls fail with an error; nothing quietly falls back to the mock. `--mock` (CLI) or
33
+ `new JevClient({ provider: "mock" })` selects the offline mock explicitly. The mock is a
34
+ deterministic stand-in for tests and wiring, not a model: its answers are not judgments.
35
+
36
+ ## The library
37
+
38
+ ```ts
39
+ import { JevClient, noul, choice, score } from "@bobobowis/jev-kit";
40
+
41
+ const jev = new JevClient(); // TypeSafe or OpenRouter from the environment; { provider: "mock" } offline
42
+ const { answers } = await jev.systemOne(
43
+ { ticket: "The export button has done nothing since Tuesday's release." },
44
+ {
45
+ bug: noul("Does the ticket report something broken?"),
46
+ area: choice("Which area is it about?", { billing: "Payments and invoices", export: "Exporting data", other: "None of these" }),
47
+ urgency: score("How urgent is it?", ["Whenever", "This week", "Today, people are blocked"]),
48
+ },
49
+ );
50
+ console.log(answers.bug, answers.area, answers.urgency);
51
+ ```
52
+
53
+ Each entry point is one layer of the lab. The functions that call Jev take an optional `decide`
54
+ function last, so tests and other clients plug in without a key.
55
+
56
+ | Import | What it has |
57
+ |---|---|
58
+ | `@bobobowis/jev-kit` | `JevClient`, `jev` (a shared client), the `noul`, `choice`, and `score` builders, `MockJev`, the wire types, `validateRequest`, `LIMITS` |
59
+ | `/policy` | `normalized`, `weighted` (scores to numbers), `CONFIDENCE`, `confidenceBand` (floor, middle, bar) |
60
+ | `/guards` | `gateBashCommand(command, cwd)`, `gateWriteCall(path, content, repo)`, `screenToolResult(tool, content)`: the Level 6 gates, with their questions and thresholds |
61
+ | `/compact` | `evaluateCompaction`, `decideTier`, `tierMessage`, `compactVerdict`, `cutPointQuestion`: the Level 7 advisor |
62
+ | `/files` | `readFileState` (a file as state, never outside `cwd`), `askFileBool`, `askFileChoice`, `askFileScore`, `askFiles` (one call per file, in parallel), `pickFirstFile`, `askJev`, `assembleState` |
63
+ | `/tools` | the agent tools as harness-neutral `JevTool` objects (`ALL_TOOLS`, `LEVEL_TOOLS`, `selectTools`), `ASK_JEV_DESCRIPTION`, `runCommand`, `gatedRunner` |
64
+ | `/hooks` | `handleHook` (normalized events), `runHarnessHook` (a Claude Code or Codex payload in, the harness's answer out), transcript usage readers |
65
+ | `/setup` | `plan` and `apply`, behind `jev setup` |
66
+ | `/events` | `createReporter`, `createDecider`, `parseEventLine`: the event channel below |
67
+ | `/ledger` | `emptyLedger`, `record`, `summarize`: Jev spend per session |
68
+ | `/capabilities` | `CAPABILITIES`, `capability(level, option, harness)`: the support table below |
69
+
70
+ ```ts
71
+ import { gateBashCommand } from "@bobobowis/jev-kit/guards";
72
+ import { askFileBool } from "@bobobowis/jev-kit/files";
73
+
74
+ const verdict = await gateBashCommand("git push --force origin main", process.cwd());
75
+ if (verdict.block) console.error(verdict.reason);
76
+
77
+ const r = await askFileBool("src/billing.ts", "Does this file round money anywhere?", process.cwd());
78
+ console.log(r.answer, r.noul); // your script, or the agent, never loads the file
79
+ ```
80
+
81
+ ## The jev command
82
+
83
+ Results go to stdout as JSON, diagnostics to stderr, so stdout pipes straight into `jq`. Every
84
+ command takes `--help`, and the ones that call Jev take `--mock`.
85
+
86
+ **`jev ask`**: one question block about your own state, files code reads for you, and a command's
87
+ output.
88
+
89
+ ```bash
90
+ jev ask --questions '{"risky":{"type":"score","instructions":"How risky is the change in output?","criteria":["Docs only","Some callers","Auth or billing, no tests"]}}' \
91
+ --command "git diff HEAD~1" | jq .answers.risky.score
92
+ jev ask --questions @questions.json --state "customer says export fails" --paths "src/**/*.ts"
93
+ ```
94
+
95
+ **`jev files`**: the same block once per file, in parallel.
96
+
97
+ ```bash
98
+ jev files "src/**/*.ts" --questions '{"todo":{"type":"noul","instructions":"Does this file leave work unfinished?"}}' | jq '.results[] | {path, answers}'
99
+ ```
100
+
101
+ **`jev gate`**: the smart if. The exit code is the answer.
102
+
103
+ ```bash
104
+ jev gate "Is this ticket about billing?" --state "I was charged twice this month" && echo billing
105
+ ```
106
+
107
+ **`jev guard`**: the Level 6 gates on a command or a file write.
108
+
109
+ ```bash
110
+ jev guard bash "git push --force origin main" # block: ... exit 1
111
+ echo 'API_KEY=sk-live-4f9a...' | jev guard write .env # block: ... exit 1
112
+ ```
113
+
114
+ **`jev setup`**, **`jev hook`**, and **`jev mcp`** are below.
115
+
116
+ | Command | Exit codes |
117
+ |---|---|
118
+ | `ask`, `files` | 0 answered, 1 refused or failed (the reason is on stderr), 2 bad options |
119
+ | `gate` | 0 yes (at or above `--threshold`, default 0.5), 1 no, 2 bad options or a state that cannot be sent, 3 Jev could not answer |
120
+ | `guard` | 0 allow, 1 block, 2 bad options, 3 Jev could not answer |
121
+ | `setup` | 0 written (or printed with `--dry-run`), 1 a config it can't safely merge, 2 bad options |
122
+ | `hook` | 0 always, 1 bad options; never 2, which both harnesses read as "block" |
123
+
124
+ `gate` and `guard` exit 3 instead of guessing when Jev cannot answer, so the script decides whether
125
+ that fails open or closed. A pre-commit hook that blocks commits adding a live credential, and lets
126
+ commits through when Jev is unreachable:
127
+
128
+ ```sh
129
+ #!/bin/sh
130
+ # .git/hooks/pre-commit
131
+ jev gate "Does output add a real credential (an API key, token, password, or private key with a real-looking value), not a placeholder?" \
132
+ --command "git diff --cached" --threshold 0.7
133
+ case $? in
134
+ 0) echo "pre-commit: this commit seems to add a credential; remove it or commit with --no-verify." >&2; exit 1 ;;
135
+ 3) echo "pre-commit: Jev unavailable, not checked." >&2 ;;
136
+ esac
137
+ exit 0
138
+ ```
139
+
140
+ ## jev setup
141
+
142
+ Writes or merges the MCP entry and the hooks into a project's agent config, and leaves everything
143
+ else alone:
144
+
145
+ ```bash
146
+ jev setup --harness claude --guards bash,write,result --tools ask_jev # .claude/settings.json, .mcp.json
147
+ jev setup --harness codex --guards bash,write --compact # a marked block in .codex/config.toml
148
+ jev setup --harness claude --level 6 --dry-run # print, write nothing
149
+ ```
150
+
151
+ Each run sets the whole selection and replaces only what an earlier run wrote. A settings file that
152
+ is not valid JSON is refused, never overwritten. Codex asks you to trust the project and its hooks
153
+ the first time.
154
+
155
+ ## Use the Jev tools in any MCP client
156
+
157
+ `jev mcp` serves the agent tools over stdio MCP:
158
+
159
+ | Tool | What it answers |
160
+ |---|---|
161
+ | `should_i_compact` | whether now is a good moment to compact (needs the Level 7 hooks' notes) |
162
+ | `ask_jev_file_bool`, `_choice`, `_score` | a yes or no, a pick, or a score about one file the agent never loads |
163
+ | `ask_jev_files`, `pick_first_file` | the same questions over many files in parallel, then which to open first |
164
+ | `ask_jev` | any question block over the agent's own state, file paths, and a command's output |
165
+
166
+ ```bash
167
+ jev mcp --level 8 # the lab's levels 7 to 10
168
+ jev mcp --tools ask_jev,ask_jev_files
169
+ ```
170
+
171
+ Claude Code, in `.mcp.json`:
172
+
173
+ ```json
174
+ { "mcpServers": { "jev": { "command": "jev", "args": ["mcp", "--tools", "ask_jev"] } } }
175
+ ```
176
+
177
+ Codex, in `~/.codex/config.toml` or a trusted project's `.codex/config.toml`:
178
+
179
+ ```toml
180
+ [mcp_servers.jev]
181
+ command = "jev"
182
+ args = ["mcp", "--tools", "ask_jev"]
183
+ default_tools_approval_mode = "approve"
184
+ env_vars = ["TYPESAFE_API_KEY", "OPENROUTER_API_KEY"] # Codex starts MCP servers without your environment
185
+ ```
186
+
187
+ Or let `jev setup` write either one.
188
+
189
+ ## Hooks: guards and the compaction advisor
190
+
191
+ `jev hook` runs as a Claude Code or Codex hook. It reads the hook payload on stdin and answers in the
192
+ harness's own format.
193
+
194
+ - **Guards.** `PreToolUse` runs the bash gate on shell commands and the write gate on file writes
195
+ (`Write`, `Edit`, `MultiEdit`, `NotebookEdit`, Codex's `apply_patch`). A block denies the call.
196
+ `PostToolUse` screens `Read` and `Bash` output for instructions aimed at the agent and attaches a
197
+ banner.
198
+ - **Compaction advisor.** `UserPromptSubmit` and `Stop` ask four questions after each turn and, when
199
+ the work has moved on and context is high, tell the agent on the next prompt to ask you to run
200
+ `/compact`.
201
+
202
+ ```bash
203
+ echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git push --force origin main"},"cwd":"."}' \
204
+ | jev hook --harness claude --event PreToolUse --guards bash
205
+ ```
206
+
207
+ Hooks fail open: a missing key, a Jev error, or a timeout (8 s by default) allows the action and
208
+ reports an error. They never exit 2. The contracts they rely on are in
209
+ [docs/NOTES-claude.md](docs/NOTES-claude.md) and [docs/NOTES-codex.md](docs/NOTES-codex.md).
210
+
211
+ ## Support by harness
212
+
213
+ The same table is exported as `CAPABILITIES` from `@bobobowis/jev-kit/capabilities`.
214
+
215
+ <!-- jev:matrix (generated by `pnpm run docs` from packages/jev-kit/src/capabilities.ts) -->
216
+
217
+ | Level | pi | Claude Code | Codex | Generic MCP |
218
+ |---|---|---|---|---|
219
+ | 6 A Bash gate | full | full | full | unavailable (an MCP client has no hooks) |
220
+ | 6 B Write gate | full | full | full (apply_patch is read from the patch text) | unavailable (an MCP client has no hooks) |
221
+ | 6 C Result screen | full | degraded (the banner is added as context, not above the output) | degraded (the banner is added as context, not above the output) | unavailable (an MCP client has no hooks) |
222
+ | 7 A Turn end hook | full | degraded (advisory after each turn, delivered on the next prompt; no compact_now) | degraded (advisory after each turn, delivered on the next prompt; no compact_now) | unavailable (an MCP client has no turn end hook) |
223
+ | 7 B On demand tool | full | degraded (no compact_now; the agent asks you to run /compact) | degraded (no compact_now; Codex can't compact from a tool) | degraded (no compact_now) |
224
+ | 7 C Pick the cut point | full | unavailable (Claude Code doesn't let a hook choose where to cut) | unavailable (Codex doesn't let a hook choose where to cut) | unavailable (an MCP client has no compaction hook) |
225
+ | 8 A ask_jev_file_bool | full | full | full (the built-in shell can't be removed) | full |
226
+ | 8 B ask_jev_file_choice | full | full | full (the built-in shell can't be removed) | full |
227
+ | 8 C ask_jev_file_score | full | full | full (the built-in shell can't be removed) | full |
228
+ | 9 A Several paths, one block | full | full | full (the built-in shell can't be removed) | full |
229
+ | 9 B A glob over a directory | full | full | full (the built-in shell can't be removed) | full |
230
+ | 9 C Recursive, then pick first | full | full | full (the built-in shell can't be removed) | full |
231
+ | 10 A Triage a failure | full | full | full (the built-in shell can't be removed) | full |
232
+ | 10 B Judge a diff before shipping | full | full | full (the built-in shell can't be removed) | full |
233
+ | 10 C Classify a request, gate a plan | full | full | full (the built-in shell can't be removed) | full |
234
+
235
+ <!-- /jev:matrix -->
236
+
237
+ ## Events
238
+
239
+ Every Jev call and hook decision is reported as one JSON line: to `$JEV_EVENT_DIR/<harness>-<pid>.jsonl`
240
+ when that is set, otherwise to stderr prefixed `JEV_EVENT `. The lab uses this to show hooks and
241
+ tool calls live. `ask`, `files`, `gate`, and `guard` keep stderr quiet unless `JEV_EVENT_DIR` is set.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What each agent level and option can do in each harness. The lab serves it with every level and
3
+ * the READMEs' table is generated from it, so the docs and the UI can't disagree.
4
+ * full: behaves as the level teaches it.
5
+ * degraded: runs, with the difference in the note.
6
+ * unavailable: the harness has no place for it; the lab won't start it.
7
+ */
8
+ export type Harness = "pi" | "claude" | "codex" | "mcp";
9
+ export type Status = "full" | "degraded" | "unavailable";
10
+ export interface Support {
11
+ status: Status;
12
+ note?: string;
13
+ }
14
+ export type OptionKey = "A" | "B" | "C";
15
+ export declare const HARNESS_LABELS: Record<Harness, string>;
16
+ export declare const HARNESS_ORDER: Harness[];
17
+ export declare const CAPABILITIES: Record<number, {
18
+ name: string;
19
+ options: Record<OptionKey, {
20
+ name: string;
21
+ support: Record<Harness, Support>;
22
+ }>;
23
+ }>;
24
+ /** One option's support in one harness; levels without hooks or tools (1 to 5) are full everywhere. */
25
+ export declare function capability(level: number, option: string, harness: Harness): Support;
26
+ /** Every option's support for a level, or null for a level that doesn't run in an agent. */
27
+ export declare function levelCapabilities(level: number): Record<OptionKey, Record<Harness, Support>> | null;
28
+ /** The matrix as a Markdown table, the one the READMEs carry between their jev:matrix markers. */
29
+ export declare function matrixMarkdown(): string;
30
+ export declare const MATRIX_START = "<!-- jev:matrix (generated by `pnpm run docs` from packages/jev-kit/src/capabilities.ts) -->";
31
+ export declare const MATRIX_END = "<!-- /jev:matrix -->";
32
+ /** Replace the table between the markers, in the text's own line endings; unchanged without markers. */
33
+ export declare function withMatrix(text: string): string;
@@ -0,0 +1,81 @@
1
+ export const HARNESS_LABELS = {
2
+ pi: "pi", claude: "Claude Code", codex: "Codex", mcp: "Generic MCP",
3
+ };
4
+ export const HARNESS_ORDER = ["pi", "claude", "codex", "mcp"];
5
+ const full = (note) => (note ? { status: "full", note } : { status: "full" });
6
+ const degraded = (note) => ({ status: "degraded", note });
7
+ const unavailable = (note) => ({ status: "unavailable", note });
8
+ const NO_HOOKS = unavailable("an MCP client has no hooks");
9
+ const ADVISORY = "advisory after each turn, delivered on the next prompt; no compact_now";
10
+ const TOOLS_FULL = {
11
+ pi: full(), claude: full(), codex: full("the built-in shell can't be removed"), mcp: full(),
12
+ };
13
+ export const CAPABILITIES = {
14
+ 6: { name: "Guardrail hooks", options: {
15
+ A: { name: "Bash gate", support: { pi: full(), claude: full(), codex: full(), mcp: NO_HOOKS } },
16
+ B: { name: "Write gate", support: { pi: full(), claude: full(), codex: full("apply_patch is read from the patch text"), mcp: NO_HOOKS } },
17
+ C: { name: "Result screen", support: {
18
+ pi: full(), claude: degraded("the banner is added as context, not above the output"),
19
+ codex: degraded("the banner is added as context, not above the output"), mcp: NO_HOOKS,
20
+ } },
21
+ } },
22
+ 7: { name: "Should I compact", options: {
23
+ A: { name: "Turn end hook", support: { pi: full(), claude: degraded(ADVISORY), codex: degraded(ADVISORY), mcp: unavailable("an MCP client has no turn end hook") } },
24
+ B: { name: "On demand tool", support: { pi: full(), claude: degraded("no compact_now; the agent asks you to run /compact"), codex: degraded("no compact_now; Codex can't compact from a tool"), mcp: degraded("no compact_now") } },
25
+ C: { name: "Pick the cut point", support: {
26
+ pi: full(), claude: unavailable("Claude Code doesn't let a hook choose where to cut"),
27
+ codex: unavailable("Codex doesn't let a hook choose where to cut"), mcp: unavailable("an MCP client has no compaction hook"),
28
+ } },
29
+ } },
30
+ 8: { name: "Cheap reads", options: {
31
+ A: { name: "ask_jev_file_bool", support: TOOLS_FULL },
32
+ B: { name: "ask_jev_file_choice", support: TOOLS_FULL },
33
+ C: { name: "ask_jev_file_score", support: TOOLS_FULL },
34
+ } },
35
+ 9: { name: "Files at scale", options: {
36
+ A: { name: "Several paths, one block", support: TOOLS_FULL },
37
+ B: { name: "A glob over a directory", support: TOOLS_FULL },
38
+ C: { name: "Recursive, then pick first", support: TOOLS_FULL },
39
+ } },
40
+ 10: { name: "Agentic Jev", options: {
41
+ A: { name: "Triage a failure", support: TOOLS_FULL },
42
+ B: { name: "Judge a diff before shipping", support: TOOLS_FULL },
43
+ C: { name: "Classify a request, gate a plan", support: TOOLS_FULL },
44
+ } },
45
+ };
46
+ /** One option's support in one harness; levels without hooks or tools (1 to 5) are full everywhere. */
47
+ export function capability(level, option, harness) {
48
+ return CAPABILITIES[level]?.options[option]?.support[harness] ?? full();
49
+ }
50
+ /** Every option's support for a level, or null for a level that doesn't run in an agent. */
51
+ export function levelCapabilities(level) {
52
+ const l = CAPABILITIES[level];
53
+ if (!l)
54
+ return null;
55
+ return { A: l.options.A.support, B: l.options.B.support, C: l.options.C.support };
56
+ }
57
+ const cell = (s) => (s.note ? `${s.status} (${s.note})` : s.status);
58
+ /** The matrix as a Markdown table, the one the READMEs carry between their jev:matrix markers. */
59
+ export function matrixMarkdown() {
60
+ const rows = [
61
+ `| Level | ${HARNESS_ORDER.map((h) => HARNESS_LABELS[h]).join(" | ")} |`,
62
+ `|---|${HARNESS_ORDER.map(() => "---").join("|")}|`,
63
+ ];
64
+ for (const [level, l] of Object.entries(CAPABILITIES)) {
65
+ for (const [key, o] of Object.entries(l.options)) {
66
+ rows.push(`| ${level} ${key} ${o.name} | ${HARNESS_ORDER.map((h) => cell(o.support[h])).join(" | ")} |`);
67
+ }
68
+ }
69
+ return rows.join("\n");
70
+ }
71
+ export const MATRIX_START = "<!-- jev:matrix (generated by `pnpm run docs` from packages/jev-kit/src/capabilities.ts) -->";
72
+ export const MATRIX_END = "<!-- /jev:matrix -->";
73
+ /** Replace the table between the markers, in the text's own line endings; unchanged without markers. */
74
+ export function withMatrix(text) {
75
+ const a = text.indexOf(MATRIX_START), b = text.indexOf(MATRIX_END);
76
+ if (a < 0 || b < a)
77
+ return text;
78
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
79
+ const table = matrixMarkdown().split("\n").join(eol);
80
+ return `${text.slice(0, a + MATRIX_START.length)}${eol}${eol}${table}${eol}${eol}${text.slice(b)}`;
81
+ }
@@ -0,0 +1,9 @@
1
+ import { type Io } from "./common.ts";
2
+ export declare const ASK_HELP = "Usage: jev ask --questions <json|@file> [state options]\n\nPrints { answers, state_summary, usage, model } as JSON. Exit 0, 1 when the call is refused or fails, 2 for bad options.\n\n --questions <json|@file> the question block, keyed by question id\n --state <text|json> your own state, plain text or a JSON object\n --state-file <file> the same, read from a file\n --paths <glob> files for code to read into files[\"path\"]; repeat for more (up to 20 files)\n --command <cmd> a command for code to run; its result goes into output (passes the bash gate)\n --cwd <dir> where paths resolve and the command runs (default: here)\n --mock the offline mock instead of a Jev key\n";
3
+ export declare const FILES_HELP = "Usage: jev files <glob|dir|file>... --questions <json|@file> [--recursive] [--cwd dir] [--mock]\n\nAsks the question block once per file, in parallel. Prints { results, skipped, calls } as JSON.\n";
4
+ export declare const GATE_HELP = "Usage: jev gate \"<yes or no question>\" [state options] [--threshold 0.5] [--yes <text>] [--no <text>] [--json]\n\nExit 0 when the probability of yes is at or above the threshold, 1 below it, 2 for bad options or a\nstate that cannot be sent, 3 when Jev could not answer. The script chooses whether 3 fails open or closed.\n\n --threshold <0..1> default 0.5\n --yes <text> what counts as yes\n --no <text> what counts as no\n --json also print { noul, threshold, yes } on stdout\n --state <text|json> your own state, plain text or a JSON object\n --state-file <file> the same, read from a file\n --paths <glob> files for code to read into files[\"path\"]; repeat for more (up to 20 files)\n --command <cmd> a command for code to run; its result goes into output (passes the bash gate)\n --cwd <dir> where paths resolve and the command runs (default: here)\n --mock the offline mock instead of a Jev key\n";
5
+ export declare const GUARD_HELP = "Usage: jev guard bash \"<command>\" | jev guard write <path> < content [--cwd dir] [--json] [--mock]\n\nThe Level 6 gates. Prints allow or block with the reason. Exit 0 allow, 1 block, 2 bad options, 3 Jev error.\n";
6
+ export declare function ask(argv: string[], io: Io): Promise<number>;
7
+ export declare function files(argv: string[], io: Io): Promise<number>;
8
+ export declare function gate(argv: string[], io: Io): Promise<number>;
9
+ export declare function guard(argv: string[], io: Io): Promise<number>;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * The general CLI, for scripts, CI, and git hooks:
3
+ * jev ask a question block about a state, files, and a command's output; JSON on stdout
4
+ * jev files the same question block about each file, in parallel
5
+ * jev gate one yes or no question; the answer is the exit code
6
+ * jev guard the Level 6 bash or write gate on a command or a file
7
+ * Results go to stdout, diagnostics to stderr. Events only go to JEV_EVENT_DIR, when it is set.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { parseArgs } from "node:util";
12
+ import { noul } from "../core/helpers.js";
13
+ import { QuestionValidationError } from "../core/types.js";
14
+ import { AskStateError, assembleState } from "../files/assemble.js";
15
+ import { gateBashCommand } from "../guards/bash-gate.js";
16
+ import { gateWriteCall } from "../guards/write-gate.js";
17
+ import { askJev } from "../tools/ask-jev.js";
18
+ import { askJevFiles } from "../tools/file-tools.js";
19
+ import { gatedRunner } from "../tools/run-command.js";
20
+ import { deciderFor, readAll } from "./common.js";
21
+ const STATE_OPTIONS = {
22
+ state: { type: "string" }, "state-file": { type: "string" }, paths: { type: "string", multiple: true },
23
+ command: { type: "string" }, cwd: { type: "string" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" },
24
+ };
25
+ const STATE_HELP = ` --state <text|json> your own state, plain text or a JSON object
26
+ --state-file <file> the same, read from a file
27
+ --paths <glob> files for code to read into files["path"]; repeat for more (up to 20 files)
28
+ --command <cmd> a command for code to run; its result goes into output (passes the bash gate)
29
+ --cwd <dir> where paths resolve and the command runs (default: here)
30
+ --mock the offline mock instead of a Jev key`;
31
+ export const ASK_HELP = `Usage: jev ask --questions <json|@file> [state options]
32
+
33
+ Prints { answers, state_summary, usage, model } as JSON. Exit 0, 1 when the call is refused or fails, 2 for bad options.
34
+
35
+ --questions <json|@file> the question block, keyed by question id
36
+ ${STATE_HELP}
37
+ `;
38
+ export const FILES_HELP = `Usage: jev files <glob|dir|file>... --questions <json|@file> [--recursive] [--cwd dir] [--mock]
39
+
40
+ Asks the question block once per file, in parallel. Prints { results, skipped, calls } as JSON.
41
+ `;
42
+ export const GATE_HELP = `Usage: jev gate "<yes or no question>" [state options] [--threshold 0.5] [--yes <text>] [--no <text>] [--json]
43
+
44
+ Exit 0 when the probability of yes is at or above the threshold, 1 below it, 2 for bad options or a
45
+ state that cannot be sent, 3 when Jev could not answer. The script chooses whether 3 fails open or closed.
46
+
47
+ --threshold <0..1> default 0.5
48
+ --yes <text> what counts as yes
49
+ --no <text> what counts as no
50
+ --json also print { noul, threshold, yes } on stdout
51
+ ${STATE_HELP}
52
+ `;
53
+ export const GUARD_HELP = `Usage: jev guard bash "<command>" | jev guard write <path> < content [--cwd dir] [--json] [--mock]
54
+
55
+ The Level 6 gates. Prints allow or block with the reason. Exit 0 allow, 1 block, 2 bad options, 3 Jev error.
56
+ `;
57
+ function parse(argv, options, io, help, name, positionals = false) {
58
+ try {
59
+ return parseArgs({ args: argv, options, strict: true, allowPositionals: positionals });
60
+ }
61
+ catch (err) {
62
+ io.stderr.write(`jev ${name}: ${err.message}\n\n${help}`);
63
+ return null;
64
+ }
65
+ }
66
+ const readQuestions = (raw) => (raw.startsWith("@") ? readFileSync(raw.slice(1), "utf8") : raw);
67
+ function context(cwd, mock, io) {
68
+ const { reporter, decide } = deciderFor("cli", { mock, quiet: true, stderr: io.stderr, client: io.client });
69
+ return { cwd, harness: "cli", decide, report: (k, p) => reporter.report(k, p) };
70
+ }
71
+ function stateText(v) {
72
+ if (v["state-file"])
73
+ return readFileSync(v["state-file"], "utf8");
74
+ return v.state;
75
+ }
76
+ export async function ask(argv, io) {
77
+ const parsed = parse(argv, { ...STATE_OPTIONS, questions: { type: "string" } }, io, ASK_HELP, "ask");
78
+ if (!parsed)
79
+ return 2;
80
+ const v = parsed.values;
81
+ if (v.help) {
82
+ io.stdout.write(ASK_HELP);
83
+ return 0;
84
+ }
85
+ if (!v.questions) {
86
+ io.stderr.write("jev ask: --questions is required\n");
87
+ return 2;
88
+ }
89
+ let questions;
90
+ try {
91
+ questions = readQuestions(v.questions);
92
+ }
93
+ catch (err) {
94
+ io.stderr.write(`jev ask: cannot read ${v.questions}: ${err.message}\n`);
95
+ return 2;
96
+ }
97
+ const ctx = context(resolve(v.cwd ?? process.cwd()), v.mock, io);
98
+ const r = await askJev.execute({ questions_json: questions, state: stateText(v), paths: v.paths, command: v.command }, ctx);
99
+ if (r.isError) {
100
+ io.stderr.write(r.text + "\n");
101
+ return 1;
102
+ }
103
+ io.stdout.write(JSON.stringify(r.payload) + "\n");
104
+ return 0;
105
+ }
106
+ export async function files(argv, io) {
107
+ const parsed = parse(argv, { questions: { type: "string" }, recursive: { type: "boolean" }, cwd: { type: "string" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" } }, io, FILES_HELP, "files", true);
108
+ if (!parsed)
109
+ return 2;
110
+ const v = parsed.values;
111
+ if (v.help) {
112
+ io.stdout.write(FILES_HELP);
113
+ return 0;
114
+ }
115
+ if (!v.questions || !parsed.positionals.length) {
116
+ io.stderr.write(`jev files: pass one or more globs and --questions\n\n${FILES_HELP}`);
117
+ return 2;
118
+ }
119
+ let questions;
120
+ try {
121
+ questions = readQuestions(v.questions);
122
+ }
123
+ catch (err) {
124
+ io.stderr.write(`jev files: cannot read ${v.questions}: ${err.message}\n`);
125
+ return 2;
126
+ }
127
+ const ctx = context(resolve(v.cwd ?? process.cwd()), v.mock, io);
128
+ const r = await askJevFiles.execute({ paths_or_globs: parsed.positionals, questions_json: questions, recursive: v.recursive ?? false }, ctx);
129
+ if (r.isError) {
130
+ io.stderr.write(r.text + "\n");
131
+ return 1;
132
+ }
133
+ io.stdout.write(JSON.stringify(r.payload) + "\n");
134
+ return 0;
135
+ }
136
+ export async function gate(argv, io) {
137
+ const parsed = parse(argv, { ...STATE_OPTIONS, threshold: { type: "string" }, yes: { type: "string" }, no: { type: "string" }, json: { type: "boolean" } }, io, GATE_HELP, "gate", true);
138
+ if (!parsed)
139
+ return 2;
140
+ const v = parsed.values;
141
+ if (v.help) {
142
+ io.stdout.write(GATE_HELP);
143
+ return 0;
144
+ }
145
+ const question = parsed.positionals.join(" ").trim();
146
+ if (!question) {
147
+ io.stderr.write(`jev gate: the question is required\n\n${GATE_HELP}`);
148
+ return 2;
149
+ }
150
+ const threshold = v.threshold === undefined ? 0.5 : Number(v.threshold);
151
+ if (!(threshold >= 0 && threshold <= 1)) {
152
+ io.stderr.write("jev gate: --threshold must be between 0 and 1\n");
153
+ return 2;
154
+ }
155
+ const cwd = resolve(v.cwd ?? process.cwd());
156
+ const ctx = context(cwd, v.mock, io);
157
+ let assembled;
158
+ try {
159
+ assembled = await assembleState({ state: stateText(v), paths: v.paths, command: v.command }, cwd, gatedRunner(ctx));
160
+ }
161
+ catch (err) {
162
+ io.stderr.write(`jev gate: ${err instanceof AskStateError ? err.message : `cannot build the state: ${err?.message ?? err}`}\n`);
163
+ return err instanceof AskStateError ? 2 : 3;
164
+ }
165
+ const criteria = v.yes || v.no ? { true: v.yes, false: v.no } : undefined;
166
+ let answer;
167
+ try {
168
+ const { answers } = await ctx.decide("gate", assembled.state, { gate: noul(question, criteria) });
169
+ answer = answers.gate;
170
+ }
171
+ catch (err) {
172
+ io.stderr.write(`jev gate: ${err?.message ?? err}\n`);
173
+ return err instanceof QuestionValidationError ? 2 : 3;
174
+ }
175
+ const yes = answer.noul >= threshold;
176
+ if (v.json)
177
+ io.stdout.write(JSON.stringify({ noul: answer.noul, threshold, yes }) + "\n");
178
+ return yes ? 0 : 1;
179
+ }
180
+ export async function guard(argv, io) {
181
+ const parsed = parse(argv, { cwd: { type: "string" }, json: { type: "boolean" }, mock: { type: "boolean" }, help: { type: "boolean", short: "h" } }, io, GUARD_HELP, "guard", true);
182
+ if (!parsed)
183
+ return 2;
184
+ const v = parsed.values;
185
+ if (v.help) {
186
+ io.stdout.write(GUARD_HELP);
187
+ return 0;
188
+ }
189
+ const [kind, ...rest] = parsed.positionals;
190
+ const target = rest.join(" ").trim();
191
+ if ((kind !== "bash" && kind !== "write") || !target) {
192
+ io.stderr.write(`jev guard: pass bash "<command>" or write <path>\n\n${GUARD_HELP}`);
193
+ return 2;
194
+ }
195
+ const cwd = resolve(v.cwd ?? process.cwd());
196
+ const ctx = context(cwd, v.mock, io);
197
+ let d;
198
+ try {
199
+ d = kind === "bash"
200
+ ? await gateBashCommand(target, cwd, (s, q) => ctx.decide("guard bash", s, q))
201
+ : await gateWriteCall(target, await readAll(io.stdin), cwd, (s, q) => ctx.decide("guard write", s, q));
202
+ }
203
+ catch (err) {
204
+ io.stderr.write(`jev guard: ${err?.message ?? err}\n`);
205
+ return 3;
206
+ }
207
+ ctx.report("hook", { hook: `guard ${kind}`, [kind === "bash" ? "command" : "path"]: target, block: d.block, reason: d.reason });
208
+ io.stdout.write(v.json ? JSON.stringify(d) + "\n" : `${d.block ? "block" : "allow"}: ${d.reason}\n`);
209
+ return d.block ? 1 : 0;
210
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What every subcommand shares: the client, the reporter, list flags, stdin, and a deadline.
3
+ */
4
+ import { JevClient } from "../core/client.ts";
5
+ import { type Decider, type Reporter } from "../events/index.ts";
6
+ export interface Io {
7
+ stdout: {
8
+ write(s: string): unknown;
9
+ };
10
+ stderr: {
11
+ write(s: string): unknown;
12
+ };
13
+ stdin: AsyncIterable<string | Buffer>;
14
+ /** Tests inject a client; commands otherwise build one from --mock and the keys. */
15
+ client?: () => Pick<JevClient, "systemOne">;
16
+ }
17
+ export declare const processIo: () => Io;
18
+ /**
19
+ * `--mock` selects the offline mock explicitly; otherwise the client picks TypeSafe or OpenRouter
20
+ * from the keys. A deadline gets its own client, so a call cut off by it is aborted, not left running.
21
+ */
22
+ export declare function clientFor(mock: boolean | undefined, timeoutMs?: number): () => Pick<JevClient, "systemOne">;
23
+ export declare function deciderFor(harness: string, opts: {
24
+ mock?: boolean;
25
+ quiet?: boolean;
26
+ stderr?: Io["stderr"];
27
+ timeoutMs?: number;
28
+ client?: Io["client"];
29
+ }): {
30
+ reporter: Reporter;
31
+ decide: Decider;
32
+ };
33
+ export declare const list: (raw: string | undefined) => string[];
34
+ export declare function readAll(stdin: Io["stdin"]): Promise<string>;
35
+ /** Resolve with the work, or with `fallback` once `ms` pass. The work keeps running but nobody waits. */
36
+ export declare function withDeadline<T>(work: Promise<T>, ms: number, fallback: () => T): Promise<T>;
37
+ export declare class UsageError extends Error {
38
+ }