@junghanacs/entwurf 0.34.2 → 0.35.1

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 (70) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/README.md +7 -0
  3. package/VERIFY.md +16 -5
  4. package/demo/README.md +109 -175
  5. package/demo/demo-baseline.sh +5 -1
  6. package/demo/demo.sh +6 -2
  7. package/demo/herdr-pi-cc-20261011.provenance.json +64 -0
  8. package/docs/acp-backend-rail.md +28 -2
  9. package/docs/assets/entwurf-demo.gif +0 -0
  10. package/docs/setup-clean-host.md +1 -1
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +46 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +67 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +9 -20
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +11 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +27 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +13 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +119 -11
  20. package/package.json +4 -2
  21. package/pi-extensions/lib/acp/backend-adapter.ts +52 -2
  22. package/pi-extensions/lib/acp/model-ids.js +77 -0
  23. package/pi-extensions/lib/acp/models.ts +15 -22
  24. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +12 -0
  25. package/pi-extensions/lib/fresh-call-dispatch.ts +25 -1
  26. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  27. package/pi-extensions/lib/mux-fresh-call.ts +3 -0
  28. package/pi-extensions/lib/resume-launch-identity.ts +12 -4
  29. package/pi-extensions/meta-bridge-pi-durable.ts +153 -13
  30. package/run.sh +118 -20
  31. package/scripts/check-acp-backend-preflight.ts +10 -2
  32. package/scripts/check-acp-cortex.ts +7 -6
  33. package/scripts/check-acp-prompt-lifecycle.ts +2 -2
  34. package/scripts/check-acp-provider-surface.ts +1 -1
  35. package/scripts/check-acp-session-reuse.ts +5 -5
  36. package/scripts/check-acp-stop-reason.ts +2 -2
  37. package/scripts/check-acp-stream-hooks.ts +2 -2
  38. package/scripts/check-acp-usage-accounting.ts +2 -2
  39. package/scripts/check-fresh-call-dispatch.ts +47 -4
  40. package/scripts/check-gate-qualification.ts +8 -5
  41. package/scripts/check-probe-ordering.ts +13 -6
  42. package/scripts/check-release-gate-outcomes.ts +159 -1
  43. package/scripts/check-resume-launch-identity.ts +38 -0
  44. package/scripts/ci-qualify-decide.sh +114 -17
  45. package/scripts/mutants/acp-cortex.json +17 -8
  46. package/scripts/mutants/acp-set-model-readback.json +28 -0
  47. package/scripts/mutants/demo-reproducibility.json +155 -0
  48. package/scripts/mutants/fresh-call-dispatch.json +11 -0
  49. package/scripts/mutants/herdr-dev-checkout.json +70 -0
  50. package/scripts/mutants/pi-durable.json +117 -0
  51. package/scripts/mutants/release-gate.json +28 -2
  52. package/scripts/mutants/resume-launch-identity.json +15 -0
  53. package/scripts/mutants/v2-visible-resume.json +11 -0
  54. package/scripts/resolve-acp-bridge.ts +1 -1
  55. package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
  56. package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
  57. package/scripts/smoke-acp-long-turn-live.ts +1 -1
  58. package/scripts/smoke-acp-mcp-live.ts +1 -1
  59. package/scripts/smoke-acp-memory-containment-live.ts +1 -1
  60. package/scripts/smoke-acp-ordering-probe-live.ts +1 -1
  61. package/scripts/smoke-acp-overlay-live.ts +1 -1
  62. package/scripts/smoke-acp-provider-live.ts +1 -1
  63. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  64. package/scripts/smoke-acp-session-reuse-live.ts +1 -1
  65. package/scripts/smoke-acp-skill-live.ts +1 -1
  66. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  67. package/scripts/smoke-acp-v2-send-live.ts +1 -1
  68. package/scripts/smoke-entwurf-chain-live.ts +1 -1
  69. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  70. package/scripts/tsconfig.json +1 -0
package/demo/README.md CHANGED
@@ -1,193 +1,127 @@
1
- # entwurf recorded demos
2
-
3
- > **Archived pre-0.12 evidence.** These scripts still describe the retired v1 `entwurf` / `entwurf_resume` / `entwurf_send` demo flow. They are kept for historical comparison and will be replaced by a v2-native retake in a post-0.12 follow-up; do not treat them as the current 0.12 runnable recipe.
4
-
5
- > **Model ids here are archived, not recommended (2026-08-20).** The `entwurf/gpt-5.4` rows below record what these GIFs actually ran on. They are stale twice over: `entwurf/gpt-5.4` is no longer a curated `entwurf` ACP id at all — that provider serves only the `claude-*` and `cortex-*` rows in `pi-extensions/lib/acp/models.ts` — and the live-spend default has since moved to `openai-codex/gpt-5.6-luna` under a cheaper plan. Read them as a record of the recording, never as a default to copy; the current defaults live in `VERIFY.md` and `NEXT.md`.
6
-
7
- Two recorded GIF scripts live here. Both use `asciinema` for capture and
8
- `agg` for cast → GIF conversion. Both share the same gitignore policy
9
- (`*.cast` global, `demo/*.gif`, `demo/*.log` — only the `.sh` and this
10
- README are tracked).
11
-
12
- ## `demo-baseline.sh` — single-pane baseline + entwurf surface
13
-
14
- Historical pre-0.12 demo, still linked from the top-level `README.md` as archived evidence. `package.json#pi.image` now points at the `entwurf` release hero instead. Two scenes driven into one pi pane:
15
-
16
- 1. **Baseline self-awareness** — the `Q-B0` + `Q-B0-CARRIER` interview,
17
- English answer enforced. The pane exposes how the running model
18
- names its harness, distinguishes native tools from MCP / custom
19
- tools, names the carrier surface for each cited piece of
20
- information, and admits unknowns instead of fabricating.
21
- 2. **Entwurf surface** — the same pane spawns a sibling via the
22
- `entwurf` tool (`mode=sync`, cross-model — driver `claude-sonnet-5`,
23
- sibling `gpt-5.4`), prints the Session ID, and quotes the sibling's
24
- one-line reply verbatim.
25
-
26
- Run from the repo root (or `demo/`):
1
+ # Recorded demos — source checkout recipes
2
+
3
+ ## Whole Herdr: Pi ↔ native Claude Code (#138)
4
+
5
+ This is the scene shown by the [README GIF](../README.md). The recording is the **whole
6
+ Herdr client**, with sidebar and tabs, without captions. It observes a real Pi citizen
7
+ opening native Claude Code, receiving its callback, sending one task, and receiving the
8
+ sibling's native Bash result. Camera focus is a view, never an address or delivery receipt.
9
+
10
+ ### Original publication versus a new take
11
+
12
+ [Original provenance](./herdr-pi-cc-20261011.provenance.json) records the approved GIF hash,
13
+ original cast hash, measured runtime/models, audited source hashes and render arguments.
14
+ The original raw cast, transcripts, native records and full manifests remain private.
15
+ **A fresh checkout cannot replay that original capture.** The command below creates a
16
+ **new LIVE take of the same scene**, not byte-identical GIF reproduction or renewed
17
+ acceptance of the original. Model text, timing, ids and terminal banners vary. The original
18
+ agg/ffprobe versions were not recorded; current tool versions cannot retroactively fill them.
19
+
20
+ ### Prerequisites — no automatic install or repair
21
+
22
+ Run from an installed development **source checkout**, not an npm consumer. The `.ts`
23
+ takes/helpers/tests are maintainer tooling and are deliberately excluded from the tarball.
24
+
25
+ - Node at the `package.json#engines.node` floor; the checkout's pinned dependencies and
26
+ current compiled bridge. After a source/bootstrap change, build with `pnpm run build-bridge`.
27
+ - Pi at the checkout's supported floor (**current floor 1.1.0**); `entwurf` on PATH must be
28
+ the intended source stable-bin. Source evidence is not registry-package acceptance.
29
+ - `herdr` on PATH at exactly `scripts/fixtures/herdr-supply.json#version`; `asciinema` v3.
30
+ - Herdr's existing Pi integration at
31
+ `${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/extensions/herdr-agent-state.ts`.
32
+ - The operator's existing Claude launcher/auth and vendor data at
33
+ `${XDG_DATA_HOME:-$HOME/.local/share}/claude`. Missing or drifted ownership is a refusal,
34
+ not permission to install, copy credentials or fix global hooks/settings.
35
+ - For offline rendering only: `agg` and `ffprobe` on PATH. Rendering is optional and
36
+ requires the original private `take.cast` **and its same-run manifest**.
37
+
38
+ Selectors for this scene are fixed: Pi `openai-codex/gpt-6.1-sol`, thinking `low`, native
39
+ Claude Code alias `sonnet`. Overrides may only restate these. An OpenRouter base-URL
40
+ redirect is refused before inference; no fallback model or automatic retry is used.
41
+ A requested alias and the native model id observed in its transcript are recorded separately.
42
+
43
+ ### Offline verification first
27
44
 
28
45
  ```bash
29
- bash demo/demo-baseline.sh
46
+ ./run.sh check-demo-reproducibility
47
+ pnpm exec tsc --noEmit -p scripts/tsconfig.json
30
48
  ```
31
49
 
32
- Output lands directly in the publish surface — re-running the script
33
- overwrites the previous take so the gallery preview always matches the
34
- current branch:
35
-
36
- - `docs/assets/entwurf-demo.cast` — raw asciinema recording (gitignored)
37
- - `docs/assets/entwurf-demo.gif` — tracked, referenced by the top-level `README.md` as archived pre-0.12 evidence
38
- - `demo/baseline-debug.log` — `ENTWURF_DEBUG=1` stderr (gitignored, stays next to the script)
50
+ The named source-only floor verb discovers `demo/**/*.test.ts` and runs Node's own test
51
+ runner, including fake-tool/child-process fixtures. It is reached by `pnpm check` and
52
+ `pnpm run check:full`; these are not Vitest tests and are not reached by the adapter beside
53
+ Vitest globs. HOME, writable XDG roots and identity carriers are sandboxed by the verb.
54
+ `agg` and `ffmpeg` are faked, but the renderer/caption fixtures decode real GIF bytes, so
55
+ this floor needs a real `ffprobe` on PATH; without it they fail (`ffprobe ENOENT`) rather
56
+ than skip. CI installs Ubuntu's `ffmpeg` package (which carries `ffprobe`) in the check job.
57
+ The receiving type fence is `scripts/tsconfig.json`, paired with the root `demo` exclusion.
39
58
 
40
- ## `demo.sh` — two-pane entwurf flow (regression guard)
59
+ ### Explicit new LIVE take
41
60
 
42
- Three-scene entwurf flow used for regression evidence, not for the
43
- public README. Two pi sessions run side-by-side in one tmux window;
44
- a background driver types the scene prompts into the sender pane.
45
-
46
- 1. **Spawn** a sonnet sibling (memory write — "tempered indigo").
47
- 2. **Resume** that same sibling (memory recall — identity preservation,
48
- the #9 regression guard).
49
- 3. **Cross-session greeting** via `entwurf_send`. *(Historical: `entwurf_send` was retired in the
50
- 0.12 cutover, and the "sent box" half of that pairing was never actually reachable — the
51
- sender-side renderer had zero producers and was removed in #120 P3. What the recording shows on
52
- the receive side is real; the send side is a box the operator could not have seen.)*
53
-
54
- Run:
61
+ Only run after deciding to spend real subscription turns:
55
62
 
56
63
  ```bash
57
- bash demo/demo.sh
64
+ LIVE=1 node demo/take-herdr-pi-cc.ts
58
65
  ```
59
66
 
60
- Output mirrors the baseline demo's layout — recording artifacts land in
61
- `docs/assets/` so the publish surface and the demo script share one
62
- naming convention:
67
+ With approved selectors/policy and without `LIVE=1`, it exits 97 **before tool preflight
68
+ or resource creation**. Invalid selectors or an OpenRouter redirect can refuse earlier
69
+ with exit 2, still without tools or writes. Once authorized, it creates a private Herdr
70
+ server/fence, starts the scene, and stops only resources whose
71
+ ownership it can prove. Failure ends observation; it cannot undo a citizen's completed work.
72
+ No automatic retry, rerender or promotion follows a failure. Failed evidence is retained.
63
73
 
64
- - `docs/assets/entwurf-entwurf.cast` — raw asciinema recording (gitignored via global `*.cast`)
65
- - `docs/assets/entwurf-entwurf.gif` — tracked, referenced by the top-level `README.md` Entwurf section
66
- - `demo/peer-debug.log` — `ENTWURF_DEBUG=1` stderr from the top pane (gitignored, stays next to the script)
67
- - `demo/sender-debug.log` — same from the bottom pane
74
+ The fence isolates Herdr XDG state and Entwurf record/mailbox roots, **not the whole host**:
75
+ HOME/auth/vendor stores remain real; the existing Claude data directory is linked into the
76
+ private fence. Pi's control socket and the addressed v2 lock use real `~/.pi/` directories.
77
+ The native transcripts, records and fence are kept, not erased by teardown. Do not run this
78
+ against an operator server or broaden cleanup to unrelated sessions.
68
79
 
69
- Watch debug live (separate terminal, before `bash demo.sh`):
80
+ The default output is `.tmp-verify/138-demo-gifs/takes/<take-id>/` (ignored); an explicit
81
+ `ENTWURF_DEMO_OUT` changes its parent. `manifest.json` contains assertions, camera views,
82
+ raw receipts, measured models and the cast digest. The stage describes source **tooling**;
83
+ `entwurfOnPath` is observed, not checked against this checkout's stable-bin. Confirm that
84
+ binding separately before calling a take source-runtime evidence. Git HEAD and dirty state
85
+ are observed; a dirty HEAD is **not** an immutable source fingerprint.
86
+ Keep the exact source and manifest with any subsequent take you intend to audit.
70
87
 
71
- ```bash
72
- tail -F demo/sender-debug.log demo/peer-debug.log
73
- ```
74
-
75
- Quick post-run greps:
76
-
77
- ```bash
78
- grep 'entwurf:debug' demo/sender-debug.log
79
- grep -E '(entwurf|model-switch)' demo/sender-debug.log
80
- ```
88
+ ### Offline rendering and publication boundary
81
89
 
82
- ## Tunables (env vars)
83
-
84
- ### `demo.sh` (entwurf two-pane)
85
-
86
- | Var | Default | Meaning |
87
- |---|---|---|
88
- | `PEER_MODEL` | `entwurf/gpt-5.4` (≡ `piat`) | top pane backend (receives greeting) |
89
- | `SENDER_MODEL` | `entwurf/claude-sonnet-5` (≡ `pias`) | bottom pane backend (drives scenes) |
90
- | `SCENE_DELAY` | `25` | seconds to wait for each scene's agent work |
91
- | `WARMUP` | `3` | seconds to wait for both pi banners |
92
- | `FINAL_PAUSE` | `5` | extra wait after scene 3 (peer reply lag) |
93
- | `GIF_SPEED` | `2.8` | agg playback multiplier (cast time → GIF time) |
94
- | `SESSION` | `entwurf-demo` | tmux session name |
95
- | `OUTDIR` | `demo/` | debug log directory (cast + gif live in `docs/assets/`) |
96
-
97
- ### `demo-baseline.sh` (single-pane baseline + entwurf)
98
-
99
- | Var | Default | Meaning |
100
- |---|---|---|
101
- | `DRIVER_MODEL` | `entwurf/claude-sonnet-5` (≡ `pias`) | the driven pane — answers Q-B0 and spawns the sibling |
102
- | `SIBLING_MODEL` | `entwurf/gpt-5.4` (≡ `piat`) | cross-backend sibling spawned in scene 2 |
103
- | `SIBLING_CWD` | repo root | cwd passed to the entwurf sibling (drives `<project-context>` injection) |
104
- | `SCENE1_DELAY` | `60` | seconds for the long English baseline answer |
105
- | `SCENE2_DELAY` | `30` | seconds for the sync entwurf round-trip |
106
- | `WARMUP` | `3` | seconds to wait for the pi banner |
107
- | `FINAL_PAUSE` | `4` | extra wait after scene 2 |
108
- | `GIF_SPEED` | `2.0` | agg playback multiplier (slower than `demo.sh` — the answer rewards reading) |
109
- | `GIF_COMPRESS` | `1` | run gifsicle after agg (`0` to skip) |
110
- | `GIF_LOSSY` | `200` | gifsicle `--lossy` strength (drop to `80` if a future scenario shows artifacts) |
111
- | `GIF_COLORS` | `64` | gifsicle `--colors` palette size |
112
- | `SESSION` | `entwurf-baseline-demo` | tmux session name |
113
- | `OUTDIR` | `demo/` | debug log directory (cast + gif live in `docs/assets/`) |
114
-
115
- ### Model ↔ alias map
116
-
117
- The demo launch lines correspond to your shell aliases:
118
-
119
- | Alias | Model id | Use as |
120
- |---|---|---|
121
- | `piao` | `entwurf/claude-opus-5` | `SENDER_MODEL` (precision scenes) |
122
- | `pias` | `entwurf/claude-sonnet-5` | `SENDER_MODEL` (default) |
123
- | `piat` | `entwurf/gpt-5.4` | `PEER_MODEL` (default) |
124
- | `piat5` | `entwurf/gpt-5.5` | either |
125
- | `piag` | `entwurf/gemini-3.1-pro-preview` | either |
126
-
127
- Each pane runs with `ENTWURF_DEBUG=1` baked in — same shape as
128
- the aliases — so every spawn/model-switch/entwurf trace lands in the
129
- debug log files (see below).
130
-
131
- Examples:
90
+ Given an existing same-run private take, no model turn is needed:
132
91
 
133
92
  ```bash
134
- # Swap peer to gemini
135
- PEER_MODEL=entwurf/gemini-3.1-pro-preview bash demo.sh
136
-
137
- # Faster pacing for short retake
138
- SCENE_DELAY=30 FINAL_PAUSE=10 bash demo.sh
139
-
140
- # All three backends in one demo would need a 3-pane variant — out of scope here.
93
+ node demo/render-take.ts <private-take-dir> <private-output.gif>
141
94
  ```
142
95
 
143
- ## Prerequisites
144
-
145
- - `pi` on PATH (current floor 1.1.0)
146
- - `entwurf` provider configured + auth ready for the selected sender/peer models
147
- - `asciinema` installed
148
- - `agg` installed (optional — only for GIF conversion)
149
- - `gifsicle` installed (optional — `demo-baseline.sh` post-compress step; skip with `GIF_COMPRESS=0`. On NixOS: `nix-shell -p gifsicle` covers both scripts)
150
- - `tmux` installed
151
-
152
- If you watch the demo live: open another terminal and run
153
- `tmux attach -t entwurf-demo -r` (read-only attach) **before** the script
154
- starts recording. Do not attach writable — keystrokes from your terminal
155
- would collide with the driver.
156
-
157
- ## Why pre-baked prompts
158
-
159
- Each scene's prompt asks the inner agent to discover state itself
160
- (`entwurf_peers`, prior Session ID from this conversation). So the driver
161
- script types static strings only — no sessionId injection, no
162
- output parsing. That's what makes it reproducible.
163
-
164
- ## Editing scenes
165
-
166
- Open `demo.sh`, edit the three `tmux send-keys -t "$SENDER_PANE" -l '...'`
167
- blocks under the `drive()` function. The `-l` flag tells tmux to send the
168
- argument literally (no key-name interpretation), so quotes inside the prompt
169
- are safe as long as you don't use single quotes inside the single-quoted
170
- argument.
171
-
172
- ## Tracked vs. ignored
173
-
174
- Tracked (committed):
175
-
176
- - `demo/demo.sh`
177
- - `demo/demo-baseline.sh`
178
- - `demo/README.md`
179
- - `docs/assets/entwurf-demo.gif` — archived pre-0.12 README demo GIF (no longer `package.json#pi.image`)
180
- - `docs/assets/entwurf-entwurf.gif` — archived pre-0.12 Entwurf section illustration
181
-
182
- Ignored (regenerable recording artifacts — both demos):
183
-
184
- - `*.cast` (global rule — applies under `docs/assets/` too)
185
- - `demo/*.log`
186
-
187
- Both demos write their recording artifacts directly into `docs/assets/`
188
- using the names above. Re-running a demo overwrites the previous take so
189
- the published preview always matches `main`. The `.cast` source stays
190
- local (global `*.cast` ignore); only the `.gif` rides the publish surface.
191
- The `gifsicle` step inside `demo-baseline.sh` makes the take reproducible
192
- without a manual compression pass — see the Tunables table for tuning the
193
- lossy/colors flags.
96
+ The renderer verifies cast digest, trims only recorder-only teardown, and promotes the
97
+ private GIF only after agg exits successfully and ffprobe decodes it. Each render keeps a
98
+ uniquely named `.render.cast` beside its output (also on failure), with name/digest in a
99
+ successful sidecar; the original take's `take.render.cast` is never overwritten. Non-live-pass takes
100
+ can render only to a private FAILED name. A mixed subject/teardown event is retained whole.
101
+ The renderer refuses repository `docs/assets/` destinations, including parent-directory
102
+ aliases: **publication is a separate reviewed copy**, never a rendering side effect.
103
+
104
+ Its email/company-path scan is deliberately narrow, **not a privacy/public-readability
105
+ certificate**: personal paths, hostnames, footers and native progress language can remain.
106
+ The operator approved the original rough pixels with their disclosed limitations. A new
107
+ render requires its own review; it must not overwrite the approved original implicitly.
108
+ The GIF and sidecar rename separately; a crash between them leaves a mismatched pair, not
109
+ atomic two-file publication. Optional `caption-take.ts` produces a separate derivative;
110
+ it is not this uncaptioned publication recipe.
111
+
112
+ ## Other source scenes
113
+
114
+ `take-pi-durable.ts`, `take-pi-acp.ts` and `take-pi-cc.ts` record **different**, private tmux
115
+ scenes. Each requires its own explicit `LIVE=1` and, like the Herdr scene, refuses a
116
+ non-approved selector or an OpenRouter base-URL redirect first (exit 2, no tools or writes).
117
+ They are not the Herdr GIF's recipe.
118
+ Their helper tests remain in the same offline discovery verb. Original FAILED takes and
119
+ offline rejudgements are distinct evidence, never replacement LIVE acceptance.
120
+
121
+ ## Archived pre-0.12 scripts
122
+
123
+ `demo-baseline.sh` and `demo.sh` retain their historical bodies for comparison, but now
124
+ **refuse immediately with exit 2**, even with `LIVE=1`. They cannot launch retired v1 flows
125
+ or overwrite today's published assets. Historical commands, selectors and old output paths
126
+ inside those bodies are tombstones, not runnable advice. Scene 3's current deterministic
127
+ delivery proof is `./run.sh check-bridge-delivery`; do not revive the v1 route.
@@ -25,7 +25,11 @@
25
25
 
26
26
  set -euo pipefail
27
27
 
28
- # ---------- config ----------
28
+ # Hard retirement: the preserved v1 body must never launch or overwrite today's approved GIF.
29
+ printf '%s\n' 'entwurf demo: archived v1 baseline refused; see demo/README.md for the source-only Herdr recipe.' >&2
30
+ exit 2
31
+
32
+ # ---------- historical body (unreachable) ----------
29
33
  SESSION=${SESSION:-entwurf-baseline-demo}
30
34
  SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
31
35
  REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
package/demo/demo.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # retired v1 verbs (`entwurf` / `entwurf_resume` / `entwurf_send`), which the 0.12
6
6
  # cutover removed; a run would fail at scene 1. Kept for historical comparison only.
7
7
  # See demo/README.md. Scene 3's delivery proof was recovered as a deterministic gate
8
- # (scripts/check-bridge-delivery.ts); the v2-native retake is a post-0.12 follow-up.
8
+ # (scripts/check-bridge-delivery.ts); current v2-native source recipes live in demo/README.md.
9
9
  #
10
10
  #
11
11
  # MODEL NOTE (2026-08-20). The model ids below are the ARCHIVED values these
@@ -28,7 +28,11 @@
28
28
 
29
29
  set -euo pipefail
30
30
 
31
- # ---------- config ----------
31
+ # Hard retirement: preserve historical source, not a second runnable v1 route.
32
+ printf '%s\n' 'entwurf demo: archived v1 peer scene refused; see demo/README.md for current source recipes.' >&2
33
+ exit 2
34
+
35
+ # ---------- historical body (unreachable) ----------
32
36
  SESSION=${SESSION:-entwurf-demo}
33
37
  SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
34
38
  REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
@@ -0,0 +1,64 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "scene": "whole-Herdr Pi to native Claude Code",
4
+ "take": "take-herdr-pi-cc-2026-10-11T11-57-22-297Z",
5
+ "startedAt": "2026-10-11T11:57:22.299Z",
6
+ "stage": "SOURCE checkout, not an npm consumer",
7
+ "gif": {
8
+ "path": "docs/assets/entwurf-demo.gif",
9
+ "sha256": "b686739b5c68fa458dd47c844711aea0f6ab5819eb3eb5d2850449922fa084c0",
10
+ "bytes": 1835518,
11
+ "seconds": 43.5,
12
+ "width": 1126,
13
+ "height": 764,
14
+ "frames": 340,
15
+ "captions": false
16
+ },
17
+ "cast": {
18
+ "sha256": "bbf8a409c58f8b48e88346d232a66ee41ac0c2acf8e605a3f568fd7d40857cf2",
19
+ "public": false
20
+ },
21
+ "runtime": {
22
+ "entwurf": "0.34.2",
23
+ "pi": "1.1.0",
24
+ "herdr": "0.9.3",
25
+ "gitHead": "9f4c9983fed1dd0ba619bc5a50f0f5abccefeef4",
26
+ "gitDirty": true,
27
+ "measuredPiModel": "openai-codex/gpt-6.1-sol",
28
+ "requestedClaudeAlias": "sonnet",
29
+ "measuredClaudeNativeModel": "claude-sonnet-5-5"
30
+ },
31
+ "auditedOriginalSourceSha256": {
32
+ "demo/take-herdr-pi-cc.ts": "f9cd30f1bb5b06236559a9f1283f0e030b300f6a17858d778c40d246327a6349",
33
+ "demo/lib/take-recorder.ts": "d47d824b90a7265c5ab6e7c4bf313b4f81b27cef836b4ddd15f971f3de386256",
34
+ "demo/render-take.ts": "f6e68157d7f8fb1f0e00a359efaa04aa30eeca1112ce2571661f39aea826b55d",
35
+ "demo/lib/herdr-take.ts": "57089d883323b2582aadd7f54f63cfce541f3a2c584bb6940313fbcdc940d83e"
36
+ },
37
+ "render": {
38
+ "aggArgs": [
39
+ "--idle-time-limit",
40
+ "2",
41
+ "--speed",
42
+ "1",
43
+ "--last-frame-duration",
44
+ "8",
45
+ "--font-size",
46
+ "14",
47
+ "--theme",
48
+ "monokai"
49
+ ],
50
+ "trimmedTailEvents": 2,
51
+ "trimmedWhat": "recorder teardown: herdr client exit (alt-screen exit + server shut down line)",
52
+ "renderedAt": "2026-10-11T11:58:32.828Z",
53
+ "aggVersion": null,
54
+ "ffprobeVersion": null,
55
+ "toolVersionEvidence": "not recorded for this original; current versions cannot fill historical fields"
56
+ },
57
+ "limits": [
58
+ "Original cast, manifests and native records remain private; this tree cannot replay that capture.",
59
+ "The tracked command is a new LIVE take recipe, not byte-identical reproduction.",
60
+ "Current maintainer source is amended; original source hashes are historical observations, not a complete import-closure seal.",
61
+ "Operator approved the rough original pixels; this is not privacy or public-readability certification.",
62
+ "No claim of 0.35.0 release, published-package acceptance or future Herdr plugin observation follows from this take."
63
+ ]
64
+ }
@@ -379,9 +379,9 @@ operator's repair named.
379
379
 
380
380
  | Seam | Claude | Cortex Code |
381
381
  |---|---|---|
382
- | Model ids | unprefixed `claude-sonnet-5`, `claude-opus-5`, `claude-fable-5-1` | `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`; prefix stripped before set-model |
382
+ | Model ids | unprefixed `claude-sonnet-5-5`, `claude-opus-5-5`, `claude-fable-5-1` | `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`; prefix stripped before set-model |
383
383
  | Launch | bundled `claude-agent-acp`; `CLAUDE_AGENT_ACP_COMMAND` override | `cortex acp serve`; optional connection; `CORTEX_ACP_COMMAND` override; never a launch-time `-m` |
384
- | Model authority | per-turn ACP set-model | per-turn ACP set-model; an unavailable curated id fails before prompt |
384
+ | Model authority | per-turn ACP set-model, its answer read back before the prompt | per-turn ACP set-model; an unavailable curated id fails before prompt; the answer is not read back |
385
385
  | Carrier | engraving in `_meta.systemPrompt` | no system-prompt carrier; engraving rides the first-user augment |
386
386
  | Overlay | `CLAUDE_CONFIG_DIR` whitelist, configured-empty hooks, native memory hidden | session-scoped isolated HOME + `SNOWFLAKE_HOME`, private `cortex/mcp.json`, measured-minimum auth passthrough |
387
387
  | MCP | explicit wire `mcpServers` | explicit declarations projected to private `mcp.json` because Cortex ignores the wire field |
@@ -390,6 +390,32 @@ operator's repair named.
390
390
 
391
391
  ### Claude
392
392
 
393
+ The unprefixed Claude curation uses exact Opus/Sonnet 5.5 ids; the ACP adapter does
394
+ not route the retired exact ids `claude-opus-5` or `claude-sonnet-5`, alias them to
395
+ a newer model, or rewrite existing session records. Cortex retains its separate
396
+ native 5 ids. Pi owns fuzzy CLI model selection before the adapter: with this
397
+ seven-row set, its 1.1.0 resolver matches the old bare 5 patterns to the Cortex rows
398
+ instead of refusing them (measured with the pure upstream resolver and synthetic rows,
399
+ not a vendor turn). So the launches Entwurf builds admit only an EXACT curated id
400
+ (`pi-extensions/lib/acp/model-ids.js`): `entwurf_fresh_call` with backend `pi` and an
401
+ `entwurf/<id>` model, and `entwurf_resume_call` of a session whose emitted provider is
402
+ `entwurf`, refuse anything else as `acp-model-not-curated` before a pane opens. A human
403
+ typing `pi --model …` keeps the upstream fuzzy behaviour, so use the full 5.5 ids there.
404
+
405
+ The adapter reads its set-model answer back before the prompt. The driver resolves the
406
+ requested id onto one of the rows it offers and answers with that row's `value` as the
407
+ model option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). Measured on
408
+ 2026-10-12 against 0.85.1 with no prompt sent: `claude-fable-5-1` answers its own row,
409
+ while `claude-sonnet-5-5` and `claude-opus-5-5` answer the alias rows `sonnet` and
410
+ `opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row resolves to is not on the
411
+ ACP wire, so `CLAUDE_SET_MODEL_ALIAS_ROWS` (`backend-adapter.ts`) declares, for each
412
+ alias-answered id, the one measured row it is accepted as, value and name together. A
413
+ missing model option, another row, or any other value/name pair refuses the turn before
414
+ the prompt, with text pi does not classify as transient. A curated id added later needs
415
+ its own measured answer before it ships. `check-acp-set-model-readback` holds the
416
+ contract offline; it does not certify the installed driver's runtime, and no 5.5 prompt
417
+ turn is claimed by this measurement.
418
+
393
419
  The bundled adapter resolves before any PATH fallback. Its overlay retains only the
394
420
  auth/runtime state required by the Claude Agent SDK and hides operator memory, hooks,
395
421
  agents, history, and local settings. Rich project/operator context rides the first-user
Binary file
@@ -913,7 +913,7 @@ operator-installed Claude runtime. Resolution order:
913
913
  2. `require.resolve(...)` against the bundled `@agentclientprotocol/claude-agent-acp`.
914
914
  3. `PATH:claude-agent-acp` fallback if package resolution fails.
915
915
 
916
- The curated Claude ids are `claude-opus-5`, `claude-sonnet-5`, and
916
+ The curated Claude ids are `claude-opus-5-5`, `claude-sonnet-5-5`, and
917
917
  `claude-fable-5-1`. Cortex exposes `cortex-auto`, `cortex-claude-opus-5`,
918
918
  `cortex-claude-sonnet-5`, and `cortex-openai-gpt-5.4`.
919
919
  `entwurfProvider.cortexConnection` or `ENTWURF_ACP_CORTEX_CONNECTION` selects
@@ -110,6 +110,44 @@ function resolveClaudeLaunch() {
110
110
  * ambient value cannot turn it off. Cortex declares nothing of the kind.
111
111
  */
112
112
  export const CLAUDE_FOREGROUND_ONLY_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" };
113
+ /**
114
+ * The set-model answer is read back before the prompt (#128). claude-agent-acp resolves the
115
+ * requested id onto one of the rows it offers and answers with that row's `value` as the model
116
+ * option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). `[측정 2026-10-12]` 0.85.1, no
117
+ * prompt: `claude-fable-5-1` answers its own row, while `claude-sonnet-5-5` and `claude-opus-5-5`
118
+ * answer the alias rows `sonnet` and `opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row
119
+ * resolves to (`ModelInfo.resolvedModel`) is not on the ACP wire, so an alias answer is accepted
120
+ * only as the exact measured row, value AND name, declared here for the requested id. No other
121
+ * spelling is matched.
122
+ */
123
+ export const CLAUDE_SET_MODEL_ALIAS_ROWS = Object.freeze({
124
+ "claude-sonnet-5-5": Object.freeze({ value: "sonnet", name: "Sonnet 5.5" }),
125
+ "claude-opus-5-5": Object.freeze({ value: "opus", name: "Opus 5.5" }),
126
+ });
127
+ /** Refuse unless the set-model answer names the requested model. A missing model option, another
128
+ * row, or a row that is not the measured alias row would let a different model answer under this id. */
129
+ export function confirmClaudeSetModelAnswer(response, nativeModelId) {
130
+ const configOptions = response?.configOptions;
131
+ const modelOption = Array.isArray(configOptions)
132
+ ? configOptions.find((option) => option?.id === "model")
133
+ : undefined;
134
+ const currentValue = modelOption?.currentValue;
135
+ if (typeof currentValue === "string" && currentValue === nativeModelId)
136
+ return;
137
+ const rows = Array.isArray(modelOption?.options)
138
+ ? modelOption.options.flatMap((row) => Array.isArray(row?.options) ? row.options : [row])
139
+ : [];
140
+ const selected = typeof currentValue === "string" ? rows.find((row) => row?.value === currentValue) : undefined;
141
+ const alias = CLAUDE_SET_MODEL_ALIAS_ROWS[nativeModelId];
142
+ if (alias !== undefined && currentValue === alias.value && selected?.name === alias.name)
143
+ return;
144
+ const answered = modelOption === undefined
145
+ ? "no model option"
146
+ : `model ${jsonText(currentValue)} named ${jsonText(selected?.name ?? null)}`;
147
+ throw new Error(`entwurf: claude-agent-acp answered set-model ${JSON.stringify(nativeModelId)} with ${answered}, which does not ` +
148
+ `name the requested model. Refusing before the prompt so a different model cannot answer under this id; ` +
149
+ `the installed driver and the curated id disagree, and repeating the turn cannot change that.`);
150
+ }
113
151
  export const claudeAdapter = {
114
152
  backend: "claude",
115
153
  // Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
@@ -161,7 +199,12 @@ export const claudeAdapter = {
161
199
  if (typeof setConfig !== "function") {
162
200
  throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
163
201
  }
164
- await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
202
+ const answer = await setConfig.call(connection, {
203
+ sessionId: acpSessionId,
204
+ configId: "model",
205
+ value: nativeModelId,
206
+ });
207
+ confirmClaudeSetModelAnswer(answer, nativeModelId);
165
208
  },
166
209
  configSignatureFields(_adapterSettings) {
167
210
  // Claude folds no extra backend-specific fields beyond backend + nativeModelId
@@ -302,7 +345,8 @@ export const cortexAdapter = {
302
345
  // serves fails loud BEFORE the prompt (`Unsupported model: …`). PR #40's
303
346
  // launch-time `-m` pin was retired for this: set-model is the single model
304
347
  // authority (resolveLaunch never passes `-m`), and "auto" is set explicitly
305
- // rather than treated as an unspoken default.
348
+ // rather than treated as an unspoken default. NO answer readback, unlike claude: nobody has
349
+ // measured what cortex's set_config_option response carries, so a check would be a guess.
306
350
  async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
307
351
  const setConfig = connection.setSessionConfigOption;
308
352
  if (typeof setConfig !== "function") {
@@ -0,0 +1,67 @@
1
+ // ACP plugin — the curated model IDs, and nothing else (#128).
2
+ //
3
+ // This leaf imports NOTHING. `models.ts` builds the registry rows from the pi-ai
4
+ // catalog; the launch seams (`fresh-call-dispatch.ts`, `resume-launch-identity.ts`)
5
+ // only need to know which IDs exist, and they ride the pi-free bridge closure, so
6
+ // they read the IDs here rather than load a vendor catalog to learn them.
7
+ //
8
+ // WHY `.js`: the same reason as `protocol.js`. Both the emit-capable root program
9
+ // (allowJs) and Node --experimental-strip-types resolve an explicit `.js` import
10
+ // literally to this file, so `models.ts` (root fence, `.js` specifiers) and the
11
+ // strip-types gates that load `models.ts` directly reach one SSOT. A `.ts` leaf
12
+ // would need a `.ts` specifier the root config cannot allow.
13
+ //
14
+ // WHY THE LAUNCH SEAMS ASK AT ALL. Pi's `--model` resolver falls back to a
15
+ // case-insensitive substring match among the provider's rows and prefers the
16
+ // lexically later id. Under one `entwurf` provider a retired `claude-sonnet-5`
17
+ // is a substring of `cortex-claude-sonnet-5`, so a launch Entwurf builds would
18
+ // silently open on the Snowflake Cortex rail instead of the Claude subscription.
19
+ // An exact match against this list is the only admission; nothing is aliased,
20
+ // substituted or rewritten.
21
+ /** The single ACP provider id under which every curated row is registered. */
22
+ export const PROVIDER_ID = "entwurf";
23
+ // The curated Claude ids. Adding one here is a commitment to verify it across
24
+ // both axes (protocol smoke + agent interview) — do not extend casually.
25
+ export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([
26
+ "claude-sonnet-5-5",
27
+ "claude-opus-5-5",
28
+ "claude-fable-5-1",
29
+ ]);
30
+ /** The anchor model whose absence is a hard registry regression, not a soft skip. */
31
+ export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5-5";
32
+ // Cortex ids carry the reserved `cortex-` prefix; the rest of each id is the
33
+ // Snowflake-native model name, which is why Cortex still says `claude-opus-5`.
34
+ export const CORTEX_MODEL_PREFIX = "cortex-";
35
+ export const SUPPORTED_CORTEX_MODEL_IDS = /** @type {const} */ ([
36
+ "cortex-auto",
37
+ "cortex-claude-opus-5",
38
+ "cortex-claude-sonnet-5",
39
+ "cortex-openai-gpt-5.4",
40
+ ]);
41
+ /** @type {ReadonlySet<string>} */
42
+ const CURATED_ACP_MODEL_IDS = new Set([...SUPPORTED_ANTHROPIC_MODEL_IDS, ...SUPPORTED_CORTEX_MODEL_IDS]);
43
+ /**
44
+ * Exact membership only — no trim, no case folding, no prefix.
45
+ * @param {string} model
46
+ * @returns {boolean}
47
+ */
48
+ export function isCuratedAcpModelId(model) {
49
+ return CURATED_ACP_MODEL_IDS.has(model);
50
+ }
51
+ /** The named refusal both launch seams answer with. */
52
+ export const ACP_MODEL_NOT_CURATED = /** @type {const} */ ("acp-model-not-curated");
53
+ export const ACP_MODEL_NOT_CURATED_HINT = `an ${PROVIDER_ID} model must be one exact curated ACP id (${[...CURATED_ACP_MODEL_IDS].join(", ")}); ` +
54
+ "Pi would otherwise pick another row by substring — a retired claude-sonnet-5 lands on cortex-claude-sonnet-5 — " +
55
+ "so nothing was opened, and nothing is aliased or rewritten";
56
+ /** Carries `reason` so callers match a FIELD, never the wording. */
57
+ export class AcpModelNotCuratedError extends Error {
58
+ /** @param {string} model */
59
+ constructor(model) {
60
+ super(`${ACP_MODEL_NOT_CURATED}: ${JSON.stringify(model)} is not an exact curated ${PROVIDER_ID} model id`);
61
+ this.name = "AcpModelNotCuratedError";
62
+ /** @readonly */
63
+ this.reason = ACP_MODEL_NOT_CURATED;
64
+ /** @readonly */
65
+ this.model = model;
66
+ }
67
+ }