patch-cc 0.2.2__tar.gz → 0.2.4__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 (42) hide show
  1. {patch_cc-0.2.2 → patch_cc-0.2.4}/PKG-INFO +2 -2
  2. {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/PLAYBOOK.md +53 -15
  3. {patch_cc-0.2.2 → patch_cc-0.2.4}/pyproject.toml +1 -1
  4. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/__init__.py +1 -1
  5. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/__init__.py +1 -1
  6. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/container.py +2 -2
  7. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/elf.py +4 -4
  8. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/macho.py +2 -1
  9. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/cli.py +3 -8
  10. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/gateway.py +4 -2
  11. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/oauth.py +1 -1
  12. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/doctor.py +9 -8
  13. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/locate.py +1 -1
  14. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/menu.py +9 -8
  15. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patcher.py +12 -10
  16. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/__init__.py +2 -4
  17. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/agents.py +1 -1
  18. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/base.py +5 -10
  19. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/codex.py +2 -6
  20. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/streaming.py +147 -296
  21. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/ui.py +1 -1
  22. patch_cc-0.2.4/uv.lock +422 -0
  23. patch_cc-0.2.2/uv.lock +0 -358
  24. {patch_cc-0.2.2 → patch_cc-0.2.4}/.github/workflows/ci.yml +0 -0
  25. {patch_cc-0.2.2 → patch_cc-0.2.4}/.github/workflows/release.yml +0 -0
  26. {patch_cc-0.2.2 → patch_cc-0.2.4}/.gitignore +0 -0
  27. {patch_cc-0.2.2 → patch_cc-0.2.4}/AGENTS.md +0 -0
  28. {patch_cc-0.2.2 → patch_cc-0.2.4}/CLAUDE.md +0 -0
  29. {patch_cc-0.2.2 → patch_cc-0.2.4}/LICENSE +0 -0
  30. {patch_cc-0.2.2 → patch_cc-0.2.4}/README.md +0 -0
  31. {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/CONDUCT.md +0 -0
  32. {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/INTERNALS.md +0 -0
  33. {patch_cc-0.2.2 → patch_cc-0.2.4}/docs/demo.gif +0 -0
  34. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/blob.py +0 -0
  35. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/bun/errors.py +0 -0
  36. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/cache.py +0 -0
  37. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/__init__.py +0 -0
  38. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/models.py +0 -0
  39. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/codex/translate.py +0 -0
  40. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/chrome.py +0 -0
  41. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/output.py +0 -0
  42. {patch_cc-0.2.2 → patch_cc-0.2.4}/src/patch_cc/patches/thinking.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -46,6 +46,27 @@ constantly. These rules are what keep matchers alive:
46
46
  every backup before removing any part of it: under the older, tighter callee
47
47
  that same probe read 1 on all 8 builds, so a measurement taken before the
48
48
  callee was dissolved does not survive it.
49
+ - **Rewrite at the dispatch point; never model the arm.** When an update is
50
+ valid at arm entry, splice it in where the code dispatches on the semantic
51
+ string — after a `case` label chain, or by wrapping the dispatch test
52
+ itself, `(test&&((update),!0))`, which preserves the test's value exactly
53
+ and so survives any surrounding composition (wrapping the *enclosing*
54
+ condition instead would run the update for the other side of a bare `||`) —
55
+ and say nothing about what the arm already does. Arm bodies are upstream's
56
+ busiest surface: 2.1.226 threaded one flag through five arms of the stream
57
+ reducer (`stmt1,stmt2;` became `if(stmt1,stmt2,d)call;`) and appended a
58
+ declarator to the head's `let`, and every literal that had modelled those
59
+ bodies — a cross-product of `mode(` vs `mode?.(`, braced vs unbraced arms,
60
+ four `displayTransform` spellings — died or was already dead weight. The
61
+ dispatch strings themselves (`"thinking_delta"`, `"message_stop"`) are the
62
+ API's vocabulary and do not churn. Same doctrine as the callee rule above:
63
+ the arm is *reached through*, the dispatch point is what the update is
64
+ *applied to*. Entry insertion does reorder against mid-arm work (the
65
+ message-stop reset now runs before upstream's `finalize()`), so it asks
66
+ that the update be independent of the arm's own channels — decide that,
67
+ don't assume it. Bound heads at grammar edges for the same reason — in a
68
+ minified bundle only `;` or `,` can follow `let{…}=t`, and which one is
69
+ noise (`(?=[;,])`, not `;`).
49
70
  - Match an optional brace **pair** conditionally (`(\{)?…(?(1)\})`), never as
50
71
  two independent `\{?` / `\}?`. A lone optional closing brace eats the
51
72
  *enclosing* block's `}` on the unbraced shape, and a rewrite that emits its
@@ -95,10 +116,18 @@ can finish.
95
116
  Every patched bundle ends with one comment line recording what was applied; its
96
117
  shape lives in [INTERNALS.md](INTERNALS.md#the-manifest). What matters here is
97
118
  that it makes `status` a parse instead of a guess — several patches are value
98
- flips (`verbose:!0`) that leave no other fingerprint — and that `is_patched`
99
- also still recognises the legacy fingerprints (`__cc_` identifiers, the old
100
- `--version` marker), so binaries patched by pre-manifest versions are not
101
- mistaken for clean.
119
+ flips (`verbose:!0`) that leave no other fingerprint — and that it is the
120
+ **only** evidence `is_patched` accepts. Authorship is declared, never inferred:
121
+ `is_patched` once also took side effects of our edits as proof — the `__cc_`
122
+ identifier prefix, the old `--version` marker — until 2.1.227 shipped
123
+ `__cc_name`/`__cc_line`/`__cc_set` shell variables of its own and every
124
+ pristine install read as patched, hard-blocking `apply`. A prefix tracks
125
+ upstream's naming fashion, never authorship (the same lesson as the `.enum(`
126
+ callee above), and once upstream ships it, it fires on every later build, so no
127
+ narrowing rescues an inferred fingerprint — `__cc_line` was already both our
128
+ arrow-param and their shell variable. Binaries patched by pre-manifest versions
129
+ therefore read as clean, which costs nothing now that Claude's daily
130
+ auto-update has long since replaced that population.
102
131
 
103
132
  ## How resilience is detected
104
133
 
@@ -174,7 +203,7 @@ first is how a manifest starts lying. The manifest is held to the same rule:
174
203
 
175
204
  ## Sub-steps, and why `live-thinking` has them
176
205
 
177
- Most patches are one rewrite. `live-thinking` is ~11 named sub-steps, because
206
+ Most patches are one rewrite. `live-thinking` is ~20 named sub-steps, because
178
207
  upstream has reshaped the stream reducer at least three times and a single hit
179
208
  count cannot tell "all landed" from "half silently drifted".
180
209
 
@@ -183,9 +212,16 @@ kinds:
183
212
 
184
213
  - **Independent fixes** (`memo-cache`, `linger`, `display-mode`, `bottom-row`,
185
214
  …) — each may or may not be present on a given build.
186
- - **Mutually-exclusive reducer variants** — `reducer-destructured` (2.1.138+),
187
- `reducer-inner` (2.1.183+), `reducer-legacy` (pre-2.1.138). On any one build
188
- exactly one should land. On 2.1.216 it is `reducer-inner`.
215
+ - **Mutually-exclusive reducer variants** — `reducer-options` (2.1.138+, the
216
+ options-bag head; whether the bag still destructures `onStreamingThinking`
217
+ is the same shape with the prop present or absent, not a second variant) and
218
+ `reducer-legacy` (pre-2.1.138, positional parameters). On any one build
219
+ exactly one should land; its count is *recognition*, one per reducer. The
220
+ state updates themselves are per-dispatch-point steps (`request-start`,
221
+ `message-stop`, `text-clear`, `message-delta-clear`, `thinking-start`,
222
+ `thinking-append`), so a build that folds one arm away reads as that point's
223
+ absence by name, never as a bare count drop inside an aggregate. On
224
+ 2.1.216–2.1.226 the variant is `reducer-options` with all six points.
189
225
 
190
226
  An *optional* sub-step that finds nothing is reported as absent, not broken —
191
227
  it is just a shape this build doesn't have. A sub-step that finds a shape but
@@ -201,11 +237,11 @@ The rest carry expectations, so their absence is checked rather than noted:
201
237
  but *proof*: they are credited only when the rewritten reducer body actually
202
238
  contains the state updates, which is what recognising a reducer does not by
203
239
  itself establish.
204
- - **The `reducer` group** — `reducer-destructured`, `reducer-inner`,
205
- `reducer-legacy`. At least one must land; none landing is the signal that
206
- upstream shipped a fourth reducer shape that needs a new variant. Two landing
207
- is allowed on purpose — a transitional build carrying two reducers would have
208
- both correctly patched, and that is no reason to cry wolf.
240
+ - **The `reducer` group** — `reducer-options`, `reducer-legacy`. At least one
241
+ must land; none landing is the signal that upstream shipped a new reducer
242
+ head that needs a new variant. Two landing is allowed on purpose — a
243
+ transitional build carrying two reducers would have both correctly patched,
244
+ and that is no reason to cry wolf.
209
245
  - **Optional** — `memo-cache`, `memo-removal`, `linger`, `bottom-row`,
210
246
  `final-summary`. The first four match nothing on 2.1.216+ and are kept for
211
247
  older builds; `discover` is a notes-only channel that records how far back the
@@ -354,11 +390,13 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
354
390
 
355
391
  ### Live thinking — `streaming.py`
356
392
 
357
- - **`live-thinking`** — the ~11-step patch above. Discovery anchor is
393
+ - **`live-thinking`** — the ~20-step patch above. Discovery anchor is
358
394
  `onStreamingThinking:` → `useState(null)` (the older `hidePastThinking`
359
395
  anchor is gone as of 2.1.216 — the fallback back-scan is load-bearing).
360
396
  Reducer anchors: `type==="stream_request_start"`, `case"thinking_delta"`,
361
- `content_block_start`.
397
+ `content_block_start` — the reducer is the function carrying all three, its
398
+ head bounded at the declarator edge, and every state update is an insertion
399
+ at a dispatch point (the rule above); the arm bodies are never matched.
362
400
  The `display-mode` sub-step defaults the request's thinking display to
363
401
  `"summarized"`; without it the API only streams summary text when the
364
402
  `showThinkingSummaries` setting is on. Two shapes: the legacy inline env
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patch-cc"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Interactive patcher for the Claude Code native binary"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -3,4 +3,4 @@
3
3
  from .container import Bundle, ContainerError, read, write
4
4
  from .errors import BunError
5
5
 
6
- __all__ = ["Bundle", "BunError", "ContainerError", "read", "write"]
6
+ __all__ = ["BunError", "Bundle", "ContainerError", "read", "write"]
@@ -85,7 +85,7 @@ def write(
85
85
  compared against ``source`` -- *before* it is moved into place. A rebuild
86
86
  bug therefore fails without ever touching the live binary.
87
87
  """
88
- import shutil # noqa: PLC0415
88
+ import shutil
89
89
 
90
90
  new_blob = blobmod.rebuild(
91
91
  bundle.blob, source.encode("utf8"), drop_bytecode=drop_bytecode
@@ -120,7 +120,7 @@ def verify(path: str, expected: str) -> None:
120
120
  """Re-extract from a written binary and assert it round-trips exactly."""
121
121
  try:
122
122
  written = read(path)
123
- except Exception as exc: # noqa: BLE001
123
+ except Exception as exc:
124
124
  raise ContainerError(f"patched binary could not be re-read: {exc}") from exc
125
125
  if written.source != expected:
126
126
  raise ContainerError(
@@ -224,11 +224,11 @@ def write_section(buf: bytes, payload: bytes, name: str = ".bun") -> bytes:
224
224
  and seg.filesz
225
225
  and seg.offset >= end
226
226
  and seg.align
227
+ and (seg.offset + delta) % seg.align != seg.vaddr % seg.align
227
228
  ):
228
- if (seg.offset + delta) % seg.align != seg.vaddr % seg.align:
229
- raise ElfError(
230
- f"shifting LOAD segment {seg.index} would break its alignment"
231
- )
229
+ raise ElfError(
230
+ f"shifting LOAD segment {seg.index} would break its alignment"
231
+ )
232
232
 
233
233
  new_size = bun.size + delta
234
234
  body = bytearray(payload)
@@ -27,7 +27,7 @@ class MachOError(BunError):
27
27
 
28
28
  def _lief():
29
29
  try:
30
- import lief # noqa: PLC0415
30
+ import lief
31
31
  except ImportError as exc: # pragma: no cover - platform dependent
32
32
  raise MachOError(
33
33
  "LIEF is required to patch macOS binaries. Install it with "
@@ -107,6 +107,7 @@ def codesign(path: str) -> None:
107
107
  ["codesign", "--sign", "-", "--force", path],
108
108
  capture_output=True,
109
109
  text=True,
110
+ check=False,
110
111
  )
111
112
  if result.returncode != 0: # pragma: no cover - macOS only
112
113
  raise MachOError(f"codesign failed: {result.stderr.strip()}")
@@ -36,7 +36,7 @@ from pathlib import Path
36
36
  from rich.markup import escape
37
37
 
38
38
  from . import cache, locate, patcher
39
- from .bun import BunError, Bundle
39
+ from .bun import Bundle, BunError
40
40
  from .codex import DEFAULT_PORT, is_valid_port
41
41
  from .codex.models import CodexModel, catalogue, is_reserved_id, is_valid_id, reconcile
42
42
  from .patches import (
@@ -525,11 +525,6 @@ def cmd_status(args) -> int:
525
525
  if isinstance(codex.get("port"), int):
526
526
  style, note = gateway_note(codex["port"])
527
527
  console.print(f" gateway: [{style}]{note}[/{style}]")
528
- elif st.patched:
529
- console.print(
530
- " [dim]patched by an older patch-cc (no manifest); "
531
- "re-apply to record one[/dim]"
532
- )
533
528
  console.print(
534
529
  f" bytecode: "
535
530
  f"{'stripped' if st.bytecode_stripped else f'{st.bytecode_size / 1e6:.0f} MB present'}"
@@ -549,7 +544,7 @@ def _doctor_target(path: str | None) -> tuple[Bundle, str] | None:
549
544
  given -- that is how any kept backup becomes a regression corpus -- while the
550
545
  installed binary falls back to its pristine copy when it is already patched.
551
546
  """
552
- from .bun import container # noqa: PLC0415
547
+ from .bun import container
553
548
 
554
549
  if path is not None:
555
550
  # A name the user typed is data, not markup: an unescaped `claude[old]`
@@ -695,7 +690,7 @@ def _baked_port() -> int | None:
695
690
  why ``serve`` asks once at startup rather than per request -- and why a re-bake
696
691
  onto a different port needs a ``serve`` restart, as the apply report says.
697
692
  """
698
- from .bun import container # noqa: PLC0415
693
+ from .bun import container
699
694
 
700
695
  install = locate.find()
701
696
  if install is None:
@@ -155,7 +155,7 @@ class _Handler(BaseHTTPRequestHandler):
155
155
  def log_message(self, *_args) -> None: # keep the terminal clean
156
156
  pass
157
157
 
158
- def do_POST(self) -> None: # noqa: N802 (BaseHTTPRequestHandler's name)
158
+ def do_POST(self) -> None:
159
159
  # One request per connection: an SSE reply is open-ended (no
160
160
  # Content-Length), so the stream ends by connection close.
161
161
  self.close_connection = True
@@ -177,7 +177,9 @@ class _Handler(BaseHTTPRequestHandler):
177
177
  # this API just as unparseable bytes are, and is answered the same
178
178
  # way. Reaching `_messages` with one used to raise straight out of
179
179
  # the handler thread: a traceback, a reset connection, no reply.
180
- raise ValueError("not a JSON object")
180
+ raise ValueError( # noqa: TRY004 - lands in json's own handler below
181
+ "not a JSON object"
182
+ )
181
183
  except ValueError:
182
184
  return self._error(
183
185
  400, "invalid_request_error", "request body must be a JSON object"
@@ -74,7 +74,7 @@ class Credentials:
74
74
  }
75
75
 
76
76
  @classmethod
77
- def from_json(cls, data: object) -> "Credentials | None":
77
+ def from_json(cls, data: object) -> Credentials | None:
78
78
  if not isinstance(data, dict):
79
79
  return None
80
80
  access, refresh = data.get("access"), data.get("refresh")
@@ -3,8 +3,8 @@
3
3
  Two different questions, deliberately kept apart:
4
4
 
5
5
  * **status** -- is the *installed* binary patched right now? Answered by the
6
- manifest comment every patched bundle ends with (plus legacy fingerprints),
7
- and the bytecode-stripped invariant.
6
+ manifest comment every patched bundle ends with, and the bytecode-stripped
7
+ invariant.
8
8
  * **dryrun** -- would our patches still apply to *this* bundle? Answered by
9
9
  running every patch and reporting per-step hits, so a silently drifted
10
10
  matcher shows up as a concrete "reducer.message_stop missed" instead of a
@@ -21,20 +21,23 @@ from dataclasses import dataclass, field
21
21
 
22
22
  from .bun import Bundle
23
23
  from .codex.models import CodexModel
24
- from .patcher import is_patched, read_manifest
24
+ from .patcher import read_manifest
25
25
  from .patches import ALL_PATCHES, Options, Outcome, Patch
26
26
  from .patches.agents import INHERIT, BuiltinAgent, discover_agents, discover_models
27
27
 
28
28
 
29
29
  @dataclass(slots=True)
30
30
  class Status:
31
- patched: bool
32
31
  bytecode_stripped: bool
33
32
  bytecode_size: int
34
33
  #: Parsed manifest for binaries patched by this tool; ``None`` when the
35
- #: binary is pristine or predates the manifest.
34
+ #: binary is pristine.
36
35
  manifest: dict | None
37
36
 
37
+ @property
38
+ def patched(self) -> bool:
39
+ return self.manifest is not None
40
+
38
41
  @property
39
42
  def patch_ids(self) -> list[str]:
40
43
  if not self.manifest:
@@ -48,12 +51,10 @@ class Status:
48
51
 
49
52
 
50
53
  def status(bundle: Bundle) -> Status:
51
- source = bundle.source
52
54
  return Status(
53
- patched=is_patched(source),
54
55
  bytecode_stripped=bundle.bytecode_size == 0,
55
56
  bytecode_size=bundle.bytecode_size,
56
- manifest=read_manifest(source),
57
+ manifest=read_manifest(bundle.source),
57
58
  )
58
59
 
59
60
 
@@ -78,7 +78,7 @@ def _resolve(path: Path) -> Installation | None:
78
78
 
79
79
  def _is_native(binary: Path) -> bool:
80
80
  """Is this file one of the containers we can actually open?"""
81
- from .bun import BunError, container # noqa: PLC0415 - keeps import order flat
81
+ from .bun import BunError, container
82
82
 
83
83
  try:
84
84
  container.detect(str(binary))
@@ -27,8 +27,9 @@ from __future__ import annotations
27
27
  import sys
28
28
  import textwrap
29
29
  import threading
30
+ from collections.abc import Callable
30
31
  from dataclasses import dataclass, field
31
- from typing import TYPE_CHECKING, Any, Callable
32
+ from typing import TYPE_CHECKING, Any
32
33
 
33
34
  from blessed import Terminal
34
35
  from rich import box
@@ -140,7 +141,7 @@ class MenuModel:
140
141
  """Everything the menu operates on, independent of the rendering engine."""
141
142
 
142
143
  install: locate.Installation
143
- status: "Status"
144
+ status: Status
144
145
  pristine: Bundle
145
146
  agents: list[BuiltinAgent]
146
147
  models: list[str]
@@ -154,8 +155,8 @@ class MenuModel:
154
155
 
155
156
  @classmethod
156
157
  def build(
157
- cls, install: locate.Installation, status: "Status", pristine: Bundle
158
- ) -> "MenuModel":
158
+ cls, install: locate.Installation, status: Status, pristine: Bundle
159
+ ) -> MenuModel:
159
160
  agents = discover_agents(pristine.source)
160
161
  models = [INHERIT, *discover_models(pristine.source)]
161
162
  model = cls(
@@ -577,7 +578,7 @@ class MenuApp:
577
578
  self.view = "select" # select | busy | report | doctor
578
579
  self.stack: list[tuple[Modal, Callable[[object], None] | None]] = []
579
580
  self.report: patcher.PatchReport | None = None
580
- self.doctor_result: "DryRun | None" = None
581
+ self.doctor_result: DryRun | None = None
581
582
  self.busy_message = ""
582
583
  self.flash = ""
583
584
  self.exit_code = 0
@@ -642,7 +643,7 @@ class MenuApp:
642
643
  attrs = termios.tcgetattr(fd)
643
644
  attrs[0] &= ~(termios.IXON | termios.IXOFF)
644
645
  termios.tcsetattr(fd, termios.TCSANOW, attrs)
645
- except Exception:
646
+ except Exception: # noqa: BLE001, S110 - best effort; no termios, no freeze risk
646
647
  pass
647
648
 
648
649
  def _key_name(self, keystroke) -> str:
@@ -1080,7 +1081,7 @@ class MenuApp:
1080
1081
 
1081
1082
  def _apply(
1082
1083
  self, selection: cache.Selection
1083
- ) -> tuple[patcher.PatchReport, "Status | None"]:
1084
+ ) -> tuple[patcher.PatchReport, Status | None]:
1084
1085
  options = selection.options
1085
1086
  if options.codex_models:
1086
1087
  # Fill in each model's name and window right before baking. A pick
@@ -1154,7 +1155,7 @@ class MenuApp:
1154
1155
  def run() -> None:
1155
1156
  try:
1156
1157
  self._worker_result = (kind, work(), None)
1157
- except Exception as exc:
1158
+ except Exception as exc: # noqa: BLE001 - the worker's failure IS the result
1158
1159
  self._worker_result = (kind, None, str(exc) or exc.__class__.__name__)
1159
1160
 
1160
1161
  threading.Thread(target=run, daemon=True).start()
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
 
12
12
  from . import __version__, locate
13
13
  from .bun import Bundle, container
14
- from .patches import ALL_PATCHES, DEFAULT_SUFFIX, SENTINEL, Options, Outcome, Patch
14
+ from .patches import ALL_PATCHES, DEFAULT_SUFFIX, Options, Outcome, Patch
15
15
 
16
16
  #: Every patched bundle ends with one comment line recording exactly what was
17
17
  #: applied. Comments cannot collide with code, survive re-extraction, and make
@@ -19,9 +19,6 @@ from .patches import ALL_PATCHES, DEFAULT_SUFFIX, SENTINEL, Options, Outcome, Pa
19
19
  #: fingerprint.
20
20
  MANIFEST_PREFIX = "//patch-cc "
21
21
 
22
- #: Fingerprints of binaries patched by versions before the manifest existed.
23
- _LEGACY_MARKER = "(Claude Code)\\n" + DEFAULT_SUFFIX
24
-
25
22
 
26
23
  class AlreadyPatchedError(RuntimeError):
27
24
  """The only source available is already patched; patching it would stack.
@@ -132,11 +129,16 @@ def read_manifest(source: str) -> dict | None:
132
129
 
133
130
 
134
131
  def is_patched(source: str) -> bool:
135
- return (
136
- ("\n" + MANIFEST_PREFIX) in source
137
- or SENTINEL in source
138
- or _LEGACY_MARKER in source
139
- )
132
+ """Whether the bundle carries our manifest -- the one mark of our work.
133
+
134
+ Authorship is declared, never inferred. This used to also sniff side
135
+ effects of our edits (the ``__cc_`` identifier prefix, the old
136
+ ``--version`` marker), until 2.1.227 shipped ``__cc_``-prefixed shell
137
+ variables of its own and every pristine install read as patched. An
138
+ inferred fingerprint is a bet that upstream's vocabulary never overlaps
139
+ ours, and once upstream ships it, it fires on every build after, forever.
140
+ """
141
+ return read_manifest(source) is not None
140
142
 
141
143
 
142
144
  def selected_patches(ids: list[str]) -> list[Patch]:
@@ -326,7 +328,7 @@ def restore(install: locate.Installation) -> Path:
326
328
  "for this version was never saved -- reinstall to get a clean binary."
327
329
  )
328
330
  # A full-file copy-back, so it works for both ELF and Mach-O.
329
- from .bun.elf import atomic_write # noqa: PLC0415
331
+ from .bun.elf import atomic_write
330
332
 
331
333
  atomic_write(str(install.binary), backup.read_bytes(), mode_from=str(backup))
332
334
  return install.binary
@@ -22,7 +22,6 @@ from .base import (
22
22
  GROUP_CHROME,
23
23
  GROUP_MODELS,
24
24
  GROUP_OUTPUT,
25
- SENTINEL,
26
25
  Options,
27
26
  Outcome,
28
27
  Patch,
@@ -64,9 +63,8 @@ __all__ = [
64
63
  "Options",
65
64
  "Outcome",
66
65
  "Patch",
67
- "SENTINEL",
66
+ "by_group",
67
+ "default_ids",
68
68
  "derived_brand",
69
69
  "ids",
70
- "default_ids",
71
- "by_group",
72
70
  ]
@@ -79,7 +79,7 @@ def _subagent_prompt(content: str, _options: Options, outcome: Outcome) -> str:
79
79
  transcript_var = transcript.group(1)
80
80
  gate_pattern = compile_js(rf"{re.escape(transcript_var)}&&({IDENT})&&")
81
81
 
82
- def drop_gate(match: re.Match[str]) -> str:
82
+ def drop_gate(match: re.Match[str], segment: str = segment) -> str:
83
83
  prompt_var = match.group(1)
84
84
  nearby = segment[match.end() : match.end() + 260]
85
85
  if f"{{prompt:{prompt_var},theme:" not in nearby:
@@ -32,11 +32,6 @@ from ..codex import DEFAULT_PORT
32
32
  if TYPE_CHECKING:
33
33
  from ..codex.models import CodexModel
34
34
 
35
- #: Prefix for every identifier we inject. These are the only reliable
36
- #: fingerprints of our work -- value flips like ``verbose:!0`` are
37
- #: indistinguishable from the 11 that upstream already ships.
38
- SENTINEL = "__cc_"
39
-
40
35
  # Groups, in display order: what renders, what runs (and how hard), how it is
41
36
  # dressed. Generic on purpose -- a group named after a feature (Thinking,
42
37
  # Subagents, Codex) fits exactly that feature, and the next patch that is
@@ -69,7 +64,7 @@ def derived_brand() -> str:
69
64
 
70
65
  try:
71
66
  user = getpass.getuser().strip()
72
- except Exception:
67
+ except Exception: # noqa: BLE001 - however it fails, the answer is the same
73
68
  user = ""
74
69
  return f"{user}'s Code" if user else DEFAULT_BRAND
75
70
 
@@ -84,7 +79,7 @@ class Options:
84
79
  #: Codex models to register and route -- ordinary patch configuration, like
85
80
  #: every field here: chosen in the menu or with ``--codex``, and recorded in
86
81
  #: the manifest by the patch that bakes it.
87
- codex_models: list["CodexModel"] = field(default_factory=list)
82
+ codex_models: list[CodexModel] = field(default_factory=list)
88
83
  #: Localhost port shared by the redirect patch and the gateway. Defaulted
89
84
  #: from the one home (:data:`patch_cc.codex.DEFAULT_PORT`) so the number
90
85
  #: never lives in two places.
@@ -116,7 +111,7 @@ class Outcome:
116
111
  applied: int = 0
117
112
  notes: list[str] = field(default_factory=list)
118
113
  #: Named sub-steps, for patches built from several independent rewrites.
119
- steps: dict[str, "Outcome"] = field(default_factory=dict)
114
+ steps: dict[str, Outcome] = field(default_factory=dict)
120
115
  #: What this sub-step's absence means (set via :meth:`step`). ``False`` --
121
116
  #: a shape some builds simply lack; ``True`` -- the patch is broken without
122
117
  #: it; a string -- name of a group of which at least one member must land.
@@ -153,7 +148,7 @@ class Outcome:
153
148
  def note(self, message: str) -> None:
154
149
  self.notes.append(message)
155
150
 
156
- def step(self, name: str, expect: bool | str = False) -> "Outcome":
151
+ def step(self, name: str, expect: bool | str = False) -> Outcome:
157
152
  """Get (or create) a named sub-outcome.
158
153
 
159
154
  A single scalar count cannot distinguish "all twelve rewrites landed"
@@ -167,7 +162,7 @@ class Outcome:
167
162
  sub.expect = expect
168
163
  return sub
169
164
 
170
- def finalize(self) -> "Outcome":
165
+ def finalize(self) -> Outcome:
171
166
  """Roll sub-step totals up into this outcome."""
172
167
  if self.steps:
173
168
  self.candidates += sum(s.candidates for s in self.steps.values())
@@ -304,12 +304,8 @@ def _register_picker(
304
304
  # because this is the only place in the picker where a model leaves Anthropic,
305
305
  # and nothing else in the list would say so.
306
306
  entries = ",".join(
307
- "{value:%s,label:%s,description:%s}"
308
- % (
309
- js_string(value),
310
- js_string(_display_name(value)),
311
- js_string(_describe(model)),
312
- )
307
+ f"{{value:{js_string(value)},label:{js_string(_display_name(value))},"
308
+ f"description:{js_string(_describe(model))}}}"
313
309
  for value, model in picks
314
310
  )
315
311
  # Every row is emitted with no build-time "already present?" filter, and dedup