@junghanacs/entwurf 0.23.0 → 0.23.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 (46) hide show
  1. package/AGENTS.md +3 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +134 -0
  4. package/VERIFY.md +41 -0
  5. package/docs/acp-backend-rail.md +96 -17
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  10. package/package.json +8 -7
  11. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  12. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  13. package/pi-extensions/lib/acp/backend.ts +4 -4
  14. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  15. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  16. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  17. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  18. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  19. package/run.sh +192 -196
  20. package/scripts/check-acp-usage-accounting.ts +9 -9
  21. package/scripts/check-gate-qualification.ts +1 -0
  22. package/scripts/check-pi-launch.ts +358 -0
  23. package/scripts/check-release-gate-outcomes.ts +7 -0
  24. package/scripts/inventory-verification-surface.ts +86 -10
  25. package/scripts/mutants/herdr-plugin-build.json +1 -1
  26. package/scripts/mutants/pi-launch.json +113 -0
  27. package/scripts/mutants/resume-args.json +6 -6
  28. package/scripts/mutants/v2-surface.json +2 -2
  29. package/scripts/mutants/v2-visible-resume.json +17 -17
  30. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  31. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  32. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  33. package/scripts/tsconfig.json +5 -0
  34. package/scripts/check-entwurf-facts.ts +0 -391
  35. package/scripts/check-entwurf-resume-args.ts +0 -122
  36. package/scripts/check-entwurf-v2-decider.ts +0 -657
  37. package/scripts/check-entwurf-v2-lock.ts +0 -371
  38. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  39. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  40. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  41. package/scripts/check-entwurf-v2-release.ts +0 -151
  42. package/scripts/check-entwurf-v2-runner.ts +0 -448
  43. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  44. package/scripts/check-entwurf-v2-send.ts +0 -458
  45. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  46. package/scripts/check-socket-discovery.ts +0 -401
package/AGENTS.md CHANGED
@@ -82,6 +82,8 @@ Keep at most ten open issues, including at most five implementation issues. Clas
82
82
 
83
83
  Two independent axes are required: deterministic/package gates and opt-in LIVE evidence. Exact commands, evidence levels, and release acceptance are owned by [VERIFY.md](./VERIFY.md); recorded host evidence by [BASELINE.md](./BASELINE.md). Run LIVE gates from scratch with ambient identity carriers stripped so artifacts and callbacks cannot bleed from the operator session.
84
84
 
85
+ A new contract's test goes beside the behavior it certifies — `pi-extensions/**/*.test.ts` or `plugins/herdr/**/*.test.mjs` — and `check-tests-beside-behavior` discovers it by glob, so landing one edits no filename list. `scripts/check-*` stays the home of LIVE, install, spawning and mixed gates; a new hand-built gate there needs a reason the vitest lane could not carry it.
86
+
85
87
  ### Scheduling
86
88
 
87
89
  ```text
@@ -97,7 +99,7 @@ implement → affected focused gates → independent review → one amendment bu
97
99
 
98
100
  ## Type and Working Boundaries
99
101
 
100
- - Each `.ts` file belongs to one declared typecheck fence; never hide a file with `exclude`. Root pi extensions use TypeBox through `@earendil-works/pi-ai`; MCP/scripts use explicit `.ts` imports required by Node strip-types. Installed surfaces route to compiled JS.
102
+ - Each `.ts` file belongs to one declared typecheck fence; `exclude` may move a file between fences but never out of all of them, so an exclude states the receiving `include` in the same change. Root pi extensions use TypeBox through `@earendil-works/pi-ai`; MCP/scripts use explicit `.ts` imports required by Node strip-types. Installed surfaces route to compiled JS. Behavior-adjacent `.test.ts` takes the fence `test/` takes — `scripts/tsconfig.json`, never the emit-capable root program — and is kept out of the tarball by a files-array negation whose tripwire is `check-pack`.
101
103
  - Make surgical, one-contract changes. Removal repairs source and its proof together; a green gate never proves retired behavior.
102
104
  - Before commit, sweep repository-wide for retired authority vocabulary and landed-plan future tense. Historical tombstones may remain; live docs, source, gates, and usage must agree.
103
105
  - Prefer capability-domain language over identity rank. Use tabs unless the project formatter requires otherwise. GLG decides commit, push, and release gates.
package/BASELINE.md CHANGED
@@ -34,6 +34,7 @@ not **UNSUPPORTED** — the deliberate never, which today is native Windows only
34
34
  | Maintainer NixOS installed package | **certified** for `0.12.8-repair.1` | 2026-07-25 registry install → doctor exit 0 (HISTORY) |
35
35
  | Secondary Ubuntu installed package | **certified** for `0.12.8-repair.1` | 2026-07-25 same artifact, isolated agent dir → doctor exit 0 (HISTORY) |
36
36
  | macOS Claude meta-bridge | NOT CERTIFIED — pending physical host | No physical-Mac doctor yet; a CI runner has no Claude login. Physical-host rental remains unscheduled; no cut has carried it. |
37
+ | 0.23.1 release acceptance (observer-amended) | accepted by GLG | `pnpm run check:full` P4 exit 0 (577 s); second aggregate `release-gate --cut` at `/tmp/entwurf-release-gate-0.23.1-retry.UIFctR` reached MUST 23/1/0 and BEHAVIOR 1/0, with only `smoke-codex-fresh-live` red because luna dropped `$`/`@` sigils in its relay (`release-gate.log:6156-6157`). The observer-amended standalone cell reran that exact leg with Codex `gpt-5.6-terra`, Pi `openai-codex/gpt-5.6-luna`: 66 assertions, exit 0; A app-server PID `3969303` `$227` ≠ fixture S `$231`; artifact `.probe-artifacts/codex-fresh-live-eZmf6p/`; pipe-pane stdout sha256 `56ee73e046e16fa95ef093fccf037670bf28b53a054bb5eec2a3b3b3ff08d93a`. Luna sigil handling is a post-cut issue, not a production-byte change. |
37
38
  | Native Codex Linux caller-DIRECTORY axis (current; #95 lane C) | **certified** | 2026-09-16, Codex 0.153.4: 65-assertion LIVE, exit 0, at `b3e07dc` with a clean worktree. Same host and topology as the lane B row below (app-server `737636` in A `$30/@41/%45`; initial Pi `20260916T154902-556eb5` `$2/@58`, Codex `20260916T155037-10ddaa` `$2/@59`, Codex-opened Pi `20260916T155102-9add8a` `$2/@61`, `seat-source=codex-title-anchor`). **hop 1 (cwd requested):** pane `#{pane_current_path}` = rollout `session_meta.cwd` = record cwd = requested scratch = `/tmp/entwurf-codex-fresh-live-db65N2`, against app-server cwd `/home/junghan/repos/gh/entwurf` read from `/proc/737636/cwd`. **hop 2 (cwd NOT requested):** the outbound Pi's pane, its own birth-written record and the Codex caller's record all read that same scratch, and its receipt names the caller-record rule. Artifact `.probe-artifacts/codex-fresh-live-w4yJBw/`; stdout `.probe-artifacts/lane-c-live-20260916T154900.log` sha256 `712050e7a8cf03ece98e7f34029ae98a92e9b2a7fd96aff87d0c1e6ba2ee9af2`. Cleanup reclaimed `@58`/`@59`/`@61` with no failure; records and transcripts preserved; app-server and `$30` untouched. 65 and 56 are different contracts — lane C added the cwd axis on top of lane B. Long floors (qualification body, frozen `check:full`) NOT run at this SHA; GLG decides host vs CI. |
38
39
  | Native Codex Linux caller-seat topology (current; #95 lane B) | **certified** | 2026-09-16, Codex 0.153.4: 56-assertion LIVE, exit 0. App-server `737636` alone in session A `$30/@41/%45`; initial Pi `20260916T144205-e876ff` `$2/@46`, omitted-placement Codex `20260916T144230-d050d7` `$2/@47`, Codex-opened Pi `$2/@48`. A ≠ S is the decisive half: the app-server env names A, so S can only have come from the caller's pane title — receipt `seat-source=codex-title-anchor`. Artifact `.probe-artifacts/codex-fresh-live-nYcGC1/run-stdout.log` sha256 `ed60c2bd…`; the 43-assertion hop-1 run that measured the D1 retirement is kept at `codex-fresh-live-4aFCDD/run-stdout.log` sha256 `8392a603…`. Cleanup reclaimed `@46`/`@47`/`@48` with no failure; app-server and `$30` untouched. |
39
40
  | Native Codex Linux home topology (PAST CONTRACT — #95 D1 retired the fixed home 2026-09-16) | **certified** for 0.21.0 as shipped | 2026-09-12, Codex 0.153.4: 57-assertion explicit-home LIVE. Initial Pi `$150/@397`; operator app-server `$158/@390/%390`; omitted-placement Codex `$158/@398`; Codex-opened Pi `$158/@399`; exact callbacks and addressed delivery both ways. Receipt and digest: `DELIVERY.md`. Deterministic closure of that axis: qualification 460/460 across 43 lanes with origin/snapshot purity green, then frozen full floor exit 0 in 506s — both pre-amendment. **Observer-amended acceptance, 2026-09-12:** standalone LIVE 48 assertions exit 0, source audit `initial-pi=3/3`/`codex=3/3` completed exact, initial Pi `$150/@431`, Codex `$158/@432`, Codex-opened Pi `$158/@433`, all three reclaimed by their own source receipts; artifact `.probe-artifacts/codex-fresh-live-fZccoK/`. The 48 and 57 counts are different contracts, not a regression. **Release acceptance at that same fingerprint:** `check:full` exit 0 502s, LIVE release gate `--cut` MUST 24/0/0 · BEHAVIOR 1/0/0 · `cut: OK`, `check-gate-qualification` 475/475 across 43 lanes with origin purity green, and the aggregate's Codex leg repeating the 48-assertion acceptance (artifact `codex-fresh-live-2oId4C`). Unrestricted attached-TUI placement remains unsupported and unclaimed. |
package/CHANGELOG.md CHANGED
@@ -4,6 +4,140 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.23.1 - 2026-09-19
8
+
9
+ ### Changed
10
+
11
+ - **The plugin runtime lock names `herdr-checkout` again, for the length of the 0.23.1 candidate
12
+ window.** This is a state, not a direction: an `npm` lock must name a version that is ALREADY
13
+ published and coherent with this checkout's `package.json`
14
+ (`certifyLockCoherence`, asserted on the committed lock by `check-herdr-runtime-bootstrap` 8a,
15
+ which sits inside `check:hermetic`), and the version bump necessarily lands before the publish.
16
+ So a cut cannot ride an npm lock naming the version it is about to create. `dd84ac0` is the
17
+ precedent and the exit: 0.23.0 was tagged on the checkout carrier and pinned to npm afterwards,
18
+ and 0.23.1 follows the same two steps. While this window is open, `smoke-herdr-raw-install-live`
19
+ reports the npm acquisition axis as a NAMED SKIP rather than a failure, and still asserts that
20
+ the runtime came from the source this ref locks.
21
+
22
+ **A pin and its inverse move the lock AND `check-herdr-plugin-build` together**, which is a fact
23
+ worth writing down because it has now been rediscovered twice. That gate reads the COMMITTED lock
24
+ in three places — the two-stage journey expects one acquisition and the locked `kind@version`, the
25
+ activation-authority cell seeds its ledger from the committed kind, and the progress cell pins the
26
+ long-step wording — so `dd84ac0` changed 90 lines of it alongside the eight lines of lock, and
27
+ this commit is the measured inverse of both halves (14/14 assertions, exit 0). No mutant is
28
+ disturbed: every claim in `scripts/mutants/herdr-plugin-build.json` names a production file
29
+ (`herdr-plugin.toml`, `lib/build.mjs`, `lib/build-progress.mjs`), none names the gate, and the
30
+ `HPB-PROGRESS-NAMED-SEQUENCE` needle re-aimed by `37b81e7` still occurs exactly once in
31
+ `lib/build.mjs`, which this commit does not touch. Teaching the gate to read either lock kind,
32
+ so neither direction needs it flipped again, is deferred to its own issue after the cut.
33
+
34
+ ### Added
35
+
36
+ - **A test written beside the behaviour it certifies is run by public `pnpm check`, and nobody
37
+ names the file** (#119 V2). `vitest.config.ts` include grows two globs —
38
+ `pi-extensions/**/*.test.ts` and `plugins/herdr/**/*.test.mjs` — and `check-tests-beside-behavior`
39
+ expands them every run and hands the resulting paths to `run_vitest`. Both halves were measured
40
+ the hard way. The first cut passed `pi-extensions plugins/herdr` as vitest POSITIONALS, which are
41
+ path substrings, so `test/pi-extensions-probe.test.ts` was selected too (measured: it ran);
42
+ expanding first keeps the selected set exactly the two globs while the shim still contains no
43
+ filename. And an empty expansion RETURNS rather than calling vitest with no arguments, because a
44
+ vitest run with no filter runs the whole include. The literal cross-check against
45
+ `vitest.config.ts` stays, because the shell cannot read vitest's resolved config and a deleted
46
+ glob would otherwise pass. Proven by three still photos in one public `pnpm check`: two failing
47
+ beside-behaviour tests turn it red (`selected 2`), and a third file planted under `test/` is not
48
+ selected.
49
+ - **`entwurf pi`** (#118 hop 2) — `exec pi --entwurf-control "$@"`, a dispatcher case with no new
50
+ bin. Two things it deliberately does NOT do, both measured on pi 0.85.1: there is no
51
+ precondition check, because an unregistered host already gets `Error: Unknown option:
52
+ --entwurf-control` and exit 1 from pi itself; and the flag is added, never deduplicated, because
53
+ passing it twice was measured byte-identical to passing it once. One recursion fence, not
54
+ copilot's two — the sentinel closes the real loop, and the case copilot's second fence covers is
55
+ measured here to be a fail-closed unknown verb rather than a loop. `check-pi-launch` reuses
56
+ `check-copilot-launch`'s fake-vendor posture and carries the `--entwurf-control` literal
57
+ cross-check, since the shell cannot import `ENTWURF_CONTROL_FLAG`. Nine mutants, each killed by
58
+ exactly its own claim; exit-status passthrough is asserted without one, because `set -euo
59
+ pipefail` couples it to `exec` and a mutant that kills two claims attributes to neither.
60
+ - **`smoke-herdr-raw-install-live`** (#118 hop 1) — the plugin's first user path, on a clean
61
+ `node:24` container with no host socket/config/cache mounted, no git `insteadOf`, and the public
62
+ remote spelled as a user would type it. It closes the one npm re-proof axis VERIFY.md demanded
63
+ and the production lock had left empty since `dd84ac0`. Receipts: **(a)** `--ref main` →
64
+ `37b81e725cde4d0a548f1c0faab4fcb5c62942b2`, **(b)** `--ref set/119-verify-herdr` →
65
+ `11ec0c38af36d5cd4a59614c3c8e07c5b5b0f9ae`, both `kind=npm @junghanacs/entwurf@0.23.0` with the
66
+ ledger widening `["pi"]` → `["pi","claude-code"]` and the pi wiring byte-identical across the
67
+ reinstall. Three facts it measured rather than assumed: herdr's offline-persist path carries
68
+ 0.9.1, so `plugin install --yes` completes with no server; `herdr integration install` refuses a
69
+ harness that has never run, because it creates `~/.pi/agent/extensions` only when the parent
70
+ exists and refuses Claude without `~/.claude`; and `--ref` takes a REMOTE REF, not an arbitrary
71
+ commit (`--ref <sha>` fails as `couldn't find remote ref`).
72
+
73
+ ### Changed
74
+
75
+ - **`plugins/herdr` 0.3.0 — the Install path, rewritten as the nine steps a raw box needs.** The
76
+ eighth and ninth are the ones nobody guesses and they come from the container measurement above:
77
+ each harness must be started once (`pi --help`, `claude mcp list`) before herdr will integrate
78
+ it. The release says what it does NOT give you, because the honest combination is this plugin
79
+ plus the runtime its lock still names: `entwurf pi` exists in the repository and is not in
80
+ `@junghanacs/entwurf@0.23.0`, so the user command stays `pi --entwurf-control` until the next
81
+ cut carries the launcher. **`runtime-lock.json` is untouched on purpose** — a pin can only move
82
+ after the version it names is published, so the 0.23.1 pin is a follow-up commit after that
83
+ release, the same two-step `dd84ac0` took for 0.23.0.
84
+ - **Thirteen v2-spine gates moved out of `scripts/` and beside the modules they certify** (#119
85
+ V3), assertion for assertion, with the inventory's denominator fixed FIRST so the move reads as
86
+ a move. `inventory-verification-surface.ts` now counts both vitest locations; without that, the
87
+ migration would have subtracted 2,799 lines from the one number that exists to make migration
88
+ honest. What actually left is 133 lines of plumbing — per-file `ok()` scaffolding, `main()`
89
+ wrappers, summary trailers — and the combined file count is unchanged, seven out and seven in
90
+ per slice.
91
+ - The six mutant-carrying lanes needed four rules the mutant-free seven did not, each learned by
92
+ getting it wrong: a QK claim must BE an `it` title (attribution reads the failed test title);
93
+ the token must appear exactly once per file (title keeps the token, assertion keeps the
94
+ sentence); the `run.sh` case survives as a mutant EXECUTION COORDINATE and keeps its
95
+ `check:contracts` entry (branch CI caught the removal — a gate a mutant names must run inside
96
+ `check:full`); and attribution is containment, not exclusivity (vitest runs every test, so one
97
+ mutation surfaces 1–15 failed titles). 25/25 mutants verified killed and attributed by hand;
98
+ 733 mutants across 58 lanes unchanged, `signatureSource` moved, gate argv did not.
99
+ - `entwurf-v2-visible-resume.test.ts` grew 445 → 908 lines and the growth is the design:
100
+ seventeen claims that shared four drives of the subject now each replay their own, because
101
+ nearly every one is an "X happened BEFORE Y" reading a call-order trace, and a trace polluted
102
+ by a neighbouring test answers wrongly while staying green.
103
+ - Three gates stay under `scripts/` with a reason: `check-entwurf-v2-contract` and
104
+ `check-entwurf-v2-production` are cross-lane (`mux-boundary.json` and `codex-native.json` call
105
+ them as gate argv), and `check-entwurf-v2-surface` generates its seven QK labels inside shared
106
+ helpers called twice from `await` blocks, so the established method would either lose
107
+ attribution or require an async `describe`.
108
+
109
+
110
+ - **The bundled Claude ACP adapter is `@agentclientprotocol/claude-agent-acp` 0.79.0** (from
111
+ 0.76.0, crossing 0.77.0/0.78.0/0.79.0; `@anthropic-ai/claude-agent-sdk` 0.3.257 → 0.3.274,
112
+ ACP wire SDK 1.4.0 and `@anthropic-ai/sdk` 0.100.1 unchanged). Three upstream releases, three
113
+ changes with a reachable shape, and **no entwurf code change** — each "inert" is a fresh
114
+ measurement, not the previous bump's argument reused:
115
+ - 0.77.0's BREAKING removal of `claudeCode.options.agent` misses us twice: our
116
+ `buildClaudeSessionMeta` never set that key, and we spawn the adapter's binary rather than
117
+ importing the removed agent-picker exports (repo-wide grep: 0 hits).
118
+ - 0.77.0's `allowDangerouslySkipPermissions` host opt-out is a NEW lever we do not pull. We set
119
+ the option nowhere, `ALLOW_BYPASS` is byte-identical across both versions, and the overlay's
120
+ `permissions.defaultMode: "bypassPermissions"` resolves the same under each, so a sibling's
121
+ effective permission mode is unchanged.
122
+ - 0.78.0's `compaction_update` / `compaction_summary_chunk` are the first new `sessionUpdate`
123
+ kinds since the pin moved, and they are gated off by our `clientCapabilities: {}` — with the
124
+ mapper's forward-compatible `default` arm as an independent second reason. The `stopReason`
125
+ axis, where "unknown is an error" actually applies, is unchanged.
126
+ - 0.79.0's shell-command permission prompts reorder the option array reject-first under the new
127
+ `defaultToNo` hint. Our approve-all policy selects by option KIND, not position, and every
128
+ upstream option builder emits an allow option, so the order-sensitive fallback is unreachable.
129
+ Re-measured and unchanged: the #96 `mcpServerStatus` evidence (2 call sites, now at
130
+ `v0.79.0 src/acp-agent.ts:1773`/`:1866`, surrounding 200-line window byte-identical) and all four
131
+ model-forcing/accounting wire calls (`setSessionConfigOption`, `sessionUsage`, `turnQuotaMeta`,
132
+ `resolveModelPreference` — byte-identical). Full per-change classification with evidence state:
133
+ [ROADMAP.md](./ROADMAP.md) 2026-09-18 bump entry and
134
+ [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) capability posture.
135
+
136
+ - **The Herdr plugin is 0.2.0 and pins its runtime to npm `@junghanacs/entwurf@0.23.0`.** Its
137
+ committed lock carries the published sha512
138
+ `sha512-ZR2VCui7JjK3w56rQSDs3AuAJMMuiXCNWH7HB52SQ3E/7p0oPhcxD+fb6Gdzi0VcBnheqxPzvJHMPQQcdYtNiw==`;
139
+ the 0.1.0 `herdr-checkout` source remains the candidate-verification path.
140
+
7
141
  ## 0.23.0 - 2026-09-18
8
142
 
9
143
  ### Added
package/VERIFY.md CHANGED
@@ -44,6 +44,45 @@ When you write a new entry, mark its rung. "L1 only" is honest; "L2 reached" is
44
44
 
45
45
  Verification here is not a benchmark. In production we exchange short turns and stop immediately to isolate a cause before resuming when something looks off. This document records **verification intent (what we look at) and pass criteria (how to judge)**; the execution shape is the agent's choice as long as the criteria are met.
46
46
 
47
+ ### Where a verification lives — two locations, and what decides
48
+
49
+ A new contract's test goes BESIDE the behaviour it certifies: `pi-extensions/**/*.test.ts` or
50
+ `plugins/herdr/**/*.test.mjs`. Nothing names those files. `check-tests-beside-behavior` expands
51
+ the two globs every run and hands the resulting paths to `run_vitest`, so landing a test edits no
52
+ list — and a failure is attributed from the JSON test TITLE (`run.sh:104-113`), never from the
53
+ code frame, which quotes an adjacent passing test's own title.
54
+
55
+ `scripts/check-*` is not a legacy pile; it is the home of what a vitest lane cannot hold. What
56
+ stays there, and why: LIVE lanes that need real accounts, models, tmux or a Docker daemon;
57
+ install and pack gates that consume a tarball; gates that spawn a subprocess or stand a server up;
58
+ gates that read product source as text; and the mutant EXECUTION COORDINATES described below. A
59
+ new hand-built gate needs a reason one of those five applies.
60
+
61
+ **Four rules bind a migrated gate that carries mutants**, and each of them was measured rather
62
+ than assumed (#119 V3 slice 2):
63
+
64
+ 1. **Every QK claim is its own `it` title.** Attribution reads the failed test title, so a QK
65
+ that lives only in an assertion message leaves its mutant unattributable.
66
+ 2. **The token appears exactly once in the file.** Putting it in the title AND the assertion
67
+ label is refused by the manifest set-integrity contract — the title keeps the token, the
68
+ assertion keeps the sentence.
69
+ 3. **The `run.sh` case survives as a mutant execution coordinate, and keeps its `check:contracts`
70
+ entry.** The case is not a discovery path: the door already finds the test. It exists because
71
+ the qualification runner re-invokes a gate's argv once per mutant, and a gate argv calling
72
+ vitest directly would skip the reporter attribution depends on. Its file filter is narrow,
73
+ because a mutant pointed at the glob shim would re-run every beside-behaviour test once per
74
+ mutant. It stays in `check:contracts` because `check-release-gate-outcomes` requires a gate a
75
+ mutant names to run inside `check:full` — reachable only through qualification, its going red
76
+ on a clean tree would be noticed by nothing but the mutant body.
77
+ 4. **Attribution is containment, not exclusivity.** `assert.ok` aborted at the first broken claim,
78
+ so exactly one label was ever reported; vitest runs every test, so one mutation surfaces
79
+ several failed titles (measured: 1–15). A kill is attributed when the claim's token is AMONG
80
+ them.
81
+
82
+ Migration moves lines BETWEEN the two axes `inventory-verification-surface.ts` counts, and only a
83
+ drop in the COMBINED number is subtraction. That is why the inventory reads both locations: a gate
84
+ that moved out of `scripts/` and into a file beside its subject would otherwise read as deleted.
85
+
47
86
  ### The canonical floor — two entry points
48
87
 
49
88
  - **Deterministic floor — two tiers (#70):** `pnpm check` is the everyday CORE — toolchain (lint + typecheck), the vitest lanes, and the pure-unit / behavioral-contract / source-topology gates plus the cheap static coherence checks. It prints its own total wall time; acceptance is ≤60s on the reference host `oracle` (an operator measurement, never a hard wall-clock gate on arbitrary hosts). `pnpm run check:full` is the FULL deterministic floor — the core plus the hermetic-integration and package/install tiers — and is what the frozen-candidate protocol, push CI, release-gate, and `prepublishOnly` run. Exact membership is the named `check:*` group scripts in `package.json` (the executable SSOT — this document records meaning and principles, not the command list); a gate changes tier by semantic-class decision, never by getting faster or slower. The FULL tier carries the qualification HEAD (`check-gate-manifests`, through `check:hermetic`); the everyday core does not, and neither tier carries the separately scheduled mutant-executing body (`check-gate-qualification`).
@@ -56,6 +95,8 @@ Verification here is not a benchmark. In production we exchange short turns and
56
95
 
57
96
  > **The herdr rail is an on-demand axis until it has run.** `smoke-herdr-fresh-call-live` opens a real caller inside a real herdr pane, has it invoke the PUBLIC `entwurf_fresh_call`, and reads the child's nonce callback — the one axis no deterministic gate reaches, since the rail is selected from the caller's own process environment. **The release aggregate does not yet require the herdr rail**: herdr is optional (a cut on a host without it must not block), and first evidence precedes promotion — a rail whose first acceptance run is days old has not earned a place in the gate that blocks every cut. Run it directly with `LIVE=1 ./run.sh smoke-herdr-fresh-call-live` whenever a cut changes herdr rail code, and promote it to a MUST only after the axis has a run history to point at. Without `LIVE=1`, or without herdr/pi/claude on PATH, it reports protocol SKIP — never a pass.
58
97
 
98
+ > **The plugin's FIRST USER PATH is its own on-demand axis, and until it runs the npm acquisition has no receipt (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. The production lock has said `npm` since `dd84ac0`, and the re-proof this same document demands above (§ source switch) still has an empty acquisition row. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server — whether 0.9.1 carries that fallback is unmeasured, and a "no" is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact seven-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
99
+
59
100
  > **The Herdr plugin's install-time build has two axes, and one of them needs the network (#116 M3-b3).** `check-herdr-plugin-build` is deterministic and rides `check:hermetic`: the listing reaches the runner through its own spawn seam (there is no fake `herdr` binary in this repo), and the one cell that drives the REAL binary reports a named SKIP on a host without it — `ENTWURF_REQUIRE_HERDR=1` makes that absence red. What it cannot have is the real acquisition: `npm pack git+https://github.com/junghan0611/entwurf.git#<full sha>` builds the bridge through `prepare`, which installs devDependencies from the registry. So `LIVE=1 ./run.sh smoke-herdr-plugin-build-live` owns that axis, and its cells are **two kinds of evidence, labelled as such**: three are the real Herdr journey — a real `herdr plugin install` driving the real `[[build]]`, with the UNCHANGED product remote redirected to a local bare clone by git `insteadOf` (the one substitution, which is what makes an unpushed candidate testable) — covering remote-commit available, the `{pi}` → `{pi, claude-code}` reinstall with OpenCode as a zero-write negative control and exactly ONE counted vendor MCP owner entry, and the post-build Herdr-commit gap (where the ledger has already rebound to the gap commit, so the honest assertion is that transition and not unchanged bytes). The fourth cell — remote-commit **unavailable** — calls the shipped runtime leaf directly with the fixed product argv and is therefore **acquisition-leaf evidence, not a journey**; routing it through Herdr would add a second explanation for one red. Its oracle is the honest one too: active runtime, ledger and harness bytes preserved, while the journal is left as the certified `installing` retry authority the transaction deliberately writes before acquiring, and a normal install afterwards is green. Its Claude cell is where the vendor question the deterministic gates refuse to claim gets answered — whether Claude Code accepts an ABSOLUTE executable as an MCP command — and it reports a named skip on a host without `claude`, never a pass. Like the herdr fresh-call rail, this smoke is **on-demand and not an aggregate MUST**. The release aggregate does not require the plugin build smoke: herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion.
60
101
 
61
102
  > **Switching the plugin's production acquisition source is a re-proof, not a configuration change (#116 M3-b3).** `runtime-lock.json` carries a closed `source` discriminant: `npm` (exact `name@version` plus the published sha512, compared against the tarball's own bytes) is the production authority, and `herdr-checkout` (the exact commit Herdr checked out, from the fixed remote) is a VERIFICATION-ONLY carrier that exists so a candidate needs no npm release. Evidence does not transfer between them. Before a release that moves the production source — to npm, or to a GitHub Release tarball plus its sha512 — the exact acquisition and integrity comparison, the installed runtime (name@version, compiled entry, three executable bins, a real `check-bridge`), the swap and torn-swap recovery, activation and deactivation, and the package-consumer proof must all be re-run against THAT source. A candidate proof green on the checkout carrier says nothing about a registry artifact, and the reverse is equally true. **Records from before this contract are refused, not migrated:** a v1 runtime journal or activation ledger fails certification by name, and the way forward on such a host is an explicit `entwurf herdr-plugin-deactivate` (or a hand-cleared runtime/ledger state) before the next install — there is no reader that accepts both schemas, because the older shape cannot say WHICH artifact it was.
@@ -92,11 +92,11 @@ undifferentiated "supported" column is what let a Claude PASS read as if it also
92
92
 
93
93
  | Surface | Declaration | Class | What a green actually says |
94
94
  |---|---|---|---|
95
- | Entwurf package | `0.20.0` | shipped baseline | the package contract these rows belong to |
95
+ | Entwurf package | `0.23.0` | shipped baseline | the package contract these rows belong to |
96
96
  | pi runtime | devDep exact `0.85.1`, peer `>=0.85.1 <0.86` | **exact** oracle + **closed range** | built and certified against 0.85.1; hosts inside the range are accepted, and the ceiling moves only on measurement |
97
97
  | ACP wire SDK | `@agentclientprotocol/sdk 1.4.0` | **exact** | the shared wire oracle both adapters speak |
98
- | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.76.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
- | Claude Agent SDK | `0.3.257` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
98
+ | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.79.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
+ | Claude Agent SDK | `0.3.274` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
100
100
  | Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
101
101
  | Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
102
102
  | Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
@@ -168,6 +168,83 @@ different reasons, and collapsing them would hide a real risk**:
168
168
  `resolvedProvider` and up onto the unconditional `session/new` path. entwurf passes `settings`
169
169
  as an OBJECT (`tool-surface.ts:153`), so the branch is never taken. That makes "entwurf never
170
170
  hands `settings` as a path" a contract rather than an accident; it is stated at that call site.
171
+ - **0.77.0's BREAKING change does not reach us, twice over.** `claudeCode.options.agent` is no
172
+ longer forwarded — the adapter now shallow-copies the options object and `delete`s `agent` on
173
+ the unconditional `session/new` path (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5934-5941 직독]`).
174
+ entwurf's `buildClaudeSessionMeta` never sets that key (`tool-surface.ts:145-199` 직독: the
175
+ literal carries `model`/`tools`/`settingSources`/`settings` plus optional `plugins`/
176
+ `disallowedTools`/`extraArgs`, and nothing else), and the removed agent-picker exports
177
+ (`DEFAULT_AGENT_ID`, `AGENT_CONFIG_ID`, `BUILTIN_AGENT_NAMES`, `discoverCustomAgents`) are
178
+ imported nowhere: `[측정 2026-09-18]` `git grep -c` over `pi-extensions/`, `test/`, `scripts/`,
179
+ `mcp/` is **0**. We never imported the adapter as a library at all — we spawn its binary.
180
+ - **0.77.0's `allowDangerouslySkipPermissions` opt-out is a new lever we deliberately do not
181
+ pull, and our effective permission mode is unchanged.** 0.76.0 sent
182
+ `allowDangerouslySkipPermissions: ALLOW_BYPASS` unconditionally and computed
183
+ `initialPermissionMode = creationOpts.permissionMode ?? resolvePermissionMode(settings…)`.
184
+ 0.79.0 computes `allowBypass = ALLOW_BYPASS && sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false`
185
+ and routes the whole thing through `resolvePermissionMode(…, logger, allowBypass)`
186
+ (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5932-5933 직독]`). entwurf sets that option
187
+ nowhere, so `undefined !== false` holds and `allowBypass === ALLOW_BYPASS`;
188
+ `ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX` is **byte-identical** between the two
189
+ versions (`dist/permissions/modes.js` 직독). Our overlay pins
190
+ `permissions.defaultMode: "bypassPermissions"` (`overlay.ts:122`), which resolves the same
191
+ under both. The lever now EXISTS for a host that wants a non-bypass sibling; declaring it is a
192
+ separate axis, not a one-line flip, and nothing in this bump takes it.
193
+ - **0.77.0's system-reminder strip never touches our first-user-message augment.**
194
+ `INJECTED_CONTEXT_MARKERS = ["system-reminder"]` joins the local-command markers in
195
+ `stripMarkerTags`, and `stripLocalCommandMetadata` has exactly two call sites
196
+ (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js grep -n]`): `:4129`, gated on the message content
197
+ containing `<local-command-stdout>`, and `:5061`, in the `session/load` transcript replay. Both
198
+ run agent→client on text coming BACK from the transcript; our augment rides client→agent on the
199
+ first `session/prompt` and is never re-emitted, because entwurf calls `session/load` nowhere
200
+ (`[측정 2026-09-18]` `git grep loadSession` in `pi-extensions/lib/acp/` hits only the
201
+ `session-store.ts` capability TYPE, never a wire call). The augment also emits no
202
+ `<system-reminder>` tag of its own (`augment.ts` 직독), so no prose of ours is strippable.
203
+ - **0.78.0's compaction update is a NEW `sessionUpdate` kind and is inert for us, twice over.**
204
+ `compaction_update` and `compaction_summary_chunk` are new in `dist/context-compaction.js`
205
+ (`[측정 2026-09-18]` `sessionUpdate: "…"` literal sweep across both dists: 0.76.0 has 15 distinct kinds,
206
+ 0.79.0 has 17, and the two new ones are exactly these). Gate one: the lifecycle's
207
+ `presentation` is `clientSupportsCompactionUpdates(this.clientCapabilities) ? "compaction_update"
208
+ : "tool_call"` (`dist/acp-agent.js:1875-1879`), and that predicate reads
209
+ `capabilities?.session?.compaction` — entwurf sends `clientCapabilities: {}` (`backend.ts:1758`),
210
+ so we keep the 0.75.0 `tool_call` presentation §11-8 already measured. The replay path
211
+ (`:5082`) is guarded by the same predicate at `:4915`. Gate two, independent: our mapper's
212
+ update switch has a `default: break` — "unknown update kinds are ignored (forward-compatible)"
213
+ (`event-mapper.ts:311-313`, `:377-378`). The "map every terminal reason, unknown is an error" rule
214
+ is about ACP **stopReason**, a different axis; `stopReason` literals are unchanged across the
215
+ two dists (`[측정 2026-09-18]` sweep: `"cancelled"` only, both versions). No code needed.
216
+ - **0.78.0's checkpoint file-change report and AIR diff counts are both behind the AIR gate.**
217
+ `supportsAgentFileChangeReport` is `clientSupportsAirCapability(capabilities, "agentFileChangeReport")`
218
+ (`dist/file-change-audit.js:27-29` 직독), and `air-extension.js`'s only delta is one added
219
+ constant `AIR_DIFF_STATS_KEY` (full-file `diff`, one line). Same `clientCapabilities: {}`
220
+ argument as the 0.76.0 `recommendedValue` entry — re-measured, not inherited.
221
+ - **0.79.0's shell-command permission prompts reach our permission handler's INPUT and change no
222
+ decision.** The change reorders options: when the CLI hints `defaultToNo` (new in 0.79.0 —
223
+ `[측정 2026-09-18]` `grep -rn defaultToNo` over the 0.76.0 dist is **0 hits**), the option array
224
+ now sorts reject-first (`dist/permissions/options.js:5-10`). entwurf's approve-all policy
225
+ selects `options.find((o) => o.kind === "allow_once" || o.kind === "allow_always")` and only
226
+ falls back to `options[0]` when that find fails (`backend.ts:824-831`) — a find by KIND, so
227
+ order cannot flip it. The fallback is unreachable besides: every builder in
228
+ `dist/permissions/options/` routes through `withOptionalUpdate`/`withGeneratedUpdate`, both of
229
+ which lead with `allowOnce()`, and the hand-rolled `tools.js` sets each carry an `allow_once` or
230
+ `allow_always` (직독 of `shared.js` + `tools.js`). The title change (Bash/PowerShell titles now
231
+ bypass `humanText` compaction) lands on a field we never read.
232
+ - **0.77.0–0.79.0's remaining fixes are unreachable under our capability posture.** The
233
+ AskUserQuestion multi-select/custom-text fixes (#1031, #1131) require form elicitation:
234
+ the adapter computes `disallowedTools = elicitationSupport.form ? [] : ["AskUserQuestion"]`
235
+ from `clientCapabilities.elicitation.form`, which our `{}` leaves false, so AskUserQuestion is
236
+ disabled on every session we open. The TaskList regex fix (#1006) is internal parsing. #1128's
237
+ tool names land on `presentation.toolCall._meta.claudeCode` on the permission-request path
238
+ (`dist/acp-agent.js:5317-5325`), which our handler ignores; our `titleForTool` already read
239
+ `_meta.claudeCode.toolName` as a fallback behind `update.title`, so it is additive at worst.
240
+ - **Our model-forcing and accounting wire calls are byte-identical across 0.76.0 → 0.79.0.**
241
+ `[측정 2026-09-18, brace-matched extraction from both dists, md5]`: `setSessionConfigOption`
242
+ (4,858 B, identical), `sessionUsage` (373 B, identical), `turnQuotaMeta` and `quotaTokenCount`
243
+ (identical md5), and `resolveModelPreference` lives in `dist/session-model.js`, whose whole file
244
+ is byte-identical (`md5 cfd031d0…` both versions). The `settings`-as-STRING-PATH branch our
245
+ call-site contract names is still on the unconditional `session/new` path
246
+ (`dist/acp-agent.js:6010-6012`).
247
+
171
248
  - **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
172
249
  synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
173
250
  `Compact conversation`, and `_meta.contextCompaction` schema v1 — where it used to arrive as
@@ -292,23 +369,25 @@ caller-session `_meta`, and cross-machine certification.
292
369
 
293
370
  A backend can return `newSession` before its declared MCP server is callable. This was
294
371
  observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
295
- path. Neither `claude-agent-acp` 0.76.0 nor the Cortex landing adds a client-side
372
+ path. Neither `claude-agent-acp` 0.79.0 nor the Cortex landing adds a client-side
296
373
  readiness fence over a session's declared MCP servers, and entwurf's common loop
297
374
  calls `mcpServerStatus()` nowhere.
298
- (Re-measured at the 0.75.1 → 0.76.0 bump, not inherited — the previous bump's argument is
299
- not reused, the way the 0.73.0 → 0.75.1 entry did not reuse 0.70.0 → 0.73.0's.
300
- `mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.75.1 and 2 at v0.76.0**
301
- `[측정 2026-09-10, upstream v0.76.0/src/acp-agent.ts read directly, grep -n]`; they first
375
+ (Re-measured at the 0.76.0 → 0.79.0 bump, not inherited — the previous bump's argument is
376
+ not reused, the way the 0.75.1 → 0.76.0 entry did not reuse 0.73.0 → 0.75.1's.
377
+ `mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.76.0 and 2 at v0.79.0**
378
+ `[측정 2026-09-18, upstream v0.79.0/src/acp-agent.ts read directly, grep -n]`; they first
302
379
  appeared in 0.71.0 via `0cbbaf3` (MCP OAuth, LLM-25012), so the ADAPTER calls it where it
303
- once did not. Both were re-read at `v0.76.0 src/acp-agent.ts:1762` and `:1855`
304
- (v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the first sits inside
305
- `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every status that is not
306
- `needs-auth`; the second polls a SINGLE named server to `connected` under an OAuth deadline.
307
- Neither waits on every declared server before `newSession` returns. That is an auth
308
- handshake, not a readiness fence, so the boundary below is unchanged. The surrounding 200
309
- lines are byte-identical and the region moved +26; the whole 0.76.0 delta is one refactor
310
- plus one opt-in AIR extension that `clientCapabilities: {}` never enables, so the other
311
- reachable-surface findings stand as re-measured at the previous bump.
380
+ once did not. Both were re-read at `v0.79.0 src/acp-agent.ts:1773` and `:1866`
381
+ (v0.76.0: `:1762` / `:1855`; v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the
382
+ first sits inside `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every
383
+ status that is not `needs-auth`; the second polls a SINGLE named server to `connected` under
384
+ an OAuth deadline. Neither waits on every declared server before `newSession` returns. That
385
+ is an auth handshake, not a readiness fence, so the boundary below is unchanged. The
386
+ surrounding 200-line window is byte-identical (`diff v0.76.0:1662-1862 v0.79.0:1673-1873`,
387
+ empty) and the region moved +11 while the file shrank 10,405 10,329 lines. The 0.77.0
388
+ agent-picker removal, the 0.78.0 compaction/checkpoint/AIR work and the 0.79.0 permission
389
+ presentation touch no part of this path, so the other reachable-surface findings stand as
390
+ re-measured in the capability-posture section above.
312
391
  This bump changes no readiness behavior and closes no part of #72.)
313
392
 
314
393
  ### 11-7-a/b. Instrument and first measurement
@@ -123,8 +123,8 @@ export const claudeAdapter = {
123
123
  },
124
124
  // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
125
125
  // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
126
- // and a context-occupancy scalar (`used`; both read at 0.76.0
127
- // `dist/acp-agent.js:3471-3486`). Both are measured, so backend.ts seals them.
126
+ // and a context-occupancy scalar (`used`; both read at 0.79.0
127
+ // `dist/acp-agent.js:3504-3519`). Both are measured, so backend.ts seals them.
128
128
  // The token partition is NOT declared here: ACP's only token carrier is a
129
129
  // per-turn round-trip aggregate, which is not what pi's four fields mean.
130
130
  sealsTurnAccounting: true,
@@ -124,7 +124,8 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
124
124
  // claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
125
125
  // JSON.parse for that form moved out of `resolvedProvider` up onto the
126
126
  // unconditional `session/new` path — a shape that WOULD reach us, and does not,
127
- // only because we never hand it a path. Keep it inline: a path would put file
127
+ // only because we never hand it a path. Still on that path at 0.79.0
128
+ // (`dist/acp-agent.js:6010-6012`). Keep it inline: a path would put file
128
129
  // IO, and its failure modes, inside every session creation.
129
130
  settings: {
130
131
  permissions: {
@@ -102,7 +102,7 @@ export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"];
102
102
  // unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
103
103
  // THIS set is a deliberate future decision, gated by a real compatible
104
104
  // control-socket predicate — never by silently mapping sessions to dead/indeterminate
105
- // (R1 핵심). check-entwurf-facts pins this == ["pi"] and asserts the native-push
105
+ // (R1 핵심). pi-extensions/lib/entwurf-facts.test.ts pins this == ["pi"] and asserts the native-push
106
106
  // domain is disjoint from it.
107
107
  export const LIVENESS_DOMAIN_BACKENDS = ["pi"];
108
108
  export function isLivenessSupported(backend) {
@@ -113,7 +113,7 @@ export function isLivenessSupported(backend) {
113
113
  // live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
114
114
  // from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
115
115
  // the socket FACT axis (entwurf_peers) yet independently measured on the native-push
116
- // axis. The two are separate rails on purpose — check-entwurf-facts pins both sets
116
+ // axis. The two are separate rails on purpose — pi-extensions/lib/entwurf-facts.test.ts pins both sets
117
117
  // and asserts their intersection is ∅ (a backend can never be in both domains).
118
118
  export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"];
119
119
  export function nativePushSupported(backend) {
@@ -79,7 +79,7 @@ export const ENTWURF_V2_LOCK_DIR = path.join(os.homedir(), ".pi", "entwurf-v2-lo
79
79
  export const LOCK_SUFFIX = ".lock";
80
80
  export const LOCK_OWNER = "entwurf_v2";
81
81
  /** The reject reason a lock conflict maps to. Kept as a literal here (the lock
82
- * primitive stays decoupled from the full contract); `check-entwurf-v2-lock`
82
+ * primitive stays decoupled from the full contract); `pi-extensions/lib/entwurf-v2-lock.test.ts`
83
83
  * cross-checks it against the contract's ENTWURF_V2_REJECT_REASONS so the two
84
84
  * cannot drift. */
85
85
  export const LOCK_CONFLICT_REASON = "target-locked";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -73,7 +73,8 @@
73
73
  "CONTRIBUTING.md",
74
74
  "CHANGELOG.md",
75
75
  "!**/__pycache__",
76
- "!**/*.pyc"
76
+ "!**/*.pyc",
77
+ "!pi-extensions/**/*.test.ts"
77
78
  ],
78
79
  "engines": {
79
80
  "node": ">=24.0.0"
@@ -82,7 +83,7 @@
82
83
  "claudeCodeFloor": ">=2.1.217"
83
84
  },
84
85
  "dependencies": {
85
- "@agentclientprotocol/claude-agent-acp": "0.76.0",
86
+ "@agentclientprotocol/claude-agent-acp": "0.79.0",
86
87
  "@agentclientprotocol/sdk": "1.4.0",
87
88
  "@anthropic-ai/sdk": "0.100.1",
88
89
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -127,7 +128,6 @@
127
128
  "check-meta-capability-source": "./run.sh check-meta-capability-source",
128
129
  "check-socket-probe": "./run.sh check-socket-probe",
129
130
  "check-project-trust-handler": "./run.sh check-project-trust-handler",
130
- "check-entwurf-v2-lock": "./run.sh check-entwurf-v2-lock",
131
131
  "check-model-lock": "./run.sh check-model-lock",
132
132
  "check-dep-versions": "./run.sh check-dep-versions",
133
133
  "check-node-floor-coherence": "./run.sh check-node-floor-coherence",
@@ -135,6 +135,7 @@
135
135
  "check-copilot-birth-hook": "./run.sh check-copilot-birth-hook",
136
136
  "check-copilot-statusline": "./run.sh check-copilot-statusline",
137
137
  "check-copilot-launch": "./run.sh check-copilot-launch",
138
+ "check-pi-launch": "./run.sh check-pi-launch",
138
139
  "smoke-copilot-statusline-state": "./run.sh smoke-copilot-statusline-state",
139
140
  "smoke-copilot-mcp-state": "./run.sh smoke-copilot-mcp-state",
140
141
  "smoke-omp-bridge-state": "./run.sh smoke-omp-bridge-state",
@@ -173,9 +174,9 @@
173
174
  "check:full": "bash scripts/check-elapsed.sh check:full check:core check:hermetic check:package",
174
175
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
175
176
  "check:toolchain": "pnpm lint && pnpm typecheck",
176
- "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
177
- "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
178
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
177
+ "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-tests-beside-behavior",
178
+ "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
179
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
179
180
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
180
181
  },
181
182
  "pi": {
@@ -46,8 +46,8 @@ import type { AcpTextBlock } from "./context.js";
46
46
  * shape) and never onto those four. A field rename upstream fails the typecheck
47
47
  * here rather than silently zeroing the operator's cache-effect badge.
48
48
  */
49
- /** One `_meta.quota.token_count` row (claude-agent-acp 0.76.0
50
- * `dist/acp-agent.js:6566-6575`). `cachedInputTokens` is cache READS — the name
49
+ /** One `_meta.quota.token_count` row (claude-agent-acp 0.79.0
50
+ * `dist/acp-agent.js:6579-6588`). `cachedInputTokens` is cache READS — the name
51
51
  * differs from `usage.cachedReadTokens` because the shape is shared with
52
52
  * codex-acp; `cachedWriteTokens` is Claude's extra sibling. */
53
53
  export type AcpQuotaTokenCount = {
@@ -64,7 +64,7 @@ export type AcpPromptResponse = {
64
64
  /** Vendor-private, version-pinned. `_meta` is a standard ACP extension slot
65
65
  * whose values a client may not assume, and `quota` is NOT in
66
66
  * claude-agent-acp's exported types — it is produced by the private
67
- * `turnQuotaMeta()` (read at 0.76.0 `dist/acp-agent.js:6549-6558`). Read
67
+ * `turnQuotaMeta()` (read at 0.79.0 `dist/acp-agent.js:6562-6571`). Read
68
68
  * defensively, never structurally required, and re-measure on a pin move. */
69
69
  _meta?: {
70
70
  quota?: {
@@ -150,7 +150,7 @@ export interface AcpAdapterSettingsParams {
150
150
  *
151
151
  * During stream handling the vendor keeps a per-message snapshot in
152
152
  * `lastAssistantUsage`, but its `usage_update` publishes only the scalar `used`
153
- * value (read at claude-agent-acp 0.76.0 `dist/acp-agent.js:3857-3882`). Until
153
+ * value (read at claude-agent-acp 0.79.0 `dist/acp-agent.js:3920-3945`). Until
154
154
  * that partition is carried on the wire, NOTHING honest can go in pi's four
155
155
  * fields, so entwurf writes none of them.
156
156
  */
@@ -342,8 +342,8 @@ export const claudeAdapter: AcpBackendAdapter = {
342
342
 
343
343
  // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
344
344
  // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
345
- // and a context-occupancy scalar (`used`; both read at 0.76.0
346
- // `dist/acp-agent.js:3471-3486`). Both are measured, so backend.ts seals them.
345
+ // and a context-occupancy scalar (`used`; both read at 0.79.0
346
+ // `dist/acp-agent.js:3504-3519`). Both are measured, so backend.ts seals them.
347
347
  // The token partition is NOT declared here: ACP's only token carrier is a
348
348
  // per-turn round-trip aggregate, which is not what pi's four fields mean.
349
349
  sealsTurnAccounting: true,
@@ -137,7 +137,7 @@ function finiteOrZero(value: unknown): number {
137
137
  * "the accounting-grade figure per the SDK" — it also counts Task subagents,
138
138
  * sidechains, and INTERNAL CALLS SUCH AS COMPACTION, so its rows "can total more
139
139
  * than `token_count`" and are "the fuller picture, not a decomposition of it"
140
- * (read at claude-agent-acp 0.76.0 `dist/acp-agent.js:6538-6558`). The narrower
140
+ * (read at claude-agent-acp 0.79.0 `dist/acp-agent.js:6551-6571`). The narrower
141
141
  * `PromptResponse.usage` (== `quota.token_count`) is the MAIN AGENT LOOP only.
142
142
  *
143
143
  * The wider one is the right numerator because the denominator already has that
@@ -1237,7 +1237,7 @@ export function streamAcpTurn(
1237
1237
  // Writing zeros is not a placeholder for a better number we could compute:
1238
1238
  // the per-request partition is genuinely absent from the wire. The vendor
1239
1239
  // builds it in `lastAssistantUsage` and sends only its scalar sum (read at
1240
- // claude-agent-acp 0.76.0 `dist/acp-agent.js:3857-3882`) — #96.
1240
+ // claude-agent-acp 0.79.0 `dist/acp-agent.js:3920-3945`) — #96.
1241
1241
  //
1242
1242
  // But silence is NOT the resting state. A cache miss the operator never sees
1243
1243
  // is a false reading, not a modest one: a session can run for hours believing
@@ -1356,9 +1356,9 @@ export function streamAcpTurn(
1356
1356
  // between them is exactly why the notice below names a MECHANISM and
1357
1357
  // never a cause: claude-agent-acp's `conversation_reset` handler only
1358
1358
  // switches the SDK to a fresh conversation and touches no cost at all
1359
- // (read at 0.76.0 `dist/acp-agent.js:4286-4293`), while claude-agent-sdk
1359
+ // (read at 0.79.0 `dist/acp-agent.js:4349-4356`), while claude-agent-sdk
1360
1360
  // separately documents that "a mid-session /clear resets the running
1361
- // total" (read at 0.3.257 `sdk.d.ts:4884`). A reset therefore PLAUSIBLY
1361
+ // total" (read at 0.3.274 `sdk.d.ts:5342`). A reset therefore PLAUSIBLY
1362
1362
  // explains a backwards total, but nothing here has MEASURED that it did,
1363
1363
  // and asserting the cause would be the same unmeasured claim this lane
1364
1364
  // exists to end.