@junghanacs/entwurf 0.19.0 → 0.20.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 (74) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +23 -5
  3. package/CHANGELOG.md +261 -0
  4. package/DELIVERY.md +59 -6
  5. package/README.md +41 -9
  6. package/VERIFY.md +9 -1
  7. package/docs/acp-backend-rail.md +41 -16
  8. package/docs/external-mcp-host.md +10 -5
  9. package/docs/fresh-cut-policy.md +3 -2
  10. package/docs/setup-clean-host.md +93 -7
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
  15. package/package.json +3 -3
  16. package/pi-extensions/acp-provider.ts +1 -1
  17. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  18. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  19. package/pi-extensions/lib/acp/backend.ts +3 -3
  20. package/pi-extensions/lib/acp/event-mapper.ts +4 -4
  21. package/pi-extensions/lib/acp/models.ts +3 -3
  22. package/pi-extensions/lib/acp/overlay.ts +6 -2
  23. package/pi-extensions/lib/acp/tool-surface.ts +6 -0
  24. package/pi-extensions/lib/meta-session.ts +25 -0
  25. package/run.sh +104 -33
  26. package/scripts/agy-bridge.sh +1 -1
  27. package/scripts/agy-imprint.sh +10 -7
  28. package/scripts/check-acp-carrier-augment.ts +6 -5
  29. package/scripts/check-acp-cortex.ts +41 -0
  30. package/scripts/check-acp-provider-surface.ts +9 -6
  31. package/scripts/check-acp-usage-accounting.ts +9 -9
  32. package/scripts/check-copilot-birth-hook.ts +3 -1
  33. package/scripts/check-copilot-receive-arm.ts +169 -21
  34. package/scripts/check-copilot-statusline.ts +3 -1
  35. package/scripts/check-gate-qualification.ts +11 -10
  36. package/scripts/check-hook-launch-topology.ts +6 -5
  37. package/scripts/check-install-surface.ts +25 -8
  38. package/scripts/check-meta-facts.ts +6 -4
  39. package/scripts/check-meta-hook-session-switch.ts +4 -3
  40. package/scripts/check-meta-identity-consumers.ts +8 -7
  41. package/scripts/check-meta-receiver-marker.ts +2 -1
  42. package/scripts/check-omp-birth-hook.ts +3 -2
  43. package/scripts/check-probe-bridge-command.ts +3 -1
  44. package/scripts/check-setup-qualification.sh +21 -0
  45. package/scripts/copilot-bridge-doctor.sh +15 -5
  46. package/scripts/copilot-bridge-install.sh +6 -2
  47. package/scripts/copilot-launch.sh +16 -2
  48. package/scripts/copilot-receive-bridge.sh +80 -13
  49. package/scripts/lib/reclaim-on-exit.ts +86 -0
  50. package/scripts/meta-bridge-doctor.sh +56 -15
  51. package/scripts/meta-bridge-install.sh +13 -7
  52. package/scripts/meta-bridge-uninstall.sh +6 -4
  53. package/scripts/mutants/acp-cortex.json +26 -2
  54. package/scripts/mutants/copilot-receive.json +26 -0
  55. package/scripts/mutants/omp-birth.json +23 -6
  56. package/scripts/mutants/pi-package-ownership.json +26 -0
  57. package/scripts/mutants/setup-verdict.json +13 -0
  58. package/scripts/omp-bridge-doctor.sh +110 -41
  59. package/scripts/omp-bridge-install.sh +6 -2
  60. package/scripts/omp-receive-install.sh +7 -2
  61. package/scripts/pi_settings_io.py +3 -1
  62. package/scripts/raw-async-delivery/README.md +10 -1
  63. package/scripts/raw-codex-measure/README.md +689 -0
  64. package/scripts/raw-codex-measure/source-audit.md +243 -0
  65. package/scripts/raw-macos-measure/README.md +148 -0
  66. package/scripts/raw-macos-measure/probe.sh +389 -0
  67. package/scripts/register-pi-package.py +38 -30
  68. package/scripts/register-pi-provider.py +3 -2
  69. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  70. package/scripts/smoke-meta-async-drift.sh +7 -5
  71. package/scripts/smoke-meta-install-state.sh +70 -19
  72. package/scripts/smoke-omp-bridge-state.sh +38 -0
  73. package/scripts/smoke-setup-verdict.sh +100 -0
  74. package/scripts/smoke-user-scope-citizen.sh +199 -71
@@ -92,10 +92,10 @@ 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.17.0` | shipped baseline | the package contract these rows belong to |
95
+ | Entwurf package | `0.20.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.75.1` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
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
99
  | Claude Agent SDK | `0.3.257` (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 |
@@ -151,6 +151,23 @@ different reasons, and collapsing them would hide a real risk**:
151
151
  entwurf passes that flag nowhere (repo grep, 0 hits). They are
152
152
  unreachable only because the common loop never invokes them (nor `logout`). Nothing upstream
153
153
  enforces that; it is our own call-site discipline, and it stops holding the moment we use one.
154
+ - **Advertised but never called — 0.76.0 adds exactly one.** Upstream #1111 puts a
155
+ `recommendedValue` on AIR config options, and it is **opt-in**: the adapter turns it on only
156
+ when `initialize`'s `clientCapabilities._meta.jetbrains.air.capabilities` names
157
+ `recommendedValue`. entwurf sends `clientCapabilities: {}` (`backend.ts:1758`), so
158
+ `useRecommendedValue` is false on every branch. The label normalization, the `default` row and
159
+ the effort selector that ride it are unreachable for a second, independent reason as well:
160
+ `[측정 2026-09-10]` `git grep -c configOptions pi-extensions/lib/acp/` is **0**, and entwurf
161
+ has never set `EFFORT_CONFIG_ID`, which is the other gate on the SDK-side `applyFlagSettings`.
162
+ The rest of 0.76.0 is one refactor: `resolveModelPreference` moved into a new
163
+ `dist/session-model.*` (diff is comments and formatting), `dist/session-effort.*` is new, and
164
+ `setSessionConfigOption` — our only model-forcing wire call — is byte-identical across
165
+ `v0.75.1..v0.76.0`.
166
+ - **One 0.76.0 change has a reachable SHAPE and is still inert for us.** When
167
+ `configuredSettings` is a STRING PATH, its `readFile` + `JSON.parse` moved out of
168
+ `resolvedProvider` and up onto the unconditional `session/new` path. entwurf passes `settings`
169
+ as an OBJECT (`tool-surface.ts:153`), so the branch is never taken. That makes "entwurf never
170
+ hands `settings` as a path" a contract rather than an accident; it is stated at that call site.
154
171
  - **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
155
172
  synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
156
173
  `Compact conversation`, and `_meta.contextCompaction` schema v1 — where it used to arrive as
@@ -197,7 +214,7 @@ operator's repair named.
197
214
 
198
215
  | Seam | Claude | Cortex Code |
199
216
  |---|---|---|
200
- | Model ids | unprefixed `claude-sonnet-5`, `claude-opus-5` | `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`; prefix stripped before set-model |
217
+ | 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 |
201
218
  | Launch | bundled `claude-agent-acp`; `CLAUDE_AGENT_ACP_COMMAND` override | `cortex acp serve`; optional connection; `CORTEX_ACP_COMMAND` override; never a launch-time `-m` |
202
219
  | Model authority | per-turn ACP set-model | per-turn ACP set-model; an unavailable curated id fails before prompt |
203
220
  | Carrier | engraving in `_meta.systemPrompt` | no system-prompt carrier; engraving rides the first-user augment |
@@ -256,6 +273,14 @@ Cortex containment was measured against the live CLI rather than copied from Cla
256
273
  exact-writes an overlay-private `cortex/mcp.json`. Non-stdio declarations fail before
257
274
  spawn. Only the `entwurf-bridge` entry receives the real operator HOME required to
258
275
  see the garden store.
276
+ - **`realHome` absoluteness:** the D10 guard judges the captured `realHome` in BOTH
277
+ path flavors (POSIX and win32), so the refusal rule states the contract rather than
278
+ the host it happens to run on. This claims no native-Windows support — native
279
+ Windows is UNSUPPORTED. The certified ACP axis remains Linux
280
+ desktop/workstation; macOS ACP turns are NOT CERTIFIED — pending physical
281
+ host. The flavor-explicit form only stops a POSIX host from reading a
282
+ drive/UNC path as relative, which is what made the defect unkillable on
283
+ Linux (PR #77).
259
284
  - **Carrier:** Cortex has no `_meta.systemPrompt` contract. The engraving is placed at
260
285
  the head of the first-user augment; claiming a system-prompt engraving is false.
261
286
 
@@ -267,23 +292,23 @@ caller-session `_meta`, and cross-machine certification.
267
292
 
268
293
  A backend can return `newSession` before its declared MCP server is callable. This was
269
294
  observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
270
- path. Neither `claude-agent-acp` 0.75.1 nor the Cortex landing adds a client-side
295
+ path. Neither `claude-agent-acp` 0.76.0 nor the Cortex landing adds a client-side
271
296
  readiness fence over a session's declared MCP servers, and entwurf's common loop
272
297
  calls `mcpServerStatus()` nowhere.
273
- (Re-measured at the 0.73.0 → 0.75.1 bump, not inherited from the previous one and the
274
- 0.70.0 → 0.73.0 argument is not reused either. `mcpServerStatus` call sites in
275
- `src/acp-agent.ts` are **2 at both v0.73.0 and v0.75.1** `[측정 2026-09-06, git grep -c]`;
276
- they first appeared in 0.71.0 via `0cbbaf3` (MCP OAuth, LLM-25012), so the ADAPTER calls it
277
- where it once did not. Both were re-read at `v0.75.1 src/acp-agent.ts:1736` and `:1829`
278
- (the 0.73.0 coordinates were `:1618` and `:1711`): the first sits inside
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
302
+ 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
279
305
  `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every status that is not
280
306
  `needs-auth`; the second polls a SINGLE named server to `connected` under an OAuth deadline.
281
307
  Neither waits on every declared server before `newSession` returns. That is an auth
282
- handshake, not a readiness fence, so the boundary below is unchanged. The other reachable-surface findings also re-measured:
283
- AIR typed failures and the AIR file-change report stay capability-gated and
284
- unadvertised by entwurf; `providers/set` / `providers/disable` stay advertised
285
- unconditionally and uncalled; native subagents, async tasks, session forks, session
286
- titles, permission-mode kinds and clear-context planning are all new-but-uncalled.
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.
287
312
  This bump changes no readiness behavior and closes no part of #72.)
288
313
 
289
314
  ### 11-7-a/b. Instrument and first measurement
@@ -376,5 +401,5 @@ Receipt, limits and the `completed`-branch gap: `scripts/raw-acp-compaction-meas
376
401
 
377
402
  - causal MCP-readiness diagnosis and, only with proof, a backend-invariant fence;
378
403
  - broader installed-host and cross-machine Cortex evidence;
379
- - any future Codex managed native-citizen lane—separate from ACP; 0.14.0 does not ship one;
404
+ - any future Codex managed native-citizen lane—separate from ACP; the lane is open as #95 and no release ships one;
380
405
  - persisted ACP resume/load, which is not implemented by today's in-memory reuse.
@@ -52,9 +52,12 @@ Prerequisites on the host running the external MCP client:
52
52
  > carrier reader trims and tests truthiness, so empty and absent are the same answer by
53
53
  > construction. Pinned by `[QK:FRESHCALL-IDENTITY-SCRUB]` in the `check-mux-fresh-call` vitest lane,
54
54
  > inside `check:full`. The DETECT half is unchanged and still load-bearing, because a seam covers
55
- > only the launches that pass through it: `doctor-omp-bridge` reads `/proc/<pid>/environ` for every
56
- > live `omp` process and goes RED on its own axis when one carries either carrier. An omp the
57
- > operator started from a pi citizen's bash never touched the seam and is still unsupported, exactly
55
+ > only the launches that pass through it: `doctor-omp-bridge` reads each live `omp`
56
+ > process's environment (on Linux, `/proc/<pid>/environ`) and goes RED on its own
57
+ > axis when one carries either carrier. The remaining uncertified Darwin axis is
58
+ > per-process environment DISCOVERY, not `/proc` absence; macOS rails are
59
+ > NOT CERTIFIED — pending physical host. An omp the operator started
60
+ > from a pi citizen's bash never touched the seam and is still unsupported, exactly
58
61
  > as this boundary says.
59
62
 
60
63
  Example env file:
@@ -166,6 +169,7 @@ Use the managed install surface rather than editing omp's files by hand:
166
169
  ```bash
167
170
  entwurf install-omp-bridge # the BIRTH extension (a garden id per visible TUI session)
168
171
  entwurf install-omp-mcp # the MCP hand (this section)
172
+ entwurf install-omp-config # the operator setting (tools.xdev: false)
169
173
  entwurf install-omp-receive # the RECEIVER extension (mailbox watch + announce-only doorbell)
170
174
 
171
175
  entwurf doctor-omp-bridge
@@ -173,7 +177,7 @@ entwurf doctor-omp-mcp
173
177
  entwurf doctor-omp-receive
174
178
  ```
175
179
 
176
- Three units, not two. The receiver is what makes the citizen answerable at all — without it omp
180
+ Four units, not two. The receiver is what makes the citizen answerable at all — without it omp
177
181
  sends under its own garden id and every reply is refused as `mailbox-undeliverable` — and it is
178
182
  also a prerequisite of visible fresh: the fresh preflight refuses this host before touching tmux
179
183
  when the receive unit is missing, rather than opening a window that can never be reached.
@@ -241,7 +245,8 @@ bridge registered, **11 devices**: omp's own `ast_edit`, `debug`, `lsp`, `browse
241
245
  seven `entwurf_*`. So the default does not merely wrap entwurf — it wraps omp's own LSP and
242
246
  debugger too.
243
247
 
244
- **Set this on any omp host that is meant to work as a citizen:**
248
+ **`entwurf setup` writes this for a detected omp host, and `entwurf install-omp-config` is
249
+ the repair leaf. The value they own:**
245
250
 
246
251
  ```yaml
247
252
  # ~/.omp/agent/config.yml
@@ -22,7 +22,8 @@ the new generation.
22
22
  ## What certification checks
23
23
 
24
24
  `certifyActiveStore` is shared by the doctor and all identity writers: pi birth,
25
- Claude's `SessionStart`, agy's imprint, and `entwurf_register_native`. Every active
25
+ Claude's `SessionStart`, Copilot's first-prompt birth hook, OMP's birth extension,
26
+ agy's imprint, and `entwurf_register_native`. Every active
26
27
  record must be:
27
28
 
28
29
  - a regular, non-symlink `.meta.json` file;
@@ -43,7 +44,7 @@ are opened without following symlinks; implementation and race proofs live besid
43
44
 
44
45
  ## Running a cut
45
46
 
46
- First close every pi, Claude Code, and agy session that could own a citizen or
47
+ First close every pi, Claude Code, Copilot, OMP, and agy session that could own a citizen or
47
48
  transport artifact. Then run:
48
49
 
49
50
  ```bash
@@ -1,8 +1,18 @@
1
1
  # Clean-host setup
2
2
 
3
- Current operator recipe for a fresh Linux desktop/workstation. The neutral npm
4
- package can install elsewhere, but Claude's garden-native meta-bridge is certified
5
- only on Linux because its strict live-owner join uses `/proc`.
3
+ Operator recipe for a POSIX host. **Linux is the certified axis** — §2–§6 are
4
+ written for it and their receipts are Linux receipts. What follows is the macOS
5
+ evidence split, and its three states must not collapse into "macOS is supported":
6
+
7
+ - **CERTIFIED (CI)** — the Entwurf-only install surface on macOS
8
+ (`macos-install-surface`). Weaker than a physical-host doctor green. §1 is
9
+ this surface.
10
+ - **NOT CERTIFIED — pending physical host** — **on macOS**, every garden-native
11
+ harness rail in §2–§6 (pi, Claude, Copilot, OMP, agy), marker join, ACP turn,
12
+ mux. Wiring may be written; that is not a rail receipt. The remaining
13
+ unmeasured Darwin doctor reason is per-process environment DISCOVERY
14
+ (start-key is portable).
15
+ - **UNSUPPORTED** — native Windows only. macOS is never this.
6
16
 
7
17
  ## Requirements
8
18
 
@@ -22,8 +32,35 @@ only on Linux because its strict live-owner join uses `/proc`.
22
32
  Claude Code >=2.1.217 is required for the managed exec-hook lifecycle. The package
23
33
  never supplies or proxies backend credentials.
24
34
 
35
+ On a physical Darwin host, `python3` may be the CommandLineTools stub (a
36
+ dialog instead of an interpreter) — unmeasured. That is a probe cell
37
+ (`scripts/raw-macos-measure/probe.sh` M3), not a certification path.
38
+
25
39
  ## 1. Install Node and entwurf
26
40
 
41
+ This section is the Entwurf-only install surface. On macOS it is
42
+ **CERTIFIED (CI)** (`macos-install-surface`): npm pack → clean consumer
43
+ install → bins → `--help` → `check-bridge` → harness-absent `setup`. That
44
+ is not a harness-rail receipt.
45
+
46
+ On Darwin, `entwurf setup` is intentionally non-green when it detects a
47
+ harness. The wiring is written; the row is FAIL:
48
+
49
+ > the wiring WAS written and nothing failed to install, but this harness
50
+ > rail is NOT CERTIFIED — pending physical host on Darwin. setup runs no
51
+ > doctor; the rail axis is owned by '<doctor>', which refuses on this
52
+ > platform. Tracking: #78.
53
+
54
+ That FAIL is honest: setup runs no doctor, and no physical-host rail
55
+ receipt exists — it is not an install failure. An Entwurf-only host (no
56
+ harness on PATH) stays green (five SKIP, `bins: PASS`, `core: PASS`,
57
+ `result: green`), which is what CERTIFIED (CI) observed.
58
+
59
+ A physical Mac can send host facts without installing entwurf or logging
60
+ into a harness: `scripts/raw-macos-measure/probe.sh` (`/bin/sh`, ~3.5s,
61
+ cells M1–M9, writes nothing). Measurement, not certification — it does
62
+ not move CERTIFIED (CI) or close a rail.
63
+
27
64
  Use the host's normal Node 24 installation. With nvm:
28
65
 
29
66
  ```bash
@@ -107,6 +144,12 @@ packageRoot↔installerRoot coupling mismatch and a package/provider managed-pat
107
144
 
108
145
  ## 2. Optional pi adapter / ACP plugin
109
146
 
147
+ This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending
148
+ physical host. Control sockets, mux, and ACP plugin hosting have no
149
+ physical-Darwin receipt. `entwurf install` may write `.pi/settings.json`
150
+ on Darwin; that is wiring, not a rail receipt. If `pi` is on PATH,
151
+ `setup` names the pi row FAIL with the §1 Darwin wording.
152
+
110
153
  Install the exact release floor, then wire the project:
111
154
 
112
155
  ```bash
@@ -134,6 +177,9 @@ The V3 record births the garden id; do not inject a pi session id manually.
134
177
 
135
178
  ## 3. Optional Claude Code native citizen
136
179
 
180
+ This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending physical
181
+ host (see the end of this section).
182
+
137
183
  First register the MCP bridge if the stable bin is not already present:
138
184
 
139
185
  ```bash
@@ -159,12 +205,21 @@ validation, or hand-inspected marker cannot replace a new real session. If the l
159
205
  form is unsupported, reinstall; if ownership is correct but the live join is absent,
160
206
  restart the affected session.
161
207
 
162
- New macOS wiring is refused because the live join is not instrumented there. Darwin
163
- uninstall remains available for cleaning an older managed install; this is an evidence
164
- boundary, not a permanent impossibility claim.
208
+ The Darwin install fence is open (`Linux | Darwin`); python3 is the
209
+ refusal, not the platform name. A completed Darwin install is still not
210
+ a rail receipt: this rail is NOT CERTIFIED — pending physical host. The
211
+ remaining unmeasured doctor reason is per-process environment DISCOVERY
212
+ of live bridge children (start-key is portable). A CI runner cannot
213
+ supply a Claude login. Uninstall is open on Darwin too. That is an
214
+ evidence boundary, not UNSUPPORTED.
165
215
 
166
216
  ## 4. Optional GitHub Copilot CLI native citizen
167
217
 
218
+ This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending
219
+ physical host. The Darwin install fence for `install-copilot-bridge` is
220
+ open (`Linux | Darwin`); a completed install is not a rail receipt. That
221
+ is an evidence boundary, not UNSUPPORTED.
222
+
168
223
  Copilot has four independently owned surfaces. `setup` composes all four when `copilot` is on
169
224
  PATH (#86 C3b); the commands below are the per-unit repair, doctor, and inverse surfaces. All
170
225
  four must be green for supported visible fresh; a manual citizen may omit the footer, but fresh
@@ -187,7 +242,12 @@ scanning for that process, checks the receiver, removes inherited pi identity ca
187
242
  the model/permission defaults. Birth occurs on the first prompt. `entwurf_fresh_call` uses this
188
243
  same managed invocation and requires the birth, MCP, receiver, and visible-identity preflight.
189
244
 
190
- ## 4b. Optional OMP (`omp`) native citizen — accepted on one host
245
+ ## 4b. Optional OMP (`omp`) native citizen — accepted on one Linux host (thinkpad)
246
+
247
+ This rail is CERTIFIED on Linux (one host, named in the version rule
248
+ below); on macOS it is NOT CERTIFIED — pending physical host. The Darwin
249
+ install fences for `install-omp-bridge` and `install-omp-receive` are
250
+ open (`Linux | Darwin`); a completed install is not a rail receipt.
191
251
 
192
252
  Three independently owned surfaces, and a boundary that is part of the instructions rather
193
253
  than a footnote. Birth, visible identity, who-sent, the MCP hand and RECEIVE are landed, and
@@ -338,6 +398,10 @@ why `disabledServers` is never the way to hide an import.
338
398
 
339
399
  ## 5. Optional Antigravity native citizen
340
400
 
401
+ This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending
402
+ physical host. The installers may write on Darwin; a green install is
403
+ not a native-push receipt.
404
+
341
405
  Install the three independently owned surfaces:
342
406
 
343
407
  ```bash
@@ -363,6 +427,10 @@ LIVE=1 AGY_CONVERSATION_ID=<id> entwurf smoke-agy-native-push-live
363
427
 
364
428
  ## 6. Optional ACP backend turns
365
429
 
430
+ ACP turns are CERTIFIED on Linux; on macOS they are NOT CERTIFIED —
431
+ pending physical host. Mux (visible fresh / resume) is the same state.
432
+ A CI runner has no Claude or Cortex login.
433
+
366
434
  Claude uses the operator's existing local Claude authentication:
367
435
 
368
436
  ```bash
@@ -386,6 +454,15 @@ After upgrading the package, rerun the managed installers for every native harne
386
454
  in use and restart their existing processes. Native plugin caches are not live-reload
387
455
  safe across launch-contract changes.
388
456
 
457
+ On Darwin the four previously closed install fences
458
+ (`install-meta-bridge`, `install-copilot-bridge`, `install-omp-bridge`,
459
+ `install-omp-receive`) now accept `Linux | Darwin` and refuse on the
460
+ real toolchain (python3 / node / harness presence), not the platform
461
+ name. Re-running them writes wiring. `setup` still names every detected
462
+ harness unit FAIL with the §1 Darwin wording — upgrade does not mint a
463
+ rail receipt. Doctors refuse on Darwin until a physical-host receipt
464
+ exists.
465
+
389
466
  If install or doctor reports an unreadable/old active citizen generation, do not edit
390
467
  records by hand. Close pi, Claude, Copilot, and agy sessions first, run
391
468
  `entwurf meta-bridge-fresh-cut`, and read its exit status before any install. Then choose the
@@ -414,6 +491,10 @@ PASS/SKIP/FAIL summary. The complete quiescence, archive, and exit-code contract
414
491
  core / full candidate floor); maintainer checkout only.
415
492
  - `check-install-container`: checkout-invisible Linux package-consumer shape using
416
493
  fixtures; not a native lifecycle proof.
494
+ - `macos-install-surface`: macOS Entwurf-only package-consumer shape, CERTIFIED (CI)
495
+ (`macos-latest` runner, run 34303884286 @ `70eda03`); not a native lifecycle proof.
496
+ - `scripts/raw-macos-measure/probe.sh`: physical-Darwin host facts (~3.5s,
497
+ no install, no login). Measurement, not a certification path.
417
498
  - `doctor-meta-bridge`: one installed real Claude host, only with a new live session.
418
499
  - `LIVE=1 entwurf release-gate /path/to/scratch --cut`: aggregate runtime acceptance (`--cut` makes any MUST SKIP red; without it the run is a diagnostic pass).
419
500
 
@@ -422,6 +503,11 @@ host verdicts are [BASELINE.md](../BASELINE.md).
422
503
 
423
504
  ## Uninstall
424
505
 
506
+ Darwin uninstall is open for the same surfaces the installers write
507
+ (the Claude uninstall fence was already open; the four install fences
508
+ now match). Running an inverse on Darwin is cleanup, not a rail
509
+ certification.
510
+
425
511
  Run only the surfaces this host owns:
426
512
 
427
513
  ```bash
@@ -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.75.1
127
- // `dist/acp-agent.js:3467-3482`). Both are measured, so backend.ts seals them.
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.
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,
@@ -49,11 +49,11 @@ export const ENTWURF_ACP_NO_AUTH_SENTINEL = "entwurf-no-auth";
49
49
  // both axes (protocol smoke + agent interview) — do not extend casually.
50
50
  // Exported so the claude backend adapter (backend-adapter.ts) can answer
51
51
  // `routeModel` without re-deriving the set from curatedClaudeModels().
52
- export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5"];
52
+ export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1"];
53
53
  /** The anchor model whose absence is a hard registry regression, not a soft skip. */
54
54
  export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5";
55
- // Anthropic's registry reports 1M for both Sonnet 5 and Opus 5, and the
56
- // entwurf surface exposes the full 1M for BOTH. Sonnet 5's 1M window is the
55
+ // Anthropic's registry reports 1M for Sonnet 5, Opus 5 and Fable 5.1, and the
56
+ // entwurf surface exposes the full 1M for all three. Sonnet 5's 1M window is the
57
57
  // whole point of the 0.12.3 bump — it is the compact-free long-context floor the
58
58
  // earlier 200K Sonnet cap could not provide. We still clamp to a 1M ceiling so a
59
59
  // future registry value can't silently inflate the surface past what we verify.
@@ -35,7 +35,7 @@
35
35
  import { createHash } from "node:crypto";
36
36
  import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
37
37
  import { homedir } from "node:os";
38
- import { join } from "node:path";
38
+ import { isAbsolute, join, win32 } from "node:path";
39
39
  /** Operator's real Claude config dir — the symlink-passthrough SOURCE. */
40
40
  export const CLAUDE_REAL_CONFIG_DIR = join(homedir(), ".claude");
41
41
  /** pi-owned overlay dir — the CLAUDE_CONFIG_DIR target for ACP child spawns. */
@@ -356,7 +356,11 @@ function cortexLinkIfExists(realPath, overlayPath) {
356
356
  * Returns the isolated HOME/SNOWFLAKE_HOME the spawn env must carry.
357
357
  */
358
358
  export function ensureCortexDualHomeOverlay(params) {
359
- if (!params.realHome || !params.realHome.startsWith("/")) {
359
+ // Absoluteness is judged in BOTH path flavors so the guard states the contract
360
+ // ("the parent captured an absolute HOME") instead of the host it happens to run
361
+ // on. This asserts nothing about native-Windows support; it only stops a POSIX
362
+ // host from silently accepting a drive/UNC path as "relative".
363
+ if (!params.realHome || !(isAbsolute(params.realHome) || win32.isAbsolute(params.realHome))) {
360
364
  throw new Error(`entwurf: cortex dual-HOME overlay requires an absolute realHome captured by the parent (got ${JSON.stringify(params.realHome)})`);
361
365
  }
362
366
  const overlaysRoot = params.overlaysRoot ?? CORTEX_OVERLAYS_ROOT;
@@ -120,6 +120,12 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
120
120
  ...(params.modelId ? { model: params.modelId } : {}),
121
121
  tools: [...params.tools],
122
122
  settingSources: [...params.settingSources],
123
+ // `settings` is an OBJECT here, and that is a contract, not a convenience.
124
+ // claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
125
+ // JSON.parse for that form moved out of `resolvedProvider` up onto the
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
128
+ // IO, and its failure modes, inside every session creation.
123
129
  settings: {
124
130
  permissions: {
125
131
  allow: [...params.permissionAllow],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.19.0",
4
- "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
3
+ "version": "0.20.1",
4
+ "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, and Antigravity; Claude and Snowflake Cortex Code ACP backends. Codex has verified delivery-probe evidence, not a managed native-citizen lane.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Junghan Kim",
@@ -82,7 +82,7 @@
82
82
  "claudeCodeFloor": ">=2.1.217"
83
83
  },
84
84
  "dependencies": {
85
- "@agentclientprotocol/claude-agent-acp": "0.75.1",
85
+ "@agentclientprotocol/claude-agent-acp": "0.76.0",
86
86
  "@agentclientprotocol/sdk": "1.4.0",
87
87
  "@anthropic-ai/sdk": "0.100.1",
88
88
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -14,7 +14,7 @@
14
14
  // registry (backend-adapter.ts), NOT a per-backend list spelled here. Adding a
15
15
  // backend therefore never edits this file. check-acp-provider-surface pins that
16
16
  // the compiled entry really registers the EXACT union
17
- // ([QK:CORTEX-PROVIDER-SIX-ROW-SURFACE]).
17
+ // ([QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE]).
18
18
  //
19
19
  // Fence: this entry rides the emit-capable root tsconfig (it is not in the root
20
20
  // `exclude` list); its lib modules are imported with `.js` suffixes (the root
@@ -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.75.1
50
- * `dist/acp-agent.js:6493-6502`). `cachedInputTokens` is cache READS — the name
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
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.75.1 `dist/acp-agent.js:6476-6485`). Read
67
+ * `turnQuotaMeta()` (read at 0.76.0 `dist/acp-agent.js:6549-6558`). 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.75.1 `dist/acp-agent.js:3853-3878`). Until
153
+ * value (read at claude-agent-acp 0.76.0 `dist/acp-agent.js:3857-3882`). 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.75.1
346
- // `dist/acp-agent.js:3467-3482`). Both are measured, so backend.ts seals them.
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.
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.75.1 `dist/acp-agent.js:6465-6485`). The narrower
140
+ * (read at claude-agent-acp 0.76.0 `dist/acp-agent.js:6538-6558`). 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.75.1 `dist/acp-agent.js:3853-3878`) — #96.
1240
+ // claude-agent-acp 0.76.0 `dist/acp-agent.js:3857-3882`) — #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,7 +1356,7 @@ 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.75.1 `dist/acp-agent.js:4282-4289`), while claude-agent-sdk
1359
+ // (read at 0.76.0 `dist/acp-agent.js:4286-4293`), while claude-agent-sdk
1360
1360
  // separately documents that "a mid-session /clear resets the running
1361
1361
  // total" (read at 0.3.257 `sdk.d.ts:4884`). A reset therefore PLAUSIBLY
1362
1362
  // explains a backwards total, but nothing here has MEASURED that it did,
@@ -60,8 +60,8 @@ export type AcpPiStreamState = {
60
60
  *
61
61
  * Last write wins, never a sum: both are latest session-level observations, and
62
62
  * one turn can legitimately see several (claude emits one per `result` message,
63
- * including a sub-agent's own — read at claude-agent-acp 0.75.1
64
- * `dist/acp-agent.js:3467-3482`), each carrying that result's current values.
63
+ * including a sub-agent's own — read at claude-agent-acp 0.76.0
64
+ * `dist/acp-agent.js:3471-3486`), each carrying that result's current values.
65
65
  */
66
66
  observedSessionCostUsd?: number;
67
67
  observedContextOccupancyTokens?: number;
@@ -346,8 +346,8 @@ export function applyAcpSessionUpdate(
346
346
  case "usage_update": {
347
347
  // `used` is OCCUPANCY-shaped — the backend's post-turn context size, not
348
348
  // the prompt response's turn aggregate. Claude sends `lastAssistantTotalUsage`
349
- // as `used` (read at claude-agent-acp 0.75.1
350
- // `dist/acp-agent.js:3867-3878`), after constructing that scalar from the
349
+ // as `used` (read at claude-agent-acp 0.76.0
350
+ // `dist/acp-agent.js:3871-3882`), after constructing that scalar from the
351
351
  // latest assistant snapshot (`:3853-3866`). pi reads `usage.totalTokens` as
352
352
  // exactly that occupancy (`calculateContextTokens(usage) = usage.totalTokens
353
353
  // || input + output + cacheRead + cacheWrite`, read at pi-coding-agent
@@ -53,13 +53,13 @@ export const ENTWURF_ACP_NO_AUTH_SENTINEL = "entwurf-no-auth";
53
53
  // both axes (protocol smoke + agent interview) — do not extend casually.
54
54
  // Exported so the claude backend adapter (backend-adapter.ts) can answer
55
55
  // `routeModel` without re-deriving the set from curatedClaudeModels().
56
- export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5"] as const;
56
+ export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1"] as const;
57
57
 
58
58
  /** The anchor model whose absence is a hard registry regression, not a soft skip. */
59
59
  export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5";
60
60
 
61
- // Anthropic's registry reports 1M for both Sonnet 5 and Opus 5, and the
62
- // entwurf surface exposes the full 1M for BOTH. Sonnet 5's 1M window is the
61
+ // Anthropic's registry reports 1M for Sonnet 5, Opus 5 and Fable 5.1, and the
62
+ // entwurf surface exposes the full 1M for all three. Sonnet 5's 1M window is the
63
63
  // whole point of the 0.12.3 bump — it is the compact-free long-context floor the
64
64
  // earlier 200K Sonnet cap could not provide. We still clamp to a 1M ceiling so a
65
65
  // future registry value can't silently inflate the surface past what we verify.
@@ -46,7 +46,7 @@ import {
46
46
  writeFileSync,
47
47
  } from "node:fs";
48
48
  import { homedir } from "node:os";
49
- import { join } from "node:path";
49
+ import { isAbsolute, join, win32 } from "node:path";
50
50
 
51
51
  import type { AcpMcpServer } from "./config.js";
52
52
 
@@ -416,7 +416,11 @@ function cortexLinkIfExists(realPath: string, overlayPath: string): void {
416
416
  * Returns the isolated HOME/SNOWFLAKE_HOME the spawn env must carry.
417
417
  */
418
418
  export function ensureCortexDualHomeOverlay(params: CortexOverlayParams): CortexOverlayResult {
419
- if (!params.realHome || !params.realHome.startsWith("/")) {
419
+ // Absoluteness is judged in BOTH path flavors so the guard states the contract
420
+ // ("the parent captured an absolute HOME") instead of the host it happens to run
421
+ // on. This asserts nothing about native-Windows support; it only stops a POSIX
422
+ // host from silently accepting a drive/UNC path as "relative".
423
+ if (!params.realHome || !(isAbsolute(params.realHome) || win32.isAbsolute(params.realHome))) {
420
424
  throw new Error(
421
425
  `entwurf: cortex dual-HOME overlay requires an absolute realHome captured by the parent (got ${JSON.stringify(params.realHome)})`,
422
426
  );
@@ -150,6 +150,12 @@ export function buildClaudeSessionMeta(
150
150
  ...(params.modelId ? { model: params.modelId } : {}),
151
151
  tools: [...params.tools],
152
152
  settingSources: [...params.settingSources],
153
+ // `settings` is an OBJECT here, and that is a contract, not a convenience.
154
+ // claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
155
+ // JSON.parse for that form moved out of `resolvedProvider` up onto the
156
+ // unconditional `session/new` path — a shape that WOULD reach us, and does not,
157
+ // only because we never hand it a path. Keep it inline: a path would put file
158
+ // IO, and its failure modes, inside every session creation.
153
159
  settings: {
154
160
  permissions: {
155
161
  allow: [...params.permissionAllow],
@@ -1622,6 +1622,31 @@ export type MarkerOwnerVerdict = "live" | "dead" | "uncertain";
1622
1622
  * comparable to each other: `linux:<starttime ticks since boot>` and `ps:<lstart
1623
1623
  * wall-clock text>` describe the same process with different numbers, so a
1624
1624
  * mismatch ACROSS schemes says nothing about whether the process changed.
1625
+ *
1626
+ * They are also NOT equally resolved. `linux:` carries `/proc` clock ticks at
1627
+ * 10 ms granularity; `ps:` carries `lstart` wall-clock text at ONE-SECOND
1628
+ * granularity [measured, oracle 2026-09-09: two children spawned 50 ms apart
1629
+ * returned identical `lstart` strings, and widening to `lstart=,command=`
1630
+ * still collided when argv was identical]. Darwin has no `/proc`, so it ALWAYS
1631
+ * takes the `ps:` branch — there the pid-reuse defense window is 100x wider
1632
+ * than on Linux. 0.20.0 ships no key change anyway, and the numbers are why.
1633
+ * A same-second pid reuse on Darwin requires pid N to die, the whole
1634
+ * sequential pid space (PID_MAX 99999, wrapping after ~99,899 spawns and
1635
+ * skipping live pids) to wrap back around to N, and the replacement to start
1636
+ * in the SAME wall-clock second — a sustained ~99,900 spawns/second for a full
1637
+ * second. Measured on the reference host: 19.9 forks/second average under real
1638
+ * load, ~14,873 forks/second pathological synthetic peak (6.7x short even at
1639
+ * the synthetic peak), with Darwin's default `kern.maxproc` (~2,500) capping
1640
+ * churn in flight besides. The error direction is one-sided: a coarser clock
1641
+ * can only make more keys compare EQUAL, and {@link classifyMarkerOwner} grants
1642
+ * `live` on equal keys — coarsening can mint a false `live` (identity grant
1643
+ * fails OPEN) and can never mint a false `dead` (the generation cut stays
1644
+ * fail-CLOSED). The migration axis is empty by measurement, not argument: a
1645
+ * read-only scan of this host's real marker store found 1,248 marker files —
1646
+ * all `linux:`, zero `ps:` keys — because on Linux `/proc` always wins, so no
1647
+ * Linux install ever minted a `ps:` key; only a Darwin install can. A
1648
+ * Darwin-side hardening decision, if one is ever wanted, belongs with the
1649
+ * physical-Mac receipts (#78), not ahead of them.
1625
1650
  */
1626
1651
  export type StartKeyScheme = "linux" | "ps";
1627
1652