patch-cc 0.3.3__tar.gz → 0.5.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 (47) hide show
  1. {patch_cc-0.3.3 → patch_cc-0.5.0}/AGENTS.md +12 -6
  2. {patch_cc-0.3.3 → patch_cc-0.5.0}/PKG-INFO +14 -12
  3. {patch_cc-0.3.3 → patch_cc-0.5.0}/README.md +13 -11
  4. patch_cc-0.5.0/docs/INTERNALS.md +266 -0
  5. {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/PLAYBOOK.md +149 -28
  6. {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/corpus.md +27 -9
  7. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/__init__.py +1 -1
  8. patch_cc-0.5.0/src/patch_cc/bun/blob.py +544 -0
  9. patch_cc-0.5.0/src/patch_cc/bun/container.py +276 -0
  10. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/cli.py +44 -8
  11. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/doctor.py +109 -2
  12. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/js.py +431 -135
  13. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/menu.py +38 -4
  14. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patcher.py +34 -29
  15. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/agents.py +9 -3
  16. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/base.py +14 -0
  17. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/chrome.py +52 -12
  18. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/codex.py +1 -3
  19. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/streaming.py +283 -124
  20. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/thinking.py +4 -4
  21. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/ui.py +31 -5
  22. patch_cc-0.3.3/docs/INTERNALS.md +0 -164
  23. patch_cc-0.3.3/src/patch_cc/bun/blob.py +0 -305
  24. patch_cc-0.3.3/src/patch_cc/bun/container.py +0 -183
  25. {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/ci.yml +0 -0
  26. {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/release-watch.yml +0 -0
  27. {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/release.yml +0 -0
  28. {patch_cc-0.3.3 → patch_cc-0.5.0}/.gitignore +0 -0
  29. {patch_cc-0.3.3 → patch_cc-0.5.0}/CLAUDE.md +0 -0
  30. {patch_cc-0.3.3 → patch_cc-0.5.0}/LICENSE +0 -0
  31. {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/CONDUCT.md +0 -0
  32. {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/demo.gif +0 -0
  33. {patch_cc-0.3.3 → patch_cc-0.5.0}/pyproject.toml +0 -0
  34. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/__init__.py +0 -0
  35. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/elf.py +0 -0
  36. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/errors.py +0 -0
  37. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/macho.py +0 -0
  38. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/cache.py +0 -0
  39. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/__init__.py +0 -0
  40. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/gateway.py +0 -0
  41. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/models.py +0 -0
  42. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/oauth.py +0 -0
  43. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/translate.py +0 -0
  44. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/locate.py +0 -0
  45. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/__init__.py +0 -0
  46. {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/output.py +0 -0
  47. {patch_cc-0.3.3 → patch_cc-0.5.0}/uv.lock +0 -0
@@ -17,9 +17,15 @@ point it at the pristine copies in `~/.local/share/patch-cc/backups/` to sweep
17
17
  older builds). There is no test suite by design — doctor against real bundles
18
18
  is the check.
19
19
 
20
- Be exact about what that covers: doctor runs the **matchers** and parses the
21
- bundle they produce. It never runs the gateway, the translator, the OAuth flow
22
- or the menu, so a change under `src/patch_cc/codex/` (the bridge all of it
23
- runtime, none of it a patch; the Codex *patch* is `patches/codex.py`, which the
24
- sweep does cover) or in `menu.py` is checked by exercising it — a real `codex
25
- serve` against a real turn and a green sweep says nothing about it.
20
+ Be exact about what that covers: doctor runs the **matchers**, parses the
21
+ bundle they produce, then **bakes it into a temp binary and executes
22
+ `--version`** so the sweep also proves every build writes and boots (the
23
+ container layer included; 2.1.246 is why). It still never runs the gateway,
24
+ the translator, the OAuth flow or the menu, so a change under
25
+ `src/patch_cc/codex/` (the bridge all of it runtime, none of it a patch; the
26
+ Codex *patch* is `patches/codex.py`, which the sweep does cover) or in
27
+ `menu.py` is checked by exercising it — a real `codex serve` against a real
28
+ turn — and a green sweep says nothing about it. Nor does `--version` exercise
29
+ a patch's *feature* at runtime: it proves the patched code loads and runs, not
30
+ that a streamed turn renders — behaviour changes still want a real turn
31
+ watched.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.3.3
3
+ Version: 0.5.0
4
4
  Summary: Interactive patcher for the Claude Code native binary
5
5
  Project-URL: Homepage, https://github.com/anfreire/patch-cc
6
6
  Project-URL: Repository, https://github.com/anfreire/patch-cc
@@ -94,7 +94,7 @@ patch-cc # then just run it
94
94
  | Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
95
95
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
96
96
  | | Custom startup name | Defaults to `<your username>'s Code` |
97
- | | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line) |
97
+ | | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line). Upstream stopped drawing the segment in 2.1.246, so newer builds offer this dimmed as *not on this build* |
98
98
 
99
99
  ## Usage
100
100
 
@@ -113,8 +113,8 @@ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
113
113
  uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
114
114
  uvx patch-cc apply --from-cache # replay your last remembered selection
115
115
  uvx patch-cc status # exactly what is applied
116
- uvx patch-cc doctor # do all patches match this build?
117
- uvx patch-cc doctor path/to/claude # ...or match some other binary
116
+ uvx patch-cc doctor # do all patches match this build, and does it bake + boot?
117
+ uvx patch-cc doctor path/to/claude # ...same, for some other binary
118
118
  uvx patch-cc list # every patch, described
119
119
  uvx patch-cc restore # put the original back
120
120
  ```
@@ -198,14 +198,16 @@ startup name / `--version` marker are visible tells too.
198
198
  ## Why native-only, and why it stays small
199
199
 
200
200
  Claude Code now ships only as a Bun single-file executable; the npm package is a
201
- wrapper that downloads it. patch-cc edits the JavaScript bundle embedded in the
202
- binary's `.bun` section in place. It also drops the entry module's stale
203
- precompiled bytecode editing the source invalidates it anyway, and it is more
204
- than half the download so on Linux, where the ELF section is rewritten in
205
- place, a patched binary is *smaller* than the original, not larger. (On macOS
206
- the freed bytes are not yet reclaimed, so the file keeps its size; it still runs
207
- correctly.) [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it)
208
- has the measurements; `patch-cc status` has yours.
201
+ wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
202
+ binary's `.bun` section in place since 2.1.242 the app is code-split across
203
+ ~1,300 of them, and patch-cc treats every one as a single surface. It also drops
204
+ the stale precompiled bytecode of the modules it edits editing a module's
205
+ source invalidates its bytecode anyway so on Linux, where the ELF section is
206
+ rewritten in place, a patched binary is *smaller* than the original, not larger
207
+ (83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
208
+ the file keeps its size; it still runs correctly.)
209
+ [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
210
+ measurements; `patch-cc status` has yours.
209
211
 
210
212
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
211
213
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
@@ -63,7 +63,7 @@ patch-cc # then just run it
63
63
  | Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
64
64
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
65
65
  | | Custom startup name | Defaults to `<your username>'s Code` |
66
- | | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line) |
66
+ | | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line). Upstream stopped drawing the segment in 2.1.246, so newer builds offer this dimmed as *not on this build* |
67
67
 
68
68
  ## Usage
69
69
 
@@ -82,8 +82,8 @@ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
82
82
  uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
83
83
  uvx patch-cc apply --from-cache # replay your last remembered selection
84
84
  uvx patch-cc status # exactly what is applied
85
- uvx patch-cc doctor # do all patches match this build?
86
- uvx patch-cc doctor path/to/claude # ...or match some other binary
85
+ uvx patch-cc doctor # do all patches match this build, and does it bake + boot?
86
+ uvx patch-cc doctor path/to/claude # ...same, for some other binary
87
87
  uvx patch-cc list # every patch, described
88
88
  uvx patch-cc restore # put the original back
89
89
  ```
@@ -167,14 +167,16 @@ startup name / `--version` marker are visible tells too.
167
167
  ## Why native-only, and why it stays small
168
168
 
169
169
  Claude Code now ships only as a Bun single-file executable; the npm package is a
170
- wrapper that downloads it. patch-cc edits the JavaScript bundle embedded in the
171
- binary's `.bun` section in place. It also drops the entry module's stale
172
- precompiled bytecode editing the source invalidates it anyway, and it is more
173
- than half the download so on Linux, where the ELF section is rewritten in
174
- place, a patched binary is *smaller* than the original, not larger. (On macOS
175
- the freed bytes are not yet reclaimed, so the file keeps its size; it still runs
176
- correctly.) [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it)
177
- has the measurements; `patch-cc status` has yours.
170
+ wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
171
+ binary's `.bun` section in place since 2.1.242 the app is code-split across
172
+ ~1,300 of them, and patch-cc treats every one as a single surface. It also drops
173
+ the stale precompiled bytecode of the modules it edits editing a module's
174
+ source invalidates its bytecode anyway so on Linux, where the ELF section is
175
+ rewritten in place, a patched binary is *smaller* than the original, not larger
176
+ (83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
177
+ the file keeps its size; it still runs correctly.)
178
+ [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
179
+ measurements; `patch-cc status` has yours.
178
180
 
179
181
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
180
182
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
@@ -0,0 +1,266 @@
1
+ # Internals
2
+
3
+ How patch-cc gets from a Claude binary to a patched, smaller one.
4
+
5
+ ## The shape of a native Claude binary
6
+
7
+ Claude Code ships as a [Bun](https://bun.sh) single-file executable. The whole
8
+ app — a ~20 MB minified JS bundle plus a few asset modules — is embedded in the
9
+ binary:
10
+
11
+ - **Linux**: an ELF section named `.bun`
12
+ - **macOS**: a Mach-O section `__BUN,__bun`
13
+ - **Windows**: a PE `.bun` section (not supported here)
14
+
15
+ Inside that section is a *Bun module graph*: a flat arena of payloads, a module
16
+ table describing them, and a trailer. Before 2.1.242 the graph was the app in one
17
+ module plus a few asset modules; since then the app is **code-split** across
18
+ ~1,300 `chunk-*.js` modules that the entry lazily imports (see
19
+ [the split](#the-21242-split-and-the-patchable-surface)). Either way patch-cc
20
+ treats every module the container declares to be JavaScript as one surface.
21
+
22
+ ```
23
+ .bun section
24
+ └── [u64 size prefix] (u32 on Bun < 1.3.4)
25
+ └── Bun blob
26
+ ├── payload arena name / contents / sourcemap / bytecode / ... bytes,
27
+ │ plus the record chain's own payloads (Bun >= 1.4.1)
28
+ ├── module table N records × 52 bytes (36 on old Bun)
29
+ ├── record chain flag-gated records (Bun >= 1.4.1), see below
30
+ ├── compileExecArgv
31
+ ├── offsets struct 32 bytes: byteCount, modulesPtr, entryId, argvPtr, flags
32
+ └── "\n---- Bun! ----\n" 15-byte trailer
33
+ ```
34
+
35
+ Every pointer is a `(u32 offset, u32 length)` pair relative to the blob start,
36
+ and pointers live in exactly three places: the module table, the offsets
37
+ struct, and the record chain. That is what makes rewriting tractable — move a
38
+ payload, fix the handful of pointers that describe it.
39
+
40
+ Two invariants ride on payload *positions* rather than pointers, and `rebuild`
41
+ preserves both by keeping every payload's inter-payload gap and its offset
42
+ phase modulo 128 — so a rebuild with no edits reproduces the blob byte for
43
+ byte, and one with edits moves payloads only in whole alignment steps:
44
+
45
+ - Bytecode payloads (module bytecode, and the record chain's bytecode blobs)
46
+ sit at blob `offset % 128 == 120`, which is 128-byte alignment once the
47
+ section's 8-byte size prefix is in front. Bun ≥ 1.4.1 deserializes bytecode
48
+ in place and calls misalignment "a runtime assertion error or segfault"
49
+ (`append_bytecode_aligned`); older Bun quietly tolerated the phase drift the
50
+ rewriter used to introduce.
51
+ - `count_z` payloads (names, contents) carry a NUL terminator in the gap
52
+ after them.
53
+
54
+ A module record (new 52-byte format) is six such pairs — `name`, `contents`,
55
+ `sourcemap`, `bytecode`, `moduleInfo`, `bytecodeOriginPath` — followed by four
56
+ `u8` flags (`encoding`, `loader`, `moduleFormat`, `side`).
57
+
58
+ Code: `src/patch_cc/bun/blob.py`.
59
+
60
+ ## The record chain (Bun ≥ 1.4.1)
61
+
62
+ 2.1.246 moved to Bun 1.4.1, whose `StandaloneModuleGraph.rs` chains optional
63
+ records directly after the module table, each announced by a new `flags` bit
64
+ and read back in flag order:
65
+
66
+ | bit | record |
67
+ |---|---|
68
+ | 5 | `[u32; modules]` — each module's WTF hash of its source text (0 = none) |
69
+ | 6 | `u32 count`, then `count` × `{u32 id, ptr}` — internal-module bytecode |
70
+ | 7 | one pointer: the **shared bytecode string table** |
71
+ | 8 | `u32` — how many leading modules load before the first `import()` |
72
+ | 9 | one pointer: the string table `moduleInfo` bodies index |
73
+
74
+ The pointers point back into the arena: the shared string table (~9.9 MB on
75
+ 2.1.246) is the string data **every chunk's bytecode references by ordinal**,
76
+ so it is load-bearing for every module we did *not* touch. The hash is JSC's
77
+ SourceCodeKey hash, how a launch that runs from bytecode avoids paging in
78
+ source text just to hash it.
79
+
80
+ patch-cc parses the chain record for record (`_parse_records` — a build with
81
+ none of the bits, which is every Bun before 1.4.1, walks zero records through
82
+ the same code), carries the pointed-at payloads through `rebuild` like any
83
+ module payload, copies the whole tail between table and offsets struct
84
+ verbatim, and re-points the pointers in place. Two details matter:
85
+
86
+ - An **edited** module's hash word is zeroed — upstream's own "none, compute
87
+ it" value — because the pristine text's hash must not key our bytes in JSC's
88
+ source cache.
89
+ - **Unknown** record bits are refused at parse: a record of unknown size
90
+ cannot be walked past nor re-pointed, and rewriting around it is exactly how
91
+ a graph gets corrupted. 2.1.246 against patch-cc ≤ 0.4.0 is the lesson: the
92
+ chain-blind rewriter dropped the records and zero-filled the string table
93
+ while every *module* round-tripped byte-perfect — matcher-green, dead at
94
+ launch, `SIGSEGV` from inside Bun's graph loader.
95
+
96
+ ## The 2.1.242 split, and the patchable surface
97
+
98
+ Through 2.1.241 the entrypoint module *was* the app: one ~28 MB `contents`
99
+ carrying every line patch-cc anchors on. 2.1.242 turned on Bun code-splitting
100
+ with lazy loading, and the shape changed under the tool:
101
+
102
+ | build | modules | entrypoint `contents` |
103
+ |---|---|---|
104
+ | 2.1.241 | 11 | 28,249,679 bytes (the whole app) |
105
+ | 2.1.243 | 1,385 | 19,952 bytes (an argv shim) |
106
+
107
+ The entrypoint is now a ~20 KB shim that parses argv and lazily
108
+ `import()`s the app across ~1,300 `/$bunfs/root/chunk-*.js` modules; ~46 MB of
109
+ JS, the largest chunk 7.3 MB. The code did not disappear — every anchor is still
110
+ in the binary — but it left the one module patch-cc used to read, and it does not
111
+ concentrate in a single chunk (`branding` spans a dozen modules, `org-label` ten,
112
+ `spinner-tips` six).
113
+
114
+ So the patchable surface is **every module the container declares to be
115
+ JavaScript**, discovered the way the entrypoint itself is discovered — off the
116
+ artifact, never hardcoded. A module's *loader* (the second trailing flag) is how
117
+ Bun decides whether to compile it as source or hand it over as opaque bytes, and
118
+ the entrypoint is by definition the JS Bun runs, so its loader *is* the JS loader
119
+ (`Blob.js_modules`). The asset modules (the native addons, the bundled
120
+ `mermaid`/`hljs`, the HTML template) carry other loaders and are left alone. A
121
+ pre-split build is the one-module case of this — `js_modules()` returns just the
122
+ monolith — so [`js.Source`](PLAYBOOK.md#the-many-module-surface) spans one module
123
+ or a thousand through the same code.
124
+
125
+ The entrypoint still matters for one thing: it is where the manifest lives and
126
+ what `status` reads, named by the offsets struct's `entry_point_id` — the same
127
+ index Bun resolves it by. Its *name* is upstream's to change and we never read
128
+ it: 2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
129
+
130
+ ## The bytecode, and why we drop it
131
+
132
+ Modules carry precompiled Bun **bytecode** — most of the binary. Before the
133
+ split only the entry module had any (~half of it); the code-split builds carry it
134
+ on nearly every chunk (232 MB of 377 on 2.1.243, across ~1,375 modules).
135
+
136
+ Any edit to a module's `contents` invalidates *that module's* bytecode; Bun
137
+ detects the mismatch and recompiles that module from source at launch. So keeping
138
+ a stale copy buys nothing — the recompile is paid either way — and dropping it
139
+ reclaims the space and guarantees our edits are what runs. patch-cc drops the
140
+ bytecode of exactly the modules it edited (`rebuild` over `changed_modules`) and
141
+ leaves every untouched module its bytecode and its fast start — which is why
142
+ the shared bytecode string table those modules' bytecode indexes
143
+ ([the record chain](#the-record-chain-bun--141)) is never droppable. On Linux,
144
+ where the ELF section is rewritten in place, the binary is smaller by exactly
145
+ the edited modules' bytecode.
146
+
147
+ Measured on 2.1.243, the full patch set:
148
+
149
+ | binary | size | bytecode | startup |
150
+ |---|---|---|---|
151
+ | pristine | 378 MB | 232 MB (every module) | ~13 ms `--version` |
152
+ | patched (edited modules' bytecode dropped) | **295 MB** | 150 MB (untouched modules) | ~15 ms |
153
+
154
+ The 83 MB reclaimed is the ~45 edited modules' bytecode; the rest stays, which is
155
+ why a split-build patched binary is smaller but not the *half* a patched monolith
156
+ was. The recompile is now per lazily-imported edited module rather than the whole
157
+ app at once, so startup barely moves. Read the current figures off any binary
158
+ with `patch-cc status` rather than off this table — the bytecode total grows every
159
+ few builds.
160
+
161
+ The size story is the **ELF** path: the `.bun` section is rewritten in place, so
162
+ the dropped bytecode is genuinely reclaimed (`container.verify` refuses a Linux
163
+ write that did not shrink by about that much). On **macOS** the file keeps its
164
+ original size: `macho.py` grows a segment but never shrinks one, so the freed
165
+ bytes stay as dead space. The binary still runs correctly (the bytecode is gone),
166
+ it is just not smaller — reclaiming it means shrinking the Mach-O segment and
167
+ re-laying `__LINKEDIT`, which is not done yet.
168
+
169
+ Every write asserts each **edited** module carries `bytecode == 0` in the binary
170
+ it produced (`container.verify`, beside the round-trip check), and `patch-cc
171
+ status` reports the total for an installed one. `doctor`'s dry run cannot — a
172
+ clean bundle still has all its bytecode by definition — but its smoke bake
173
+ writes a temp binary through the same `container.write` and then *executes* it,
174
+ so the sweep exercises the assert, and the loader itself, on every corpus
175
+ build. If a future Bun build makes bytecode authoritative over source, that
176
+ assert is the tripwire — every edit would silently no-op otherwise.
177
+
178
+ ## Writing it back without ballooning
179
+
180
+ `.bun` is the last *allocated* ELF section; only non-allocated metadata
181
+ (`.comment`, `.symtab`, `.strtab`, `.shstrtab`) follows it. patch-cc rewrites
182
+ the ELF bytes in place:
183
+
184
+ 1. Splice the new (smaller) blob over the old `.bun` bytes.
185
+ 2. Shift `e_shoff`, `e_phoff`, and the trailing non-alloc sections/segments by
186
+ the size delta.
187
+ 3. Grow or shrink the containing `PT_LOAD` segment's `filesz`/`memsz` to match.
188
+
189
+ `.bun` keeps its original file offset. This is deliberately *not* done with a
190
+ general ELF library: LIEF rebuilds the binary and relocates `.bun` so its file
191
+ offset equals its virtual address (`0x20000000`), which inflates the file to
192
+ ~715 MB. Raw in-place surgery avoids that entirely.
193
+
194
+ Guards refuse anything that could corrupt the mapping: allocated sections after
195
+ `.bun`, growth into a header table, an unrelated spanning segment, or a
196
+ misaligned `PT_LOAD` shift. If any fires, the write aborts rather than guesses.
197
+
198
+ Code: `src/patch_cc/bun/elf.py`. macOS uses LIEF (`macho.py`) — Mach-O segment
199
+ growth is page-aligned and bounded, with no relocation pathology, and every
200
+ edit is followed by an ad-hoc `codesign` (mandatory on Apple Silicon).
201
+
202
+ ## The manifest
203
+
204
+ Every patched bundle carries a single comment line — appended to the **entry
205
+ module**, the one module always present and always re-extracted, and the one
206
+ `status` reads — describing its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it
207
+ means for matcher health:
208
+
209
+ ```
210
+ //patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"suffix":...,
211
+ "models":{...},"org":...,"codex":{"port":8817,"models":["gpt-5.6-sol"]}}
212
+ ```
213
+
214
+ Every key after `patches` is a configurable patch's own, declared in one place
215
+ (`Patch.setting`) so the manifest here, the cache, and the menu cannot spell it
216
+ three ways. Each is written only when *that* patch landed **and** has a value
217
+ worth recording, so `status` can never assert a name, marker, or model the
218
+ bundle does not contain — and so this is the *widest* the line gets, not its
219
+ fixed shape.
220
+
221
+ That line is why `patch-cc status` can name exactly what is applied: several
222
+ patches are value flips (`verbose:!0`) that leave no other trace. A comment
223
+ can't collide with code and travels with the bundle through extract/repack.
224
+ The menu also reads it to pre-select the current patch set — the binary is the
225
+ state.
226
+
227
+ Each key records what was *asked for*, never what was derived from it. `codex`
228
+ carries model ids and a port and nothing else: a Codex model's display name and
229
+ context window are already baked into the bundle, and repeating them here would
230
+ be a second copy — one that a relabelling upstream could make disagree with the
231
+ binary it claims to describe. That is also what makes the manifest the single
232
+ home for the gateway port: `codex serve` and `codex status` read it from here
233
+ rather than from a store of their own.
234
+
235
+ ## Safety
236
+
237
+ - Before the first patch of a version, the pristine binary is copied to
238
+ `~/.local/share/patch-cc/backups/`. `restore` copies it back — never an
239
+ inverse patch (insertions cascade, so a reverse diff is meaningless).
240
+ - Patching always starts from that pristine copy, so re-applying never stacks
241
+ edits on edits, and an apply where **nothing lands** — every selected patch
242
+ broken, so the manifest would claim nothing — leaves the binary untouched
243
+ entirely (stripping bytecode for nothing would only slow startup). A patch
244
+ that *lands* still writes even where it changed no bytes, because landing
245
+ includes an override the build already satisfies: the manifest records what
246
+ was asked and verified present, so `status` can report it.
247
+ - The bundle is parsed, and any syntax error aborts before the binary or the
248
+ backup is touched — the two checks below answer "did we write what we meant
249
+ to", which a corrupt splice satisfies perfectly. The parse is the same one
250
+ the patches locate with, so it costs nothing extra and sits at *every* batch
251
+ of edits as well as on the final bytes: a patch that produces rubble is named
252
+ and dropped rather than aborting the run. See
253
+ [PLAYBOOK.md](PLAYBOOK.md#the-syntax-gate).
254
+ - Every write is verified: patch-cc re-extracts the JS from the binary it just
255
+ wrote and asserts every module equals what it meant to write, that each
256
+ module it edited carries no leftover bytecode to run instead of the edit, and
257
+ that the graph around the modules survived — the record chain kept its
258
+ length and flags, its arena payloads (the shared bytecode string table above
259
+ all) round-trip byte-identical, and every source-hash word is the pristine
260
+ one, except an edited module's, which must be zero. The chain checks exist
261
+ because 2.1.246 failed *only* there: every module compared equal while the
262
+ written binary was dead.
263
+ - Patching a binary that is already marked, when no pristine backup exists, is
264
+ refused outright — there is nothing clean to start from, and our edits change
265
+ lengths, so a second pass would corrupt rather than update. `restore` or a
266
+ reinstall are the only honest fixes; there is deliberately no override.
@@ -152,6 +152,56 @@ gone green, and every one buys a branch against a single build. The counting
152
152
  below is the net for drift; the rule above is the only thing standing between a
153
153
  repair and the next repair.
154
154
 
155
+ ## The many-module surface
156
+
157
+ Through 2.1.241 the bundle was one module — the entrypoint carried the whole app
158
+ — and every matcher searched that one buffer. 2.1.242 code-split it: the
159
+ entrypoint became a ~20 KB argv shim that lazily imports the app across ~1,300
160
+ `chunk-*.js` modules ([INTERNALS](INTERNALS.md#the-21242-split-and-the-patchable-surface)).
161
+ The anchors did not move — they scattered. So `js.Source` **spans every
162
+ JavaScript module the container declares** (`Blob.js_modules`, discovered off the
163
+ entrypoint's own loader), and the one-module world is exactly the many-module
164
+ world with one module: `find`/`literals`/`count` sweep every module, `apply`
165
+ routes each edit to the module its node came from, and a patch says
166
+ `source.find(name)` / `source.apply(edits)` unchanged. This is representation
167
+ change of the crudest kind — the same code in more files — so it *moved* no
168
+ invariant and abolished none; the surface widened, the rules held. Three things
169
+ the move made explicit, each a rule the single-module world could leave unsaid
170
+ because the module was the bundle:
171
+
172
+ - **A node carries its module; an offset does not.** An `Edit` is built from a
173
+ node, and the node's program root (`node.id`, distinct across parses) names the
174
+ module the edit belongs to, so a batch spanning a dozen chunks routes each
175
+ splice home and reparses only the modules it touched. It is why dedup keys are
176
+ the node's `id`, never its `start_byte`: a minified offset repeats in a
177
+ thousand modules, and two different sites at the same local offset are two
178
+ sites, not one to skip.
179
+
180
+ - **A minified local is scoped to its module.** Resolving one — a hoisted
181
+ `agentType:uu` constant read through its value — searches *its own* module
182
+ (`Source.find_local`), never the bundle, where `uu` binds a string in a dozen
183
+ unrelated chunks and `js.only` would rightly refuse to choose. Before the split
184
+ the module *was* the bundle, so a bundle-wide `find` was a module-wide one; the
185
+ two only came apart when the app was dealt across files. The same lesson the
186
+ playbook already states for identifiers within a build — "a name is a spelling
187
+ until its scope is said" — now reaches the module boundary too. A scope search
188
+ that climbs outward (`live-thinking`'s state and memo resolution) reads the
189
+ module wrapper by what it *is* — the pre-split monolith's whole-body IIFE is a
190
+ module of one top-level statement; a split module's `program` is dozens, and
191
+ its top-level components are real scopes to search — never by a fixed position,
192
+ which read every split component as the un-searchable wrapper and found nothing.
193
+
194
+ - **The gate reads code, not linkage.** tree-sitter's JavaScript grammar does not
195
+ model a reserved word as an import/export alias (`export{x as if}`,
196
+ `import{if as a}` — legal ES2015, which the split minifier emits when its
197
+ two-letter alias generator lands on `if`/`in`/`do`), so it plants a localized
198
+ `ERROR` inside the clause. patch-cc never locates in or edits module linkage —
199
+ every anchor is executable code — so `Source.defect` reads *past* an error
200
+ confined to an import/export statement and returns the first one that is not,
201
+ while a splice that broke real code still lands outside linkage and is still
202
+ caught. It is the gate's scope stated precisely, not a tolerance bolted on: the
203
+ eight such modules on 2.1.243 parse clean for every purpose the tool has.
204
+
155
205
  ## Discovery instead of hardcoding
156
206
 
157
207
  Anything the binary can enumerate for us, it does:
@@ -259,6 +309,20 @@ isolation was cheaper and could not see the one ordering this project calls
259
309
  load-bearing — `codex-models` registering the ids `subagent-models` then pins —
260
310
  nor anything at all about the bundle the run produced, which it discarded.
261
311
 
312
+ The composed result is then **baked and executed**: doctor writes it into a
313
+ temp binary through the same `container.write` as a real apply — staging,
314
+ round-trip verification, the container checks — and runs `<binary> --version`,
315
+ expecting the version-marker suffix in the output (our own edit's print, so the
316
+ check proves patched code *executes*, not merely that the binary boots around
317
+ it). Matching and running are different truths with different failure owners:
318
+ matchers break in the patches, the run breaks in the container layer, and
319
+ 2.1.246 is the build where they split — every matcher green, every module
320
+ round-tripping byte-perfect, and the written binary `SIGSEGV`ing in Bun's graph
321
+ loader because what the rewrite had destroyed (the Bun 1.4.1 record chain and
322
+ the shared bytecode string table, docs/INTERNALS.md) lives in bytes no module
323
+ owns. The temp binary is always removed; the backup under test is never
324
+ written to.
325
+
262
326
  Three readings of the two numbers, kept distinct:
263
327
 
264
328
  | symptom | meaning |
@@ -313,7 +377,9 @@ re-deriving it.
313
377
 
314
378
  Both numbers above are counts, and a count cannot tell a rewrite that landed
315
379
  from a rewrite that landed *one prop-name to the left*. So the bundle is parsed,
316
- and any `ERROR` or `MISSING` node aborts (`src/patch_cc/js.py`).
380
+ and any `ERROR` or `MISSING` node **outside module linkage** aborts
381
+ (`src/patch_cc/js.py`; the linkage read-past is
382
+ [above](#the-many-module-surface)).
317
383
 
318
384
  The parse is not a separate pass any more: locating already needs it, and an
319
385
  edit costs one incremental reparse — ~75 ms on a 25 MB bundle against ~3 s for a
@@ -493,6 +559,10 @@ that broke a patch along with every note.
493
559
  patch-cc extract ~/.local/share/claude/versions/<ver> > clean.js
494
560
  ```
495
561
 
562
+ Since 2.1.242 that is every JavaScript module concatenated, each behind a
563
+ `// ==== patch-cc module <n> ====` header, so `rg` still works over one file
564
+ and the header says which chunk a hit lives in.
565
+
496
566
  2. Run `patch-cc doctor`. Note which patch dropped to `candidates == 0`, or —
497
567
  for `live-thinking` — which sub-step.
498
568
 
@@ -552,6 +622,12 @@ that broke a patch along with every note.
552
622
  enumerated with its hashes in [corpus.md](corpus.md) — so both sides are
553
623
  recomputed from the binaries themselves.
554
624
 
625
+ Each build's report ends with its baked binary's own `--version` output (the
626
+ smoke run above), so the sweep also proves every corpus build still *bakes
627
+ and boots* — the half matcher counts cannot see, and the half 2.1.246 broke
628
+ while every count held. A moved runtime line is as much a finding as a moved
629
+ count.
630
+
555
631
  ## Patch reference
556
632
 
557
633
  Grouped by source module. The menu's three groups (Output & display, Models &
@@ -792,28 +868,37 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
792
868
  *initialised* to is deliberately not asked: `useState(null)` is every
793
869
  build's spelling and `useState(void 0)` would be the same state, while the
794
870
  setter is the identity that matters.
795
- - **The state has two homes, and the handed setter names both.** Through
796
- 2.1.235 the scope that hands `onStreamingThinking` declared the state
797
- itself, and the state is the array pattern binding the handed setter
798
- `useState`'s pair. 2.1.236 moved it into an external stream store
799
- (`subscribe`/`getSnapshot`/`_publish` — the `useSyncExternalStore` shape)
800
- and hands `<store>.setStreamingThinking` instead; the same scope reads the
801
- store back by destructuring the hook call it hands the store to
802
- (`{streamingToolUses:…}=useX(<store>)`), so the state is that pattern's own
803
- `streamingThinking` binding upstream's the day it takes one, the goal
804
- achieved, and until then ours, inserted at the front of the pattern. The
805
- pattern is proven the snapshot read by the store expression itself: the
806
- call's *only* argument is the very expression the setter was read off, one
807
- answer or none (`js.only`). Sole argument is deliberate a second is a
808
- selector whose result is no longer the snapshot, and extending a pattern of
809
- unknowable provenance binds `undefined` with every count green. The
810
- insertion also pays for a witness the way `thinking-summaries` does: the
811
- field it binds must still be named by the bundle's own objects (the store's
812
- snapshot initialiser, its publish call), so a store that renames the field
813
- reads as the step reporting the store rather than threading `undefined`.
814
- Two things come free with the store: its setter takes functional updaters —
815
- React's own contract, which the reducer splices already speak and it
816
- hides a finished block itself after 30 s, upstream's own linger.
871
+ - **The state has two homes, and each is proven by the only name it has.**
872
+ Through 2.1.235 the scope that hands `onStreamingThinking` declared the
873
+ state itself, and the state is the array pattern binding the handed setter
874
+ `useState`'s pair, which nothing else names. 2.1.236 moved it into an
875
+ external stream store (`subscribe`/`getSnapshot`/`_publish` — the
876
+ `useSyncExternalStore` shape) that a component reads back by destructuring
877
+ a hook call (`{streamingToolUses:…}=useX(<store>)`), so the state is that
878
+ pattern's own `streamingThinking` binding upstream's the day it takes
879
+ one, the goal achieved, and until then ours, inserted at the front of the
880
+ pattern. The pattern names *itself*: it binds the store's own
881
+ `streamingToolUses` field the name the store publishes and the
882
+ transcript signature already rests on and that membership is its whole
883
+ identity, provided its value is a call taking exactly one argument, one
884
+ answer or none (`js.only`). It was once proven by the handing instead
885
+ "the call's only argument is the very expression the setter was read off"
886
+ and 2.1.246 moved the handing into the engine
887
+ (`this.stream.setStreamingThinking`), out of every component, without
888
+ moving the state: four renders that plainly draw the conversation read as
889
+ having none in scope, with `onStreamingThinking:` still twice in the
890
+ bundle. The handed setter was `agentDefinitions` again a neighbour
891
+ standing in for the thing itself, one more fact upstream had to keep.
892
+ Sole argument stays deliberate a second is a selector whose result is
893
+ no longer the snapshot, and extending a pattern of unknowable provenance
894
+ binds `undefined` with every count green. The insertion also pays for a
895
+ witness the way `thinking-summaries` does: the field it binds must still
896
+ be named by the bundle's own objects (the store's snapshot initialiser,
897
+ its publish call), so a store that renames the field reads as the step
898
+ reporting the store rather than threading `undefined`. Two things come
899
+ free with the store: its setter takes functional updaters — React's own
900
+ contract, which the reducer splices already speak — and it hides a
901
+ finished block itself after 30 s, upstream's own linger.
817
902
 
818
903
  A render was once selected by *position* — the observation that the real sites
819
904
  fall after the state's `useState` declaration. That is worth recording as a
@@ -856,11 +941,29 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
856
941
  than carried over from whichever renderer the first step happened to see
857
942
  last: a second component with the same four props donated its local to the
858
943
  first one's memo, spliced an identifier that component cannot see, and
859
- reported both steps green. The memo itself is found by what it computes (a
860
- `useMemo` over a `flatMap` that wraps each streaming block as a message),
861
- which is what a claim about the *text* around the block (`.contentBlock]}`)
862
- only approximated: one sibling property beside `content` read as the whole
863
- computation being gone.
944
+ reported both steps green. The memo itself is found by what it computes — an
945
+ arrow whose whole body is a `flatMap` that wraps each streaming block as a
946
+ message, and the call that takes that arrow never by the memo hook's name.
947
+ The hook is one more minified spelling: the monolith reaches it as
948
+ `<React>.useMemo(...)` (the property name survives a member read), but a
949
+ code-split module imports it under a local (`import{useMemo as te}` → `te(...)`),
950
+ so keying on `useMemo` read a build that memoizes exactly the same way as one
951
+ with no memo at all, and the rewrite reuses the memo's *own* callee rather than
952
+ re-spelling `.useMemo`. What a claim about the *text* around the block
953
+ (`.contentBlock]}`) only approximated is the computation itself: one sibling
954
+ property beside `content` read as the whole thing being gone. The wrapped
955
+ block is asked by membership among the element's *possible values*
956
+ (`js.values` — the same value routing the codex resolvers read), never asked
957
+ to be the read: 2.1.246 minted stable ids for streamed blocks
958
+ (`ce?{...S.contentBlock,id:V}:S.contentBlock`), and the exact-node question
959
+ read the same wrap one choice deeper as no wrap at all — nine required steps
960
+ to zero, every anchor count standing. The rewrite reuses upstream's callback
961
+ and dependencies verbatim — the callback invoked per entry with the same
962
+ three arguments `flatMap` hands it, each dependency spread rather than
963
+ transcribed — because a rebuilt copy is a copy free to shed whatever
964
+ upstream adds next: the re-spell this replaced would have silently dropped
965
+ the 2.1.246 minting, and with it went the uuid-stamp identity that re-spell
966
+ had to prove and the reuse no longer asks.
864
967
 
865
968
  **`display-mode`** defaults the request's thinking display to `"summarized"`;
866
969
  without it the API only streams summary text when the `showThinkingSummaries`
@@ -1251,6 +1354,24 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
1251
1354
  touched: the `/status` Organization/Email rows, the login screen, and the
1252
1355
  org's startup message (`"Message from <org>:"`) still show the real account.
1253
1356
 
1357
+ **2.1.246 retired the surface.** Upstream deleted the welcome-banner variant
1358
+ that composed the segment; the surviving banner draws `model · billing` with
1359
+ no org anywhere — the hidden state an empty `--org-label` asks for, now
1360
+ upstream's own default. That is a semantic change, not a matcher to repair
1361
+ (the `help-title` precedent), and it is *absence*, reported apart from broken
1362
+ (CONDUCT): the patch declares its surface (`Patch.absence`, answered by the
1363
+ same `_org_segments` its candidates count off, so the two cannot disagree),
1364
+ and every surface derives its answer from the bundle in hand — the menu shows
1365
+ the row dimmed (`not on this build`, unselectable), an explicit `--org-label`
1366
+ is refused at the front door with the same sentence, a cached replay skips it
1367
+ with a note, and `doctor` prints a `-` row apart from ✓/✗ and stays green.
1368
+ Nothing is keyed on a version, so a build that composes the segment again
1369
+ un-dims the row with no code change. What absence cannot tell apart is a
1370
+ composition respelled out of the locator's sight — that build would read as
1371
+ absent, visibly dimmed on a screen that still draws the segment — and the
1372
+ sweep over the corpus, where thirty-four builds carry the surface, is what
1373
+ keeps the locator honest.
1374
+
1254
1375
  ## Removed patches
1255
1376
 
1256
1377
  Kept here so nobody reintroduces them without knowing why they left: