@junghanacs/entwurf 0.38.1 → 0.39.0

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.
package/BASELINE.md CHANGED
@@ -23,7 +23,8 @@ or **unverified**. Since 0.20.0 the table also carries two qualifiers:
23
23
  runner is not a physical host, so bare **certified** still means a physical-host
24
24
  doctor green; and **NOT CERTIFIED — pending physical host** marks a rail whose
25
25
  missing evidence only a physical host can supply. That is an evidence boundary,
26
- not **UNSUPPORTED** — the deliberate never, which today is native Windows only.
26
+ not **UNSUPPORTED** — the deliberate never, which today names the native-Windows surfaces
27
+ outside the experimental preview (rows below).
27
28
 
28
29
  | Surface | Verdict | Evidence |
29
30
  |---|---|---|
@@ -43,8 +44,11 @@ not **UNSUPPORTED** — the deliberate never, which today is native Windows only
43
44
  | Doctor fail-closed on unreadable environ | Linux measured; Darwin unmeasured | Copilot launch-flag and omp identity-carrier: missing `/proc/<pid>/environ` as predicate INPUT is `UNVERIFIABLE` (non-green), not a benign note. omp distinguishes `pgrep` exit 1 (absent) from exit 2 (enumeration failed); copilot distinguishes ENOENT/ESRCH (gone) from other errno (read denied). |
44
45
  | setup on uncertified platform with harness present | named non-green | All 13 harness units ask `harness_rail_certified_platform()`; uncertified → FAIL whose wording is not "install failed". Harness-absent Darwin setup (the CI cell) can still be computed green. |
45
46
  | macOS physical-host probe | shipped, unrun on a Mac | `scripts/raw-macos-measure/` — `/bin/sh`, cells M1–M9, 3.5 s; needs neither entwurf install nor login. Still unmeasured: Darwin foreign-process environ (`ps -Eww`) and whether macOS `python3` is a CommandLineTools stub. |
46
- | WSL2 | unverified | Counts as Linux evidence, never a Windows surface (AGENTS Hard Rule 17) |
47
- | native Windows | UNSUPPORTED | Locked pending GLG; a scope decision, not an evidence gap |
47
+ | WSL2 Entwurf-only install + zero-token Pi citizen (GitHub Actions `windows-2025` runner, not a physical machine) | shape-only | Experimental, non-voting CI (not read by the release oracle): 2026-10-18, `.github/workflows/wsl2-install-surface.yml` run 39098773010 @ `b03d646` ([#78 receipt](https://github.com/junghan0611/entwurf/issues/78#issuecomment-6245947301)). Guest kernel `6.18.40.1-microsoft-standard-WSL2`, host `wsl --list --verbose` VERSION 2, guest fstype quoted raw as `ext2/ext3`. The run's packed tgz digest is equal across producer, host and guest; guest Node 24.18.1; harness-absent `setup` green with protected auth/harness state unchanged. Pi 1.3.0 (sha512 pinned to the registry integrity) wired by `setup`: one V3 record whose filename is its gid, a gid-keyed control socket with a native connect, `peer-facts` alive; only `get_state` was sent and no `agent_start` was observed. Pi creating an empty `auth.json` is not a login. It certifies no harness rail. Linux-guest runtime evidence only: not native-Windows evidence, and not a substitute for the required Linux CI jobs. |
48
+ | WSL2 delivery, non-pi harnesses, vendor turn, physical user machine | unverified | No receipt. Authentication and model turns were deliberately excluded from the CI cells' scope (not a CI impossibility); non-pi harnesses, delivery, fresh/resume and physical machines are unmeasured. |
49
+ | native Windows experimental preview: Pi citizen + `entwurf_v2` delivery (GitHub Actions `windows-2025` x64 runner, Node 24.18.1, Pi 1.3.0; 0.39.0 candidate, not yet released; release and npm publication pending; the evidence below is the feature-branch CI artifact at `3b65bdc`) | shape-only | Experimental, non-voting CI (not read by the release oracle): 2026-10-18, `.github/workflows/windows-native-preview.yml` run 39123555360 @ `3b65bdc`, with the required `ci.yml` four jobs green at the same SHA (run 39123555224). Build: MSVC (VS 2026, toolset v145, SDK 10.0.26100.0), two builds byte-identical (`entwurf_win.node` sha256 `9837893521ec…`), imports advapi32 + kernel32 with `node.exe` the only delay-load; node-gyp 13.1.0 and the Node headers/`node.lib` pinned. The pack carries exactly the addon and its manifest; npm installed it into a sandboxed consumer prefix; Pi came from a tarball checked against its pinned SRI. Six addon cells PASS (final-component reparse and non-regular refusals on the read handle, no connect through a symlink to a pipe, one-deadline timeout with reclaim, pipe-name stability and loader refusals). B1: two `pi --entwurf-control` citizens, one V3 record each, record-derived pipes alive through the product probe, `peer-facts` alive, graceful exit with both addresses dead afterwards. D1: A's model called `entwurf_v2` toward B's garden id with a nonce; A's receipt `control-socket → sent`, B's model context and B's transcript `entwurf-message` entry from A carry it. The model is an offline pi-ai `fauxProvider`: no vendor turn and no comprehension claim. The transcript fact is the harness's assertion; the raw transcript is not archived. The run used the elevated Administrators owner branch, so user-SID-only privacy is not measured. Pi's `peerDependencies` warning was observed, not waived. Not evidence for any other harness on Windows. |
50
+ | native Windows: own pre-existing pipe-name birth refusal, record link/rename syscalls, descendant cleanup after a forced kill, cross-user, mixed elevation, remote callers, user-SID-only privacy, physical user machine | unverified | No receipt; the preview cells name these as unmeasured. |
51
+ | native Windows: other harnesses, `setup` and the `.sh` bins, fresh/resume, mux/Herdr/ACP, ARM64, records over 16 MiB, reparse-backed store folders | UNSUPPORTED | Outside the preview's scope by decision; the preview loader refuses an install without the Windows addon by name. |
48
52
 
49
53
  Notes the table cannot carry without becoming prose again:
50
54
 
@@ -65,7 +69,8 @@ Notes the table cannot carry without becoming prose again:
65
69
  - **macOS evidence is split, not a support claim.** The Entwurf-only install surface is
66
70
  CERTIFIED (CI) (row above); the harness rails are NOT CERTIFIED — pending
67
71
  physical host (rental remains unscheduled). The package-level `os` field stays
68
- unrestricted, and native Windows is UNSUPPORTED.
72
+ unrestricted, and native Windows is an experimental preview (0.39.0 candidate, not
73
+ yet released; release and npm publication pending) with its own named gaps (rows above), not a supported platform.
69
74
 
70
75
  **Operator acceptance rule:** on a claimed Claude host, reinstall from the released
71
76
  artifact, restart every old Claude process, open a new session, and run the doctor
package/CHANGELOG.md CHANGED
@@ -4,6 +4,93 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.39.0 - 2026-10-18
8
+
9
+ ### Native Windows (experimental preview, #78)
10
+
11
+ - **A native-Windows Pi citizen, shape-only.** On Windows x64 the package carries a small
12
+ N-API addon (`native/win32-x64/entwurf_win.node` plus its `manifest.json`), loaded by
13
+ `pi-extensions/lib/win32-native.js` only after the manifest's schema, platform, arch,
14
+ file, sha256 and `napiVersion` 8 agree with the bytes (`ENTWURF_WIN32_UNSUPPORTED` /
15
+ `ENTWURF_WIN32_NATIVE_DRIFT` otherwise). It supplies the two primitives POSIX gets from
16
+ the OS: the record read refuses a final-component reparse point (`ELOOP`) and a
17
+ non-regular file (`ENTWURF_ENOTREG`) on the read handle itself, and the control
18
+ address is a named pipe `\\.\pipe\entwurf-<sha256(root)[:16]>-<gid>` whose
19
+ probe and RPC check the server's owner SID and connect at identification level. The
20
+ POSIX `O_NOFOLLOW` and ancestor policies are unchanged, and non-Windows hosts take the
21
+ existing POSIX branches of the shared modules; each axis's own CI and LIVE gates are
22
+ that evidence, not a claim of zero impact.
23
+ - **Wiring.** `meta-session`, `control-socket-path`, `socket-probe`, `socket-discovery`,
24
+ `entwurf-control-rpc`, `entwurf-control` and the bridge take the win32 branch through
25
+ that loader; pipe names come from records and are never stat-ed; nothing unlinks or
26
+ GCs a pipe on win32.
27
+ - **Carrier and provenance.** The release carrier is produced in CI (MSVC, `/Brepro`, two builds
28
+ compared byte-for-byte, imports exactly advapi32 + kernel32 with `node.exe` the only
29
+ delay-load, node-gyp 13.1.0 and the Node headers/`node.lib` pinned) and ships with
30
+ node-gyp's MIT notice (`LICENSES/node-gyp.txt`). `check-pack` reports the carrier as
31
+ `absent`, `pair-broken`, `invalid` or `present <sha>` through the loader's own verifier.
32
+ A Linux checkout pack is addon-less, so this release's candidate is the preview
33
+ workflow's pack at the prepared SHA (release skill M3w), not a local `npm pack`.
34
+ - **Measured scope.** windows-native-preview run 39123555360 @ `3b65bdc` on a GitHub
35
+ Actions `windows-2025` x64 runner, Node 24.18.1, Pi 1.3.0: six addon cells, then two
36
+ `pi --entwurf-control` citizens (one V3 record each, pipes alive through the product
37
+ probe, `peer-facts`, graceful exit with both addresses dead) and one garden-id
38
+ `entwurf_v2` delivery between them (`control-socket → sent`, B's model context and
39
+ transcript entry from A), with an offline pi-ai `fauxProvider`: no vendor turn.
40
+ - **Limits.** Experimental and shape-only; not a supported platform. UNSUPPORTED: other
41
+ harnesses, `setup` and the `.sh` bins, fresh/resume, mux/Herdr/ACP, ARM64, records over
42
+ 16 MiB, reparse-backed store folders. Unmeasured: the own pre-existing pipe-name birth
43
+ refusal, record link/rename syscalls, descendant cleanup after a forced kill,
44
+ cross-user, mixed elevation and remote callers, user-SID-only privacy (the runs took
45
+ the elevated Administrators owner branch), worker/environment teardown with a pipe
46
+ call in flight, and any physical user machine. Rows in BASELINE.
47
+
48
+ ### Verification
49
+
50
+ - **Windows workflows (#78).** `windows-native-probe.yml`, `-e1a.yml`, `-e1b.yml` and
51
+ `-preview.yml` are experimental and non-voting; their push triggers name the feature
52
+ branch, so on main they run only by dispatch. The release oracle still reads only the
53
+ four required `ci.yml` jobs. New gates `check-win32-preview` and
54
+ `check-pack-win32-carrier` (in `check:contracts`); one new mutant lane, `win32-preview`
55
+ (19 claims), and the carrier's 2 claims appended to the existing `pack-install` lane.
56
+ The committed set is now 1000 mutants across 73 lanes.
57
+ - **Experimental WSL2 workflow (#78).** `.github/workflows/wsl2-install-surface.yml`
58
+ installs this run's packed tgz inside a real WSL2 guest on a GitHub-hosted Windows runner,
59
+ runs a harness-absent `setup`, then births a zero-token Pi citizen (record, socket,
60
+ `peer-facts`; `get_state` only). It is not a release axis: the release oracle reads only
61
+ `ci.yml`, whose four required jobs are unchanged. It runs when the workflow file changes
62
+ on main, or by manual dispatch. No WSL2 support, harness rail or physical-machine claim.
63
+
64
+ ### Release boundary and verification
65
+
66
+ - **Implementation commits.** WSL2 `afcda79`..`9b492a4` (landed `86f0eb5`); native
67
+ Windows `95933c7`..`5da8f12`, landed with the carrier supply contract `32fcc1c`
68
+ (exact push CI 39128022109, four jobs green). The runtime proof stays on `3b65bdc`'s
69
+ own SHA. Coordinator-routed independent reviews (primary and supplemental) closed
70
+ every shipped implementation leaf.
71
+ - Kill-power re-measured at this shipped SHA: none (all 1000 committed claims not
72
+ re-proved, #144).
73
+ - Earlier-candidate PARTIAL (operator-attested, not this SHA): head 864e026877ca surface
74
+ 200536056020 -- `["bash","run.sh","check-win32-preview"]` 19/19 receipt b4de9e681dfb
75
+ - Earlier-candidate PARTIAL (operator-attested, not this SHA): head 864e026877ca surface
76
+ 200536056020 -- `["bash","run.sh","check-pack-win32-carrier"]` 2/2 receipt
77
+ c51ef11b84a6
78
+ - Earlier-candidate PARTIAL (operator-attested, not this SHA): head 3e527cdbea81 surface
79
+ 8f5cc4c34473 -- `["bash","run.sh","check-node-floor-coherence"]` 2/2 receipt
80
+ 6c0ec40a291f
81
+ - **Static/LIVE split.** Local P4/M0 `check:full` is not run on this operator host (its
82
+ FULL includes Herdr checks prohibited here) and is not counted as passed: the whole
83
+ static floor is owed to this prepared SHA's exact push CI.
84
+ - **Cut blockers.** VERIFY §3.4 (strict `doctor-meta-bridge` on an installed 0.39.0
85
+ Claude host in a new post-install session) and §3.7 (three agy doctors and a same-gid
86
+ native-push reply in a new operator conversation) need the operator and are not met by
87
+ this record; no tag or GitHub release until they are or GLG decides otherwise. npm
88
+ publication is outside the current authorization and was not performed; it needs a
89
+ separate GLG GO.
90
+ - **Herdr follows core.** `plugins/herdr/runtime-lock.json` stays on the closed
91
+ `herdr-checkout` source (comment re-dated only); no plugin manifest version, registry
92
+ pin or snapshot changes in this release.
93
+
7
94
  ## 0.38.1 - 2026-10-17
8
95
 
9
96
  ### pi-durable
@@ -0,0 +1,24 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net>
4
+
5
+ Permission is hereby granted, free of charge, to any person
6
+ obtaining a copy of this software and associated documentation
7
+ files (the "Software"), to deal in the Software without
8
+ restriction, including without limitation the rights to use,
9
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the
11
+ Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -78,8 +78,20 @@ Plain MCP registration alone does not make a garden citizen.
78
78
  installation is **CERTIFIED (CI)**, weaker than a physical-host doctor; its native
79
79
  harness rails, ACP and mux remain **NOT CERTIFIED — pending physical host**.
80
80
  Detected harness wiring on Darwin can therefore be written while setup stays
81
- non-green. Native Windows is **UNSUPPORTED**. The package has no npm `os`
82
- restriction; that is not certification of every platform's harness rails.
81
+ non-green. WSL2: a CI runner (experimental) measured the Entwurf-only install and a
82
+ zero-token Pi citizen; general, physical-machine or harness-parity WSL2 support is **not
83
+ declared**, and delivery, other harnesses and physical machines are unmeasured. Native
84
+ Windows has an **experimental first preview**, a CI-built x64 addon on `main` for the
85
+ 0.39.0 candidate (not yet released; release and npm publication pending); the registry's
86
+ 0.38.1 has no Windows addon. On a GitHub Actions `windows-2025` x64 runner with Node
87
+ 24.18.1 and Pi 1.3.0, a CI-built artifact was installed, wired with `pi install`, birthed
88
+ two Pi citizens and
89
+ delivered one garden-id `entwurf_v2` message between them, with an offline scripted model
90
+ and no vendor turn. Every other native-Windows surface (other harnesses, `setup` and the
91
+ `.sh` bins, fresh/resume, mux/Herdr/ACP, physical machines) is **not declared**; the
92
+ measured scope and its named gaps are in [BASELINE](./BASELINE.md#release-host-baseline).
93
+ The package has no npm `os` restriction; that is not certification of every platform's
94
+ harness rails.
83
95
 
84
96
  ### Native durable (pi-durable)
85
97
 
package/VERIFY.md CHANGED
@@ -276,14 +276,16 @@ caller/target/cross-harness LIVE are owed by `smoke-pi-durable-fresh-live`, a re
276
276
  | Linux artifact consumer | required `check-install-container` CI job against one preserved candidate | Fixtures prove package/oracle shape, not a real Claude lifecycle. |
277
277
  | Exact release commit | the exact SHA's push run: all four required CI jobs green (the qualification body is not read, #144) | A different green SHA is not transferable evidence. |
278
278
  | macOS Entwurf-only install | required `macos-install-surface` CI job at the exact release SHA | Shape-only, CERTIFIED (CI): certifies the Entwurf-only install path on macOS only — never Claude/Copilot/OMP/agy wiring, the pi rail, ACP or mux. Promoted from non-voting (`continue-on-error`) to required in 0.20.0 after its first CERTIFIED (CI) green on a GitHub Actions `macos-latest` runner (not a physical host): run 34303884286 @ `70eda03` (2026-09-09), 65s, all 10 steps success — the packed candidate installed clean, `entwurf --help` and `entwurf check-bridge` ran (the latter listing the seven `entwurf-bridge` MCP tools of that date; eight today), an all-harness-absent `entwurf setup` reached `result: green` across the five documented SKIP probes (`pi`/`claude`/`agy`/`copilot`/`omp`), and five checked paths (`.pi`, `settings.json`, `.gemini`, `.copilot`, `auth.json.bak`) plus a byte-identical `auth.json` proved zero harness/credential writes. A macOS runner failure now blocks every release cut, the same as any other required job. |
279
+ | Native-Windows carrier (carrier-bearing release, from 0.39.0) | one `windows-native-preview.yml` dispatch at the exact prepared SHA after its four `ci.yml` jobs are green: accepted build receipt (two byte-identical MSVC builds, exact import set), `pack` printing `present`, eight Windows addon/citizen cells on the installed tarball; that run's `win32-preview-tgz` is the release candidate, retrieved (never packed locally) with its `SHA256SUMS`, the Windows-printed install hash and the downloaded hash equal, then accepted by the Linux `check-install-container` unchanged (release skill M3w) | Experimental, shape-only, non-voting for `ci.yml`: no vendor turn, x64 runner only, named gaps in BASELINE. The four `ci.yml` jobs (incl. macOS) saw an addon-less pack, so the carrier-bearing tarball's macOS install is unmeasured. Artifacts expire after 14 days; the preserved copy is the durable one. |
279
280
  | LIVE runtime | `LIVE=1 ./run.sh release-gate <scratch> --cut` plus any shipped on-demand backend axis | `--cut` enforces `SKIP=0`; a red wired gate blocks the cut. |
280
281
  | Native Claude host | installed strict doctor against a new real session | Missing live join is `NOT CERTIFIED`, not a fixture PASS. |
281
282
  | Native agy host | three doctors plus conversation-id-gated native-push round trip | Aggregate release-gate does not own an agy conversation id. |
282
283
  | Native Codex host | caller-seat topology accepted 2026-09-16 (56 assertions, exit 0); caller-DIRECTORY axis accepted the same day (65 assertions, exit 0) | Operator-owned birth/MCP/statusline/terminal-title atoms and the on-demand four-coordinate chain are measured on Linux at Codex 0.153.4: the app-server alone in session A (`$30/@41/%45`, pid 737636), and initial Pi / omitted-placement fresh Codex / Codex-opened outbound Pi all in a different session S (`$2`, `@46`/`@47`/`@48`; `@58`/`@59`/`@61` in the lane C run); exact callbacks and addressed delivery both ways. A ≠ S is the load-bearing half — the app-server's inherited `TMUX` names A, so the outbound Pi landing in S can only have come from the caller's own pane title, and its receipt records `seat-source=codex-title-anchor`. **#95 lane C added the second axis to the same card: WHERE the sibling starts.** Hop 1 joins four authorities that cannot borrow from each other — the pane's `#{pane_current_path}`, the vendor rollout's `session_meta.cwd`, the Entwurf record and the requested scratch — and hop 1's leg requests a cwd while hop 2 requests NONE, so hop 2's directory can only come from the Codex caller's own record; both are asserted different from the app-server's live `/proc/<pid>/cwd`. 65 and 56 are different contracts, not a regression. The 2026-09-12 fixed-home chain is PAST CONTRACT (#95 D1); `DELIVERY.md` owns every receipt and digest. Placement beside a TUI whose thread nobody named is unsupported and unclaimed. |
283
284
 
284
285
  The repo-local `entwurf-release` skill owns the `land → prepare → make → publish`
285
- state machine. Each mode is a separate GLG authorization. Preserve one candidate,
286
- accept and publish those exact bytes without repacking, and record its digest and
286
+ state machine. Each mode is a separate GLG authorization. Preserve one candidate
287
+ (for a carrier-bearing release, the one retrieved from the preview run, never a local
288
+ pack), accept and publish those exact bytes without repacking, and record its digest and
287
289
  consumer image outside this standing protocol.
288
290
 
289
291
  ### Verifying the two capabilities a gate cannot fully judge
@@ -385,6 +387,13 @@ not certified, a detected harness is named non-green (FAIL) whose wording is
385
387
  not an install failure: the wiring WAS written, but the rail is NOT CERTIFIED —
386
388
  pending physical host. That FAIL is the honest setup verdict, not a skip.
387
389
 
390
+ WSL2: a separate experimental workflow (`.github/workflows/wsl2-install-surface.yml`)
391
+ measures the Entwurf-only install and a zero-token Pi citizen inside a real WSL2 guest on
392
+ a GitHub-hosted Windows runner. The release oracle does not read it
393
+ (`verify-exact-ci.sh` selects only `ci.yml` runs), so it is not a release axis and a red
394
+ there blocks no cut. It runs when that workflow file changes on main, or by manual
395
+ dispatch. Recorded in [BASELINE.md](./BASELINE.md).
396
+
388
397
  ### 1.1 Variables (optional)
389
398
 
390
399
  ```bash
@@ -518,7 +527,7 @@ Pass: user/assistant turns accumulate normally; the transcript is not broken/emp
518
527
  The minimum passing bar:
519
528
 
520
529
  1. **Deterministic floor green:** `pnpm run check:full` passes (the core tier plus the hermetic-integration and package/install tiers, incl. `check-pack`), in the exact-SHA CI push run of the same prepared candidate (criterion 2's run). The qualification body is not a criterion (#144).
521
- 2. **All four required CI jobs green on the exact release commit:** `check`, `install-surface`, the required Linux `artifact-consumer`, and `macos-install-surface` — promoted from non-voting to required in 0.20.0 (see the release acceptance axes table above for what its green run does and does not certify); preserve the Linux artifact-consumer's tarball digest and image identity. A macOS runner failure now blocks the cut like any other required job.
530
+ 2. **All four required CI jobs green on the exact release commit:** `check`, `install-surface`, the required Linux `artifact-consumer`, and `macos-install-surface` — promoted from non-voting to required in 0.20.0 (see the release acceptance axes table above for what its green run does and does not certify); preserve the Linux artifact-consumer's tarball digest and image identity. For a carrier-bearing release those CI tarballs are addon-less and are NOT the release candidate: the exact release bytes are the M3w-retrieved preview tarball, accepted separately by the eight Windows cells and the Linux container (axes row "Native-Windows carrier"). The four-job floor, macOS included, is unchanged. A macOS runner failure now blocks the cut like any other required job.
522
531
  3. **Live floor MUST green:** `LIVE=1 ./run.sh release-gate <dir> --cut` exits 0 reporting `MUST PASS=N FAIL=0 SKIP=0`; with `--cut` a single SKIP is red, so the exit code itself now carries this condition. A BEHAVIOR FAIL is advisory, not blocking.
523
532
  4. **Native-host doctor green where the Claude meta-bridge is claimed:** a new post-install Claude session exists, live evidence is present, and the installed `doctor-meta-bridge` exits 0. `NOT CERTIFIED` is a release failure for that host, not a skip.
524
533
  5. **Honest self-recognition:** the bridged model identifies its actual harness/backend, lists `entwurf-bridge` as the single MCP server with its eight current tools, and presents a backend-native (not normalized) tool surface.
@@ -18,7 +18,9 @@ evidence split, and its three states must not collapse into "macOS is supported"
18
18
  ACP turn, mux. Wiring may be written; that is not a rail receipt. The remaining
19
19
  unmeasured Darwin doctor reason is per-process environment DISCOVERY
20
20
  (start-key is portable).
21
- - **UNSUPPORTED** — native Windows only. macOS is never this.
21
+ - **UNSUPPORTED** — native Windows `setup` and its harness rails. macOS is never this.
22
+ An experimental native-Windows Pi preview (0.39.0 candidate, not yet released; release
23
+ and npm publication pending) exists outside `setup`; see [BASELINE](../BASELINE.md#release-host-baseline).
22
24
 
23
25
  ## Requirements
24
26
 
@@ -84,6 +84,7 @@ import { probeNativeSenderAlive, reconcileSenderIdentityClaims, resolveCodexRequ
84
84
  import { applyOmpBridgeChildRootPolicy, defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaIdentityByGardenId, readMetaInbox, readMetaReceiverMarker, readMetaSenderMarker, requireBackend, } from "../../../pi-extensions/lib/meta-session.js";
85
85
  import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
86
86
  import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
87
+ import { probeSocketLiveness } from "../../../pi-extensions/lib/socket-probe.js";
87
88
  const HOME = os.homedir();
88
89
  // ============================================================================
89
90
  // OMP root policy — FIRST, before any lazy default-root consumer (#87 B1).
@@ -110,6 +111,14 @@ catch (err) {
110
111
  // Directory SOURCE is this adapter's own policy — the bridge honours an explicit
111
112
  // ENTWURF_DIR override the pi side does not. The path GRAMMAR is the shared leaf.
112
113
  const ENTWURF_DIR = process.env.ENTWURF_DIR ?? defaultControlSocketDir(HOME);
114
+ /** Is this session's own control address up? A socket file's presence on Linux/macOS. On
115
+ * Windows (#78) the address is a named pipe, which has no file to look at and must not be
116
+ * opened by a stat (that connects): the owner-validated probe answers instead. */
117
+ async function controlAddressPresent(socketPath) {
118
+ if (process.platform === "win32")
119
+ return (await probeSocketLiveness(socketPath)) === "alive";
120
+ return existsSync(socketPath);
121
+ }
113
122
  // ============================================================================
114
123
  // Live control-socket discovery for entwurf_peers lives in the TS fact-provider
115
124
  // (pi-extensions/lib/entwurf-fact-provider.ts → listEntwurfFacts), which the
@@ -171,7 +180,7 @@ class EntwurfSenderIdentityError extends Error {
171
180
  "the send is then marked external/non-replyable).");
172
181
  }
173
182
  }
174
- function buildStrictPiSenderEnvelope() {
183
+ async function buildStrictPiSenderEnvelope() {
175
184
  const sessionId = process.env.PI_SESSION_ID?.trim();
176
185
  const agentId = process.env.PI_AGENT_ID?.trim();
177
186
  const cwd = process.cwd();
@@ -195,7 +204,7 @@ function buildStrictPiSenderEnvelope() {
195
204
  const socketPath = controlSocketPathIn(ENTWURF_DIR, sessionId);
196
205
  const self = computeSelfAddressability({
197
206
  origin: "pi-session",
198
- socketAlive: existsSync(socketPath),
207
+ socketAlive: await controlAddressPresent(socketPath),
199
208
  socketPathComputable: true,
200
209
  });
201
210
  return {
@@ -258,7 +267,7 @@ async function buildMetaSenderEnvelope(identity, cwd, trustedMarker) {
258
267
  async function resolveAuthoritativeSender(context, cwd = process.cwd()) {
259
268
  const sessionId = process.env.PI_SESSION_ID?.trim();
260
269
  const agentId = process.env.PI_AGENT_ID?.trim();
261
- const pi = sessionId && agentId && cwd ? buildStrictPiSenderEnvelope() : null;
270
+ const pi = sessionId && agentId && cwd ? await buildStrictPiSenderEnvelope() : null;
262
271
  const marker = resolveTrustedMetaSenderIdentity({
263
272
  markerPath: process.env.ENTWURF_META_SENDER_MARKER?.trim() || undefined,
264
273
  });
@@ -463,7 +472,7 @@ server.tool("entwurf_self", "Return this caller's authoritative identity envelop
463
472
  // live socket). The old code synthesized the path and printed it as if
464
473
  // it existed — a lie when the session has no --entwurf-control (SE-1).
465
474
  const socketPath = controlSocketPathIn(ENTWURF_DIR, sender.sessionId);
466
- const socketState = existsSync(socketPath) ? "alive" : "expected";
475
+ const socketState = (await controlAddressPresent(socketPath)) ? "alive" : "expected";
467
476
  extra.socketPath = socketPath;
468
477
  extra.socketState = socketState;
469
478
  lines.push(socketState === "alive"
@@ -24,8 +24,17 @@
24
24
  * adapter keeps its own root policy: the pi side derives from HOME, the MCP
25
25
  * bridge honours `ENTWURF_DIR`. What is unified is the grammar, not the policy.
26
26
  *
27
- * Keep dependency-free except `node:path`.
27
+ * Windows (#78) has no socket files: the address is a named pipe,
28
+ * `\\.\pipe\entwurf-<sha256(canonRoot(dir))[:16]>-<gardenId>`. The digest keeps two
29
+ * store roots on one host apart without putting a path in the pipe namespace; it is not
30
+ * a new address axis (still the garden id plus the store root). The inverse grammar has
31
+ * no Windows half because nothing scans the pipe namespace: candidates come from records.
32
+ *
33
+ * Keep dependency-free except node builtins (`node:path`, and `node:crypto`/`node:fs`
34
+ * for the Windows root canonicalisation only).
28
35
  */
36
+ import { createHash } from "node:crypto";
37
+ import * as fs from "node:fs";
29
38
  import * as path from "node:path";
30
39
  /** Control-socket filename suffix. The stem is a bare garden id. */
31
40
  export const CONTROL_SOCKET_SUFFIX = ".sock";
@@ -47,8 +56,54 @@ export function defaultControlSocketDir(home) {
47
56
  * @returns {string}
48
57
  */
49
58
  export function controlSocketPathIn(dir, gardenId) {
59
+ if (process.platform === "win32")
60
+ return win32PipeNameFor(dir, gardenId);
50
61
  return path.join(dir, `${gardenId}${CONTROL_SOCKET_SUFFIX}`);
51
62
  }
63
+ /** Every Windows control pipe this package names starts here. */
64
+ export const WIN32_PIPE_PREFIX = "\\\\.\\pipe\\entwurf-";
65
+ /**
66
+ * The one spelling of a Windows store root, so Pi A, Pi B, the bridge and peer-facts
67
+ * derive the same pipe name from differently written paths. The nearest EXISTING ancestor
68
+ * of `path.win32.resolve(dir)` goes through `realpath` (8.3 names expanded, case from the
69
+ * filesystem); the components below it are appended as written. Separators become `\`,
70
+ * there is no trailing separator, and the whole string is lower-cased. Nothing is created
71
+ * or touched, so the control dir need not exist yet. A failure other than ENOENT throws.
72
+ *
73
+ * @param {string} dir
74
+ * @param {(p: string) => string} [realpath]
75
+ * @returns {string}
76
+ */
77
+ export function canonRootWin32(dir, realpath = fs.realpathSync.native) {
78
+ let head = path.win32.resolve(dir);
79
+ /** @type {string[]} */
80
+ const tail = [];
81
+ for (;;) {
82
+ try {
83
+ const joined = path.win32.join(realpath(head), ...tail);
84
+ return joined.replace(/[\\/]+$/, "").toLowerCase();
85
+ }
86
+ catch (err) {
87
+ const parent = path.win32.dirname(head);
88
+ if ( /** @type {{ code?: unknown }} */(err).code !== "ENOENT" || parent === head)
89
+ throw err;
90
+ tail.unshift(path.win32.basename(head));
91
+ head = parent;
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Forward grammar on Windows: `\\.\pipe\entwurf-<16 hex>-<gardenId>`.
97
+ *
98
+ * @param {string} dir
99
+ * @param {string} gardenId
100
+ * @param {(p: string) => string} [realpath]
101
+ * @returns {string}
102
+ */
103
+ export function win32PipeNameFor(dir, gardenId, realpath) {
104
+ const digest = createHash("sha256").update(canonRootWin32(dir, realpath), "utf8").digest("hex").slice(0, 16);
105
+ return `${WIN32_PIPE_PREFIX}${digest}-${gardenId}`;
106
+ }
52
107
  /**
53
108
  * Inverse grammar: a socket filename back to its garden id, or `null` when the
54
109
  * name does not carry the suffix.
@@ -14,6 +14,7 @@
14
14
  * asked to open, and touches nothing pi-specific — it is a pure transport over a path.
15
15
  */
16
16
  import * as net from "node:net";
17
+ import { win32Native } from "./win32-native.js";
17
18
  /**
18
19
  * THE `<sender_info>` synthesis — the one place the sender envelope becomes
19
20
  * message text. ONE consumer since the visible-first cut (#50 C3):
@@ -101,6 +102,8 @@ export async function fetchControlSocketRuntimeInfo(socketPath, options = {}) {
101
102
  }
102
103
  export async function sendRpcCommand(socketPath, command, options = {}) {
103
104
  const { timeout = 5000 } = options;
105
+ if (process.platform === "win32")
106
+ return await sendPipeRpcCommand(socketPath, command, timeout);
104
107
  return new Promise((resolve, reject) => {
105
108
  const socket = net.createConnection(socketPath);
106
109
  socket.setEncoding("utf8");
@@ -173,3 +176,25 @@ export async function sendRpcCommand(socketPath, command, options = {}) {
173
176
  });
174
177
  });
175
178
  }
179
+ /**
180
+ * The Windows client (#78). The addon makes one owner-validated connect, writes the request
181
+ * line and returns the first response line, all inside one deadline; a timeout is
182
+ * `ETIMEDOUT`, a pipe owned by anyone but this token's expected owner is `EPERM` before any
183
+ * byte. The protocol is request-bound on its own connection, so that first line IS the
184
+ * answer: anything other than this command's response is refused, never waited past.
185
+ */
186
+ async function sendPipeRpcCommand(pipeName, command, timeout) {
187
+ const { responseLine } = await win32Native().pipeRpc(pipeName, JSON.stringify(command), Math.max(0, Math.floor(timeout)));
188
+ let msg;
189
+ try {
190
+ msg = JSON.parse(responseLine.trim());
191
+ }
192
+ catch {
193
+ throw new Error(`control pipe answered ${command.type} with a line that is not JSON`);
194
+ }
195
+ const response = msg;
196
+ if (response?.type !== "response" || response.command !== command.type) {
197
+ throw new Error(`control pipe answered something other than the ${command.type} response`);
198
+ }
199
+ return { response };
200
+ }
@@ -52,6 +52,7 @@
52
52
  import { execFileSync } from "node:child_process";
53
53
  import * as crypto from "node:crypto";
54
54
  import * as fs from "node:fs";
55
+ import { createRequire } from "node:module";
55
56
  import * as os from "node:os";
56
57
  import * as path from "node:path";
57
58
  import { generateSessionId, SESSION_ID_RE } from "./session-id.js";
@@ -845,6 +846,8 @@ const O_NONBLOCK_FLAG = typeof fs.constants.O_NONBLOCK === "number" ? fs.constan
845
846
  * Shaping a message is the job of the caller that knows what the answer is FOR.
846
847
  */
847
848
  export function readStoreRecordFile(file) {
849
+ if (process.platform === "win32")
850
+ return win32RecordReader()(file).toString("utf8");
848
851
  if (O_NOFOLLOW_FLAG === 0) {
849
852
  throw new MetaRecordError(`cannot read meta-record ${path.basename(file)} safely: this platform exposes no O_NOFOLLOW, so a record ` +
850
853
  "read cannot refuse a symlink swapped in at the final component. entwurf certifies Linux only.");
@@ -863,6 +866,17 @@ export function readStoreRecordFile(file) {
863
866
  fs.closeSync(fd);
864
867
  }
865
868
  }
869
+ /**
870
+ * Windows has no `O_NOFOLLOW` (libuv defines it as 0), so the same three decisions — a
871
+ * final-component reparse point refused as `ELOOP`, a non-regular entry as
872
+ * `ENTWURF_ENOTREG`, both on the handle the read uses — are made by the preview addon
873
+ * (#78). It is reached only here and only on win32, through `createRequire`, so the
874
+ * installers that copy this file without its siblings never load it on Linux or macOS.
875
+ */
876
+ function win32RecordReader() {
877
+ const native = createRequire(import.meta.url)("./win32-native.js");
878
+ return native.win32ReadNoFollow;
879
+ }
866
880
  /** The reader every store-WIDE scan pairs with {@link readActiveStoreEntries}. The
867
881
  * caller's entry kind decides the POLICY — whether a non-regular entry is a defect
868
882
  * (certification), a diagnostic (listing) or a non-candidate (the rival scan) — because
@@ -51,7 +51,7 @@
51
51
  */
52
52
  import * as fs from "node:fs/promises";
53
53
  import * as os from "node:os";
54
- import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir, gardenIdFromSocketFilename, } from "./control-socket-path.js";
54
+ import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir, gardenIdFromSocketFilename, WIN32_PIPE_PREFIX, } from "./control-socket-path.js";
55
55
  import { SESSION_ID_RE } from "./session-id.js";
56
56
  import { probeSocketLiveness } from "./socket-probe.js";
57
57
  /** Canonical control-socket directory; the socket filename IS the gardenId
@@ -81,7 +81,9 @@ export function controlSocketPath(gardenId, dir = CONTROL_SOCKET_DIR) {
81
81
  * `record-less-socket` reject (a diagnostic state) — no probe, no acceptance.
82
82
  */
83
83
  export function isRecordLessSocketCandidate(inspection) {
84
- return inspection.kind === "socket-file";
84
+ // A Windows pipe name (#78) is never lstat-ed (see inspectControlSocketPath), so its
85
+ // `socket-file` carries no presence evidence and cannot confirm a record-less socket.
86
+ return inspection.kind === "socket-file" && !inspection.socketPath.startsWith(WIN32_PIPE_PREFIX);
85
87
  }
86
88
  /**
87
89
  * Inspect the EXACT control-socket path given (no gid re-derivation) and classify it by
@@ -94,6 +96,12 @@ export function isRecordLessSocketCandidate(inspection) {
94
96
  * symlink is caught as an address-conflict and never connected, P1).
95
97
  */
96
98
  export async function inspectControlSocketPath(socketPath, lstatFn = (p) => fs.lstat(p)) {
99
+ // Windows (#78): the address is a named pipe. The pipe filesystem has no symlinks to
100
+ // catch, and opening the name to stat it would CONNECT to it and consume an instance.
101
+ // So the name is never stat-ed: it is safe to probe because the probe itself refuses a
102
+ // pipe whose owner is not this token's expected owner before any byte.
103
+ if (socketPath.startsWith(WIN32_PIPE_PREFIX))
104
+ return { kind: "socket-file", socketPath };
97
105
  let st;
98
106
  try {
99
107
  st = await lstatFn(socketPath);
@@ -160,8 +168,12 @@ export async function mapInspectionToLiveness(inspection, probeSocket) {
160
168
  */
161
169
  export async function scanSocketProbes(piCitizenGardenIds, deps = {}) {
162
170
  const dir = deps.dir ?? CONTROL_SOCKET_DIR;
171
+ // Windows (#78) has no socket files to list and nothing scans the pipe namespace: the
172
+ // candidates are the record-backed citizens alone.
163
173
  const readdir = deps.readdir ??
164
174
  (async (d) => {
175
+ if (process.platform === "win32")
176
+ return [];
165
177
  const dirents = await fs.readdir(d, { withFileTypes: true });
166
178
  return dirents.map((e) => ({ name: e.name, isSymbolicLink: e.isSymbolicLink() }));
167
179
  });
@@ -28,6 +28,7 @@
28
28
  * hidden — `entwurf_peers` shows it and the v2 decider rejects on it by name.
29
29
  */
30
30
  import * as net from "node:net";
31
+ import { win32Native } from "./win32-native.js";
31
32
  export const DEFAULT_PROBE_TIMEOUT_MS = 300;
32
33
  /**
33
34
  * Pure classification of a connect-time error code into liveness. Only the two
@@ -56,6 +57,8 @@ export function shouldUnlinkOnGc(liveness) {
56
57
  */
57
58
  export async function probeSocketLiveness(socketPath, opts = {}) {
58
59
  const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
60
+ if (process.platform === "win32")
61
+ return await probePipeLiveness(socketPath, timeoutMs);
59
62
  return await new Promise((resolve) => {
60
63
  const socket = net.createConnection(socketPath);
61
64
  const timer = setTimeout(() => {
@@ -76,3 +79,21 @@ export async function probeSocketLiveness(socketPath, opts = {}) {
76
79
  });
77
80
  });
78
81
  }
82
+ /**
83
+ * The Windows probe (#78): the control address is a named pipe, and a connect alone would
84
+ * talk to whoever holds the name. The addon connects within one deadline and refuses a pipe
85
+ * whose owner is not this token's expected owner (`EPERM`) before any byte. Only `ENOENT`
86
+ * (no instance) is dead; a refused owner, a busy pipe past the deadline and every Win32
87
+ * failure stay indeterminate. A missing or drifted addon is not a liveness fact: that
88
+ * refusal throws.
89
+ */
90
+ async function probePipeLiveness(pipeName, timeoutMs) {
91
+ const native = win32Native();
92
+ try {
93
+ await native.pipeProbe(pipeName, Math.max(0, Math.floor(timeoutMs)));
94
+ return "alive";
95
+ }
96
+ catch (err) {
97
+ return classifyConnectError(err.code);
98
+ }
99
+ }