patch-cc 0.3.0__tar.gz → 0.3.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 (43) hide show
  1. {patch_cc-0.3.0 → patch_cc-0.3.2}/PKG-INFO +1 -1
  2. {patch_cc-0.3.0 → patch_cc-0.3.2}/docs/CONDUCT.md +1 -1
  3. {patch_cc-0.3.0 → patch_cc-0.3.2}/docs/PLAYBOOK.md +98 -33
  4. {patch_cc-0.3.0 → patch_cc-0.3.2}/docs/corpus.md +10 -5
  5. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/__init__.py +1 -1
  6. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/agents.py +15 -9
  7. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/base.py +31 -6
  8. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/chrome.py +2 -1
  9. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/codex.py +156 -40
  10. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/streaming.py +77 -32
  11. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/thinking.py +7 -5
  12. {patch_cc-0.3.0 → patch_cc-0.3.2}/.github/workflows/ci.yml +0 -0
  13. {patch_cc-0.3.0 → patch_cc-0.3.2}/.github/workflows/release.yml +0 -0
  14. {patch_cc-0.3.0 → patch_cc-0.3.2}/.gitignore +0 -0
  15. {patch_cc-0.3.0 → patch_cc-0.3.2}/AGENTS.md +0 -0
  16. {patch_cc-0.3.0 → patch_cc-0.3.2}/CLAUDE.md +0 -0
  17. {patch_cc-0.3.0 → patch_cc-0.3.2}/LICENSE +0 -0
  18. {patch_cc-0.3.0 → patch_cc-0.3.2}/README.md +0 -0
  19. {patch_cc-0.3.0 → patch_cc-0.3.2}/docs/INTERNALS.md +0 -0
  20. {patch_cc-0.3.0 → patch_cc-0.3.2}/docs/demo.gif +0 -0
  21. {patch_cc-0.3.0 → patch_cc-0.3.2}/pyproject.toml +0 -0
  22. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/__init__.py +0 -0
  23. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/blob.py +0 -0
  24. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/container.py +0 -0
  25. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/elf.py +0 -0
  26. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/errors.py +0 -0
  27. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/bun/macho.py +0 -0
  28. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/cache.py +0 -0
  29. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/cli.py +0 -0
  30. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/codex/__init__.py +0 -0
  31. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/codex/gateway.py +0 -0
  32. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/codex/models.py +0 -0
  33. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/codex/oauth.py +0 -0
  34. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/codex/translate.py +0 -0
  35. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/doctor.py +0 -0
  36. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/js.py +0 -0
  37. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/locate.py +0 -0
  38. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/menu.py +0 -0
  39. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patcher.py +0 -0
  40. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/__init__.py +0 -0
  41. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/patches/output.py +0 -0
  42. {patch_cc-0.3.0 → patch_cc-0.3.2}/src/patch_cc/ui.py +0 -0
  43. {patch_cc-0.3.0 → patch_cc-0.3.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -56,7 +56,7 @@ shape that already absorbs every case, found rather than bolted on.
56
56
  shape this build simply lacks — most patches carry several — not a regression.
57
57
  Keep "gone", "already applied", and "not on this build" as distinct signals;
58
58
  never collapse them into one number. Which one a sub-step's silence means is
59
- not guesswork: declare it (`expect=True`) so a green tick cannot cover a dead
59
+ not guesswork: declare it (`Outcome.declare`) so a green tick cannot cover a dead
60
60
  feature. A step nobody declared is a step that cannot report its own death:
61
61
  `branding` carried none, so a name badge that stopped being a bold render was
62
62
  a green run, an unchanged banner, and a manifest asserting the new name. See
@@ -116,6 +116,18 @@ The tree removes the *mechanical* fragility. It does not decide these:
116
116
  that branch verbatim rather than constructing a flush call; `org-label` builds
117
117
  its line from upstream's own no-org string, so nothing here spells the
118
118
  separator. What you copy cannot drift from what you copied it from.
119
+ - **Match an identity by the weakest claim that still proves it; hold a rewrite
120
+ to the exact node.** The two jobs pull in opposite directions.
121
+ `thinking-inline`'s null-guard neutralises only a `return` that answers
122
+ exactly `null`, because deleting a conditional answer would delete behaviour
123
+ — for a rewrite, doubt means stop. The codex resolvers' *identity* is the
124
+ opposite case: "rejects an unknown model" was once asked as "the default
125
+ returns exactly `null`", and 2.1.234 wrapping that same rejection in a dead
126
+ recognizer (`return vXu(e)?xVe(t):null`, the stub answering `!1` — behaviour
127
+ identical) read the resolver as gone with every anchor count standing.
128
+ Identity asks what the node *can* answer — membership among its possible
129
+ values, the same rule props and arrays already follow — and exactness there
130
+ is a break waiting on upstream's next harmless addition.
119
131
  - **Never anchor on a minified local** (`A_`, `mET`, `wg6`). Unchanged, and now
120
132
  structural: you capture the node, so there is never a reason to spell one.
121
133
 
@@ -356,13 +368,13 @@ and why `prop-threading` resolves scope itself.
356
368
 
357
369
  Counting alone cannot tell "this build lacks that shape" from "the feature is
358
370
  dead": a patch whose optional shapes rewrite happily while a load-bearing one
359
- is gone still changes bytes, and would read as green. So each sub-step declares
360
- what its absence *means* (`Outcome.step(..., expect=...)`):
371
+ is gone still changes bytes, and would read as green. So each sub-step is
372
+ declared with what its absence *means* (`Outcome.declare`):
361
373
 
362
374
  | mark | meaning | absence is |
363
375
  |---|---|---|
364
- | *(default)* | a shape only some builds carry | informational |
365
- | `expect=True` | the patch does nothing useful without it | a regression |
376
+ | `optional` | a shape only some builds carry | informational |
377
+ | `required` | the patch does nothing useful without it | a regression |
366
378
 
367
379
  `Outcome.unmet()` turns a violation into a sentence (`required step
368
380
  group-routing found nothing`); `Outcome.failures()` adds any exception the
@@ -392,9 +404,15 @@ Three rules keep the net from having holes:
392
404
  feature are now checked by the markers their builders emit
393
405
  (`streaming._CORE_UPDATES`), which no amount of neighbouring churn can fake.
394
406
  - **Declare an expectation before the work, not inside it.** A step created only
395
- by its own success cannot report its own absence. `_live_thinking` registers
396
- the core updates up front; `agents.bypassed_agents` resolves the pinned agent
397
- from the helper's durable head so a drifted body still has a step to fail.
407
+ by its own success cannot report its own absence. This is the API's shape,
408
+ not a discipline: `Outcome.declare` is the only way a step comes to exist
409
+ (up front, required and optional named apart), and `outcome.step(name)`
410
+ retrieves — an undeclared name raises, so a typo cannot mint a silently
411
+ optional step. Work that is owed conditionally declares under the same
412
+ condition it runs (`codex._register_context`), and a name resolved from the
413
+ bundle is declared the moment it resolves — `agents.bypassed_agents` has no
414
+ step to name until the helper's guard gives up the pinned agent, and a guard
415
+ that vanishes leaves the always-printed note, with no name left to declare.
398
416
 
399
417
  `apply` acts on the verdict: a broken patch is re-run out of the final pass, so
400
418
  its orphan edits never reach the binary, the manifest never claims it, and the
@@ -572,6 +590,20 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
572
590
  arm's head was baked in as the file's length, `newLines:String(t)`, counted
573
591
  and green.
574
592
 
593
+ **Alive by eye:** ask for a new file *via the Write tool* — the row reads
594
+ `Added N lines` over green `+` rows. Two things legitimately show no diff and
595
+ are not this patch failing: a file created through **Bash** (heredoc, `tee`,
596
+ a script) is Bash output and never enters this renderer; and upstream keeps
597
+ planting early returns *ahead of* the spliced render — plan-directory files
598
+ ("/plan to preview"), condensed contexts (subagent progress rows, session
599
+ summaries: "Wrote N lines to path"), and scratchpad/`.workshop.*` files in
600
+ non-verbose re-renders ("Wrote N lines ⧉"). The live conversation renders
601
+ the current message verbose, so the diff shows there today; those guards are
602
+ the patch's effective coverage narrowing upstream-side, which `doctor` —
603
+ counting matchers, not paths — cannot see. Measured on 2.1.234 (live,
604
+ resume, plain/workshop/scratchpad files): every reachable path drew the
605
+ diff.
606
+
575
607
  ### Thinking — `thinking.py`
576
608
 
577
609
  - **`thinking-summaries`** — stop echoing the account's server-side experiment
@@ -713,16 +745,28 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
713
745
  - **`live-thinking`** — the fourteen-step patch above.
714
746
 
715
747
  **`prop-threading`** inserts `streamingThinking:<state>,` before the
716
- `agentDefinitions` property of every props bag that also carries
717
- `conversationId` and `messages`. Three things do the work, and each replaced
718
- something that had broken:
719
-
720
- - **Identity is membership.** The props that make a render a conversation
721
- render, asked of one object rather than of a span of text. Four matchers
722
- once modelled thisa brace-free `createElement` call, an ordered prompt
723
- renderer, and two regexes differing only in the order two call sites listed
724
- the same props. 2.1.229 killed the ordered pair at once by inserting
725
- `onRateLimitAutoQueueContinue:` between two of them.
748
+ `conversationId` property of every props bag *handed to a component* — an
749
+ argument that also carries `messages`. Being an argument is part of the
750
+ identity: a module-level literal, a return-value payload, or a config object
751
+ may legitimately carry the pair, and none of them is a render. Three things
752
+ do the work, and each replaced something that had broken:
753
+
754
+ - **Identity is membership of the essential props alone.** The props that
755
+ make a render a conversation render, asked of one object rather than of a
756
+ span of text. Four matchers once modelled this a brace-free
757
+ `createElement` call, an ordered prompt renderer, and two regexes differing
758
+ only in the order two call sites listed the same props. 2.1.229 killed the
759
+ ordered pair at once by inserting `onRateLimitAutoQueueContinue:` between
760
+ two of them. The membership then carried `agentDefinitions` as a third
761
+ conjunct — witness and insertion point in one — and 2.1.235 retired that
762
+ prop from the bag: an identity resting on a *neighbour* read a build that
763
+ plainly drew four conversation renders as drawing none, with every anchor
764
+ count standing (`agentDefinitions` still occurs 97 times — as other
765
+ components' prop). The conversation pair is what the render *is*; the
766
+ neighbour was one more thing upstream had to keep, and upstream owes the
767
+ matcher nothing. Same class as the resolver default on 2.1.234: the
768
+ identity must be the weakest claim that still proves it, and every conjunct
769
+ past that is a break waiting on a harmless refactor.
726
770
  - **The insertion point is a prop boundary**, so nothing computes where a
727
771
  literal ends. Being an `object` and not an `object_pattern` is what
728
772
  separates a prop being *passed* from one being *received*; the regex spelled
@@ -770,7 +814,17 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
770
814
 
771
815
  **`transcript-signature` and `inline-extras`** are one hand-off and two
772
816
  steps: the first threads `streamingThinking` into every renderer whose
773
- signature carries the transcript trio, the second rewrites that renderer's
817
+ signature carries `messages` and `streamingToolUses`, inserting before the
818
+ latter — an insertion point inside the identity, for `prop-threading`'s
819
+ reason. The signature was a trio until `showAllInTranscript` was measured to
820
+ discriminate nothing on any build in the corpus: a third conjunct that was
821
+ find-anchor, identity and insertion point in one, the exact triple role
822
+ `agentDefinitions` held when 2.1.235 retired it. Deriving the renderer from
823
+ its consumer instead — the scope that binds what the extras memo computes
824
+ over — was measured and rejected: the memo's receiver is a react-compiler
825
+ memoized *local* on every current build, with real dataflow between it and
826
+ the signature, and a dataflow pass is a tool this project deliberately does
827
+ not build. The second step rewrites that renderer's
774
828
  extras memo to draw the live block in order. What the memo reads is resolved
775
829
  in **its own scope** — the prop its enclosing function was handed — rather
776
830
  than carried over from whichever renderer the first step happened to see
@@ -854,10 +908,10 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
854
908
  `CLAUDE_CODE_DISABLE_EXPLORE_INHERIT_CAP` escape hatch in the middle, which
855
909
  silently cost every Explore override until the body matcher learned to skip
856
910
  intervening statements. The body is a node now, so there is nothing left to
857
- skip. Resolving the agent from the guard alone is what makes a future body
858
- reshape *loud*: we still know an override is at stake, so `bypass:<agent>` is
859
- a required step that fails, instead of the agent's identity vanishing with the
860
- match and the step never existing.
911
+ skip. Resolving the agent from the guard alone is why a future body reshape
912
+ costs nothing at all: the body is replaced whole, however upstream grows it,
913
+ and `bypass:<agent>` stands in the report as the record that an override was
914
+ at stake.
861
915
 
862
916
  If the **guard** goes too there is no step to fail — nothing left names the
863
917
  agent — and no way to tell "upstream stopped pinning" from "the guard
@@ -883,14 +937,14 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
883
937
  |---|---|---|
884
938
  | `enum`\* | the Task tool's `model` enum, so a subagent can be pinned to a Codex id | `agents.model_enums` — the same arrays `discover_models` reads |
885
939
  | `validator`\* | the known-model array — it gates *resolution*, not just acceptance | the array whose strings *include* `sonnet`/`opus`/`haiku`/`opusplan` — membership, never their order |
886
- | `resolver`\* | the override resolver (managed `availableModels` only) | the `"best"` arm whose `switch` answers `null` for an unknown model |
940
+ | `resolver`\* | the override resolver (managed `availableModels` only) | the `"best"` arm whose `switch` *can* answer `null` for an unknown model |
887
941
  | `general-resolver`\* | the resolver every ordinary request uses | the `"best"` arm whose `switch` has no answer of its own for one |
888
942
  | `redirect`\* | swaps the request origin to `127.0.0.1:<port>` | the `buildRequest` method that builds a URL (the others — two to four across the corpus — only delegate to `super`) |
889
943
  | `picker` | the `/model` list | the function every row list is handed to: it loops, adds, and returns its first parameter |
890
944
  | `context` | the real context window | the function that *reads* `CLAUDE_CODE_MAX_CONTEXT_TOKENS` as a member and returns what it read, taking the model as a parameter; the table goes before its first statement |
891
945
  | `registry` | the binary's own model table — the status-line name, effort capabilities, `/advisor` eligibility | the object carrying both `models` and `aliases`, confirmed by its entries carrying `id`/`family`/`display_name` |
892
946
 
893
- \* required (`expect=True`) — without any one of them the feature is dead.
947
+ \* required — without any one of them the feature is dead.
894
948
  `picker`, `context` and `registry` are refinements: absent, you can still
895
949
  type `/model <id>` and get the 200k default under the model's raw id.
896
950
  `context` has no step at all when no chosen model reports a window — there
@@ -994,15 +1048,26 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
994
1048
  registered, with the manifest asserting the override. Now the dropped patch
995
1049
  takes its pins down with it.
996
1050
 
997
- The two resolvers are told apart by **what they answer, never by their
998
- minified names or by a statement form** — the override one rejects an unknown
999
- model with `default:return null`, the general one has no answer for it and
1000
- falls through to handing the name back. That is a fact about what each
1001
- resolver is *for*. Braces around an arm are not: they were the discriminator
1002
- once, and putting a `let` in the general resolver's `case"best"` was enough to
1003
- make every family shortcut vanish with the patch still green and seven of
1004
- seven steps applied. The `buildRequest` methods and the
1005
- `models`-shaped objects are likewise separated by what they *do* and *carry*.
1051
+ The two resolvers are told apart by **what each answers for an unknown
1052
+ model, never by their minified names or by a statement form** — `null` is
1053
+ among what the override one's default *can* answer, read off the grammar's
1054
+ own value routing (a ternary answers with either branch, `||`/`??` with
1055
+ their right side; 2.1.234 wrapped the same rejection in a dead recognizer,
1056
+ `return vXu(e)?xVe(t):null` with the stub answering `!1`, and the exact
1057
+ spelling read a behaviourally identical build as the resolver being gone),
1058
+ while the general one has no answer of its own — nothing in its default
1059
+ scoped to return or throw and falls through to handing the name back.
1060
+ Both identities are asked positively, of switches whose labels carry the
1061
+ built-in models (the membership `validator` asks of its array, asked of the
1062
+ labels — a throwaway `case"best":` elsewhere is nothing, not an arm to
1063
+ classify), and an arm answering neither *raises* instead of swelling the
1064
+ other side: under the complement this replaced, a reshaped override slid
1065
+ silently into the general list, and only the required step's zero was left
1066
+ to speak. Braces around an arm are no discriminator either: they were, once,
1067
+ and a `let` in the general resolver's `case"best"` was enough to make every
1068
+ family shortcut vanish with the patch still green and seven of seven steps
1069
+ applied. The `buildRequest` methods and the `models`-shaped objects are
1070
+ likewise separated by what they *do* and *carry*.
1006
1071
 
1007
1072
  **Two resolvers, and why both.** The **general** one turns `opus`
1008
1073
  into `claude-opus-4-8`, its return value *replaces* the model before the
@@ -20,7 +20,7 @@ matches means the file changed under you, not that a number moved.
20
20
 
21
21
  ## On disk now
22
22
 
23
- The reproducible set on the machine this was written on — 11 distinct binaries
23
+ The reproducible set on the machine this was written on — 13 distinct binaries
24
24
  by content (the SHA-256 is the whole file, `sha256sum <version>.orig`):
25
25
 
26
26
  | version | size | sha256 |
@@ -36,13 +36,18 @@ by content (the SHA-256 is the whole file, `sha256sum <version>.orig`):
36
36
  | `2.1.227` | 304 MB | `6832dc3f1797b890b71116e5f2dbbf9a83fd3d0498c235b4b0f9cd0e6e499ad6` |
37
37
  | `2.1.228` | 309 MB | `d535985e6941a3eb00179ccd7f52ceb0c6623a0305a518ebc4e6514f84a94c99` |
38
38
  | `2.1.233` | 325 MB | `55d281096f57d411ebbdd94dbf5e9ff3accb7c05713e37348c2c11d4b83bf9d9` |
39
+ | `2.1.234` | 328 MB | `3473601ea695d5bf769c5b202844d4cb4fbf723ae995450fcb6973204775c84a` |
40
+ | `2.1.235` | 331 MB | `bfcf0ae2dbf94b2b6a106074aabf3938b9a10889c3b678e4cb5a00c03274d5d5` |
39
41
 
40
42
  The backup directory holds a few more files than rows here, and that is not a
41
43
  discrepancy: a pre-0.2.0 backup doubled the version into its name
42
- (`2.1.216.2.1.216.orig`), and a binary installed under a non-version name is
43
- saved as `claude.unknown-<hash>.orig`. Both of the extras present hash-match the
44
- `2.1.216` row above same bytes, a different filename — which is why the count
45
- of *files* (13) and the count of *distinct binaries* (11) differ.
44
+ (`2.1.216.2.1.216.orig`), a binary installed under a non-version name is
45
+ saved as `claude.unknown-<hash>.orig`, and a binary patched under a
46
+ non-canonical filename keeps that filename (`2.1.235` entered the corpus as a
47
+ downloaded `claude-2.1.235`, so its backup is `claude-2.1.235.orig`). The two
48
+ extra 2.1.216-era files hash-match the `2.1.216` row above — same bytes, a
49
+ different filename — which is why the count of *files* (15) and the count of
50
+ *distinct binaries* (13) differ.
46
51
 
47
52
  ## Rebuild or extend it
48
53
 
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -100,7 +100,8 @@ def _subagent_prompt(source: Source, _options: Options, outcome: Outcome) -> Sou
100
100
  ``transcript && content && ...`` conjunction, which gates the agent's
101
101
  *output* and is not this patch's business.
102
102
  """
103
- gate = outcome.step("gate", expect=True)
103
+ outcome.declare(required=("gate",))
104
+ gate = outcome.step("gate")
104
105
  edits: list[Edit] = []
105
106
  seen: set[int] = set()
106
107
 
@@ -346,7 +347,7 @@ def discover_models(source: Source) -> list[str]:
346
347
  by a guessed default. A hardcoded fallback (`haiku/sonnet/opus`) read a lost
347
348
  enum as those three: `doctor` printed a plausible list, every pin to one of
348
349
  them landed, and a pin to a name the real bundle accepts was refused -- the
349
- same emptiness that `codex-models`' own `enum` step (`expect=True`) reports
350
+ same emptiness that `codex-models`' own required `enum` step reports
350
351
  as broken in the same run. With nothing to offer, the offer is empty:
351
352
  `discover_models` returns `[]`, `doctor` shows `models: inherit` alone, and a
352
353
  requested pin fails its required step rather than landing on a name nothing
@@ -481,13 +482,14 @@ def _subagent_models(source: Source, options: Options, outcome: Outcome) -> Sour
481
482
  found = {agent.name: agent for agent in agents}
482
483
  edits: list[Edit] = []
483
484
 
485
+ # Required: every override reaching a patch has already been validated
486
+ # against this bundle by its surface (CLI, --from-cache, or the menu), so
487
+ # one that cannot be written is not a shape this build lacks -- it is the
488
+ # asked-for change failing. Left optional, the patch stayed green, the
489
+ # binary shipped without the override, and the manifest claimed it.
490
+ outcome.declare(required=tuple(sorted(options.subagent_models)))
484
491
  for name, target in sorted(options.subagent_models.items()):
485
- # Required: every override reaching a patch has already been validated
486
- # against this bundle by its surface (CLI, --from-cache, or the menu),
487
- # so one that cannot be written is not a shape this build lacks -- it is
488
- # the asked-for change failing. Left optional, the patch stayed green,
489
- # the binary shipped without the override, and the manifest claimed it.
490
- step = outcome.step(name, expect=True)
492
+ step = outcome.step(name)
491
493
  if target not in offered:
492
494
  step.note(f"model {target!r} not offered by this bundle; skipped")
493
495
  continue
@@ -522,7 +524,11 @@ def _subagent_models(source: Source, options: Options, outcome: Outcome) -> Sour
522
524
  for bypass in bypasses:
523
525
  if bypass.agent not in options.subagent_models:
524
526
  continue
525
- step = outcome.step(f"bypass:{bypass.agent}", expect=True)
527
+ # Declared the moment the guard resolves the agent's name -- the
528
+ # earliest this step *can* exist -- while a vanished guard stays the
529
+ # note above, with no name left to declare.
530
+ outcome.declare(required=(f"bypass:{bypass.agent}",))
531
+ step = outcome.step(f"bypass:{bypass.agent}")
526
532
  step.candidates += 1
527
533
  step.applied += 1
528
534
  edits.append(
@@ -109,7 +109,7 @@ class Outcome:
109
109
  notes: list[str] = field(default_factory=list)
110
110
  #: Named sub-steps, for patches built from several independent rewrites.
111
111
  steps: dict[str, Outcome] = field(default_factory=dict)
112
- #: What this sub-step's absence means (set via :meth:`step`). ``False`` --
112
+ #: What this sub-step's absence means (set via :meth:`declare`). ``False`` --
113
113
  #: a shape some builds simply lack; ``True`` -- the patch is broken without
114
114
  #: it.
115
115
  expect: bool = False
@@ -167,18 +167,43 @@ class Outcome:
167
167
  def note(self, message: str) -> None:
168
168
  self.notes.append(message)
169
169
 
170
- def step(self, name: str, expect: bool = False) -> Outcome:
171
- """Get (or create) a named sub-outcome.
170
+ def declare(
171
+ self, required: tuple[str, ...] = (), optional: tuple[str, ...] = ()
172
+ ) -> None:
173
+ """Create sub-steps, before any of them does its work.
174
+
175
+ Declaring is the only way a step comes to exist (:meth:`step` only
176
+ retrieves), which makes "declare an expectation before the work" the
177
+ API's shape instead of each patch's discipline: a code path that never
178
+ runs leaves a required step at 0/0 with a verdict to fail, where a step
179
+ created by its own success could never report its own absence --
180
+ `branding`'s badge went unrenamed under exactly that silence, and the
181
+ lazily-created step was the hole the discipline papered over.
182
+ Conditional work declares under the same condition it runs (`context`),
183
+ and a name resolved from the bundle is declared the moment it resolves
184
+ (`bypass:<agent>`).
185
+ """
186
+ for name in required:
187
+ self.steps.setdefault(name, Outcome()).expect = True
188
+ for name in optional:
189
+ self.steps.setdefault(name, Outcome())
190
+
191
+ def step(self, name: str) -> Outcome:
192
+ """A declared sub-step, to record work against.
172
193
 
173
194
  A single scalar count cannot distinguish "all twelve rewrites landed"
174
195
  from "six landed and six silently drifted" -- which is exactly how
175
196
  upstream's live-thinking patch hides its own regressions. Recording each
176
197
  rewrite separately turns that into an actionable "reducer.message_stop
177
198
  missed".
199
+
200
+ Retrieval only: a name nobody declared is a programming error and
201
+ raises, which :meth:`Patch.run` reports as the patch broken -- loud,
202
+ never a silently-optional step minted by a typo.
178
203
  """
179
- sub = self.steps.setdefault(name, Outcome())
180
- sub.expect = sub.expect or expect
181
- return sub
204
+ if name not in self.steps:
205
+ raise RuntimeError(f"step {name!r} was never declared")
206
+ return self.steps[name]
182
207
 
183
208
  def finalize(self) -> Outcome:
184
209
  """Roll sub-step totals up into this outcome."""
@@ -268,7 +268,8 @@ def _branding(source: Source, options: Options, outcome: Outcome) -> Source:
268
268
  # rather than a green tick over a binary that still says Claude Code. The
269
269
  # other two are sentences upstream may reword or drop, and their absence is
270
270
  # a build lacking a shape, not a dead feature.
271
- badge = outcome.step("badge", expect=True)
271
+ outcome.declare(required=("badge",), optional=("styled", "welcome"))
272
+ badge = outcome.step("badge")
272
273
  styled = outcome.step("styled")
273
274
  welcome = outcome.step("welcome")
274
275
 
@@ -29,7 +29,7 @@ and the subagent pins: the ids to register, and the port to route them to.
29
29
  Nothing here reads a store of its own, which is what makes the binary's manifest
30
30
  the one record of what a patched Claude Code answers to.
31
31
 
32
- Required steps (``expect=True``): without any one, the feature is dead --
32
+ Required steps: without any one, the feature is dead --
33
33
  ``enum`` (subagent use), ``validator`` and ``resolver`` (the model is accepted
34
34
  and resolves -- an id to itself, a shortcut to the newest id in its family),
35
35
  ``redirect`` (requests actually reach the gateway). ``general-resolver`` is
@@ -165,52 +165,153 @@ def claimed_model_names(source: Source) -> set[str]:
165
165
  # ----------------------------------------------------------------- resolvers
166
166
  #
167
167
  # The binary has TWO model resolvers and a shortcut needs an arm in both. They
168
- # are told apart by what they *answer*, never by their minified names or by a
169
- # statement form: the override resolver (reached only when managed
170
- # `availableModels` are active) rejects an unknown name with `null`; the general
171
- # resolver -- the one every ordinary request uses, which turns `opus` into
172
- # `claude-opus-4-8` -- has no answer of its own for one and falls through to
173
- # passing it straight back.
168
+ # are told apart by what each *answers* for an unknown model, never by their
169
+ # minified names or by a statement form: the override resolver (reached only
170
+ # when managed `availableModels` are active) has a closed list and rejects --
171
+ # null is among what its default can answer -- while the general resolver (the
172
+ # one every ordinary request uses, which turns `opus` into `claude-opus-4-8`)
173
+ # has no answer of its own and falls through to passing the name straight back.
174
+ # Both identities are asked positively, of every model resolver, and an arm
175
+ # answering neither raises instead of swelling the other side: a complement
176
+ # cannot report its own absence.
174
177
 
175
178
 
176
179
  def _best_arms(source: Source) -> list[js.Node]:
177
- """Every arm labelled ``"best"``, which is where new model arms are spliced.
180
+ """Every model resolver's ``"best"`` arm -- where new model arms are spliced.
178
181
 
179
182
  The label is the name and the arm is the node; ``case"best":`` written out
180
- is the two of them with the minifier's spacing in between.
183
+ is the two of them with the minifier's spacing in between. The switch also
184
+ has to *be* a model resolver -- its labels carry the built-in models, the
185
+ same membership `_validators` asks of its array -- because everything here
186
+ applies to every arm of the kind: a throwaway ``case"best":`` in unrelated
187
+ code must be nothing to a registration that would splice into it and to a
188
+ classifier that would be asked what it is (`_resolvers`).
181
189
  """
182
190
  found = []
183
191
  for node in source.find(_BEST):
184
192
  arm = js.up(node, "switch_case")
185
- if arm is not None and arm.child_by_field_name("value") == node:
193
+ if (
194
+ arm is not None
195
+ and arm.child_by_field_name("value") == node
196
+ and set(_BUILT_IN_MODELS) <= _labels(arm)
197
+ ):
186
198
  found.append(arm)
187
199
  return found
188
200
 
189
201
 
190
- def _rejects_unknown(arm: js.Node) -> bool:
191
- """Does this arm's ``switch`` answer ``null`` when nothing matched?
202
+ def _labels(arm: js.Node) -> set[str]:
203
+ """The string labels of this arm's whole ``switch``, as their values.
192
204
 
193
- The two resolvers differ in what an unknown model means to them, which is a
194
- fact about what each is *for*: the override resolver has a closed list and
195
- rejects, the general one passes the name through for something downstream to
196
- judge. Whether either spells an arm as ``case"best":return f()`` or
197
- ``case"best":{return f()}`` is the minifier's business -- and telling them
205
+ A set: which names the switch dispatches on is the question, and neither
206
+ their order nor what sits between them is part of it.
207
+ """
208
+ return {
209
+ js.text(value)[1:-1]
210
+ for case in js.children(arm.parent)
211
+ if case.type == "switch_case"
212
+ for value in [case.child_by_field_name("value")]
213
+ if value is not None and value.type == "string"
214
+ }
215
+
216
+
217
+ def _answer(statement: js.Node) -> js.Node | None:
218
+ """The expression a ``return`` answers with -- nothing for a bare ``return;``."""
219
+ parts = [child for child in js.children(statement) if child.type != "comment"]
220
+ return parts[0] if parts else None
221
+
222
+
223
+ def _may_answer_null(expr: js.Node | None) -> bool:
224
+ """Can this expression's value be ``null``?
225
+
226
+ Asked of the grammar's own value routing, so the rejection keeps counting
227
+ however much recognition upstream composes in front of it: a ternary
228
+ answers with either branch, parentheses and a sequence with their last
229
+ expression, ``||`` and ``??`` with their right side (a null on their left
230
+ is exactly what both exist to pass over), ``&&`` with either side (null is
231
+ falsy), an assignment with the value it assigns. Everything else -- a call,
232
+ an identifier, an ``await`` -- is opaque: it may well evaluate to null, but
233
+ the grammar does not say so, and a rejection hidden behind one is a new
234
+ shape to be told about (`_resolvers` raises), never one to absorb.
235
+
236
+ 2.1.234 is why this is a question about *possible* answers and not the
237
+ answer's spelling: the override default became ``return vXu(e)?xVe(t):null``
238
+ with ``vXu`` a stub returning ``!1`` -- behaviour identical to ``return
239
+ null`` -- and "exactly a bare null" read that build's rejection as gone.
240
+ """
241
+ if expr is None:
242
+ return False
243
+ if expr.type == "null":
244
+ return True
245
+ if expr.type == "ternary_expression":
246
+ return any(
247
+ _may_answer_null(expr.child_by_field_name(field))
248
+ for field in ("consequence", "alternative")
249
+ )
250
+ if expr.type in ("parenthesized_expression", "sequence_expression"):
251
+ parts = [child for child in js.children(expr) if child.type != "comment"]
252
+ return bool(parts) and _may_answer_null(parts[-1])
253
+ if expr.type == "assignment_expression":
254
+ return _may_answer_null(expr.child_by_field_name("right"))
255
+ if expr.type == "binary_expression":
256
+ operator = expr.child_by_field_name("operator")
257
+ sides = {"&&": ("left", "right"), "||": ("right",), "??": ("right",)}
258
+ return any(
259
+ _may_answer_null(expr.child_by_field_name(field))
260
+ for field in sides.get("" if operator is None else operator.type, ())
261
+ )
262
+ return False
263
+
264
+
265
+ def _resolvers(source: Source) -> tuple[list[js.Node], list[js.Node]]:
266
+ """Every model resolver, classified: (override, general).
267
+
268
+ Each identity is asked positively of the arm's own ``switch``: the override
269
+ resolver's default *can answer null* (`_may_answer_null`, over the
270
+ default's scoped returns -- a callback's ``return`` answers for the
271
+ callback); the general resolver's default has no answer of its own --
272
+ absent, or with nothing scoped to return or throw -- so an unknown model
273
+ falls out of the switch and back to the caller's passthrough. Whether
274
+ either spells an arm as ``case"best":return f()`` or
275
+ ``case"best":{return f()}`` is the minifier's business -- telling them
198
276
  apart by that was a silent failure waiting: brace the general resolver (a
199
277
  `let` in the arm is enough) and every family shortcut disappeared with the
200
278
  patch still green.
279
+
280
+ The identities exclude each other by construction, and an arm holding
281
+ neither -- a default that answers something, never null -- raises, which
282
+ `Patch.run` turns into one broken patch naming the new shape (the promise
283
+ `js.only` keeps for cardinality, kept here for classification). Under the
284
+ complement this replaced, such an arm slid silently into the general list,
285
+ and only a required step's zero was left to say anything at all.
201
286
  """
202
- default = next(
203
- (
204
- sibling
205
- for sibling in js.children(arm.parent)
206
- if sibling.type == "switch_default"
207
- ),
208
- None,
209
- )
210
- if default is None:
211
- return False
212
- answer = js.first(default, js.of_type("return_statement"))
213
- return answer is not None and [c.type for c in answer.named_children] == ["null"]
287
+ override: list[js.Node] = []
288
+ general: list[js.Node] = []
289
+ for arm in _best_arms(source):
290
+ default = next(
291
+ (
292
+ sibling
293
+ for sibling in js.children(arm.parent)
294
+ if sibling.type == "switch_default"
295
+ ),
296
+ None,
297
+ )
298
+ answers = js.every(default, js.of_type("return_statement"), scoped=True)
299
+ refusals = js.every(default, js.of_type("throw_statement"), scoped=True)
300
+ if not answers and not refusals:
301
+ general.append(arm)
302
+ elif any(_may_answer_null(_answer(statement)) for statement in answers):
303
+ override.append(arm)
304
+ else:
305
+ answered = [_answer(statement) for statement in answers]
306
+ kinds = sorted(
307
+ {"nothing" if value is None else value.type for value in answered}
308
+ | ({"a throw"} if refusals else set())
309
+ )
310
+ raise ValueError(
311
+ "a model resolver neither rejects an unknown model nor passes "
312
+ f"it through: its default answers {', '.join(kinds)}"
313
+ )
314
+ return override, general
214
315
 
215
316
 
216
317
  def _existing_arms(arm: js.Node) -> set[str]:
@@ -241,12 +342,12 @@ def _arms(resolution: dict[str, str]) -> str:
241
342
 
242
343
  def _override_resolvers(source: Source) -> list[js.Node]:
243
344
  """Every resolver reached only when managed ``availableModels`` are active."""
244
- return [arm for arm in _best_arms(source) if _rejects_unknown(arm)]
345
+ return _resolvers(source)[0]
245
346
 
246
347
 
247
348
  def _general_resolvers(source: Source) -> list[js.Node]:
248
349
  """Every resolver an ordinary request goes through."""
249
- return [arm for arm in _best_arms(source) if not _rejects_unknown(arm)]
350
+ return _resolvers(source)[1]
250
351
 
251
352
 
252
353
  def _register_arms(
@@ -562,8 +663,8 @@ def _register_context(source: Source, models, outcome: Outcome) -> Source:
562
663
  # place to bake it -- a drifted window resolver then reads as this step
563
664
  # finding nothing, not as no step at all. Created *after* the search, a
564
665
  # renamed CLAUDE_CODE_MAX_CONTEXT_TOKENS produced seven steps, no note and no
565
- # absent-step line, the exact silence PLAYBOOK's "declare an expectation
566
- # before the work" rule (and `_live_thinking`) exists to break.
666
+ # absent-step line, the exact silence `Outcome.declare` now exists to break.
667
+ outcome.declare(optional=("context",))
567
668
  step = outcome.step("context")
568
669
  resolver = _window_resolver(source)
569
670
  body = js.body(resolver) if resolver is not None else None
@@ -760,6 +861,11 @@ def _codex_models(source: Source, options: Options, outcome: Outcome) -> Source:
760
861
  return source
761
862
 
762
863
  model_ids = [m.id for m in options.codex_models]
864
+ # Classified once here for the gate and the note; the registrations below
865
+ # re-derive from the source each hands the next, since every batch of edits
866
+ # is a new parse. A count that moves on a green run is the early warning.
867
+ override, general = _resolvers(source)
868
+ outcome.note(f"resolvers: {len(override)} override, {len(general)} general")
763
869
  # Family shortcuts (`sol` -> the newest `gpt-<ver>-sol`) resolve to an id
764
870
  # before the request is built, so they ride only the surfaces that accept,
765
871
  # resolve, and show a model -- validator, both resolvers, picker. They need no
@@ -770,33 +876,43 @@ def _codex_models(source: Source, options: Options, outcome: Outcome) -> Source:
770
876
  # so absent it, no shortcut is registered anywhere and the ids (which need none
771
877
  # of this) carry on untouched.
772
878
  shorts = family_aliases(options.codex_models)
773
- if shorts and not _general_resolvers(source):
879
+ if shorts and not general:
774
880
  outcome.note("general model resolver anchor drifted; shortcuts skipped")
775
881
  shorts = {}
776
882
 
777
- source = _register(
778
- source, model_enums(source), model_ids, outcome.step("enum", expect=True)
883
+ # `general-resolver` is owed exactly when a shortcut is; `context` declares
884
+ # itself where its own work is owed (see _register_context).
885
+ outcome.declare(
886
+ required=(
887
+ "enum",
888
+ "validator",
889
+ "resolver",
890
+ *(("general-resolver",) if shorts else ()),
891
+ "redirect",
892
+ ),
893
+ optional=("picker", "registry"),
779
894
  )
895
+ source = _register(source, model_enums(source), model_ids, outcome.step("enum"))
780
896
  source = _register(
781
897
  source,
782
898
  _validators(source),
783
899
  model_ids + list(shorts),
784
- outcome.step("validator", expect=True),
900
+ outcome.step("validator"),
785
901
  )
786
902
  source = _register_arms(
787
903
  source,
788
904
  _override_resolvers(source),
789
905
  {i: i for i in model_ids} | shorts,
790
- outcome.step("resolver", expect=True),
906
+ outcome.step("resolver"),
791
907
  )
792
908
  if shorts:
793
909
  source = _register_arms(
794
910
  source,
795
911
  _general_resolvers(source),
796
912
  shorts,
797
- outcome.step("general-resolver", expect=True),
913
+ outcome.step("general-resolver"),
798
914
  )
799
- source = _redirect(source, options, outcome.step("redirect", expect=True))
915
+ source = _redirect(source, options, outcome.step("redirect"))
800
916
  source = _register_picker(
801
917
  source, options.codex_models, shorts, outcome.step("picker")
802
918
  )
@@ -116,13 +116,22 @@ def _delta(event: str, setter: str, helper: str) -> str:
116
116
  #: props freely and inserts new ones between them, and neither is a fact about a
117
117
  #: set. Two regexes once differed only in the order two call sites listed the
118
118
  #: same props, and 2.1.229 killed both at once by inserting
119
- #: `onRateLimitAutoQueueContinue:` between two of them.
119
+ #: `onRateLimitAutoQueueContinue:` between two of them. The pair is also the
120
+ #: *whole* identity: it once had `agentDefinitions` beside it as witness and
121
+ #: insertion point, and 2.1.235 retired that prop from the bag -- an identity
122
+ #: resting on a neighbour reported no conversation renders on a build that
123
+ #: plainly drew four. The conversation is what the render is *for*; the props
124
+ #: that name it are the weakest claim that still proves it.
120
125
  _CONVERSATION = ("conversationId", "messages")
121
-
122
- _AGENT_DEFINITIONS = "agentDefinitions"
123
126
  _STREAMING = "streamingThinking"
124
127
  _SETTER = "onStreamingThinking"
125
- _TRANSCRIPT_SIGNATURE = ("messages", "streamingToolUses", "showAllInTranscript")
128
+ #: What the transcript renderer's signature *is*: the conversation it draws and
129
+ #: the streaming tool-uses the extras memo computes over. A third conjunct
130
+ #: (`showAllInTranscript`) stood here as find-anchor, identity and insertion
131
+ #: point in one -- the exact triple role `agentDefinitions` held in
132
+ #: `prop-threading` when 2.1.235 retired it -- and discriminates nothing on any
133
+ #: build in the corpus: the pair already names the same one renderer.
134
+ _TRANSCRIPT_SIGNATURE = ("messages", "streamingToolUses")
126
135
  _INJECTED = "__cc_streamingThinking"
127
136
 
128
137
 
@@ -140,26 +149,34 @@ def _outermost(scope: js.Node) -> bool:
140
149
  def _conversation_renders(source: Source) -> list[js.Node]:
141
150
  """Every props bag that is a conversation render, as the object it is.
142
151
 
143
- ``agentDefinitions`` is both a witness and the insertion point -- a new
144
- property immediately before an existing one is valid in any object literal
145
- -- so nothing here computes where the bag *ends*. That question used to be
146
- the whole difficulty: delimiting an object literal in minified JS means
147
- telling a regex literal from division, and a hand-rolled scanner that
148
- guesses returns a `}` that is merely wrong.
152
+ The identity is one of the identity's own props -- a new property
153
+ immediately before an existing one is valid in any object literal -- so
154
+ nothing here computes where the bag *ends*, and the insertion point cannot
155
+ be absent from a bag the identity admitted. Ending the literal used to be
156
+ the whole difficulty: delimiting one in minified JS means telling a regex
157
+ literal from division, and a hand-rolled scanner that guesses returns a
158
+ `}` that is merely wrong.
149
159
 
150
160
  Being an ``object`` and not an ``object_pattern`` is what separates a prop
151
161
  being *passed* from one being *received*; inserting into the latter would
152
162
  rebind a local. The regex spelled that as a call-opening it had to match.
163
+ A render bag is also *handed to* a component -- an argument -- which is
164
+ what keeps a module-level literal, a return-value payload, or a config
165
+ object that happens to carry the pair from being read as a render: those
166
+ are the decoys the pair alone admits, and being an argument is a fact
167
+ about what a render *is*, not about how this build spells one.
153
168
  """
154
169
  found = []
155
- for node in source.find(_AGENT_DEFINITIONS):
170
+ for node in source.find(_CONVERSATION[0]):
156
171
  pair = js.named(node)
157
172
  if pair is None:
158
173
  continue
159
174
  bag = js.owner(pair)
160
175
  if bag is None or bag.type != "object":
161
176
  continue
162
- if js.carries(bag, *_CONVERSATION):
177
+ if bag.parent is None or bag.parent.type != "arguments":
178
+ continue
179
+ if js.carries(bag, *_CONVERSATION[1:]):
163
180
  found.append(bag)
164
181
  return found
165
182
 
@@ -237,7 +254,7 @@ def _state_in_scope(site: js.Node) -> str | None:
237
254
 
238
255
  def _step_prop_threading(source: Source, outcome: Outcome) -> Source:
239
256
  """Pass the live-thinking state into the renderers that need it."""
240
- step = outcome.step("prop-threading", expect=True)
257
+ step = outcome.step("prop-threading")
241
258
  edits = []
242
259
  renders = _conversation_renders(source)
243
260
  # Printed every run, green ones included: an early warning held back until
@@ -253,7 +270,7 @@ def _step_prop_threading(source: Source, outcome: Outcome) -> Source:
253
270
  step.applied += 1
254
271
  edits.append(
255
272
  Edit.before(
256
- js.entry(js.props(bag)[_AGENT_DEFINITIONS]), f"{_STREAMING}:{state},"
273
+ js.entry(js.props(bag)[_CONVERSATION[0]]), f"{_STREAMING}:{state},"
257
274
  )
258
275
  )
259
276
  return source.apply(edits)
@@ -327,7 +344,7 @@ def _step_display_mode(source: Source, outcome: Outcome) -> Source:
327
344
  gains a default. Whatever guards reach it, and in whatever order the
328
345
  declaration lists them, is untouched because it is never matched.
329
346
  """
330
- step = outcome.step("display-mode", expect=True)
347
+ step = outcome.step("display-mode")
331
348
  edits = []
332
349
  seen: set[int] = set()
333
350
 
@@ -487,11 +504,24 @@ def _step_final_summary(source: Source, outcome: Outcome) -> Source:
487
504
 
488
505
 
489
506
  def _step_transcript_signature(source: Source, outcome: Outcome) -> Source:
490
- """Make sure the transcript renderer actually receives the live state."""
491
- step = outcome.step("transcript-signature", expect=True)
507
+ """Make sure the transcript renderer actually receives the live state.
508
+
509
+ The insertion point is one of the identity's own names, so it cannot be
510
+ absent from a pattern the identity admitted -- the guarantee
511
+ ``prop-threading`` moved to after 2.1.235 retired its third conjunct.
512
+
513
+ Deriving the renderer from its consumer instead -- the scope that binds
514
+ what the streaming-extras memo computes over -- was measured and rejected:
515
+ on every current build the memo's receiver is a react-compiler memoized
516
+ *local* (``Te=useMemo(...)``), with real dataflow between it and the
517
+ signature, and a dataflow pass is a tool this project deliberately does
518
+ not build. Membership of the pair is the weakest claim the grammar can
519
+ still prove.
520
+ """
521
+ step = outcome.step("transcript-signature")
492
522
  edits = []
493
523
 
494
- for node in source.find(_TRANSCRIPT_SIGNATURE[2]):
524
+ for node in source.find(_TRANSCRIPT_SIGNATURE[1]):
495
525
  pattern = js.owner(node)
496
526
  # A destructured parameter list, not a props bag being passed: this is
497
527
  # the renderer's own signature, and the same property names appear on
@@ -509,7 +539,7 @@ def _step_transcript_signature(source: Source, outcome: Outcome) -> Source:
509
539
  continue
510
540
  edits.append(
511
541
  Edit.before(
512
- js.entry(carried[_TRANSCRIPT_SIGNATURE[2]]),
542
+ js.entry(carried[_TRANSCRIPT_SIGNATURE[1]]),
513
543
  f"{_STREAMING}:{_INJECTED},",
514
544
  )
515
545
  )
@@ -593,7 +623,7 @@ def _flatmap_extras(source: Source) -> tuple[js.Node, js.Node] | None:
593
623
 
594
624
  def _step_inline_extras(source: Source, found: Discovery, outcome: Outcome) -> Source:
595
625
  """Render live thinking inline, ordered with streaming tool-use blocks."""
596
- step = outcome.step("inline-extras", expect=True)
626
+ step = outcome.step("inline-extras")
597
627
  computed = _flatmap_extras(source)
598
628
  if computed is None:
599
629
  step.note("no streaming-extras memo in this build")
@@ -891,7 +921,7 @@ def _step_reducer(source: Source, found: Discovery, outcome: Outcome) -> Source:
891
921
  away reads as that point's absence by name rather than as a bare count drop
892
922
  inside an aggregate.
893
923
  """
894
- step = outcome.step("reducer", expect=True)
924
+ step = outcome.step("reducer")
895
925
  helper = found.create_message_helper
896
926
  handler = _reducer(source)
897
927
  if handler is None or helper is None:
@@ -937,27 +967,27 @@ def _step_reducer(source: Source, found: Discovery, outcome: Outcome) -> Source:
937
967
  handler,
938
968
  (_REQUEST_START,),
939
969
  f"{setter}?.(null)",
940
- outcome.step("request-start", expect=True),
970
+ outcome.step("request-start"),
941
971
  )
942
972
  edits += _dispatch(
943
973
  handler,
944
974
  (_MESSAGE_STOP,),
945
975
  ended,
946
- outcome.step("message-stop", expect=True),
976
+ outcome.step("message-stop"),
947
977
  on="event",
948
978
  )
949
979
  edits += _dispatch(
950
980
  handler,
951
981
  ("text",),
952
982
  cleared,
953
- outcome.step("text-clear", expect=True),
983
+ outcome.step("text-clear"),
954
984
  on="content_block",
955
985
  )
956
986
  edits += _dispatch(
957
987
  handler,
958
988
  ("message_delta",),
959
989
  cleared,
960
- outcome.step("message-delta-clear", expect=True),
990
+ outcome.step("message-delta-clear"),
961
991
  on="event",
962
992
  )
963
993
  edits += _dispatch(
@@ -982,11 +1012,26 @@ def _step_reducer(source: Source, found: Discovery, outcome: Outcome) -> Source:
982
1012
 
983
1013
  def _live_thinking(source: Source, _options: Options, outcome: Outcome) -> Source:
984
1014
  found = Discovery()
985
- # Declared before anything runs: an expectation that only comes into
986
- # existence once its own rewrite succeeds can never report that rewrite
987
- # missing -- which is exactly the silence being designed out here.
988
- for name, _marker in _CORE_UPDATES:
989
- outcome.step(name, expect=True)
1015
+ # Every step this patch can take, declared before anything runs: an
1016
+ # expectation that only comes into existence once its own rewrite succeeds
1017
+ # can never report that rewrite missing. The dispatch points are declared
1018
+ # here too, so a build whose reducer is gone fails *them* by name as well,
1019
+ # instead of them never having existed.
1020
+ outcome.declare(
1021
+ required=(
1022
+ "prop-threading",
1023
+ "display-mode",
1024
+ "transcript-signature",
1025
+ "inline-extras",
1026
+ "reducer",
1027
+ "request-start",
1028
+ "message-stop",
1029
+ "text-clear",
1030
+ "message-delta-clear",
1031
+ *(name for name, _marker in _CORE_UPDATES),
1032
+ ),
1033
+ optional=("final-summary", "thinking-start", "thinking-append"),
1034
+ )
990
1035
 
991
1036
  source = _step_prop_threading(source, outcome)
992
1037
  source = _step_display_mode(source, outcome)
@@ -999,7 +1044,7 @@ def _live_thinking(source: Source, _options: Options, outcome: Outcome) -> Sourc
999
1044
  # updates *are* the feature, and every other step can land without them.
1000
1045
  for name, marker in _CORE_UPDATES:
1001
1046
  if source.count(marker):
1002
- step = outcome.step(name, expect=True)
1047
+ step = outcome.step(name)
1003
1048
  step.candidates += 1
1004
1049
  step.applied += 1
1005
1050
  return source
@@ -1018,7 +1063,7 @@ PATCHES = [
1018
1063
  f'case"{_THINKING_DELTA}"',
1019
1064
  f'"{_REQUEST_START}"',
1020
1065
  _CONTENT_BLOCK_START,
1021
- _AGENT_DEFINITIONS,
1066
+ _CONVERSATION[0],
1022
1067
  ),
1023
1068
  ),
1024
1069
  ]
@@ -145,7 +145,7 @@ def _ungrouped(arm: js.Node) -> js.Node | None:
145
145
 
146
146
  def _reroute_grouping(source: Source, outcome: Outcome) -> Source:
147
147
  """Send finished thinking messages to the visible list, not the group."""
148
- step = outcome.step("group-routing", expect=True)
148
+ step = outcome.step("group-routing")
149
149
  edits: list[Edit] = []
150
150
  seen: set[int] = set()
151
151
 
@@ -255,8 +255,9 @@ def _thinking_inline(source: Source, _options: Options, outcome: Outcome) -> Sou
255
255
  the value forced to ``!0`` here and the name the early return is keyed on
256
256
  there, so neither rewrite is looking for a gate the other cannot see.
257
257
  """
258
- guard = outcome.step("null-guard", expect=True)
259
- props = outcome.step("renderer-props", expect=True)
258
+ outcome.declare(required=("group-routing", "null-guard", "renderer-props"))
259
+ guard = outcome.step("null-guard")
260
+ props = outcome.step("renderer-props")
260
261
  source = _reroute_grouping(source, outcome)
261
262
  edits: list[Edit] = []
262
263
 
@@ -439,8 +440,9 @@ def _whitelist(node: js.Node) -> js.Node | None:
439
440
 
440
441
  def _max_effort(source: Source, _options: Options, outcome: Outcome) -> Source:
441
442
  """Let ``/effort max`` save as the default for new sessions."""
442
- gate = outcome.step("gate", expect=True)
443
- schema = outcome.step("schema", expect=True)
443
+ outcome.declare(required=("gate", "schema"))
444
+ gate = outcome.step("gate")
445
+ schema = outcome.step("schema")
444
446
  edits: list[Edit] = []
445
447
  seen: set[int] = set()
446
448
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes