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.
- subcortex-0.3.0/PKG-INFO +287 -0
- subcortex-0.3.0/README.md +276 -0
- subcortex-0.3.0/pyproject.toml +27 -0
- subcortex-0.3.0/setup.cfg +4 -0
- subcortex-0.3.0/src/subcortex/__init__.py +3 -0
- subcortex-0.3.0/src/subcortex/__main__.py +3 -0
- subcortex-0.3.0/src/subcortex/adapters/__init__.py +48 -0
- subcortex-0.3.0/src/subcortex/adapters/base.py +230 -0
- subcortex-0.3.0/src/subcortex/adapters/claude_family.py +133 -0
- subcortex-0.3.0/src/subcortex/adapters/codex.py +87 -0
- subcortex-0.3.0/src/subcortex/adapters/copilot.py +60 -0
- subcortex-0.3.0/src/subcortex/adapters/cursor.py +36 -0
- subcortex-0.3.0/src/subcortex/adapters/docker_agent.py +115 -0
- subcortex-0.3.0/src/subcortex/adapters/gemini_family.py +60 -0
- subcortex-0.3.0/src/subcortex/adapters/grok.py +98 -0
- subcortex-0.3.0/src/subcortex/adapters/kimi_code.py +138 -0
- subcortex-0.3.0/src/subcortex/adapters/letta_vibe.py +96 -0
- subcortex-0.3.0/src/subcortex/adapters/openhands.py +153 -0
- subcortex-0.3.0/src/subcortex/auth.py +59 -0
- subcortex-0.3.0/src/subcortex/backends/__init__.py +23 -0
- subcortex-0.3.0/src/subcortex/backends/base.py +22 -0
- subcortex-0.3.0/src/subcortex/backends/jev.py +460 -0
- subcortex-0.3.0/src/subcortex/backends/laya.py +149 -0
- subcortex-0.3.0/src/subcortex/cli.py +809 -0
- subcortex-0.3.0/src/subcortex/client.py +77 -0
- subcortex-0.3.0/src/subcortex/config.py +263 -0
- subcortex-0.3.0/src/subcortex/daemon.py +502 -0
- subcortex-0.3.0/src/subcortex/evalset.py +241 -0
- subcortex-0.3.0/src/subcortex/hook.py +254 -0
- subcortex-0.3.0/src/subcortex/installers/__init__.py +62 -0
- subcortex-0.3.0/src/subcortex/installers/amp.py +39 -0
- subcortex-0.3.0/src/subcortex/installers/base.py +874 -0
- subcortex-0.3.0/src/subcortex/installers/claude_family.py +229 -0
- subcortex-0.3.0/src/subcortex/installers/codex.py +110 -0
- subcortex-0.3.0/src/subcortex/installers/copilot.py +65 -0
- subcortex-0.3.0/src/subcortex/installers/crush.py +36 -0
- subcortex-0.3.0/src/subcortex/installers/cursor.py +79 -0
- subcortex-0.3.0/src/subcortex/installers/gemini_family.py +83 -0
- subcortex-0.3.0/src/subcortex/installers/goose.py +186 -0
- subcortex-0.3.0/src/subcortex/installers/kimi_code.py +71 -0
- subcortex-0.3.0/src/subcortex/installers/mcp_only.py +111 -0
- subcortex-0.3.0/src/subcortex/installers/more_hooks.py +184 -0
- subcortex-0.3.0/src/subcortex/installers/opencode.py +66 -0
- subcortex-0.3.0/src/subcortex/installers/openhands.py +84 -0
- subcortex-0.3.0/src/subcortex/installers/pi_cline.py +53 -0
- subcortex-0.3.0/src/subcortex/ledger.py +92 -0
- subcortex-0.3.0/src/subcortex/localhttp.py +59 -0
- subcortex-0.3.0/src/subcortex/mcp_server.py +187 -0
- subcortex-0.3.0/src/subcortex/metrics.py +56 -0
- subcortex-0.3.0/src/subcortex/plugins/amp/subcortex.ts +258 -0
- subcortex-0.3.0/src/subcortex/plugins/cline/subcortex.ts +340 -0
- subcortex-0.3.0/src/subcortex/plugins/opencode/subcortex.ts +265 -0
- subcortex-0.3.0/src/subcortex/plugins/pi/subcortex.ts +292 -0
- subcortex-0.3.0/src/subcortex/policy.py +341 -0
- subcortex-0.3.0/src/subcortex/presets.py +163 -0
- subcortex-0.3.0/src/subcortex/provision.py +188 -0
- subcortex-0.3.0/src/subcortex/service.py +149 -0
- subcortex-0.3.0/src/subcortex/state.py +137 -0
- subcortex-0.3.0/src/subcortex/transcript.py +211 -0
- subcortex-0.3.0/src/subcortex/tuis.py +51 -0
- subcortex-0.3.0/src/subcortex/ui.py +319 -0
- subcortex-0.3.0/src/subcortex/verdicts.py +233 -0
- subcortex-0.3.0/src/subcortex/wizard.py +474 -0
- subcortex-0.3.0/src/subcortex.egg-info/PKG-INFO +287 -0
- subcortex-0.3.0/src/subcortex.egg-info/SOURCES.txt +89 -0
- subcortex-0.3.0/src/subcortex.egg-info/dependency_links.txt +1 -0
- subcortex-0.3.0/src/subcortex.egg-info/entry_points.txt +3 -0
- subcortex-0.3.0/src/subcortex.egg-info/requires.txt +8 -0
- subcortex-0.3.0/src/subcortex.egg-info/top_level.txt +1 -0
- subcortex-0.3.0/tests/test_adapters.py +474 -0
- subcortex-0.3.0/tests/test_cli.py +276 -0
- subcortex-0.3.0/tests/test_config.py +81 -0
- subcortex-0.3.0/tests/test_daemon.py +315 -0
- subcortex-0.3.0/tests/test_e2e.py +246 -0
- subcortex-0.3.0/tests/test_e2e_codex.py +312 -0
- subcortex-0.3.0/tests/test_e2e_gemini.py +364 -0
- subcortex-0.3.0/tests/test_e2e_more.py +336 -0
- subcortex-0.3.0/tests/test_e2e_plugins.py +437 -0
- subcortex-0.3.0/tests/test_e2e_vendor.py +1338 -0
- subcortex-0.3.0/tests/test_hook_process.py +112 -0
- subcortex-0.3.0/tests/test_installer_base.py +151 -0
- subcortex-0.3.0/tests/test_installers.py +321 -0
- subcortex-0.3.0/tests/test_jev.py +301 -0
- subcortex-0.3.0/tests/test_lifecycle.py +179 -0
- subcortex-0.3.0/tests/test_mcp.py +123 -0
- subcortex-0.3.0/tests/test_plugins.py +298 -0
- subcortex-0.3.0/tests/test_policy.py +240 -0
- subcortex-0.3.0/tests/test_setup.py +291 -0
- subcortex-0.3.0/tests/test_state.py +306 -0
- subcortex-0.3.0/tests/test_transcript.py +115 -0
- subcortex-0.3.0/tests/test_verdicts.py +166 -0
subcortex-0.3.0/PKG-INFO
ADDED
|
@@ -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,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)
|