subcortex 0.3.0__tar.gz

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 (91) hide show
  1. subcortex-0.3.0/PKG-INFO +287 -0
  2. subcortex-0.3.0/README.md +276 -0
  3. subcortex-0.3.0/pyproject.toml +27 -0
  4. subcortex-0.3.0/setup.cfg +4 -0
  5. subcortex-0.3.0/src/subcortex/__init__.py +3 -0
  6. subcortex-0.3.0/src/subcortex/__main__.py +3 -0
  7. subcortex-0.3.0/src/subcortex/adapters/__init__.py +48 -0
  8. subcortex-0.3.0/src/subcortex/adapters/base.py +230 -0
  9. subcortex-0.3.0/src/subcortex/adapters/claude_family.py +133 -0
  10. subcortex-0.3.0/src/subcortex/adapters/codex.py +87 -0
  11. subcortex-0.3.0/src/subcortex/adapters/copilot.py +60 -0
  12. subcortex-0.3.0/src/subcortex/adapters/cursor.py +36 -0
  13. subcortex-0.3.0/src/subcortex/adapters/docker_agent.py +115 -0
  14. subcortex-0.3.0/src/subcortex/adapters/gemini_family.py +60 -0
  15. subcortex-0.3.0/src/subcortex/adapters/grok.py +98 -0
  16. subcortex-0.3.0/src/subcortex/adapters/kimi_code.py +138 -0
  17. subcortex-0.3.0/src/subcortex/adapters/letta_vibe.py +96 -0
  18. subcortex-0.3.0/src/subcortex/adapters/openhands.py +153 -0
  19. subcortex-0.3.0/src/subcortex/auth.py +59 -0
  20. subcortex-0.3.0/src/subcortex/backends/__init__.py +23 -0
  21. subcortex-0.3.0/src/subcortex/backends/base.py +22 -0
  22. subcortex-0.3.0/src/subcortex/backends/jev.py +460 -0
  23. subcortex-0.3.0/src/subcortex/backends/laya.py +149 -0
  24. subcortex-0.3.0/src/subcortex/cli.py +809 -0
  25. subcortex-0.3.0/src/subcortex/client.py +77 -0
  26. subcortex-0.3.0/src/subcortex/config.py +263 -0
  27. subcortex-0.3.0/src/subcortex/daemon.py +502 -0
  28. subcortex-0.3.0/src/subcortex/evalset.py +241 -0
  29. subcortex-0.3.0/src/subcortex/hook.py +254 -0
  30. subcortex-0.3.0/src/subcortex/installers/__init__.py +62 -0
  31. subcortex-0.3.0/src/subcortex/installers/amp.py +39 -0
  32. subcortex-0.3.0/src/subcortex/installers/base.py +874 -0
  33. subcortex-0.3.0/src/subcortex/installers/claude_family.py +229 -0
  34. subcortex-0.3.0/src/subcortex/installers/codex.py +110 -0
  35. subcortex-0.3.0/src/subcortex/installers/copilot.py +65 -0
  36. subcortex-0.3.0/src/subcortex/installers/crush.py +36 -0
  37. subcortex-0.3.0/src/subcortex/installers/cursor.py +79 -0
  38. subcortex-0.3.0/src/subcortex/installers/gemini_family.py +83 -0
  39. subcortex-0.3.0/src/subcortex/installers/goose.py +186 -0
  40. subcortex-0.3.0/src/subcortex/installers/kimi_code.py +71 -0
  41. subcortex-0.3.0/src/subcortex/installers/mcp_only.py +111 -0
  42. subcortex-0.3.0/src/subcortex/installers/more_hooks.py +184 -0
  43. subcortex-0.3.0/src/subcortex/installers/opencode.py +66 -0
  44. subcortex-0.3.0/src/subcortex/installers/openhands.py +84 -0
  45. subcortex-0.3.0/src/subcortex/installers/pi_cline.py +53 -0
  46. subcortex-0.3.0/src/subcortex/ledger.py +92 -0
  47. subcortex-0.3.0/src/subcortex/localhttp.py +59 -0
  48. subcortex-0.3.0/src/subcortex/mcp_server.py +187 -0
  49. subcortex-0.3.0/src/subcortex/metrics.py +56 -0
  50. subcortex-0.3.0/src/subcortex/plugins/amp/subcortex.ts +258 -0
  51. subcortex-0.3.0/src/subcortex/plugins/cline/subcortex.ts +340 -0
  52. subcortex-0.3.0/src/subcortex/plugins/opencode/subcortex.ts +265 -0
  53. subcortex-0.3.0/src/subcortex/plugins/pi/subcortex.ts +292 -0
  54. subcortex-0.3.0/src/subcortex/policy.py +341 -0
  55. subcortex-0.3.0/src/subcortex/presets.py +163 -0
  56. subcortex-0.3.0/src/subcortex/provision.py +188 -0
  57. subcortex-0.3.0/src/subcortex/service.py +149 -0
  58. subcortex-0.3.0/src/subcortex/state.py +137 -0
  59. subcortex-0.3.0/src/subcortex/transcript.py +211 -0
  60. subcortex-0.3.0/src/subcortex/tuis.py +51 -0
  61. subcortex-0.3.0/src/subcortex/ui.py +319 -0
  62. subcortex-0.3.0/src/subcortex/verdicts.py +233 -0
  63. subcortex-0.3.0/src/subcortex/wizard.py +474 -0
  64. subcortex-0.3.0/src/subcortex.egg-info/PKG-INFO +287 -0
  65. subcortex-0.3.0/src/subcortex.egg-info/SOURCES.txt +89 -0
  66. subcortex-0.3.0/src/subcortex.egg-info/dependency_links.txt +1 -0
  67. subcortex-0.3.0/src/subcortex.egg-info/entry_points.txt +3 -0
  68. subcortex-0.3.0/src/subcortex.egg-info/requires.txt +8 -0
  69. subcortex-0.3.0/src/subcortex.egg-info/top_level.txt +1 -0
  70. subcortex-0.3.0/tests/test_adapters.py +474 -0
  71. subcortex-0.3.0/tests/test_cli.py +276 -0
  72. subcortex-0.3.0/tests/test_config.py +81 -0
  73. subcortex-0.3.0/tests/test_daemon.py +315 -0
  74. subcortex-0.3.0/tests/test_e2e.py +246 -0
  75. subcortex-0.3.0/tests/test_e2e_codex.py +312 -0
  76. subcortex-0.3.0/tests/test_e2e_gemini.py +364 -0
  77. subcortex-0.3.0/tests/test_e2e_more.py +336 -0
  78. subcortex-0.3.0/tests/test_e2e_plugins.py +437 -0
  79. subcortex-0.3.0/tests/test_e2e_vendor.py +1338 -0
  80. subcortex-0.3.0/tests/test_hook_process.py +112 -0
  81. subcortex-0.3.0/tests/test_installer_base.py +151 -0
  82. subcortex-0.3.0/tests/test_installers.py +321 -0
  83. subcortex-0.3.0/tests/test_jev.py +301 -0
  84. subcortex-0.3.0/tests/test_lifecycle.py +179 -0
  85. subcortex-0.3.0/tests/test_mcp.py +123 -0
  86. subcortex-0.3.0/tests/test_plugins.py +298 -0
  87. subcortex-0.3.0/tests/test_policy.py +240 -0
  88. subcortex-0.3.0/tests/test_setup.py +291 -0
  89. subcortex-0.3.0/tests/test_state.py +306 -0
  90. subcortex-0.3.0/tests/test_transcript.py +115 -0
  91. subcortex-0.3.0/tests/test_verdicts.py +166 -0
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.4
2
+ Name: subcortex
3
+ Version: 0.3.0
4
+ Summary: Local decision layer for coding-agent TUIs: a warm daemon answering System-1 questions in milliseconds
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Provides-Extra: laya
9
+ Requires-Dist: laya-mlx>=0.1.0; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "laya"
10
+ Requires-Dist: laya>=0.3.4; (sys_platform != "darwin" or platform_machine != "arm64") and extra == "laya"
11
+
12
+ # subcortex
13
+
14
+ A local decision layer for coding-agent TUIs. A small daemon keeps a
15
+ typed-decision model warm and answers System-1 questions — *is this request
16
+ simple? is this tool output still needed for it?* — in milliseconds, so the big
17
+ model doesn't spend tokens on them. Hooks, plugins and MCP wire it into 29
18
+ terminal coding agents.
19
+
20
+ Two decision backends:
21
+
22
+ - **jev** — hosted typed-decision API (TypeSafe `jev-latest`, or any endpoint
23
+ speaking the same wire shape: OpenRouter, Vercel AI Gateway). ~250 ms per
24
+ decision over a kept-alive connection, $0.042 per million input tokens.
25
+ - **laya** — local model ([laya-mlx](https://pypi.org/project/laya-mlx/) on
26
+ Apple Silicon, [laya](https://pypi.org/project/laya/) elsewhere). Private,
27
+ offline, ~10 ms per question. Weaker judgments, so it acts more cautiously
28
+ (see [How good are the decisions?](#how-good-are-the-decisions)).
29
+
30
+ ## What it does inside a TUI
31
+
32
+ 1. **Prompt hint** — a request the model rates *simple* gets a one-line,
33
+ factual note ("the most direct, minimal change is likely sufficient").
34
+ 2. **Output trimming** — a large, successful shell output that the model
35
+ judges *not needed for the user's current request* is cut to its head and
36
+ tail; lines in the middle that mention the request or warn about something
37
+ are kept, and anything that looks like a failure is never touched. The
38
+ user's latest request is the evidence: without one, nothing is trimmed.
39
+ Laya doesn't trim unless you opt in.
40
+ 3. **Compaction snapshot** — before context compaction, the last few messages
41
+ are saved to disk (never vetoing anything).
42
+ 4. **Compaction restore** — after compaction they are handed back as context,
43
+ exactly once, and only deleted after they were delivered.
44
+
45
+ Each TUI gets whichever of these its extension seam can safely support:
46
+
47
+ | TUI | Seam | Hint | Trim | Compaction |
48
+ |---|---|---|---|---|
49
+ | Claude Code | hooks | ✅ | ✅ `updatedToolOutput` | ✅ PreCompact → SessionStart |
50
+ | Codex CLI ≥ 0.133 | hooks (`/hooks` trust) | ✅ | ✅ `continue:false` + `reason` | ✅ |
51
+ | Open Interpreter | hooks (Codex engine) | ✅ | ✅ | ✅ |
52
+ | Qoder CLI | hooks | ✅ | ✅ | ✅ |
53
+ | CodeBuddy Code | hooks | ✅ | — hook gets no output | ✅ on auto-compaction |
54
+ | GitHub Copilot CLI ≥ 1.0.67 | hooks | ✅ | ✅ `modifiedResult` | ✅ restored with next prompt |
55
+ | Factory Droid | hooks | ✅ (not one-shot `exec`) | — can only append | ✅ |
56
+ | Qwen Code ≥ 0.16 | hooks | ✅ | — truncates natively | ✅ |
57
+ | Gemini CLI ≥ 0.27 | hooks | ✅ | — truncates natively | ◐ on `/compress` |
58
+ | Cursor CLI | hooks | ✅ interactive | — Shell can't be replaced | ✅ restored with next prompt |
59
+ | Kimi Code CLI ≥ 0.33 | hooks | ✅ | — output hook ignored | ✅ restored with next prompt |
60
+ | OpenHands CLI ≥ 1.12 | hooks | ✅ | — truncates natively | ✅ via its event log |
61
+ | Grok Build | hooks | — output discarded | ✅ tagged `updatedToolOutput` | ✅ restored on next shell call |
62
+ | Docker Agent ≥ 1.137 | hooks | ✅ | ✅ `updated_tool_response` | ✅ restored with next prompt |
63
+ | Mistral Vibe ≥ 2.25.5 | hooks | — no event | ✅ `post_tool` reason (from a turn's 2nd call) | — no event |
64
+ | Letta Code | hooks | ✅ interactive | — | — |
65
+ | Junie CLI | hooks | ✅ interactive | — | — |
66
+ | Devin CLI | hooks | ✅ | — | — |
67
+ | OpenCode ≥ 1.1.62 | plugin | ✅ | ✅ bash only | ✅ into the compaction prompt |
68
+ | Kilo Code CLI | plugin | ✅ | ✅ | ✅ |
69
+ | Amp | plugin | ✅ | ✅ | ✅ rolling snapshot |
70
+ | Pi ≥ 0.87 | plugin | ✅ | ✅ | ✅ re-inserted after the summary |
71
+ | Cline CLI ≥ 3.0.62 | plugin | ✅ | ✅ | ✅ on the next request |
72
+ | Crush, Goose, Warp, Zed, Kiro, Auggie | MCP | on-demand tools | — | — |
73
+ | Aider | none | — | `subcortex wrap` for test/lint | — |
74
+
75
+ "—" means the TUI has no seam that can do it without blocking or failing a
76
+ tool call, so subcortex doesn't try. Per-TUI details, config paths and caveats:
77
+ [docs/tuis.md](docs/tuis.md).
78
+
79
+ **Verified end to end** — each real TUI binary, sandboxed, against a mock model
80
+ API, asserting on what it actually sent to the model: Claude Code 2.1.278,
81
+ Codex 0.155.1, Open Interpreter 0.0.45, Gemini CLI 0.60.0, Qwen Code 0.24.3,
82
+ Kimi Code 2.0.2, OpenCode 1.18.31, Grok Build 1.0.40, Pi 0.87.0, Cline 3.0.62,
83
+ Docker Agent 1.142.0, Mistral Vibe 2.25.5, Factory Droid 0.224.0, Copilot CLI
84
+ 1.0.87, OpenHands 1.16.0, Letta Code 0.32.15, CodeBuddy 2.156.0, Junie 26.9.21,
85
+ Devin CLI 3000.11.1, Cursor CLI 2026.09.18, Amp (plugin runtime), and the MCP
86
+ integrations of Crush 0.96.1, Goose 1.51.0 and Auggie 0.36.0. Qoder and Kiro
87
+ need a vendor login to run at all; their hook/MCP formats were checked against
88
+ their shipped code. Settings for MCP servers belong in `config.json`: some
89
+ TUIs (Auggie) start MCP servers without your environment variables.
90
+
91
+ ## Install
92
+
93
+ ```sh
94
+ curl -fsSL https://raw.githubusercontent.com/pavlealeksic/subcortex/main/install.sh | sh
95
+ ```
96
+
97
+ That installs the CLI into its own environment (uv, pipx or a private venv)
98
+ and starts **`subcortex setup`**, an interactive walk-through:
99
+
100
+ 1. **Backend** — Laya (local; installs `laya-mlx`/`laya` into a dedicated
101
+ environment and downloads the model) or Jev (hosted; your API key is kept in
102
+ the environment or in a mode-600 file).
103
+ 2. **Behaviors** — prompt hints, output trimming, compaction snapshots.
104
+ 3. **TUIs** — a checklist of every supported TUI, with the ones found on this
105
+ machine preselected.
106
+ 4. **Review** — each file that will change, with its diff on request; every
107
+ install self-tests its hook commands before writing.
108
+ 5. **Daemon** — start it now, and optionally at login (launchd / systemd).
109
+
110
+ Arrow keys and space in a terminal; plain numbered prompts elsewhere. Run it
111
+ again any time to change your choices; `subcortex setup --yes --backend jev
112
+ --tuis detected` runs it unattended. Or install by hand:
113
+
114
+ ```sh
115
+ pip install subcortex # daemon + jev backend (zero dependencies)
116
+ pip install "subcortex[laya]" # + local laya backend
117
+ ```
118
+
119
+ ## Wire it into a TUI (without the wizard)
120
+
121
+ ```sh
122
+ subcortex install # checklist of TUIs (in a terminal)
123
+ subcortex tuis # supported TUIs, what's installed, what's on PATH
124
+ subcortex install claude-code --dry-run # show exactly what would change
125
+ subcortex install claude-code # shows the diff, asks, self-tests, writes
126
+ subcortex install codex gemini-cli --yes # several at once, no prompt
127
+ subcortex install detected # every supported TUI found on PATH
128
+ subcortex install cursor --mcp # also register the on-demand MCP server
129
+ subcortex uninstall claude-code # removes exactly what subcortex added
130
+ ```
131
+
132
+ ## How good are the decisions?
133
+
134
+ A probability is not an accuracy, so every rule was chosen on labeled
135
+ examples and then checked on examples it had never seen
136
+ ([docs/calibration.md](docs/calibration.md)). The mistakes that matter are a
137
+ "simple" hint on complex work and a trim of output the request needs; there
138
+ were none:
139
+
140
+ | | hints: simple requests | hints: complex requests | trims: disposable output | trims: needed output |
141
+ |---|---|---|---|---|
142
+ | **jev** (calibration / held out) | 17/20 · 12/12 | **0/20 · 0/12** | 12/12 · 6/6 | **0/12 · 0/6** |
143
+ | **laya** (calibration / held out) | 5/20 · 5/12 | **0/20 · 0/12** | off by default¹ | — |
144
+
145
+ ¹ Laya's output judgments trimmed 2 of 6 needed outputs on the held-out set, so
146
+ Laya only trims if you set `thresholds.output_needed_threshold` yourself.
147
+
148
+ ```sh
149
+ subcortex eval # the same check against your backend and thresholds
150
+ ```
151
+
152
+ It exits non-zero on any hinted complex request or trimmed needed output.
153
+
154
+ ## Safety
155
+
156
+ A hook that misbehaves can take a TUI down: an earlier version of this project
157
+ exited 2 from a Claude Code hook, and exit 2 means "block this prompt". People
158
+ run this inside real work, so every one of these is enforced by tests:
159
+
160
+ - **Hooks always exit 0 and never stall a turn.** One hardened entry point
161
+ catches everything; a watchdog ends it silently when its budget (4 s) is
162
+ spent — including a C-level backstop for work that holds the interpreter
163
+ lock; stray prints never reach stdout; nothing is written to stderr.
164
+ - **Blocking responses can't be emitted.** A guard drops `decision: block/deny`,
165
+ `continue: false`, permission denials and the like — except where a TUI's
166
+ spec uses one as a non-blocking replacement, for that event only.
167
+ - **Your environment can't break it.** Hooks run as
168
+ `'<python>' -I -m subcortex.hook <tui> <event> 2>/dev/null || true`:
169
+ isolated from `PYTHONPATH` and from the project directory (a repo's own
170
+ `json.py` can't run inside a hook or the daemon), and a missing
171
+ installation stays harmless.
172
+ - **Nothing is sent to a proxy.** Hooks and plugins reach the daemon over a
173
+ direct socket to 127.0.0.1, whatever `HTTP_PROXY` says.
174
+ - **Many sessions at once are fine.** Per-session state is keyed by TUI and
175
+ session, written atomically, kept private (0600/0700) and changed under a
176
+ lock; a compaction snapshot is restored exactly once and deleted only after
177
+ delivery. The daemon answers bursts (backlog 256), sheds load it can't serve
178
+ in time (503, the hook passes through), and serializes the local model.
179
+ - **Only you can use the daemon.** Every request carries a per-user token from
180
+ a 0600 file; requests from web pages (Origin, foreign Host, non-JSON) are
181
+ refused.
182
+ - **Installs are self-tested.** Before writing anything, the installer runs the
183
+ exact commands it's about to write — with the daemon down, against a stub
184
+ daemon, and with the executable missing — and refuses to write if any run
185
+ exits non-zero, writes to stderr, emits a blocking response, runs over
186
+ budget, or stays silent where it must answer.
187
+ - **Config edits are conservative.** A diff is shown and confirmed first;
188
+ writes are atomic with timestamped backups; a symlinked (dotfiles) config
189
+ stays a symlink; a file the TUI changed during install is re-merged, not
190
+ overwritten; files that aren't plain JSON / valid TOML are refused; only
191
+ subcortex's own entries are ever removed.
192
+ - **Other TUIs' hooks are respected.** Cursor, Droid, Grok Build, Devin and
193
+ Cortex Code also execute hooks from `~/.claude/settings.json`; the Claude
194
+ Code adapter recognises foreign payloads and stays silent.
195
+
196
+ ## Privacy
197
+
198
+ - **laya** keeps everything on your machine.
199
+ - **jev** receives, per decision, your latest request, the tool call and a
200
+ ~1.5 KB head+tail excerpt of a large output — capped, and with anything that
201
+ looks like a secret (API keys, tokens, passwords, private keys, credentials
202
+ in URLs) masked first.
203
+ - Local state (remembered requests, compaction snapshots) is private to your
204
+ user and pruned automatically; the ledger behind `subcortex stats` holds
205
+ counts only, never content.
206
+
207
+ ## Daemon & settings
208
+
209
+ ```sh
210
+ subcortex doctor # check config, backend, daemon, installed hooks
211
+ subcortex config # show every setting (config edit: interactive)
212
+ subcortex config set thresholds.min_output_chars 8000
213
+ subcortex service install # start the daemon at login (launchd / systemd --user)
214
+ subcortex serve # start the background daemon (127.0.0.1:7707)
215
+ subcortex stats # hints, trims (tokens saved), restores and jev cost, per TUI
216
+ subcortex eval # labeled examples through your backend and thresholds
217
+ subcortex serve --stop
218
+ subcortex decide --state "Refactor the parser to be iterative" \
219
+ --questions '{"hard": {"type": "noul", "instructions": "Is `prompt` hard?"}}'
220
+ ```
221
+
222
+ HTTP surface (all on `127.0.0.1:<port>`; every request but `/health` carries
223
+ the per-user token from `~/.local/share/subcortex/token`):
224
+
225
+ - `POST /decide` `{state, questions}` → `{success, answers, model?, usage?}`
226
+ - `POST /verdict/prompt` `{prompt}` → `{success, verdict: {label, confidence, signals}}`
227
+ - `POST /verdict/output` `{output, context, task}` → `{success, verdict: {needed, p_needed, signals}}`
228
+ - `POST /v1/prompt-hint`, `/v1/tool-output`, `/v1/snapshot`, `/v1/restore` —
229
+ the four behaviors, for plugins (see [docs/adding-a-tui.md](docs/adding-a-tui.md))
230
+ - `GET /health`, `GET /stats`
231
+
232
+ ## Configuration
233
+
234
+ `~/.config/subcortex/config.json` (`SUBCORTEX_CONFIG` overrides the path;
235
+ `SUBCORTEX_*` env vars override keys):
236
+
237
+ ```json
238
+ {
239
+ "backend": "laya",
240
+ "port": 7707,
241
+ "features": {"prompt_hint": true, "trim_output": true, "compaction_snapshot": true},
242
+ "thresholds": {"prompt_simple_confidence": null, "output_needed_threshold": null, "min_output_chars": 6000},
243
+ "hooks": {"autostart_daemon": true, "budget_s": 4.0, "http_timeout_s": 3.0, "head_chars": 1000, "tail_chars": 500,
244
+ "snapshot_messages": 5, "snapshot_chars": 500}
245
+ }
246
+ ```
247
+
248
+ `null` thresholds use the rule calibrated for the active backend; a number
249
+ replaces that rule's primary threshold (check the effect with `subcortex eval`).
250
+ The daemon picks up config changes without a restart.
251
+
252
+ Hooks start the daemon themselves when they find it down (at most once a
253
+ minute; set `hooks.autostart_daemon` to `false` or `SUBCORTEX_AUTOSTART=0` to
254
+ turn that off) — the hook that notices still passes through untouched.
255
+ `subcortex doctor` also checks every installed integration's hook executable.
256
+
257
+ Runtime state (daemon log/PID/token, compaction snapshots, the stats ledger, `hooks.log`) lives in
258
+ `~/.local/share/subcortex/` (`SUBCORTEX_DATA_DIR`). Set `SUBCORTEX_DEBUG=1` to
259
+ log every hook invocation to `hooks.log`.
260
+
261
+ ## Any other TUI
262
+
263
+ ```sh
264
+ subcortex mcp # stdio MCP server: subcortex_decide / _classify_prompt / _judge_output
265
+ subcortex wrap -- <command> # run a command, trim its disposable output, keep its exit code
266
+ ```
267
+
268
+ MCP tools are model-invoked (the agent spends tokens calling them), so hooks
269
+ or plugins are preferred wherever a TUI has them. Adding a native adapter:
270
+ [docs/adding-a-tui.md](docs/adding-a-tui.md).
271
+
272
+ ## Development
273
+
274
+ ```sh
275
+ python3 -m venv .venv && .venv/bin/pip install -e .
276
+ .venv/bin/python -m unittest discover -s tests # no model or API key needed; bun runs the plugin tests
277
+ SUBCORTEX_E2E=1 .venv/bin/python -m unittest discover -s tests -p "test_e2e*.py" # real TUIs, mock LLM API
278
+ ```
279
+
280
+ The unit suite includes concurrency proofs (racing threads and processes,
281
+ bursts against the daemon), watchdog and proxy-immunity checks, and runs every
282
+ TS plugin under Bun. The end-to-end suites drive real TUI binaries against a
283
+ local fake LLM API (`tests/e2e/`) — no network, no cost — and assert on what
284
+ each TUI actually sent to the model. They never touch your real TUI configs.
285
+
286
+ Releases publish to PyPI via Trusted Publishing (`.github/workflows/publish.yml`):
287
+ bump `version` in `pyproject.toml`, then `gh release create v<X.Y.Z> --generate-notes`.
@@ -0,0 +1,276 @@
1
+ # subcortex
2
+
3
+ A local decision layer for coding-agent TUIs. A small daemon keeps a
4
+ typed-decision model warm and answers System-1 questions — *is this request
5
+ simple? is this tool output still needed for it?* — in milliseconds, so the big
6
+ model doesn't spend tokens on them. Hooks, plugins and MCP wire it into 29
7
+ terminal coding agents.
8
+
9
+ Two decision backends:
10
+
11
+ - **jev** — hosted typed-decision API (TypeSafe `jev-latest`, or any endpoint
12
+ speaking the same wire shape: OpenRouter, Vercel AI Gateway). ~250 ms per
13
+ decision over a kept-alive connection, $0.042 per million input tokens.
14
+ - **laya** — local model ([laya-mlx](https://pypi.org/project/laya-mlx/) on
15
+ Apple Silicon, [laya](https://pypi.org/project/laya/) elsewhere). Private,
16
+ offline, ~10 ms per question. Weaker judgments, so it acts more cautiously
17
+ (see [How good are the decisions?](#how-good-are-the-decisions)).
18
+
19
+ ## What it does inside a TUI
20
+
21
+ 1. **Prompt hint** — a request the model rates *simple* gets a one-line,
22
+ factual note ("the most direct, minimal change is likely sufficient").
23
+ 2. **Output trimming** — a large, successful shell output that the model
24
+ judges *not needed for the user's current request* is cut to its head and
25
+ tail; lines in the middle that mention the request or warn about something
26
+ are kept, and anything that looks like a failure is never touched. The
27
+ user's latest request is the evidence: without one, nothing is trimmed.
28
+ Laya doesn't trim unless you opt in.
29
+ 3. **Compaction snapshot** — before context compaction, the last few messages
30
+ are saved to disk (never vetoing anything).
31
+ 4. **Compaction restore** — after compaction they are handed back as context,
32
+ exactly once, and only deleted after they were delivered.
33
+
34
+ Each TUI gets whichever of these its extension seam can safely support:
35
+
36
+ | TUI | Seam | Hint | Trim | Compaction |
37
+ |---|---|---|---|---|
38
+ | Claude Code | hooks | ✅ | ✅ `updatedToolOutput` | ✅ PreCompact → SessionStart |
39
+ | Codex CLI ≥ 0.133 | hooks (`/hooks` trust) | ✅ | ✅ `continue:false` + `reason` | ✅ |
40
+ | Open Interpreter | hooks (Codex engine) | ✅ | ✅ | ✅ |
41
+ | Qoder CLI | hooks | ✅ | ✅ | ✅ |
42
+ | CodeBuddy Code | hooks | ✅ | — hook gets no output | ✅ on auto-compaction |
43
+ | GitHub Copilot CLI ≥ 1.0.67 | hooks | ✅ | ✅ `modifiedResult` | ✅ restored with next prompt |
44
+ | Factory Droid | hooks | ✅ (not one-shot `exec`) | — can only append | ✅ |
45
+ | Qwen Code ≥ 0.16 | hooks | ✅ | — truncates natively | ✅ |
46
+ | Gemini CLI ≥ 0.27 | hooks | ✅ | — truncates natively | ◐ on `/compress` |
47
+ | Cursor CLI | hooks | ✅ interactive | — Shell can't be replaced | ✅ restored with next prompt |
48
+ | Kimi Code CLI ≥ 0.33 | hooks | ✅ | — output hook ignored | ✅ restored with next prompt |
49
+ | OpenHands CLI ≥ 1.12 | hooks | ✅ | — truncates natively | ✅ via its event log |
50
+ | Grok Build | hooks | — output discarded | ✅ tagged `updatedToolOutput` | ✅ restored on next shell call |
51
+ | Docker Agent ≥ 1.137 | hooks | ✅ | ✅ `updated_tool_response` | ✅ restored with next prompt |
52
+ | Mistral Vibe ≥ 2.25.5 | hooks | — no event | ✅ `post_tool` reason (from a turn's 2nd call) | — no event |
53
+ | Letta Code | hooks | ✅ interactive | — | — |
54
+ | Junie CLI | hooks | ✅ interactive | — | — |
55
+ | Devin CLI | hooks | ✅ | — | — |
56
+ | OpenCode ≥ 1.1.62 | plugin | ✅ | ✅ bash only | ✅ into the compaction prompt |
57
+ | Kilo Code CLI | plugin | ✅ | ✅ | ✅ |
58
+ | Amp | plugin | ✅ | ✅ | ✅ rolling snapshot |
59
+ | Pi ≥ 0.87 | plugin | ✅ | ✅ | ✅ re-inserted after the summary |
60
+ | Cline CLI ≥ 3.0.62 | plugin | ✅ | ✅ | ✅ on the next request |
61
+ | Crush, Goose, Warp, Zed, Kiro, Auggie | MCP | on-demand tools | — | — |
62
+ | Aider | none | — | `subcortex wrap` for test/lint | — |
63
+
64
+ "—" means the TUI has no seam that can do it without blocking or failing a
65
+ tool call, so subcortex doesn't try. Per-TUI details, config paths and caveats:
66
+ [docs/tuis.md](docs/tuis.md).
67
+
68
+ **Verified end to end** — each real TUI binary, sandboxed, against a mock model
69
+ API, asserting on what it actually sent to the model: Claude Code 2.1.278,
70
+ Codex 0.155.1, Open Interpreter 0.0.45, Gemini CLI 0.60.0, Qwen Code 0.24.3,
71
+ Kimi Code 2.0.2, OpenCode 1.18.31, Grok Build 1.0.40, Pi 0.87.0, Cline 3.0.62,
72
+ Docker Agent 1.142.0, Mistral Vibe 2.25.5, Factory Droid 0.224.0, Copilot CLI
73
+ 1.0.87, OpenHands 1.16.0, Letta Code 0.32.15, CodeBuddy 2.156.0, Junie 26.9.21,
74
+ Devin CLI 3000.11.1, Cursor CLI 2026.09.18, Amp (plugin runtime), and the MCP
75
+ integrations of Crush 0.96.1, Goose 1.51.0 and Auggie 0.36.0. Qoder and Kiro
76
+ need a vendor login to run at all; their hook/MCP formats were checked against
77
+ their shipped code. Settings for MCP servers belong in `config.json`: some
78
+ TUIs (Auggie) start MCP servers without your environment variables.
79
+
80
+ ## Install
81
+
82
+ ```sh
83
+ curl -fsSL https://raw.githubusercontent.com/pavlealeksic/subcortex/main/install.sh | sh
84
+ ```
85
+
86
+ That installs the CLI into its own environment (uv, pipx or a private venv)
87
+ and starts **`subcortex setup`**, an interactive walk-through:
88
+
89
+ 1. **Backend** — Laya (local; installs `laya-mlx`/`laya` into a dedicated
90
+ environment and downloads the model) or Jev (hosted; your API key is kept in
91
+ the environment or in a mode-600 file).
92
+ 2. **Behaviors** — prompt hints, output trimming, compaction snapshots.
93
+ 3. **TUIs** — a checklist of every supported TUI, with the ones found on this
94
+ machine preselected.
95
+ 4. **Review** — each file that will change, with its diff on request; every
96
+ install self-tests its hook commands before writing.
97
+ 5. **Daemon** — start it now, and optionally at login (launchd / systemd).
98
+
99
+ Arrow keys and space in a terminal; plain numbered prompts elsewhere. Run it
100
+ again any time to change your choices; `subcortex setup --yes --backend jev
101
+ --tuis detected` runs it unattended. Or install by hand:
102
+
103
+ ```sh
104
+ pip install subcortex # daemon + jev backend (zero dependencies)
105
+ pip install "subcortex[laya]" # + local laya backend
106
+ ```
107
+
108
+ ## Wire it into a TUI (without the wizard)
109
+
110
+ ```sh
111
+ subcortex install # checklist of TUIs (in a terminal)
112
+ subcortex tuis # supported TUIs, what's installed, what's on PATH
113
+ subcortex install claude-code --dry-run # show exactly what would change
114
+ subcortex install claude-code # shows the diff, asks, self-tests, writes
115
+ subcortex install codex gemini-cli --yes # several at once, no prompt
116
+ subcortex install detected # every supported TUI found on PATH
117
+ subcortex install cursor --mcp # also register the on-demand MCP server
118
+ subcortex uninstall claude-code # removes exactly what subcortex added
119
+ ```
120
+
121
+ ## How good are the decisions?
122
+
123
+ A probability is not an accuracy, so every rule was chosen on labeled
124
+ examples and then checked on examples it had never seen
125
+ ([docs/calibration.md](docs/calibration.md)). The mistakes that matter are a
126
+ "simple" hint on complex work and a trim of output the request needs; there
127
+ were none:
128
+
129
+ | | hints: simple requests | hints: complex requests | trims: disposable output | trims: needed output |
130
+ |---|---|---|---|---|
131
+ | **jev** (calibration / held out) | 17/20 · 12/12 | **0/20 · 0/12** | 12/12 · 6/6 | **0/12 · 0/6** |
132
+ | **laya** (calibration / held out) | 5/20 · 5/12 | **0/20 · 0/12** | off by default¹ | — |
133
+
134
+ ¹ Laya's output judgments trimmed 2 of 6 needed outputs on the held-out set, so
135
+ Laya only trims if you set `thresholds.output_needed_threshold` yourself.
136
+
137
+ ```sh
138
+ subcortex eval # the same check against your backend and thresholds
139
+ ```
140
+
141
+ It exits non-zero on any hinted complex request or trimmed needed output.
142
+
143
+ ## Safety
144
+
145
+ A hook that misbehaves can take a TUI down: an earlier version of this project
146
+ exited 2 from a Claude Code hook, and exit 2 means "block this prompt". People
147
+ run this inside real work, so every one of these is enforced by tests:
148
+
149
+ - **Hooks always exit 0 and never stall a turn.** One hardened entry point
150
+ catches everything; a watchdog ends it silently when its budget (4 s) is
151
+ spent — including a C-level backstop for work that holds the interpreter
152
+ lock; stray prints never reach stdout; nothing is written to stderr.
153
+ - **Blocking responses can't be emitted.** A guard drops `decision: block/deny`,
154
+ `continue: false`, permission denials and the like — except where a TUI's
155
+ spec uses one as a non-blocking replacement, for that event only.
156
+ - **Your environment can't break it.** Hooks run as
157
+ `'<python>' -I -m subcortex.hook <tui> <event> 2>/dev/null || true`:
158
+ isolated from `PYTHONPATH` and from the project directory (a repo's own
159
+ `json.py` can't run inside a hook or the daemon), and a missing
160
+ installation stays harmless.
161
+ - **Nothing is sent to a proxy.** Hooks and plugins reach the daemon over a
162
+ direct socket to 127.0.0.1, whatever `HTTP_PROXY` says.
163
+ - **Many sessions at once are fine.** Per-session state is keyed by TUI and
164
+ session, written atomically, kept private (0600/0700) and changed under a
165
+ lock; a compaction snapshot is restored exactly once and deleted only after
166
+ delivery. The daemon answers bursts (backlog 256), sheds load it can't serve
167
+ in time (503, the hook passes through), and serializes the local model.
168
+ - **Only you can use the daemon.** Every request carries a per-user token from
169
+ a 0600 file; requests from web pages (Origin, foreign Host, non-JSON) are
170
+ refused.
171
+ - **Installs are self-tested.** Before writing anything, the installer runs the
172
+ exact commands it's about to write — with the daemon down, against a stub
173
+ daemon, and with the executable missing — and refuses to write if any run
174
+ exits non-zero, writes to stderr, emits a blocking response, runs over
175
+ budget, or stays silent where it must answer.
176
+ - **Config edits are conservative.** A diff is shown and confirmed first;
177
+ writes are atomic with timestamped backups; a symlinked (dotfiles) config
178
+ stays a symlink; a file the TUI changed during install is re-merged, not
179
+ overwritten; files that aren't plain JSON / valid TOML are refused; only
180
+ subcortex's own entries are ever removed.
181
+ - **Other TUIs' hooks are respected.** Cursor, Droid, Grok Build, Devin and
182
+ Cortex Code also execute hooks from `~/.claude/settings.json`; the Claude
183
+ Code adapter recognises foreign payloads and stays silent.
184
+
185
+ ## Privacy
186
+
187
+ - **laya** keeps everything on your machine.
188
+ - **jev** receives, per decision, your latest request, the tool call and a
189
+ ~1.5 KB head+tail excerpt of a large output — capped, and with anything that
190
+ looks like a secret (API keys, tokens, passwords, private keys, credentials
191
+ in URLs) masked first.
192
+ - Local state (remembered requests, compaction snapshots) is private to your
193
+ user and pruned automatically; the ledger behind `subcortex stats` holds
194
+ counts only, never content.
195
+
196
+ ## Daemon & settings
197
+
198
+ ```sh
199
+ subcortex doctor # check config, backend, daemon, installed hooks
200
+ subcortex config # show every setting (config edit: interactive)
201
+ subcortex config set thresholds.min_output_chars 8000
202
+ subcortex service install # start the daemon at login (launchd / systemd --user)
203
+ subcortex serve # start the background daemon (127.0.0.1:7707)
204
+ subcortex stats # hints, trims (tokens saved), restores and jev cost, per TUI
205
+ subcortex eval # labeled examples through your backend and thresholds
206
+ subcortex serve --stop
207
+ subcortex decide --state "Refactor the parser to be iterative" \
208
+ --questions '{"hard": {"type": "noul", "instructions": "Is `prompt` hard?"}}'
209
+ ```
210
+
211
+ HTTP surface (all on `127.0.0.1:<port>`; every request but `/health` carries
212
+ the per-user token from `~/.local/share/subcortex/token`):
213
+
214
+ - `POST /decide` `{state, questions}` → `{success, answers, model?, usage?}`
215
+ - `POST /verdict/prompt` `{prompt}` → `{success, verdict: {label, confidence, signals}}`
216
+ - `POST /verdict/output` `{output, context, task}` → `{success, verdict: {needed, p_needed, signals}}`
217
+ - `POST /v1/prompt-hint`, `/v1/tool-output`, `/v1/snapshot`, `/v1/restore` —
218
+ the four behaviors, for plugins (see [docs/adding-a-tui.md](docs/adding-a-tui.md))
219
+ - `GET /health`, `GET /stats`
220
+
221
+ ## Configuration
222
+
223
+ `~/.config/subcortex/config.json` (`SUBCORTEX_CONFIG` overrides the path;
224
+ `SUBCORTEX_*` env vars override keys):
225
+
226
+ ```json
227
+ {
228
+ "backend": "laya",
229
+ "port": 7707,
230
+ "features": {"prompt_hint": true, "trim_output": true, "compaction_snapshot": true},
231
+ "thresholds": {"prompt_simple_confidence": null, "output_needed_threshold": null, "min_output_chars": 6000},
232
+ "hooks": {"autostart_daemon": true, "budget_s": 4.0, "http_timeout_s": 3.0, "head_chars": 1000, "tail_chars": 500,
233
+ "snapshot_messages": 5, "snapshot_chars": 500}
234
+ }
235
+ ```
236
+
237
+ `null` thresholds use the rule calibrated for the active backend; a number
238
+ replaces that rule's primary threshold (check the effect with `subcortex eval`).
239
+ The daemon picks up config changes without a restart.
240
+
241
+ Hooks start the daemon themselves when they find it down (at most once a
242
+ minute; set `hooks.autostart_daemon` to `false` or `SUBCORTEX_AUTOSTART=0` to
243
+ turn that off) — the hook that notices still passes through untouched.
244
+ `subcortex doctor` also checks every installed integration's hook executable.
245
+
246
+ Runtime state (daemon log/PID/token, compaction snapshots, the stats ledger, `hooks.log`) lives in
247
+ `~/.local/share/subcortex/` (`SUBCORTEX_DATA_DIR`). Set `SUBCORTEX_DEBUG=1` to
248
+ log every hook invocation to `hooks.log`.
249
+
250
+ ## Any other TUI
251
+
252
+ ```sh
253
+ subcortex mcp # stdio MCP server: subcortex_decide / _classify_prompt / _judge_output
254
+ subcortex wrap -- <command> # run a command, trim its disposable output, keep its exit code
255
+ ```
256
+
257
+ MCP tools are model-invoked (the agent spends tokens calling them), so hooks
258
+ or plugins are preferred wherever a TUI has them. Adding a native adapter:
259
+ [docs/adding-a-tui.md](docs/adding-a-tui.md).
260
+
261
+ ## Development
262
+
263
+ ```sh
264
+ python3 -m venv .venv && .venv/bin/pip install -e .
265
+ .venv/bin/python -m unittest discover -s tests # no model or API key needed; bun runs the plugin tests
266
+ SUBCORTEX_E2E=1 .venv/bin/python -m unittest discover -s tests -p "test_e2e*.py" # real TUIs, mock LLM API
267
+ ```
268
+
269
+ The unit suite includes concurrency proofs (racing threads and processes,
270
+ bursts against the daemon), watchdog and proxy-immunity checks, and runs every
271
+ TS plugin under Bun. The end-to-end suites drive real TUI binaries against a
272
+ local fake LLM API (`tests/e2e/`) — no network, no cost — and assert on what
273
+ each TUI actually sent to the model. They never touch your real TUI configs.
274
+
275
+ Releases publish to PyPI via Trusted Publishing (`.github/workflows/publish.yml`):
276
+ bump `version` in `pyproject.toml`, then `gh release create v<X.Y.Z> --generate-notes`.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "subcortex"
7
+ version = "0.3.0"
8
+ description = "Local decision layer for coding-agent TUIs: a warm daemon answering System-1 questions in milliseconds"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "Apache-2.0"
12
+
13
+ [project.optional-dependencies]
14
+ laya = [
15
+ "laya-mlx>=0.1.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
16
+ "laya>=0.3.4; sys_platform != 'darwin' or platform_machine != 'arm64'",
17
+ ]
18
+
19
+ [project.scripts]
20
+ subcortex = "subcortex.cli:main"
21
+ subcortex-hook = "subcortex.hook:entry"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.setuptools.package-data]
27
+ subcortex = ["plugins/*/*.ts", "plugins/*/*.js", "plugins/*/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """subcortex — a local decision layer for coding-agent TUIs."""
2
+
3
+ __version__ = "0.3.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,48 @@
1
+ """Registry of command-hook adapters (lazy: a hook process imports only its own)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from typing import Dict, List, Optional
7
+
8
+ from ..tuis import canonical
9
+ from .base import HookAdapter
10
+
11
+ # canonical name -> "module:Class" (relative to this package)
12
+ _ADAPTERS: Dict[str, str] = {
13
+ "claude-code": "claude_family:ClaudeCodeAdapter",
14
+ "qoder": "claude_family:QoderAdapter",
15
+ "codebuddy": "claude_family:CodeBuddyAdapter",
16
+ "droid": "claude_family:DroidAdapter",
17
+ "junie": "claude_family:JunieAdapter",
18
+ "devin": "claude_family:DevinAdapter",
19
+ "codex": "codex:CodexAdapter",
20
+ "open-interpreter": "codex:OpenInterpreterAdapter",
21
+ "gemini-cli": "gemini_family:GeminiCliAdapter",
22
+ "qwen-code": "gemini_family:QwenCodeAdapter",
23
+ "cursor": "cursor:CursorAdapter",
24
+ "copilot": "copilot:CopilotAdapter",
25
+ "kimi-code": "kimi_code:KimiCodeAdapter",
26
+ "openhands": "openhands:OpenHandsAdapter",
27
+ "grok-build": "grok:GrokBuildAdapter",
28
+ "docker-agent": "docker_agent:DockerAgentAdapter",
29
+ "letta": "letta_vibe:LettaAdapter",
30
+ "vibe": "letta_vibe:VibeAdapter",
31
+ }
32
+
33
+
34
+ def canonical_name(name: str) -> Optional[str]:
35
+ return canonical(name, _ADAPTERS)
36
+
37
+
38
+ def get_adapter(name: str) -> Optional[HookAdapter]:
39
+ key = canonical_name(name)
40
+ if key is None:
41
+ return None
42
+ module_name, cls_name = _ADAPTERS[key].split(":")
43
+ module = importlib.import_module(f".{module_name}", __name__)
44
+ return getattr(module, cls_name)()
45
+
46
+
47
+ def names() -> List[str]:
48
+ return sorted(_ADAPTERS)