patch-cc 0.3.2__tar.gz → 0.3.3__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.
- patch_cc-0.3.3/.github/workflows/release-watch.yml +164 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/PKG-INFO +1 -1
- {patch_cc-0.3.2 → patch_cc-0.3.3}/docs/PLAYBOOK.md +31 -5
- {patch_cc-0.3.2 → patch_cc-0.3.3}/docs/corpus.md +28 -26
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/streaming.py +137 -44
- {patch_cc-0.3.2 → patch_cc-0.3.3}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/.github/workflows/release.yml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/.gitignore +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/AGENTS.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/CLAUDE.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/LICENSE +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/README.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/docs/CONDUCT.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/docs/INTERNALS.md +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/docs/demo.gif +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/pyproject.toml +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/blob.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/container.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/cli.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/doctor.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/js.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/menu.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patcher.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/__init__.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/agents.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/base.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/chrome.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/codex.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/patches/thinking.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/src/patch_cc/ui.py +0 -0
- {patch_cc-0.3.2 → patch_cc-0.3.3}/uv.lock +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
name: Release watch
|
|
2
|
+
|
|
3
|
+
# Fires the "patch-cc release watch" cloud routine the moment Anthropic ships a
|
|
4
|
+
# new Claude Code build, instead of leaving it to the routine's own daily cron.
|
|
5
|
+
#
|
|
6
|
+
# The routine already knows how to decide whether anything moved -- it compares
|
|
7
|
+
# the upstream version and the checkout HEAD against the ledger in issue #1 and
|
|
8
|
+
# exits silently when neither did. Its schedule was never the interesting part;
|
|
9
|
+
# it was just a poll, once a day, which meant a release that broke a patch could
|
|
10
|
+
# sit unnoticed for up to 24 hours. This workflow moves the poll here, where a
|
|
11
|
+
# tick is one curl instead of a cloud session, and only wakes the routine when
|
|
12
|
+
# the version actually changes.
|
|
13
|
+
#
|
|
14
|
+
# The routine keeps its daily schedule as a backstop: if this workflow breaks or
|
|
15
|
+
# GitHub skips the schedule, the watch degrades to its old once-a-day behaviour
|
|
16
|
+
# rather than going silent. That is why a failed poll below is a warning and not
|
|
17
|
+
# a red run -- a flaky fetch must not page anyone.
|
|
18
|
+
#
|
|
19
|
+
# SETUP (one time, by hand -- there is no API for token creation):
|
|
20
|
+
# claude.ai/code/routines -> "patch-cc release watch" -> pencil icon
|
|
21
|
+
# -> Add another trigger -> API -> Generate token (shown once)
|
|
22
|
+
# -> store it as the repo secret ROUTINE_FIRE_TOKEN.
|
|
23
|
+
|
|
24
|
+
on:
|
|
25
|
+
schedule:
|
|
26
|
+
# Every 10 minutes. GitHub delays scheduled runs under load, so read this as
|
|
27
|
+
# "within ~10-25 min of a release", not as a guarantee.
|
|
28
|
+
- cron: "*/10 * * * *"
|
|
29
|
+
workflow_dispatch:
|
|
30
|
+
|
|
31
|
+
concurrency:
|
|
32
|
+
group: release-watch
|
|
33
|
+
cancel-in-progress: false
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
issues: read
|
|
38
|
+
|
|
39
|
+
env:
|
|
40
|
+
# The channel file the Claude Code installer itself reads -- the same source
|
|
41
|
+
# the routine treats as authoritative, so the two never disagree about what
|
|
42
|
+
# "the current release" means.
|
|
43
|
+
CHANNEL_URL: https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest
|
|
44
|
+
# Not a secret: firing needs the bearer token, and that is one.
|
|
45
|
+
ROUTINE_ID: trig_01WrgxdCZahJEHD3NNYcetqS
|
|
46
|
+
LEDGER_ISSUE: "1"
|
|
47
|
+
|
|
48
|
+
jobs:
|
|
49
|
+
watch:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- name: Read the release channel
|
|
53
|
+
id: upstream
|
|
54
|
+
run: |
|
|
55
|
+
set -uo pipefail
|
|
56
|
+
if ! v=$(curl -fsSL --max-time 30 --retry 3 --retry-delay 5 "$CHANNEL_URL"); then
|
|
57
|
+
echo "::warning::could not reach the release channel; skipping this tick"
|
|
58
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
v=$(printf '%s' "$v" | tr -d '[:space:]')
|
|
62
|
+
case "$v" in
|
|
63
|
+
[0-9]*.[0-9]*.[0-9]*) ;;
|
|
64
|
+
*)
|
|
65
|
+
echo "::warning::channel returned something that is not a version: '$v'"
|
|
66
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
67
|
+
exit 0
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
echo "version=$v" >> "$GITHUB_OUTPUT"
|
|
71
|
+
echo "upstream: $v"
|
|
72
|
+
|
|
73
|
+
# First guard: did an earlier tick already fire for this version? The
|
|
74
|
+
# routine takes several minutes to run (it downloads a ~275 MB bundle), so
|
|
75
|
+
# without this every tick during that window would fire it again.
|
|
76
|
+
- name: Already fired for this version?
|
|
77
|
+
id: marker
|
|
78
|
+
if: steps.upstream.outputs.skip != '1'
|
|
79
|
+
uses: actions/cache/restore@v4
|
|
80
|
+
with:
|
|
81
|
+
path: .fired
|
|
82
|
+
key: fired-${{ steps.upstream.outputs.version }}
|
|
83
|
+
lookup-only: true
|
|
84
|
+
|
|
85
|
+
# Second guard, and the authoritative one: the ledger records the version
|
|
86
|
+
# the routine has actually finished processing. This is what catches the
|
|
87
|
+
# case where the cache entry was evicted but the work is long done.
|
|
88
|
+
- name: Read the ledger's last-version
|
|
89
|
+
id: ledger
|
|
90
|
+
if: steps.upstream.outputs.skip != '1' && steps.marker.outputs.cache-hit != 'true'
|
|
91
|
+
env:
|
|
92
|
+
GH_TOKEN: ${{ github.token }}
|
|
93
|
+
run: |
|
|
94
|
+
set -uo pipefail
|
|
95
|
+
if ! body=$(gh issue view "$LEDGER_ISSUE" --repo "$GITHUB_REPOSITORY" --json body -q .body); then
|
|
96
|
+
echo "::warning::could not read the ledger; skipping this tick rather than guessing"
|
|
97
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
last=$(printf '%s\n' "$body" | sed -n 's/^last-version:[[:space:]]*//p' | head -1 | tr -d '[:space:]')
|
|
101
|
+
echo "last=$last" >> "$GITHUB_OUTPUT"
|
|
102
|
+
echo "ledger: $last"
|
|
103
|
+
|
|
104
|
+
- name: Fire the routine
|
|
105
|
+
id: fire
|
|
106
|
+
if: >-
|
|
107
|
+
steps.upstream.outputs.skip != '1' &&
|
|
108
|
+
steps.marker.outputs.cache-hit != 'true' &&
|
|
109
|
+
steps.ledger.outputs.skip != '1' &&
|
|
110
|
+
steps.ledger.outputs.last != steps.upstream.outputs.version
|
|
111
|
+
env:
|
|
112
|
+
TOKEN: ${{ secrets.ROUTINE_FIRE_TOKEN }}
|
|
113
|
+
VERSION: ${{ steps.upstream.outputs.version }}
|
|
114
|
+
LAST: ${{ steps.ledger.outputs.last }}
|
|
115
|
+
run: |
|
|
116
|
+
set -uo pipefail
|
|
117
|
+
if [ -z "${TOKEN:-}" ]; then
|
|
118
|
+
echo "::error::ROUTINE_FIRE_TOKEN is not set -- see the setup note at the top of this file"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
body=$(printf 'Claude Code %s is on the release channel; the ledger last recorded %s. This line is a hint for the run log only -- re-derive the version yourself from the channel, per your step 2.' "$VERSION" "${LAST:-(none)}")
|
|
122
|
+
payload=$(jq -nc --arg t "$body" '{text: $t}')
|
|
123
|
+
out=$(mktemp)
|
|
124
|
+
code=$(curl -sS -o "$out" -w '%{http_code}' -X POST \
|
|
125
|
+
"https://api.anthropic.com/v1/claude_code/routines/$ROUTINE_ID/fire" \
|
|
126
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
127
|
+
-H "anthropic-version: 2023-06-01" \
|
|
128
|
+
-H "anthropic-beta: experimental-cc-routine-2026-04-01" \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d "$payload") || code=000
|
|
131
|
+
echo "HTTP $code"
|
|
132
|
+
cat "$out"
|
|
133
|
+
echo
|
|
134
|
+
if [ "$code" != "200" ]; then
|
|
135
|
+
# Deliberately not marking the version as fired, so the next tick
|
|
136
|
+
# retries. A 429 here just means the daily run cap was reached.
|
|
137
|
+
echo "::error::fire failed with HTTP $code"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo "url=$(jq -r '.claude_code_session_url // empty' "$out")" >> "$GITHUB_OUTPUT"
|
|
141
|
+
: > .fired
|
|
142
|
+
|
|
143
|
+
# Only reached on a 200, so the marker never claims a fire that failed.
|
|
144
|
+
- name: Mark this version as fired
|
|
145
|
+
if: steps.fire.outcome == 'success'
|
|
146
|
+
uses: actions/cache/save@v4
|
|
147
|
+
with:
|
|
148
|
+
path: .fired
|
|
149
|
+
key: fired-${{ steps.upstream.outputs.version }}
|
|
150
|
+
|
|
151
|
+
- name: Summary
|
|
152
|
+
if: always()
|
|
153
|
+
run: |
|
|
154
|
+
{
|
|
155
|
+
echo "### Release watch"
|
|
156
|
+
echo
|
|
157
|
+
echo "- upstream: \`${{ steps.upstream.outputs.version || 'unreadable' }}\`"
|
|
158
|
+
echo "- ledger last-version: \`${{ steps.ledger.outputs.last || 'not read' }}\`"
|
|
159
|
+
if [ -n "${{ steps.fire.outputs.url }}" ]; then
|
|
160
|
+
echo "- **fired the routine** -> ${{ steps.fire.outputs.url }}"
|
|
161
|
+
else
|
|
162
|
+
echo "- no fire (nothing moved, or already fired for this version)"
|
|
163
|
+
fi
|
|
164
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -455,7 +455,11 @@ Each sub-step records its own `candidates`/`applied`:
|
|
|
455
455
|
reducer whose setter threading applied while every arm had drifted once
|
|
456
456
|
reported hits and streamed nothing.
|
|
457
457
|
- **`final-summary`**, optional: redacted thinking in the closing summary is a
|
|
458
|
-
refinement, not the feature.
|
|
458
|
+
refinement, not the feature. Its guard is whichever enclosing `if` *tests the
|
|
459
|
+
block* — 2.1.236 nested an experiment gate between the test and the summary
|
|
460
|
+
it guards, and "the nearest `if`" read a shape that had merely moved as one
|
|
461
|
+
that was gone. The climb is bounded by the function that declares the block,
|
|
462
|
+
because a receiver's name is only a spelling until its scope is said.
|
|
459
463
|
|
|
460
464
|
An *optional* sub-step that finds nothing is reported as absent, not broken —
|
|
461
465
|
it is just a shape this build doesn't have. A sub-step that finds a shape but
|
|
@@ -788,6 +792,28 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
788
792
|
*initialised* to is deliberately not asked: `useState(null)` is every
|
|
789
793
|
build's spelling and `useState(void 0)` would be the same state, while the
|
|
790
794
|
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.
|
|
791
817
|
|
|
792
818
|
A render was once selected by *position* — the observation that the real sites
|
|
793
819
|
fall after the state's `useState` declaration. That is worth recording as a
|
|
@@ -1245,10 +1271,10 @@ enumerated with their hashes in [corpus.md](corpus.md). Each removed matcher hit
|
|
|
1245
1271
|
can still be running; each was carried as "kept for older builds", which is a
|
|
1246
1272
|
claim the corpus disproves. The verbatim matchers were re-run from `HEAD` over
|
|
1247
1273
|
the whole corpus after the move, and they are still zero everywhere. The
|
|
1248
|
-
measurements this section quotes were taken during the move across
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1274
|
+
measurements this section quotes were taken during the move across
|
|
1275
|
+
2.1.210 → 2.1.233 (2.1.230 was never published) — a span the corpus has since
|
|
1276
|
+
grown to hold in full, so they are re-checkable rather than historical, and
|
|
1277
|
+
re-running `doctor` over it is one command (below).
|
|
1252
1278
|
|
|
1253
1279
|
- **`live-thinking` / `reducer-legacy`** — the pre-2.1.138 reducer with
|
|
1254
1280
|
positional parameters. Its removal collapsed the `reducer` variant *group*
|
|
@@ -11,7 +11,8 @@ Before the first patch of a version, patch-cc copies the pristine binary to
|
|
|
11
11
|
`~/.local/share/patch-cc/backups/<version>.orig` ([INTERNALS.md](INTERNALS.md#safety)).
|
|
12
12
|
The corpus is exactly those copies: not a fixture checked into the repo (each is
|
|
13
13
|
~300 MB), but a set that **accretes on its own** as Claude auto-updates and you
|
|
14
|
-
re-patch
|
|
14
|
+
re-patch — and that can be backfilled by saving any pristine native binary under
|
|
15
|
+
the same name. `doctor <path>` reads one; the sweep below reads all of them.
|
|
15
16
|
|
|
16
17
|
The binaries are the artifact, not this file — `doctor` recomputes every count
|
|
17
18
|
from them, so nothing here can drift from what a matcher actually does. This
|
|
@@ -20,42 +21,52 @@ matches means the file changed under you, not that a number moved.
|
|
|
20
21
|
|
|
21
22
|
## On disk now
|
|
22
23
|
|
|
23
|
-
The
|
|
24
|
-
|
|
24
|
+
The whole published span `2.1.210` → `2.1.236` — 2.1.230 was never published —
|
|
25
|
+
one pristine binary per version, 26 in all. Two pairs share a byte count
|
|
26
|
+
(`2.1.225`/`2.1.226`, `2.1.229`/`2.1.231`) and are still distinct binaries,
|
|
27
|
+
which is why the identity column is the hash (of the whole file,
|
|
28
|
+
`sha256sum <version>.orig`) and never the size:
|
|
25
29
|
|
|
26
30
|
| version | size | sha256 |
|
|
27
31
|
|---|---|---|
|
|
32
|
+
| `2.1.210` | 261 MB | `e7d2ceb53ed4c2ced1fe7fc1c6331c98dc5f7b4c9b2722d9c5fa3dd5dff6f719` |
|
|
33
|
+
| `2.1.211` | 262 MB | `8272c8a474ac9ea1bc35f19b9f7c7e7dc4dc4eb6d5ad3e484b19335ac72446b2` |
|
|
34
|
+
| `2.1.212` | 264 MB | `044a88cf3a5180776617fd3da1238dcbf9141ddec449a39cf7d2af1ac78e684e` |
|
|
35
|
+
| `2.1.213` | 265 MB | `7999631426e1b6e4444e4ecf9cd8a63a05a0411ccfe503927d4c9d57bc41bc64` |
|
|
36
|
+
| `2.1.214` | 265 MB | `3c029136f7c81f54ed4a38e9d52e655aad536433dbbde50519c8c31bb646ad14` |
|
|
37
|
+
| `2.1.215` | 265 MB | `c1efffaaf370aa187cb6a09dd93d4e511c646899b0078476f83791b664bde7fe` |
|
|
28
38
|
| `2.1.216` | 267 MB | `74deca45220b8080ec75ab099bd5a5980e41a2b5879846a008fb115d436de085` |
|
|
29
39
|
| `2.1.217` | 269 MB | `2630fc5dc6db61bc03f86b95daf47766e5ed5b61873f7bb7cfea764c5ac5a9ba` |
|
|
30
40
|
| `2.1.218` | 273 MB | `e12071751a9336b8af1012c103358ff04ac18f9aaff4a738cff7ba5cdfaf63f2` |
|
|
31
41
|
| `2.1.219` | 275 MB | `22cfd6f5b3061c0391ba84e9cf8c9deaa37783aac18b004d42ec061e98f00691` |
|
|
32
42
|
| `2.1.220` | 275 MB | `674f61f20ff306f3100cf9200e4c36c4b70278b5bef2884549819b942a89c863` |
|
|
33
43
|
| `2.1.221` | 289 MB | `60db8e88d42c24b5199c92cfd56ec88370c510c3789c6f364af748354f087ada` |
|
|
44
|
+
| `2.1.222` | 289 MB | `10caae8f22b915c26bfff0e013a4d45608c4f1ae287583626569156f447730e5` |
|
|
34
45
|
| `2.1.223` | 291 MB | `98226474f802e3094d6a86c5ade8883c16206d0fcb5c400b7401c800063e99d7` |
|
|
46
|
+
| `2.1.224` | 296 MB | `a2b5add7dc4bcd8eaa029f4e8bdac4df7769b4073698db7989d206baf9419c2d` |
|
|
47
|
+
| `2.1.225` | 298 MB | `0a3be8d18cb0f5357d38ce2d588601753a60b44cc9c622579ed8b8405dee231e` |
|
|
35
48
|
| `2.1.226` | 298 MB | `4e9bec1177ce9690e8bd988b710ac24105e70da428dd094c5adcbbe786a55555` |
|
|
36
49
|
| `2.1.227` | 304 MB | `6832dc3f1797b890b71116e5f2dbbf9a83fd3d0498c235b4b0f9cd0e6e499ad6` |
|
|
37
50
|
| `2.1.228` | 309 MB | `d535985e6941a3eb00179ccd7f52ceb0c6623a0305a518ebc4e6514f84a94c99` |
|
|
51
|
+
| `2.1.229` | 311 MB | `200338139a3df04a9ad22233837d1fb53fb6dffa21cd82e47559bfaa115acc1b` |
|
|
52
|
+
| `2.1.231` | 311 MB | `47a01daebf794f6c86c13d1875ad6e5be0627029ad8600731161f24018ecde5b` |
|
|
53
|
+
| `2.1.232` | 323 MB | `61d23f8749136907d586d5b11831ea8a5234d4c1dea40a5e55c33b52e204c6d1` |
|
|
38
54
|
| `2.1.233` | 325 MB | `55d281096f57d411ebbdd94dbf5e9ff3accb7c05713e37348c2c11d4b83bf9d9` |
|
|
39
55
|
| `2.1.234` | 328 MB | `3473601ea695d5bf769c5b202844d4cb4fbf723ae995450fcb6973204775c84a` |
|
|
40
56
|
| `2.1.235` | 331 MB | `bfcf0ae2dbf94b2b6a106074aabf3938b9a10889c3b678e4cb5a00c03274d5d5` |
|
|
57
|
+
| `2.1.236` | 335 MB | `6c8818fa22187aa555c242be4abbacc44d6b71a32ac9631ee7b2b5d12f51f752` |
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
59
|
+
This set covers the span the playbook's tree-move measurements were taken over
|
|
60
|
+
(`2.1.210` → `2.1.233`), so those numbers are re-checkable here rather than
|
|
61
|
+
historical.
|
|
51
62
|
|
|
52
63
|
## Rebuild or extend it
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
patching anything:
|
|
65
|
+
The corpus grows every time patch-cc touches a new build — each first patch of
|
|
66
|
+
a version leaves its `.orig` — and a version this machine never patched joins
|
|
67
|
+
the same way: save its pristine native binary as
|
|
68
|
+
`~/.local/share/patch-cc/backups/<version>.orig`. To read the JS a given binary
|
|
69
|
+
carries without patching anything:
|
|
59
70
|
|
|
60
71
|
```bash
|
|
61
72
|
patch-cc extract ~/.local/share/patch-cc/backups/2.1.233.orig > 2.1.233.js
|
|
@@ -75,12 +86,3 @@ sweep
|
|
|
75
86
|
|
|
76
87
|
`doctor` is read-only — it runs the matchers and parses their output, and never
|
|
77
88
|
writes a binary — so the sweep is safe to run against every backup at any time.
|
|
78
|
-
|
|
79
|
-
## Provenance of the wider span
|
|
80
|
-
|
|
81
|
-
Some measurements in the playbook were taken during "the move to the tree"
|
|
82
|
-
across a wider set than is durably on disk now — the range `2.1.210` → `2.1.233`
|
|
83
|
-
(2.1.230 was never published), whose earliest builds predate this backup set.
|
|
84
|
-
Those numbers were real when taken; the set above is what reproduces today, and
|
|
85
|
-
a claim that names a build not listed here is a historical one this file does
|
|
86
|
-
not stand behind byte-for-byte.
|
|
@@ -132,6 +132,9 @@ _SETTER = "onStreamingThinking"
|
|
|
132
132
|
#: `prop-threading` when 2.1.235 retired it -- and discriminates nothing on any
|
|
133
133
|
#: build in the corpus: the pair already names the same one renderer.
|
|
134
134
|
_TRANSCRIPT_SIGNATURE = ("messages", "streamingToolUses")
|
|
135
|
+
#: Our local for the threaded state, wherever one has to be minted: the
|
|
136
|
+
#: renderer signatures `transcript-signature` extends, and the store-snapshot
|
|
137
|
+
#: pattern `prop-threading` extends on builds that keep the state in a store.
|
|
135
138
|
_INJECTED = "__cc_streamingThinking"
|
|
136
139
|
|
|
137
140
|
|
|
@@ -199,8 +202,10 @@ def _bound_in_scope(site: js.Node, prop: str) -> str | None:
|
|
|
199
202
|
return None
|
|
200
203
|
|
|
201
204
|
|
|
202
|
-
def _state_in_scope(site: js.Node) -> str | None:
|
|
203
|
-
"""The live-thinking state
|
|
205
|
+
def _state_in_scope(site: js.Node) -> tuple[str, js.Node | None] | None:
|
|
206
|
+
"""The live-thinking state at this render: the name to thread, and the
|
|
207
|
+
snapshot pattern to extend first when the state has to be read out of a
|
|
208
|
+
store rather than a binding upstream already made.
|
|
204
209
|
|
|
205
210
|
Resolved from the site outwards, so what is threaded is a variable actually
|
|
206
211
|
in scope where it is threaded -- the one thing the old matcher named as
|
|
@@ -211,34 +216,48 @@ def _state_in_scope(site: js.Node) -> str | None:
|
|
|
211
216
|
gate could see it; it throws when that component renders.
|
|
212
217
|
|
|
213
218
|
The state is identified by its setter: the scope that holds live thinking
|
|
214
|
-
is the scope that hands ``onStreamingThinking`` to the reducer.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
which a nested function's is not and a block's own is not either.
|
|
219
|
-
|
|
220
|
-
the
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
219
|
+
is the scope that hands ``onStreamingThinking`` to the reducer. The setter
|
|
220
|
+
is asked of the whole subtree on purpose -- a scope may hand it from inside
|
|
221
|
+
a callback, and that says nothing about where the state lives -- while the
|
|
222
|
+
binding threaded has to be one this render can *see* (:func:`js.visible`),
|
|
223
|
+
which a nested function's is not and a block's own is not either.
|
|
224
|
+
|
|
225
|
+
What the handed setter *is* has two upstream spellings, and each names the
|
|
226
|
+
state its own way:
|
|
227
|
+
|
|
228
|
+
- **A bare identifier** -- a ``useState`` setter, every build through
|
|
229
|
+
2.1.235. The state is the array pattern that binds it: the pair whose
|
|
230
|
+
second name is the setter this scope hands the reducer *is* the live
|
|
231
|
+
thinking state, whatever produced it (``useState(null)`` and
|
|
232
|
+
``useState(void 0)`` are the same state, so the initialiser is never
|
|
233
|
+
asked).
|
|
234
|
+
- **A member read on a store** -- 2.1.236 moved the state into an external
|
|
235
|
+
stream store (``subscribe``/``getSnapshot``/``_publish``) and hands the
|
|
236
|
+
reducer ``<store>.setStreamingThinking``. The scope reads that store back
|
|
237
|
+
by destructuring the hook call it hands the same store to
|
|
238
|
+
(``{streamingToolUses:…}=useX(<store>)``), so the state is that pattern's
|
|
239
|
+
own ``streamingThinking`` binding: upstream's, the day it takes one --
|
|
240
|
+
the goal achieved, same as every other judged-on-achievement step -- and
|
|
241
|
+
until then ours, inserted into the pattern. The pattern is proven the
|
|
242
|
+
snapshot read by the store expression itself: the call's *only* argument
|
|
243
|
+
is the very expression the setter was read off, with one answer or none
|
|
244
|
+
(:func:`js.only`). Sole argument is deliberate -- a second argument is a
|
|
245
|
+
selector whose result is no longer the snapshot, and extending a pattern
|
|
246
|
+
of unknowable provenance binds ``undefined`` with every count green, so
|
|
247
|
+
that shape is refused loudly instead.
|
|
230
248
|
"""
|
|
231
249
|
scope = js.climb(site, lambda n: n.type in js.FUNCTIONS)
|
|
232
250
|
while scope is not None and not _outermost(scope):
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
handed = [
|
|
252
|
+
value
|
|
235
253
|
for node in js.every(
|
|
236
254
|
scope,
|
|
237
255
|
lambda n: n.type == "property_identifier" and js.text(n) == _SETTER,
|
|
238
256
|
)
|
|
239
257
|
if (pair := js.named(node)) is not None
|
|
240
258
|
and (value := pair.child_by_field_name("value")) is not None
|
|
241
|
-
|
|
259
|
+
]
|
|
260
|
+
setters = {js.text(js.binding(value)) for value in handed}
|
|
242
261
|
for declarator in js.every(scope, js.of_type("variable_declarator")):
|
|
243
262
|
name = declarator.child_by_field_name("name")
|
|
244
263
|
if name is None or name.type != "array_pattern":
|
|
@@ -247,7 +266,33 @@ def _state_in_scope(site: js.Node) -> str | None:
|
|
|
247
266
|
continue
|
|
248
267
|
bound = [js.text(child) for child in name.named_children]
|
|
249
268
|
if len(bound) == 2 and bound[1] in setters:
|
|
250
|
-
return bound[0]
|
|
269
|
+
return bound[0], None
|
|
270
|
+
snapshots: dict[int, js.Node] = {}
|
|
271
|
+
for setter in (v for v in handed if v.type == "member_expression"):
|
|
272
|
+
store = js.text(js.receiver(setter))
|
|
273
|
+
for declarator in js.every(scope, js.of_type("variable_declarator")):
|
|
274
|
+
name = declarator.child_by_field_name("name")
|
|
275
|
+
value = declarator.child_by_field_name("value")
|
|
276
|
+
if name is None or name.type != "object_pattern":
|
|
277
|
+
continue
|
|
278
|
+
if not js.children(name) or value is None:
|
|
279
|
+
continue
|
|
280
|
+
if value.type != "call_expression":
|
|
281
|
+
continue
|
|
282
|
+
taken = js.arguments(value)
|
|
283
|
+
if len(taken) != 1 or js.text(taken[0]) != store:
|
|
284
|
+
continue
|
|
285
|
+
if not js.visible(declarator, site):
|
|
286
|
+
continue
|
|
287
|
+
snapshots[declarator.start_byte] = name
|
|
288
|
+
pattern = js.only(
|
|
289
|
+
list(snapshots.values()), "snapshot reads of the live-thinking store"
|
|
290
|
+
)
|
|
291
|
+
if pattern is not None:
|
|
292
|
+
carried = js.props(pattern)
|
|
293
|
+
if _STREAMING in carried:
|
|
294
|
+
return js.text(js.binding(carried[_STREAMING])), None
|
|
295
|
+
return _INJECTED, pattern
|
|
251
296
|
scope = js.climb(scope.parent, lambda n: n.type in js.FUNCTIONS)
|
|
252
297
|
return None
|
|
253
298
|
|
|
@@ -257,22 +302,55 @@ def _step_prop_threading(source: Source, outcome: Outcome) -> Source:
|
|
|
257
302
|
step = outcome.step("prop-threading")
|
|
258
303
|
edits = []
|
|
259
304
|
renders = _conversation_renders(source)
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
305
|
+
# The durable witness behind a snapshot insertion: the field it binds is
|
|
306
|
+
# one the bundle's own objects still name (the store's snapshot initialiser
|
|
307
|
+
# and its publish call, on 2.1.236). Extending the pattern is a rewrite the
|
|
308
|
+
# matcher can only vouch for itself, and a store that renames the field
|
|
309
|
+
# would leave it threading `undefined` with every count green -- the same
|
|
310
|
+
# hole `thinking-summaries` pays a header-name count for.
|
|
311
|
+
field_named = any(
|
|
312
|
+
(pair := js.named(node)) is not None and pair.type == "pair"
|
|
313
|
+
for node in source.find(_STREAMING)
|
|
314
|
+
)
|
|
315
|
+
extended: set[int] = set()
|
|
316
|
+
unreached = 0
|
|
264
317
|
for bag in renders:
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
318
|
+
resolved = _state_in_scope(bag)
|
|
319
|
+
if resolved is None:
|
|
320
|
+
# By design on every build we hold: the resume view, the transcript
|
|
321
|
+
# overlay and the message picker draw conversations too, in scopes
|
|
322
|
+
# where nothing ever streams. Counted rather than worth a sentence
|
|
323
|
+
# each -- the note below carries the number, and the number moving
|
|
324
|
+
# is the signal (a render newly skipped was the only sign of the
|
|
325
|
+
# 2.1.232 out-of-scope threading).
|
|
326
|
+
unreached += 1
|
|
327
|
+
continue
|
|
328
|
+
state, pattern = resolved
|
|
329
|
+
if pattern is not None and not field_named:
|
|
330
|
+
step.note(f"the stream store no longer names a {_STREAMING} field")
|
|
268
331
|
continue
|
|
269
332
|
step.candidates += 1
|
|
270
333
|
step.applied += 1
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
334
|
+
carried = js.props(bag)
|
|
335
|
+
if _STREAMING in carried:
|
|
336
|
+
# Upstream already threads it into this render; the goal achieved.
|
|
337
|
+
continue
|
|
338
|
+
if pattern is not None and pattern.start_byte not in extended:
|
|
339
|
+
# Two renders in one scope read one snapshot: the pattern gains the
|
|
340
|
+
# field once, at the front, which is valid whatever it ends with.
|
|
341
|
+
extended.add(pattern.start_byte)
|
|
342
|
+
edits.append(
|
|
343
|
+
Edit.before(pattern.named_children[0], f"{_STREAMING}:{state},")
|
|
274
344
|
)
|
|
345
|
+
edits.append(
|
|
346
|
+
Edit.before(js.entry(carried[_CONVERSATION[0]]), f"{_STREAMING}:{state},")
|
|
275
347
|
)
|
|
348
|
+
# Printed every run, green ones included: an early warning held back until
|
|
349
|
+
# something breaks arrives too late to be one.
|
|
350
|
+
step.note(
|
|
351
|
+
f"{len(renders)} conversation render(s)"
|
|
352
|
+
+ (f", {unreached} with no live-thinking state in scope" if unreached else "")
|
|
353
|
+
)
|
|
276
354
|
return source.apply(edits)
|
|
277
355
|
|
|
278
356
|
|
|
@@ -473,19 +551,34 @@ def _step_final_summary(source: Source, outcome: Outcome) -> Source:
|
|
|
473
551
|
if not js.reads(thinking, _SUMMARY_PROPS[0]):
|
|
474
552
|
continue
|
|
475
553
|
block = js.text(js.receiver(thinking))
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
scope = js.climb(
|
|
481
|
-
|
|
554
|
+
# The block's home is the function that produced it -- the one holding
|
|
555
|
+
# the `.find` whose predicate selects thinking -- resolved from the
|
|
556
|
+
# summary outwards, since the summary itself sits inside the callback
|
|
557
|
+
# that hands it over.
|
|
558
|
+
scope = js.climb(summary, lambda n: n.type in js.FUNCTIONS)
|
|
559
|
+
predicate = None
|
|
560
|
+
while scope is not None and not _outermost(scope):
|
|
561
|
+
if (predicate := _selects_thinking(scope, block)) is not None:
|
|
562
|
+
break
|
|
563
|
+
scope = js.climb(scope.parent, lambda n: n.type in js.FUNCTIONS)
|
|
564
|
+
if predicate is None or scope is None:
|
|
482
565
|
continue
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
566
|
+
# Which `if` guards the summary is answered by what its condition
|
|
567
|
+
# tests, never by being the nearest: 2.1.236 nested an unrelated gate
|
|
568
|
+
# between the thinking test and the summary it guards, and the nearest
|
|
569
|
+
# `if` read a shape that had merely moved as one that was gone. The
|
|
570
|
+
# climb stays inside the block's own home, because its name is only a
|
|
571
|
+
# spelling until the scope it belongs to is said.
|
|
572
|
+
guard = js.up(summary, "if_statement")
|
|
573
|
+
test = None
|
|
574
|
+
while (
|
|
575
|
+
guard is not None
|
|
576
|
+
and guard.start_byte >= scope.start_byte
|
|
577
|
+
and (test := _tests_thinking(guard.child_by_field_name("condition"), block))
|
|
578
|
+
is None
|
|
579
|
+
):
|
|
580
|
+
guard = js.up(guard.parent, "if_statement")
|
|
581
|
+
if test is None:
|
|
489
582
|
continue
|
|
490
583
|
step.candidates += 1
|
|
491
584
|
step.applied += 1
|
|
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
|
|
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
|
|
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
|