chad-code 2.0.0__tar.gz → 2.0.2__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 (109) hide show
  1. chad_code-2.0.2/PKG-INFO +274 -0
  2. chad_code-2.0.2/README.md +239 -0
  3. {chad_code-2.0.0 → chad_code-2.0.2}/pyproject.toml +7 -2
  4. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/__init__.py +1 -1
  5. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/agent.py +19 -3
  6. chad_code-2.0.2/src/chad_code.egg-info/PKG-INFO +274 -0
  7. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad_code.egg-info/requires.txt +1 -1
  8. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_agent.py +67 -3
  9. chad_code-2.0.0/PKG-INFO +0 -333
  10. chad_code-2.0.0/README.md +0 -298
  11. chad_code-2.0.0/src/chad_code.egg-info/PKG-INFO +0 -333
  12. {chad_code-2.0.0 → chad_code-2.0.2}/LICENSE +0 -0
  13. {chad_code-2.0.0 → chad_code-2.0.2}/setup.cfg +0 -0
  14. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/ambient.py +0 -0
  15. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/atif.py +0 -0
  16. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/base_engine.py +0 -0
  17. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/bench.py +0 -0
  18. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/checkpoint.py +0 -0
  19. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/cli.py +0 -0
  20. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/compaction.py +0 -0
  21. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/completion_engine.py +0 -0
  22. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/config.py +0 -0
  23. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/diag.py +0 -0
  24. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/engine.py +0 -0
  25. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/guardrails.py +0 -0
  26. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/ignore.py +0 -0
  27. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/levers.py +0 -0
  28. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mcp.py +0 -0
  29. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mcp_oauth.py +0 -0
  30. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mlx_dflash.py +0 -0
  31. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mlx_fastpath.py +0 -0
  32. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mlx_qmm_mma.py +0 -0
  33. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/mlx_qsdpa.py +0 -0
  34. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/LICENSE +0 -0
  35. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/__init__.py +0 -0
  36. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/alignment.py +0 -0
  37. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/attention.py +0 -0
  38. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/audio.py +0 -0
  39. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/cache.py +0 -0
  40. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/conformer.py +0 -0
  41. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/ctc.py +0 -0
  42. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/parakeet.py +0 -0
  43. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/rnnt.py +0 -0
  44. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/tokenizer.py +0 -0
  45. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/parakeet/utils.py +0 -0
  46. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/prompt.py +0 -0
  47. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/prove.py +0 -0
  48. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/render.py +0 -0
  49. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/repomap.py +0 -0
  50. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/seatbelt.py +0 -0
  51. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/serve.py +0 -0
  52. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/session.py +0 -0
  53. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/skills.py +0 -0
  54. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/speech.py +0 -0
  55. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/spill.py +0 -0
  56. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/syntaxgate.py +0 -0
  57. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/toolcall_parse.py +0 -0
  58. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/tools.py +0 -0
  59. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/tui.py +0 -0
  60. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad/validate.py +0 -0
  61. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad_code.egg-info/SOURCES.txt +0 -0
  62. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad_code.egg-info/dependency_links.txt +0 -0
  63. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad_code.egg-info/entry_points.txt +0 -0
  64. {chad_code-2.0.0 → chad_code-2.0.2}/src/chad_code.egg-info/top_level.txt +0 -0
  65. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_agent_e2e.py +0 -0
  66. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_agent_guards.py +0 -0
  67. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_ambient.py +0 -0
  68. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_atif.py +0 -0
  69. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_bench.py +0 -0
  70. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_checkpoint.py +0 -0
  71. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_cli.py +0 -0
  72. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_compaction.py +0 -0
  73. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_completion_engine.py +0 -0
  74. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_config.py +0 -0
  75. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_confirm_preview.py +0 -0
  76. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_edit.py +0 -0
  77. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_engine.py +0 -0
  78. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_engine_dflash.py +0 -0
  79. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_engine_kvquant.py +0 -0
  80. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_engine_pld_hybrid.py +0 -0
  81. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_engine_pld_wide.py +0 -0
  82. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_feel_pack.py +0 -0
  83. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_gate.py +0 -0
  84. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_ignore.py +0 -0
  85. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_intent.py +0 -0
  86. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_levers.py +0 -0
  87. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_log_redaction.py +0 -0
  88. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_mcp.py +0 -0
  89. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_mcp_oauth.py +0 -0
  90. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_mlx_fastpath.py +0 -0
  91. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_mlx_qmm_mma.py +0 -0
  92. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_mlx_qsdpa.py +0 -0
  93. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_prove.py +0 -0
  94. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_render.py +0 -0
  95. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_repomap.py +0 -0
  96. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_repomap_polyglot.py +0 -0
  97. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_seatbelt.py +0 -0
  98. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_serve.py +0 -0
  99. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_session.py +0 -0
  100. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_skills.py +0 -0
  101. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_speech.py +0 -0
  102. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_speech_tui.py +0 -0
  103. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_spill.py +0 -0
  104. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_syntaxgate.py +0 -0
  105. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_toolcall_dialect.py +0 -0
  106. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_toolcall_parse.py +0 -0
  107. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_tools.py +0 -0
  108. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_tui.py +0 -0
  109. {chad_code-2.0.0 → chad_code-2.0.2}/tests/test_validate.py +0 -0
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: chad-code
3
+ Version: 2.0.2
4
+ Summary: Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/nathansutton/chad
7
+ Project-URL: Issues, https://github.com/nathansutton/chad/issues
8
+ Keywords: mlx,coding-agent,apple-silicon,local-llm,tui
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Code Generators
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: mlx==0.32.0; sys_platform == "darwin"
21
+ Requires-Dist: mlx-lm<0.32,>=0.31.3; sys_platform == "darwin"
22
+ Requires-Dist: transformers<5.13,>=5.0
23
+ Requires-Dist: jinja2>=3.1
24
+ Requires-Dist: numpy<3,>=1.26
25
+ Requires-Dist: huggingface-hub>=1.22.0
26
+ Requires-Dist: prompt_toolkit>=3.0.43
27
+ Requires-Dist: tree-sitter>=0.26.0
28
+ Requires-Dist: tree-sitter-language-pack>=1.12.2
29
+ Requires-Dist: mcp<3,>=2
30
+ Provides-Extra: highlight
31
+ Requires-Dist: pygments>=2.17; extra == "highlight"
32
+ Provides-Extra: speech
33
+ Requires-Dist: sounddevice>=0.5; extra == "speech"
34
+ Dynamic: license-file
35
+
36
+ # chad: a local Claude-Code-style coding agent for your laptop
37
+
38
+ [![tests](https://github.com/nathansutton/chad/actions/workflows/tests.yml/badge.svg)](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
39
+
40
+ <img src="docs/claude-vs-chad.jpg" width="66%" alt="Two staircase newel posts side by side: Claude is a hand-carved wooden horse head, chad is a scuffed plastic toy horse tied on with twine">
41
+
42
+ > Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
43
+ > *Coding under supervision.*
44
+
45
+ chad is a single-user coding agent that runs entirely on an Apple Silicon Mac via
46
+ [MLX](https://github.com/ml-explore/mlx). One 27B model and no API key. Every other
47
+ harness assumes a datacenter on the other end of a socket; chad assumes a laptop,
48
+ and the whole design falls out of that. (Not affiliated with Anthropic.)
49
+
50
+ ## Try it
51
+
52
+ ```bash
53
+ uvx chad-code # runs anywhere; the command is still `chad`
54
+ uvx chad-code prove # offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
55
+ ```
56
+
57
+ The first run asks, then downloads the model once (~13 GB) into the shared Hugging
58
+ Face cache. While it downloads, `cd` into a project and think of a scoped first ask:
59
+ *"fix the failing test in `tests/test_x.py`"* lands, *"improve my codebase"* flails.
60
+
61
+ chad targets 24 GB and nothing smaller. It runs below that and tells you it is doing so,
62
+ but 13 GB of weights sit resident before a single token of context, so a 16 GB Mac gets a
63
+ window too small to work in.
64
+
65
+ The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
66
+
67
+ ![chad fixing a failing test end to end: reason, read, edit, run pytest, confirm green, all on a local model](docs/demo.gif)
68
+
69
+ > Real session, unedited (the silent prefill is cut). A local model finds the cent that
70
+ > floor division loses, fixes it, and verifies itself, then runs your own `pytest` in your
71
+ > own shell. Recorded with `--yolo` so nothing pauses for a keypress; the default mode stops
72
+ > and asks before every edit and every command.
73
+
74
+ ## Same model, same Mac, stock engine
75
+
76
+ The question worth answering: what do you gain over pointing a generic local-model tool at
77
+ the same weights? Qwen3.8-27B at the same `UD-Q3_K_XL` recipe (Unsloth's GGUF for
78
+ llama.cpp, chad's MLX conversion of the same bit map), the same M4 Pro (24 GB), one engine
79
+ resident at a time, each measured with its own benchmark on a 512-token prompt and a
80
+ 128-token generation.
81
+
82
+ | Engine | Prefill (512-tok prompt) | Decode (128 tok) | Speculative decoding |
83
+ |---|---|---|---|
84
+ | llama.cpp `llama-bench` (stock, build 10470) | 102 tok/s | 10.9 tok/s | none for this model |
85
+ | **chad**, serial (`CHAD_NO_DFLASH=1`) | 99 tok/s | 18.1 tok/s | off |
86
+ | **chad**, default | 98 tok/s | **62 tok/s**¹ | DFlash2 block drafter |
87
+
88
+ A 200-token function body takes roughly 18 seconds at 10.9 tok/s and 3 at 62. That is the
89
+ difference between a batch job and a pair programmer, and closing it is what the project is
90
+ for.
91
+
92
+ Ollama does not get its own row: it is llama.cpp underneath with no speculative decoding for
93
+ this model, and on the same GGUF (0.32.15, Modelfile `FROM` only) it measures 96 tok/s
94
+ prefill and the same **10.9** decode.
95
+
96
+ ¹ 62 is a ceiling: `chad-bench`'s prompt is tiled code the drafter reads easily. Replayed
97
+ against ten real mid-session contexts from `~/.chad/sessions` (12–19k tokens, tool results in
98
+ place, 384-token decodes) the same engine measures **31.7 tok/s median / 21.4 floor** greedy
99
+ against 14.8 serial, and **27.6 / 17.7** thinking against 13.9. That ~2× is what a session
100
+ lives at. llama.cpp's Metal path for this hybrid architecture was not profiled, so read its
101
+ row as what a fitted engine buys, not as a verdict on llama.cpp.
102
+
103
+ Method, the longer runs and the caveats are in
104
+ [Throughput & performance](docs/benchmarks.md#same-model-same-mac-stock-engine); the rows are
105
+ committed under [`benchmarks/stock/_runs/`](benchmarks/stock/); reproduce them with
106
+ `uv run python benchmarks/stock/stock.py {llama,chad}`.
107
+
108
+ ## Where the speed comes from
109
+
110
+ chad owns its inference loop instead of talking to a server, and the engine is fitted to the
111
+ one checkpoint it ships:
112
+
113
+ - **DFlash2 block speculation.** A bundled 1.9B drafter ([z-lab's DFlash2](https://huggingface.co/z-lab/Qwen3.8-27B-DFlash2),
114
+ ported to MLX and quantized) proposes a whole block of tokens from the main model's own
115
+ hidden states. The main model verifies the block in one batched forward, and exact
116
+ rejection sampling keeps every emitted token the model's own.
117
+ - **A persistent prefix KV cache.** The transcript is kept a strict token-prefix of the live
118
+ cache, so a follow-up step prefills the ~16 tokens it appended instead of the 5,000 it
119
+ already read: **~0.75 s per step instead of ~50 s**. Any server with prompt caching gets
120
+ the easy case; the work is holding it true across compaction, truncated turns and restarts.
121
+ The system prefix is checkpointed to disk, so the second session in a project starts warm
122
+ (75.6 s → 5.5 s to the first tool call).
123
+ - **Fused Metal kernels.** Quantized-KV attention, a small-M matmul for speculative verify,
124
+ and a compiled single-token layer step, chosen per machine at load time, no knobs.
125
+
126
+ ## What chad gives up
127
+
128
+ He has some of the same moves (tool use, plan mode, a real TUI) but he is a blunter
129
+ instrument:
130
+
131
+ | | **Claude** | **chad** 🗿 |
132
+ |-----------------|-------------------------------------------------|---------------------------------|
133
+ | **Range** | every workflow, every person, incredible nuance | one job: code, on your machine |
134
+ | **Runs** | anywhere: cloud, IDE, terminal, phone | your mac. that's it. |
135
+ | **Brain** | a frontier model in a datacenter | one 27B on your SSD |
136
+ | **Disposition** | understands what you *meant* | does what you *said* |
137
+ | **Harness** | open-ended, anything you can imagine | plan. execute. nothing else. |
138
+ | **When wrong** | reasons a way out | already shipped |
139
+
140
+ Five tools: `bash`, `edit`, `write`, `write_todos`, `done`. `bash` is the primary one
141
+ because the model already knows `rg` and `sed -n`. The todo list is a checklist the model
142
+ copies forward and ticks. Speed and the Claude Code muscle memory got the engineering
143
+ budget; everything else is deliberately plain.
144
+
145
+ ## Interactive UX
146
+
147
+ `uv run chad` launches a full-screen terminal UI (built on prompt_toolkit):
148
+
149
+ - **shift-tab cycles permission modes**: `normal` (confirm each bash/write/edit) →
150
+ `auto-accept edits` (edits land silently, **terminal commands still ask**) → `yolo`
151
+ (nothing asks) → `plan mode` (read-only: investigate and propose a numbered plan) → back.
152
+ - **type-ahead message queue.** Keep typing while the agent works; messages run in order.
153
+ - **ctrl-c interrupts the running turn** without killing the session. **↑prefilled /
154
+ ↓generated** token counts show an advancing **%** on an unavoidable full re-prefill, so it
155
+ is never silent.
156
+ - **`@file` / `@dir` mentions** and **`!command` shell passthrough.** Pull a file into
157
+ context inline, or run a shell command without invoking the model.
158
+ - **voice mode, all local.** `/speech`, then ctrl-t to talk: Parakeet-on-MLX transcribes into
159
+ the input box for you to review before Enter sends it, and replies are read aloud via macOS
160
+ `say`. A word table teaches it your identifiers. Needs the `speech` extra
161
+ ([details](docs/configuration.md#voice-mode-speech)).
162
+
163
+ `uv run chad --help` is the source of truth:
164
+
165
+ | Flag | What it does |
166
+ |---|---|
167
+ | `-c, --continue` | resume this directory's **most recent** session (non-destructive) |
168
+ | `--resume` | list recent sessions, pick one by number (interactive TTY only) |
169
+ | `--plan` | start in read-only plan mode (investigate and propose, edits blocked) |
170
+ | `--yolo` | auto-approve bash/write/edit (skip confirm prompts) |
171
+ | `--no-think` | skip the model's `<think>` blocks, faster on well-scoped work |
172
+ | `--model` | `auto` (the shipped default), or any HF repo id / local model dir |
173
+ | `--repl` | plain line REPL instead of the TUI |
174
+
175
+ Three subcommands, each with its own `--help`: `chad prove` (the offline smoke test),
176
+ `chad serve` ([serve this Mac's model to a container or the LAN](docs/configuration.md#serving-the-local-model-to-a-container-chad-serve)),
177
+ and `chad levers` (print the result-channel lever registry as JSON, for A/B ablation).
178
+
179
+ A headless task (positional, or piped with no TTY) auto-approves mutating tools and runs
180
+ greedy (temp 0). Every conversation is persisted under `~/.chad/sessions/`, and every resume
181
+ forks a new branch rather than overwriting.
182
+
183
+ ## The model
184
+
185
+ chad ships exactly one, downloaded once into the shared Hugging Face cache
186
+ (`~/.cache/huggingface`, reused across every project). There is no picker and no size tier.
187
+
188
+ | Model | Quant | Footprint |
189
+ |---|---|---|
190
+ | [Qwen3.8-27B `UD-Q3_K_XL-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-UD-Q3_K_XL-DFlash2-MLX) | 3-bit group-64 body, 5-bit `lm_head`, bundled 4-bit DFlash2 drafter | ~13 GB resident, 262k native context |
191
+
192
+ Qwen3.8-27B is **dense** (64 layers: 48 GatedDeltaNet + 16 full attention), so every
193
+ parameter is on the critical path for every token and the quant is where decode speed comes
194
+ from. The bits go where held-out perplexity says they pay: `lm_head` is a second full
195
+ 1.27B-param tensor and is held at 5-bit, while `embed_tokens` is a lookup table whose error
196
+ never compounds through a matmul, so it is cheapest. The name follows
197
+ [Unsloth's convention](https://docs.unsloth.ai/) (`UD-…`), though the quant is MLX group-64
198
+ affine, not a llama.cpp k-quant. The drafter ships in the same repo, pre-quantized.
199
+
200
+ `--model <repo or local dir>` runs different weights through the same engine and stays a
201
+ first-class escape hatch. The drafter, the fused-attention coverage, the decode fastpath and
202
+ the context governor are all fitted to the shipped checkpoint, so other weights run slower;
203
+ they do not break.
204
+
205
+ ## Installing & upgrading
206
+
207
+ The one-line quickstart (`uvx chad-code`) is up top. The other ways in:
208
+
209
+ ```bash
210
+ uv tool install chad-code # install for good, then it's just `chad`
211
+ uvx --from git+https://github.com/nathansutton/chad chad # bleeding-edge main, no clone
212
+ ```
213
+
214
+ Or from a clone (the dev path):
215
+
216
+ ```bash
217
+ uv sync # install deps + the `chad` entrypoint (one time)
218
+ uv run chad # full-screen TUI
219
+ uv run chad "add a --json flag to main.py and update the tests" # one-shot, headless
220
+ uv run chad -c # resume this directory's last conversation
221
+ ```
222
+
223
+ **Optional extras.** Two features are opt-in because they pull deps not every install wants:
224
+ `speech` (voice mode: a mic library, no torch) and `highlight` (syntax colour in diffs and
225
+ previews). An extra rides on the install spec, not on a separate command, so how you add it
226
+ depends on how you installed chad:
227
+
228
+ ```bash
229
+ uv tool install --force 'chad-code[speech]' # add to an existing `uv tool` install
230
+ uvx --from 'chad-code[speech]' chad # one-off run, nothing installed
231
+ uv sync --extra speech # from a clone
232
+ ```
233
+
234
+ `/speech` in the TUI prints whichever of those matches your install, so you never have to
235
+ work it out from here.
236
+
237
+ **Upgrading** depends on how you installed: `uv tool upgrade chad-code`, `uvx --refresh
238
+ chad-code`, or `git pull && uv sync` for a clone. What changed lands in
239
+ [`CHANGELOG.md`](CHANGELOG.md). Model weights are versioned separately, so a code upgrade
240
+ never re-downloads the model.
241
+
242
+ **Development.** `uv sync` once, then `uv run pytest -q`. The fast unit gate loads **no model
243
+ weights**, runs in seconds, and is what CI runs. For throughput on your own machine, use
244
+ `uv run chad-bench` (see [Throughput & performance](docs/benchmarks.md)).
245
+
246
+ ## Extending chad
247
+
248
+ chad speaks the same two extension formats as Claude Code:
249
+
250
+ - **[Agent Skills](https://agentskills.io).** Drop a `SKILL.md` folder in
251
+ `./.claude/skills/` and it becomes a slash command: `/ship`, `/investigate the flaky test`.
252
+ Skills cost nothing until you run one, because chad puts no skill catalog in the system
253
+ prompt.
254
+ - **[MCP servers](https://modelcontextprotocol.io).** Configure stdio or HTTP servers in
255
+ `./.mcp.json` to expose external tools (GitHub, Postgres, Linear, Slack, …) alongside
256
+ chad's builtins, with static-token and OAuth auth.
257
+
258
+ Both are covered in full in the [Configuration reference](docs/configuration.md).
259
+
260
+ ## Documentation
261
+
262
+ - **[Design & internals](docs/design.md)** covers why prefill is the bill, the persistent
263
+ prefix cache, the trimmable/append-only trade, why the tool surface is five tools, and the
264
+ ideas borrowed from other agents.
265
+ - **[Throughput & performance](docs/benchmarks.md)** has the prefill, decode and warm-step
266
+ numbers you can reproduce with `chad-bench`, the stock-engine comparison, and what the
267
+ cross-session warm start is worth.
268
+ - **[Configuration reference](docs/configuration.md)** documents Agent Skills, MCP servers,
269
+ the context window, every environment variable, and the safety opt-outs.
270
+ - **[Troubleshooting](docs/troubleshooting.md)** is the symptom→knob map for when a session
271
+ rambles, loops, or slows.
272
+ - **[Contributing](CONTRIBUTING.md)** says what lands easily and what needs a conversation
273
+ first.
274
+
@@ -0,0 +1,239 @@
1
+ # chad: a local Claude-Code-style coding agent for your laptop
2
+
3
+ [![tests](https://github.com/nathansutton/chad/actions/workflows/tests.yml/badge.svg)](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
4
+
5
+ <img src="docs/claude-vs-chad.jpg" width="66%" alt="Two staircase newel posts side by side: Claude is a hand-carved wooden horse head, chad is a scuffed plastic toy horse tied on with twine">
6
+
7
+ > Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
8
+ > *Coding under supervision.*
9
+
10
+ chad is a single-user coding agent that runs entirely on an Apple Silicon Mac via
11
+ [MLX](https://github.com/ml-explore/mlx). One 27B model and no API key. Every other
12
+ harness assumes a datacenter on the other end of a socket; chad assumes a laptop,
13
+ and the whole design falls out of that. (Not affiliated with Anthropic.)
14
+
15
+ ## Try it
16
+
17
+ ```bash
18
+ uvx chad-code # runs anywhere; the command is still `chad`
19
+ uvx chad-code prove # offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
20
+ ```
21
+
22
+ The first run asks, then downloads the model once (~13 GB) into the shared Hugging
23
+ Face cache. While it downloads, `cd` into a project and think of a scoped first ask:
24
+ *"fix the failing test in `tests/test_x.py`"* lands, *"improve my codebase"* flails.
25
+
26
+ chad targets 24 GB and nothing smaller. It runs below that and tells you it is doing so,
27
+ but 13 GB of weights sit resident before a single token of context, so a 16 GB Mac gets a
28
+ window too small to work in.
29
+
30
+ The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
31
+
32
+ ![chad fixing a failing test end to end: reason, read, edit, run pytest, confirm green, all on a local model](docs/demo.gif)
33
+
34
+ > Real session, unedited (the silent prefill is cut). A local model finds the cent that
35
+ > floor division loses, fixes it, and verifies itself, then runs your own `pytest` in your
36
+ > own shell. Recorded with `--yolo` so nothing pauses for a keypress; the default mode stops
37
+ > and asks before every edit and every command.
38
+
39
+ ## Same model, same Mac, stock engine
40
+
41
+ The question worth answering: what do you gain over pointing a generic local-model tool at
42
+ the same weights? Qwen3.8-27B at the same `UD-Q3_K_XL` recipe (Unsloth's GGUF for
43
+ llama.cpp, chad's MLX conversion of the same bit map), the same M4 Pro (24 GB), one engine
44
+ resident at a time, each measured with its own benchmark on a 512-token prompt and a
45
+ 128-token generation.
46
+
47
+ | Engine | Prefill (512-tok prompt) | Decode (128 tok) | Speculative decoding |
48
+ |---|---|---|---|
49
+ | llama.cpp `llama-bench` (stock, build 10470) | 102 tok/s | 10.9 tok/s | none for this model |
50
+ | **chad**, serial (`CHAD_NO_DFLASH=1`) | 99 tok/s | 18.1 tok/s | off |
51
+ | **chad**, default | 98 tok/s | **62 tok/s**¹ | DFlash2 block drafter |
52
+
53
+ A 200-token function body takes roughly 18 seconds at 10.9 tok/s and 3 at 62. That is the
54
+ difference between a batch job and a pair programmer, and closing it is what the project is
55
+ for.
56
+
57
+ Ollama does not get its own row: it is llama.cpp underneath with no speculative decoding for
58
+ this model, and on the same GGUF (0.32.15, Modelfile `FROM` only) it measures 96 tok/s
59
+ prefill and the same **10.9** decode.
60
+
61
+ ¹ 62 is a ceiling: `chad-bench`'s prompt is tiled code the drafter reads easily. Replayed
62
+ against ten real mid-session contexts from `~/.chad/sessions` (12–19k tokens, tool results in
63
+ place, 384-token decodes) the same engine measures **31.7 tok/s median / 21.4 floor** greedy
64
+ against 14.8 serial, and **27.6 / 17.7** thinking against 13.9. That ~2× is what a session
65
+ lives at. llama.cpp's Metal path for this hybrid architecture was not profiled, so read its
66
+ row as what a fitted engine buys, not as a verdict on llama.cpp.
67
+
68
+ Method, the longer runs and the caveats are in
69
+ [Throughput & performance](docs/benchmarks.md#same-model-same-mac-stock-engine); the rows are
70
+ committed under [`benchmarks/stock/_runs/`](benchmarks/stock/); reproduce them with
71
+ `uv run python benchmarks/stock/stock.py {llama,chad}`.
72
+
73
+ ## Where the speed comes from
74
+
75
+ chad owns its inference loop instead of talking to a server, and the engine is fitted to the
76
+ one checkpoint it ships:
77
+
78
+ - **DFlash2 block speculation.** A bundled 1.9B drafter ([z-lab's DFlash2](https://huggingface.co/z-lab/Qwen3.8-27B-DFlash2),
79
+ ported to MLX and quantized) proposes a whole block of tokens from the main model's own
80
+ hidden states. The main model verifies the block in one batched forward, and exact
81
+ rejection sampling keeps every emitted token the model's own.
82
+ - **A persistent prefix KV cache.** The transcript is kept a strict token-prefix of the live
83
+ cache, so a follow-up step prefills the ~16 tokens it appended instead of the 5,000 it
84
+ already read: **~0.75 s per step instead of ~50 s**. Any server with prompt caching gets
85
+ the easy case; the work is holding it true across compaction, truncated turns and restarts.
86
+ The system prefix is checkpointed to disk, so the second session in a project starts warm
87
+ (75.6 s → 5.5 s to the first tool call).
88
+ - **Fused Metal kernels.** Quantized-KV attention, a small-M matmul for speculative verify,
89
+ and a compiled single-token layer step, chosen per machine at load time, no knobs.
90
+
91
+ ## What chad gives up
92
+
93
+ He has some of the same moves (tool use, plan mode, a real TUI) but he is a blunter
94
+ instrument:
95
+
96
+ | | **Claude** | **chad** 🗿 |
97
+ |-----------------|-------------------------------------------------|---------------------------------|
98
+ | **Range** | every workflow, every person, incredible nuance | one job: code, on your machine |
99
+ | **Runs** | anywhere: cloud, IDE, terminal, phone | your mac. that's it. |
100
+ | **Brain** | a frontier model in a datacenter | one 27B on your SSD |
101
+ | **Disposition** | understands what you *meant* | does what you *said* |
102
+ | **Harness** | open-ended, anything you can imagine | plan. execute. nothing else. |
103
+ | **When wrong** | reasons a way out | already shipped |
104
+
105
+ Five tools: `bash`, `edit`, `write`, `write_todos`, `done`. `bash` is the primary one
106
+ because the model already knows `rg` and `sed -n`. The todo list is a checklist the model
107
+ copies forward and ticks. Speed and the Claude Code muscle memory got the engineering
108
+ budget; everything else is deliberately plain.
109
+
110
+ ## Interactive UX
111
+
112
+ `uv run chad` launches a full-screen terminal UI (built on prompt_toolkit):
113
+
114
+ - **shift-tab cycles permission modes**: `normal` (confirm each bash/write/edit) →
115
+ `auto-accept edits` (edits land silently, **terminal commands still ask**) → `yolo`
116
+ (nothing asks) → `plan mode` (read-only: investigate and propose a numbered plan) → back.
117
+ - **type-ahead message queue.** Keep typing while the agent works; messages run in order.
118
+ - **ctrl-c interrupts the running turn** without killing the session. **↑prefilled /
119
+ ↓generated** token counts show an advancing **%** on an unavoidable full re-prefill, so it
120
+ is never silent.
121
+ - **`@file` / `@dir` mentions** and **`!command` shell passthrough.** Pull a file into
122
+ context inline, or run a shell command without invoking the model.
123
+ - **voice mode, all local.** `/speech`, then ctrl-t to talk: Parakeet-on-MLX transcribes into
124
+ the input box for you to review before Enter sends it, and replies are read aloud via macOS
125
+ `say`. A word table teaches it your identifiers. Needs the `speech` extra
126
+ ([details](docs/configuration.md#voice-mode-speech)).
127
+
128
+ `uv run chad --help` is the source of truth:
129
+
130
+ | Flag | What it does |
131
+ |---|---|
132
+ | `-c, --continue` | resume this directory's **most recent** session (non-destructive) |
133
+ | `--resume` | list recent sessions, pick one by number (interactive TTY only) |
134
+ | `--plan` | start in read-only plan mode (investigate and propose, edits blocked) |
135
+ | `--yolo` | auto-approve bash/write/edit (skip confirm prompts) |
136
+ | `--no-think` | skip the model's `<think>` blocks, faster on well-scoped work |
137
+ | `--model` | `auto` (the shipped default), or any HF repo id / local model dir |
138
+ | `--repl` | plain line REPL instead of the TUI |
139
+
140
+ Three subcommands, each with its own `--help`: `chad prove` (the offline smoke test),
141
+ `chad serve` ([serve this Mac's model to a container or the LAN](docs/configuration.md#serving-the-local-model-to-a-container-chad-serve)),
142
+ and `chad levers` (print the result-channel lever registry as JSON, for A/B ablation).
143
+
144
+ A headless task (positional, or piped with no TTY) auto-approves mutating tools and runs
145
+ greedy (temp 0). Every conversation is persisted under `~/.chad/sessions/`, and every resume
146
+ forks a new branch rather than overwriting.
147
+
148
+ ## The model
149
+
150
+ chad ships exactly one, downloaded once into the shared Hugging Face cache
151
+ (`~/.cache/huggingface`, reused across every project). There is no picker and no size tier.
152
+
153
+ | Model | Quant | Footprint |
154
+ |---|---|---|
155
+ | [Qwen3.8-27B `UD-Q3_K_XL-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-UD-Q3_K_XL-DFlash2-MLX) | 3-bit group-64 body, 5-bit `lm_head`, bundled 4-bit DFlash2 drafter | ~13 GB resident, 262k native context |
156
+
157
+ Qwen3.8-27B is **dense** (64 layers: 48 GatedDeltaNet + 16 full attention), so every
158
+ parameter is on the critical path for every token and the quant is where decode speed comes
159
+ from. The bits go where held-out perplexity says they pay: `lm_head` is a second full
160
+ 1.27B-param tensor and is held at 5-bit, while `embed_tokens` is a lookup table whose error
161
+ never compounds through a matmul, so it is cheapest. The name follows
162
+ [Unsloth's convention](https://docs.unsloth.ai/) (`UD-…`), though the quant is MLX group-64
163
+ affine, not a llama.cpp k-quant. The drafter ships in the same repo, pre-quantized.
164
+
165
+ `--model <repo or local dir>` runs different weights through the same engine and stays a
166
+ first-class escape hatch. The drafter, the fused-attention coverage, the decode fastpath and
167
+ the context governor are all fitted to the shipped checkpoint, so other weights run slower;
168
+ they do not break.
169
+
170
+ ## Installing & upgrading
171
+
172
+ The one-line quickstart (`uvx chad-code`) is up top. The other ways in:
173
+
174
+ ```bash
175
+ uv tool install chad-code # install for good, then it's just `chad`
176
+ uvx --from git+https://github.com/nathansutton/chad chad # bleeding-edge main, no clone
177
+ ```
178
+
179
+ Or from a clone (the dev path):
180
+
181
+ ```bash
182
+ uv sync # install deps + the `chad` entrypoint (one time)
183
+ uv run chad # full-screen TUI
184
+ uv run chad "add a --json flag to main.py and update the tests" # one-shot, headless
185
+ uv run chad -c # resume this directory's last conversation
186
+ ```
187
+
188
+ **Optional extras.** Two features are opt-in because they pull deps not every install wants:
189
+ `speech` (voice mode: a mic library, no torch) and `highlight` (syntax colour in diffs and
190
+ previews). An extra rides on the install spec, not on a separate command, so how you add it
191
+ depends on how you installed chad:
192
+
193
+ ```bash
194
+ uv tool install --force 'chad-code[speech]' # add to an existing `uv tool` install
195
+ uvx --from 'chad-code[speech]' chad # one-off run, nothing installed
196
+ uv sync --extra speech # from a clone
197
+ ```
198
+
199
+ `/speech` in the TUI prints whichever of those matches your install, so you never have to
200
+ work it out from here.
201
+
202
+ **Upgrading** depends on how you installed: `uv tool upgrade chad-code`, `uvx --refresh
203
+ chad-code`, or `git pull && uv sync` for a clone. What changed lands in
204
+ [`CHANGELOG.md`](CHANGELOG.md). Model weights are versioned separately, so a code upgrade
205
+ never re-downloads the model.
206
+
207
+ **Development.** `uv sync` once, then `uv run pytest -q`. The fast unit gate loads **no model
208
+ weights**, runs in seconds, and is what CI runs. For throughput on your own machine, use
209
+ `uv run chad-bench` (see [Throughput & performance](docs/benchmarks.md)).
210
+
211
+ ## Extending chad
212
+
213
+ chad speaks the same two extension formats as Claude Code:
214
+
215
+ - **[Agent Skills](https://agentskills.io).** Drop a `SKILL.md` folder in
216
+ `./.claude/skills/` and it becomes a slash command: `/ship`, `/investigate the flaky test`.
217
+ Skills cost nothing until you run one, because chad puts no skill catalog in the system
218
+ prompt.
219
+ - **[MCP servers](https://modelcontextprotocol.io).** Configure stdio or HTTP servers in
220
+ `./.mcp.json` to expose external tools (GitHub, Postgres, Linear, Slack, …) alongside
221
+ chad's builtins, with static-token and OAuth auth.
222
+
223
+ Both are covered in full in the [Configuration reference](docs/configuration.md).
224
+
225
+ ## Documentation
226
+
227
+ - **[Design & internals](docs/design.md)** covers why prefill is the bill, the persistent
228
+ prefix cache, the trimmable/append-only trade, why the tool surface is five tools, and the
229
+ ideas borrowed from other agents.
230
+ - **[Throughput & performance](docs/benchmarks.md)** has the prefill, decode and warm-step
231
+ numbers you can reproduce with `chad-bench`, the stock-engine comparison, and what the
232
+ cross-session warm start is worth.
233
+ - **[Configuration reference](docs/configuration.md)** documents Agent Skills, MCP servers,
234
+ the context window, every environment variable, and the safety opt-outs.
235
+ - **[Troubleshooting](docs/troubleshooting.md)** is the symptom→knob map for when a session
236
+ rambles, loops, or slows.
237
+ - **[Contributing](CONTRIBUTING.md)** says what lands easily and what needs a conversation
238
+ first.
239
+
@@ -4,7 +4,7 @@
4
4
  # import name, and command name are independent. `uvx chad-code` runs the alias
5
5
  # script added under [project.scripts].
6
6
  name = "chad-code"
7
- version = "2.0.0"
7
+ version = "2.0.2"
8
8
  description = "Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,7 +37,12 @@ dependencies = [
37
37
  # exceptions instead of a SIGABRT (ml-explore/mlx#3523) — engine._prefill/generate
38
38
  # lean on that for the on-24GB recovery path. Perf re-gated
39
39
  # on the bump: prefill/decode within noise of 0.31.2 on the shipped model.
40
- "mlx>=0.32.0,<0.33; sys_platform == 'darwin'",
40
+ # mlx 0.32.1: EXCLUDED — produces degenerate generations with chad's kernel stack
41
+ # (garbage think blocks, repetition loops, hallucinated tasks from the first token;
42
+ # A/B-confirmed 2026-08-23: same wheel, same prompt, sane on ==0.32.0, garbled on
43
+ # ==0.32.1). Point releases must be re-gated against the custom QSDPA/DFlash2/MMA
44
+ # kernels before widening this pin.
45
+ "mlx==0.32.0; sys_platform == 'darwin'",
41
46
  "mlx-lm>=0.31.3,<0.32; sys_platform == 'darwin'",
42
47
  # mlx-lm requires transformers>=5.0, but 5.13.0 regressed AutoTokenizer.register:
43
48
  # it now does `key.__module__.startswith("transformers.")` with no hasattr guard, and
@@ -4,7 +4,7 @@ A flat collection of cooperating modules behind one console script (``chad``):
4
4
  the inference engine, the tool layer, the agent loop, and the terminal UI.
5
5
  """
6
6
 
7
- __version__ = "2.0.0"
7
+ __version__ = "2.0.2"
8
8
 
9
9
  # chad sets no MLX_* runtime vars. MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER
10
10
  # and MLX_MAX_MB_PER_BUFFER were each measured end-to-end on the 35B and every
@@ -206,8 +206,15 @@ def close_unclosed_think(text: str, thinking: bool) -> str:
206
206
  re-prefill of the whole transcript next step (measured: tens of thousands of tokens
207
207
  at large context). Appending the missing `</think>` keeps the cached tokens a strict
208
208
  prefix of the re-render, so only a couple of tokens prefill instead. No-op when
209
- thinking is off or the block is already closed."""
210
- if thinking and "<think>" not in text and "</think>" not in text and text:
209
+ thinking is off or the block is already closed.
210
+
211
+ Only the CLOSE decides this. An earlier guard also required no `<think>` in the
212
+ text, which read a stray opening tag — one the model wrote inside its own reasoning,
213
+ not the template's — as "already a block" and skipped the append. That turn then
214
+ reached `split_inline_reasoning` with no `</think>` to split on, so it was stored raw
215
+ and re-rendered with an injected empty think block: the exact full re-prefill this
216
+ function exists to prevent, on the longest turns, where it costs the most."""
217
+ if thinking and "</think>" not in text and text:
211
218
  return text + "\n</think>"
212
219
  return text
213
220
 
@@ -237,8 +244,17 @@ def split_inline_reasoning(m: dict) -> dict:
237
244
  if m.get("role") != "assistant" or "</think>" not in content:
238
245
  return m
239
246
  head, _, tail = content.partition("</think>")
247
+ head = head.rstrip("\n")
248
+ # Strip a LEADING `<think>` only. The generation prompt already opened the block, so
249
+ # any later tag is reasoning the model wrote, not a delimiter. Taking the LAST one
250
+ # (`split("<think>")[-1]`) silently dropped every reasoning token before a stray tag:
251
+ # transcript content lost, and the shortened re-render diverges from the cache right
252
+ # where the turn began.
253
+ unwrapped = head.lstrip()
254
+ if unwrapped.startswith("<think>"):
255
+ head = unwrapped[len("<think>"):]
240
256
  return {**m,
241
- "reasoning_content": head.rstrip("\n").split("<think>")[-1].lstrip("\n"),
257
+ "reasoning_content": head.lstrip("\n"),
242
258
  "content": tail.lstrip("\n")}
243
259
 
244
260