@junghanacs/entwurf 0.12.4 → 0.12.6

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/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.12.6 — 2026-07-03
8
+
9
+ ### Fixed
10
+
11
+ - **Claude Code meta-bridge live marketplace sources now live outside the checkout for dev and npm installs alike.** Development clones and installed packages both assemble the plugin bundle under `$XDG_DATA_HOME/entwurf/meta-bridge/.assembled`; the checkout or `node_modules` tree is now only the source origin. This removes the class where `pnpm check`, `git clean -xfd`, or a repo-local uninstall smoke could delete a user-scope Claude marketplace source and leave new Claude Code sessions at statusline `?`. The installer still chooses `.ts` vs compiled `.js` hook artifacts by source origin, but the live registration path is the same XDG artifact lane.
12
+ - **Meta-bridge uninstall now uses the recorded live-artifact path and fails before side effects when the state is corrupt.** The install state's `assembledMarketplacePath` is the SSOT for uninstall, doctor, and state checks. Uninstall validates the full `…/entwurf/meta-bridge/.assembled` suffix before removing Claude plugin/MCP registrations or deleting state, so malformed paths such as `/` or `…/entwurf/meta-bridge/not-assembled` fail loud with zero Claude side effects and leave state/artifacts intact. `state.py check` also rejects missing, empty, or malformed recorded paths even when settings agree with the corruption.
13
+ - **User-scope pi package registration is now part of setup.** `./run.sh setup` registers `entwurf` in user-scope pi settings so `--entwurf-control` and the provider surface load from foreign cwd sessions, not only from a project-local `.pi/settings.json`. A hermetic `smoke-user-scope-citizen` covers idempotency, stale normalization, remove symmetry, unrelated-key preservation, and corrupt-shape fail-loud behavior.
14
+
15
+ ### Changed
16
+
17
+ - **The install toolchain is now aligned to one pnpm 11 lane and one setup surface.** The repo dropped the obsolete `packageManager` pin and `.npmrc`, moved pnpm policy to `pnpm-workspace.yaml`, updated CI to pnpm 11.9, and treats `./run.sh setup` as the single local install path instead of a split `pi install` flow.
18
+ - **The next delivery lane is pinned before implementation.** `NEXT.md` now records the agy/Antigravity delivery adapter constraints: direct-inject/native-push is separate from pi's control-socket liveness domain, registration is explicit, receiver markers are not reused for native-push replyability, and install/doctor/uninstall must inherit the source-origin/live-artifact boundary proven by this release.
19
+ - **The future fresh-spawn lane stays mux-visible by default.** The mux driver plan now keeps minting and mux transport separate, treats tmux/zmx as drivers behind a leaf interface, and keeps fresh pi-native GPT launch visible instead of reviving hidden background one-shots as the default.
20
+
21
+ ### Verification
22
+
23
+ - `pnpm check` passed on 2026-07-03 after the XDG live-artifact move, recorded-path uninstall hardening, user-scope citizen smoke, pnpm 11 setup cleanup, and NEXT lane updates.
24
+ - `./run.sh smoke-meta-install-state` passed with the new direct install→XDG proof, recorded-path mismatch case, corrupt basename/missing-field fail-loud cases, and checkout-internal `.assembled` boundary fingerprint.
25
+ - `./run.sh doctor-meta-bridge` passed on 2026-07-03 with source/assembled/installed writer parity all v2 and the assembled bundle under `$XDG_DATA_HOME/entwurf/meta-bridge/.assembled`.
26
+ - `./run.sh check-pack-install` passed on 2026-07-03, proving the npm consumer path keeps the stable XDG marketplace source and user-scope pi citizen registration.
27
+ - `LIVE=1 ./run.sh release-gate /tmp/psa-release-gate-0.12.6.ddCcmz` passed on 2026-07-03; log `/tmp/pi-tmux-entwurf-release-gate-0126.log`; summary `MUST: PASS=17 FAIL=0 SKIP=0`, advisory `BEHAVIOR: PASS=0 FAIL=1` (`smoke-resident-garden-guard` positive `/gnew T3` model-in-loop identity turn; non-blocking).
28
+
29
+ ## 0.12.5 — 2026-07-01
30
+
31
+ ### Fixed
32
+
33
+ - **The plugin hook now runs as compiled JS when installed — the strip-types class is closed for good.** The SessionStart/UserPromptSubmit hook ran as a raw `meta-bridge-hook.ts`. When the marketplace source sits below `node_modules` (a global npm/pnpm install), Claude executes that `.ts` directly and Node refuses strip-types there (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so every session failed. The hook was the last `.ts`-at-runtime surface; `build-bridge` now emits `dist/pi-extensions/meta-bridge-hook.js` and the installer copies that compiled closure (dev clones still run the `.ts` for transparent editing), mirroring the same installed-vs-dev split `start.sh` (0.12.1) and the store-doctor (0.12.4) already use. `check-pack-install` now runs the installed compiled hook **from under `node_modules` with plain `node`** and proves a raw `.ts` at the same location is refused — the exact fence that broke real hosts is a green regression. The doctor's writer-version parity compares `meta-session.<js|ts>` by install mode so a compiled bundle no longer false-STALEs against the `.ts` source.
34
+ - **Installed meta-bridge paths are now stable across package upgrades.** The installed statusline uses a new `entwurf-statusline` bin shim (matching the existing `entwurf-bridge` MCP shim), and installed meta-bridge marketplace assemblies move out of versioned pnpm store paths into a version-stable operator data directory. Package upgrades still require `entwurf install-meta-bridge` to refresh the Claude plugin bundle/cache, but old-store dead links no longer strand `statusLine` or marketplace source paths. The doctor now also executes the cached SessionStart hook once in an isolated temp agent dir, catching stale plugin-cache / hook-syntax failures before the next real Claude Code session.
35
+
36
+ ### Verification
37
+
38
+ - Oracle and local review both reproduced the installed hook fence: compiled `meta-bridge-hook.js` runs from under `node_modules` with plain `node`, while the raw `.ts` at the same location fails specifically with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
39
+ - `./run.sh check-pack` and `./run.sh check-pack-install` passed on 2026-07-01 with the hook-JS tarball requirements, baked `hooks.json` `.js` assertion, and raw-`.ts` fence reproduction.
40
+ - `pnpm check` passed on 2026-07-01 after the hook-JS + stable installed-path changes.
41
+ - `./run.sh install-meta-bridge && ./run.sh doctor-meta-bridge` passed from the development clone after re-materializing the gitignored `.assembled` bundle.
42
+
7
43
  ## 0.12.4 — 2026-07-01
8
44
 
9
45
  ### Fixed
package/README.md CHANGED
@@ -80,10 +80,11 @@ A few words that look unusual for a coding tool.
80
80
  adapter that hosts the ACP plugin and live control-socket surface, but the base
81
81
  install is **not** `pi install npm:...` anymore.
82
82
 
83
- The package exposes two bins:
83
+ The package exposes three bins:
84
84
 
85
85
  - `entwurf` → `run.sh` (installer, checks, meta-bridge doctor/install)
86
86
  - `entwurf-bridge` → the MCP stdio launcher (`mcp/entwurf-bridge/start.sh`)
87
+ - `entwurf-statusline` → the Claude Code statusline renderer (`scripts/meta-bridge-statusline.sh`); a stable bin shim so a package upgrade tracks the current version without rewriting Claude settings
87
88
 
88
89
  The bridge does not provide Claude credentials, tokens, or subscription access,
89
90
  and does not bypass any backend auth. Whatever the operator's local `claude` /
@@ -187,6 +188,13 @@ raw `.ts` helpers under `node_modules`. If the doctor reports
187
188
  `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, reinstall a current package before
188
189
  trusting the floor result.
189
190
 
191
+ After upgrading a globally installed package, run `entwurf install-meta-bridge`
192
+ again before trusting native Claude Code sessions. The installed statusline and
193
+ MCP entries use stable bin shims, and the marketplace source uses a version-stable
194
+ operator data dir, but the Claude plugin bundle/cache still has to be
195
+ re-materialized for the new package writer. Existing Claude Code sessions should
196
+ be restarted after reinstall.
197
+
190
198
  For manual configuration, [`pi/settings.reference.json`](./pi/settings.reference.json)
191
199
  shows the pi adapter settings shape, and the external-host examples below show
192
200
  plain MCP registrations.
package/VERIFY.md CHANGED
@@ -88,46 +88,36 @@ The goal is not merely "invoke Claude Code." We want:
88
88
 
89
89
  ## 1. Setup
90
90
 
91
- entwurf supports two install paths; both end in the same runtime state (a valid `.pi/settings.json` with `entwurfProvider.mcpServers` wired) and differ only in who owns the checkout.
91
+ **One install command to remember: `./run.sh setup <project>`.** It is idempotent — re-run the exact same command whenever anything looks wrong. There is no second install surface to juggle: from a clone `setup` runs the whole floor in order.
92
92
 
93
- | Path | Who | Shape |
94
- |------|-----|-------|
95
- | **AConsumer** | end-user of pi | `pi install git:…` + one `run.sh install .` |
96
- | **B — Developer** | contributor / first user | `git clone …` + `pi install ./` + `run.sh install …` |
97
-
98
- ### 1.1 Path A — consumer install
93
+ 1. `pnpm install` bundles pi (a dev/peer dependency; no separate `pi install` step) and builds the bridge
94
+ 2. project wiring → `<project>/.pi/settings.json` `entwurfProvider.mcpServers.entwurf-bridge`
95
+ 3. meta-bridge global plugin only when a native harness (`claude`) is on PATH; a pi-only host skips it cleanly
96
+ 4. `entwurf-bridge` install smoke (`validate_entwurf_bridge`)
99
97
 
100
98
  ```bash
101
- pi install git:github.com/junghan0611/entwurf # pi auto-clones + installs deps
102
- cd /path/to/consumer-project
103
- ~/.pi/agent/git/github.com/junghan0611/entwurf/run.sh install . # wire bundled mcpServers
104
- pi --list-models entwurf # curated model surface
105
- pi --provider entwurf --model claude-sonnet-5 -p "reply with ok only" # one-turn smoke
99
+ git clone https://github.com/junghan0611/entwurf /path/to/entwurf && cd $_
100
+ ./run.sh setup /path/to/consumer-project
101
+ # re-run the SAME command any time to repair a broken install
106
102
  ```
107
103
 
108
- Expected: the package appears under pi's `User packages`; `install .` logs `added entwurfProvider.mcpServers.entwurf-bridge` + `updated <project>/.pi/settings.json`; the smoke returns a one-word reply (full bootstrap ACP session bridge → clean shutdown). Note: `~/.pi/agent/git/.../entwurf` is pi-managed — do not edit it (a `pi update` overwrites). Step 2 is still required after `pi install git:…`.
104
+ Expected tail: `DONE: entwurf setup (pi package + meta-bridge + v2 install smoke) green.` On a host with `claude`, verify the native wiring with `./run.sh doctor-meta-bridge`.
105
+
106
+ The wiring / meta-bridge / smoke steps are internal building blocks of `setup` (`install_local_package`, `scripts/meta-bridge-install.sh`, `validate_entwurf_bridge`) — call `setup`, never the parts. Consumers who `npm install @junghanacs/entwurf` get the obvious npm surface; that path is not the developer concern here.
109
107
 
110
- ### 1.2 Path B — developer install
108
+ ### 1.1 Variables (optional)
111
109
 
112
110
  ```bash
113
- git clone https://github.com/junghan0611/entwurf /path/to/entwurf && cd $_
114
- pnpm install # pnpm is the pinned packageManager
115
- pi install ./
116
- ./run.sh install /path/to/consumer-project
117
- pnpm check # full deterministic floor
118
- pi --provider entwurf --model claude-sonnet-5 -p "reply with ok only"
119
- LIVE=1 ./run.sh release-gate /path/to/consumer-project
111
+ export REPO_DIR=/path/to/entwurf
112
+ export PROJECT_DIR=/path/to/consumer-project
113
+ cd "$REPO_DIR" && ./run.sh setup "$PROJECT_DIR"
120
114
  ```
121
115
 
122
- `install` is idempotent; user-authored `mcpServers.<name>` overrides survive a re-run (`preserved (user override: …)`). `run.sh remove <dir>` deletes only entries whose command matches the repo-authored launcher.
123
-
124
- ### 1.3 Setup shortcut + variables
116
+ ### 1.2 Live acceptance (optional)
125
117
 
126
118
  ```bash
127
- export REPO_DIR=... # Path A: $HOME/.pi/agent/git/github.com/junghan0611/entwurf ; Path B: your clone
128
- export PROJECT_DIR=/path/to/consumer-project
129
- export CACHE_DIR=$HOME/.pi/agent/cache/entwurf/sessions
130
- cd "$REPO_DIR" && ./run.sh setup "$PROJECT_DIR"
119
+ LIVE=1 ./run.sh release-gate /path/to/consumer-project
120
+ pi --provider entwurf --model claude-sonnet-5 -p "reply with ok only" # one-turn smoke
131
121
  ```
132
122
 
133
123
  `setup` runs `pnpm install` + `install` + meta-bridge (native harness) + the v2 install smoke; a green `setup` implies the settings.json wiring and install surface are healthy. The full live floor is still `LIVE=1 ./run.sh release-gate`.
@@ -4,11 +4,11 @@ End-to-end install of **entwurf** on a host with only `git` available — no
4
4
  node, no npm package, no pi binary, no dotfiles. The point is to validate the
5
5
  public install surface as an outside user would experience it.
6
6
 
7
- > **Scope.** This is the entwurf 0.12.4 install recipe. The base package install
7
+ > **Scope.** This is the entwurf 0.12.5 install recipe. The base package install
8
8
  > is **neutral npm**, not `pi install npm:...`. Pi is an optional adapter lane for
9
- > the ACP provider / control-socket runtime. The 0.12.4 floor fix is explicitly
9
+ > the ACP provider / control-socket runtime. The 0.12.5 floor fix is explicitly
10
10
  > covered here: installed packages under `node_modules` must not run raw `.ts`
11
- > doctor helpers through Node strip-types.
11
+ > bridge, doctor, or plugin-hook helpers through Node strip-types.
12
12
 
13
13
  `entwurf` is a garden-citizen dispatch substrate and meta-bridge. It does not
14
14
  provide, copy, or mediate backend credentials — it lets the official backend CLI
@@ -31,7 +31,7 @@ ssh cleanhost 'uname -a; whoami; which git node npm pi claude 2>/dev/null'
31
31
  |---|---|---|
32
32
  | Node | **24** recommended; `>=22.6.0` minimum | `engines.node` (Node strip-types / ESM runtime) |
33
33
  | npm | bundled with Node 24 | public package install path |
34
- | entwurf | `@junghanacs/entwurf` | neutral npm package; exposes `entwurf` and `entwurf-bridge` bins |
34
+ | entwurf | `@junghanacs/entwurf` | neutral npm package; exposes `entwurf`, `entwurf-bridge`, and `entwurf-statusline` bins |
35
35
  | pi binary | **optional**, `@earendil-works/pi-coding-agent >=0.80.3 <0.81` | needed only for the pi adapter / ACP provider / spawn-bg resume lane |
36
36
 
37
37
  ## Stage 0 — Node 24 via nvm
@@ -64,6 +64,7 @@ npm install -g @junghanacs/entwurf
64
64
 
65
65
  which entwurf
66
66
  which entwurf-bridge
67
+ which entwurf-statusline
67
68
  entwurf --help | head -5
68
69
  ```
69
70
 
@@ -77,8 +78,11 @@ Prove the installed MCP server answers `tools/list` from inside `node_modules`.
77
78
  This is the first `node_modules` strip-types regression fixed in 0.12.0: Node
78
79
  refuses `--experimental-strip-types` for `.ts` under `node_modules`, so the
79
80
  installed package must boot the prebuilt JS under `mcp/entwurf-bridge/dist/`.
80
- The 0.12.4 doctor fix below applies the same installed-vs-dev split to
81
- `doctor-meta-bridge` helper checks.
81
+ The same installed-vs-dev split then closes the other two `.ts`-at-runtime
82
+ surfaces on that same fence: the `doctor-meta-bridge` store-scan helper (0.12.4)
83
+ and the plugin `SessionStart`/`UserPromptSubmit` hook (0.12.5, runs the compiled
84
+ `dist/pi-extensions/meta-bridge-hook.js` when installed). Installed packages run
85
+ tsc-emitted JS on every one of these; dev clones keep the `.ts` source.
82
86
 
83
87
  ```bash
84
88
  node --input-type=module <<'JS'
@@ -146,17 +150,29 @@ entwurf doctor-meta-bridge
146
150
  ```
147
151
 
148
152
  On an installed package (`.../node_modules/@junghanacs/entwurf`), the doctor must
149
- not try to strip-types-run raw `.ts` helpers. In the output, check for these two
150
- 0.12.4 floor-regression signals:
153
+ not try to strip-types-run raw `.ts` helpers or hooks. In the output, check for
154
+ these 0.12.5 floor-regression signals:
151
155
 
152
156
  ```text
157
+ ok cached SessionStart hook executes cleanly in an isolated temp agent dir
153
158
  ok full store scan: no corrupt records, duplicate nativeSessionId, body/filename drift, or backend↔wakeMode contradiction
154
159
  ok check-entwurf-v2-surface: shipped surface source present; exhaustive source-shape gate is a repo/release invariant (not run under node_modules)
155
160
  ```
156
161
 
157
- If either section reports `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, the host
158
- is still running a pre-0.12.4 package or a broken tarball. Reinstall the current
159
- package and re-run `entwurf install-meta-bridge && entwurf doctor-meta-bridge`.
162
+ If any of those sections reports `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,
163
+ the host is still running a pre-0.12.5 package or a broken tarball. Reinstall the
164
+ current package and re-run `entwurf install-meta-bridge && entwurf doctor-meta-bridge`.
165
+
166
+ Upgrade invariant: every global npm/pnpm package upgrade must be followed by
167
+ `entwurf install-meta-bridge` from that same installed binary and then
168
+ `entwurf doctor-meta-bridge`. Installed statusline/MCP entries use stable bin
169
+ shims and the marketplace source lives in a version-stable operator data dir, but
170
+ package managers still do not re-materialize Claude's plugin bundle/cache. If a
171
+ dev checkout's `./run.sh doctor-meta-bridge` expects repo-owned paths while the
172
+ global install intentionally owns the meta-bridge (or the reverse), that is an
173
+ ownership mismatch — run the doctor from the surface that intentionally owns the
174
+ install, or reinstall from the other surface. Restart already-open Claude Code
175
+ sessions after changing the meta-bridge install.
160
176
 
161
177
  A plain external MCP host can call tools but is non-replyable. A garden-native
162
178
  meta-session has a garden id, a mailbox, and a trusted sender marker; it can call
@@ -0,0 +1,218 @@
1
+ /**
2
+ * meta-bridge-hook — the Claude Code native-session entry shell (1.0.0 step 4).
3
+ *
4
+ * THE load-bearing hook. Shipped inside a plugin bundle whose `hooks/hooks.json`
5
+ * wires it to `SessionStart` (and `CwdChanged` / `UserPromptSubmit`). Because a
6
+ * plugin's hooks load at STARTUP (a bare skill's load only on invocation, after
7
+ * SessionStart has passed), this fires on every native Claude Code open and makes
8
+ * that session a garden citizen WITHOUT any pi JSONL of its own:
9
+ *
10
+ * stdin {session_id, transcript_path, cwd} (Claude hook envelope)
11
+ * -> upsertMetaSession(claude-code) idempotent create/attach the record
12
+ * -> gardenId the session's garden address
13
+ * -> arm watchPath <mailbox>/<gardenId>/inbox.signal idle-wake doorbell
14
+ * -> stdout hookSpecificOutput.watchPaths (SessionStart / CwdChanged only)
15
+ *
16
+ * The record is keyed/looked-up by `native_session_id` (THE authority,
17
+ * scanByNativeId); the idle-wake mailbox is keyed by GARDEN id — that is the
18
+ * whole point of the meta-bridge over the raw per-session prototype: one garden
19
+ * address a sender can target, decoupled from the backend's native id grammar.
20
+ *
21
+ * FAILURE POLICY (decided, see NEXT.md step 4): the runtime hook is
22
+ * BEST-EFFORT + LOG. It never screams into the user's terminal and never blocks
23
+ * startup — on any error it appends a line to `<pi-agent-dir>/meta-bridge-hook.log`
24
+ * and emits an empty `{}` (no arm). The fail-LOUD surface is the doctor
25
+ * (step 5), which reads that log + the meta-record dir to catch a silent miss.
26
+ *
27
+ * watchPaths can be emitted only from SessionStart / CwdChanged / FileChanged, so
28
+ * a UserPromptSubmit fire does a degraded RECORD backfill (upsert) but cannot
29
+ * re-arm the idle watch — the record's address is restored, the wake is not.
30
+ *
31
+ * Run (dev clone): `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
32
+ * Run (installed): `<node> <plugin-root>/meta-bridge-hook.js` — the tsc-emitted
33
+ * closure (build-bridge → dist), because Node REFUSES strip-types on a `.ts` below
34
+ * node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
35
+ * the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
36
+ * Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); `rewriteRelativeImportExtensions`
37
+ * rewrites that to `./lib/meta-session.js` in the emitted closure. The installer
38
+ * copies the lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
39
+ */
40
+ import * as fs from "node:fs";
41
+ import * as path from "node:path";
42
+ import { defaultMetaMailboxDir, defaultMetaSessionsDir, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
43
+ function logLine(level, message) {
44
+ try {
45
+ // dirname(meta-sessions) == the pi agent dir — no extra resolver export needed.
46
+ const file = path.join(path.dirname(defaultMetaSessionsDir()), "meta-bridge-hook.log");
47
+ fs.mkdirSync(path.dirname(file), { recursive: true });
48
+ fs.appendFileSync(file, `${new Date().toISOString()} ${level} ${message}\n`);
49
+ }
50
+ catch {
51
+ /* logging is best-effort; a broken log must not break the session */
52
+ }
53
+ }
54
+ /** Emit a hook response on stdout and exit 0. `{}` means "did nothing, do not block startup". */
55
+ function emit(payload) {
56
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
57
+ process.exit(0);
58
+ }
59
+ /**
60
+ * Map the hook event to a receiver-marker arm provenance. ONLY the genuinely
61
+ * arm-capable events map; any other (a future/unknown hook event) returns null so we
62
+ * never mint an "active receiver" presence we cannot back — fail-closed, not an
63
+ * optimistic session-start. UserPromptSubmit never reaches here (it early-returns
64
+ * before the arm block).
65
+ */
66
+ function armProvenanceFor(eventName) {
67
+ if (eventName === "SessionStart")
68
+ return "session-start";
69
+ if (eventName === "CwdChanged")
70
+ return "cwd-changed";
71
+ if (eventName === "FileChanged")
72
+ return "file-changed";
73
+ return null;
74
+ }
75
+ function main() {
76
+ let raw;
77
+ try {
78
+ raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin (the Claude hook envelope)
79
+ }
80
+ catch (err) {
81
+ logLine("ERROR", `stdin read failed: ${err instanceof Error ? err.message : String(err)}`);
82
+ emit({});
83
+ }
84
+ let env;
85
+ try {
86
+ const parsed = JSON.parse(raw);
87
+ if (typeof parsed !== "object" || parsed === null)
88
+ throw new Error("envelope is not an object");
89
+ env = parsed;
90
+ }
91
+ catch (err) {
92
+ logLine("ERROR", `envelope parse failed: ${err instanceof Error ? err.message : String(err)}`);
93
+ emit({});
94
+ }
95
+ const sessionId = typeof env.session_id === "string" ? env.session_id : "";
96
+ const transcriptPath = typeof env.transcript_path === "string" ? env.transcript_path : "";
97
+ // cwd: prefer the envelope's, fall back to the process cwd (the hook runs in the session's cwd).
98
+ const cwd = typeof env.cwd === "string" && env.cwd.length > 0 ? env.cwd : process.cwd();
99
+ const modelEnvelope = env.model;
100
+ const model = typeof modelEnvelope === "object" &&
101
+ modelEnvelope !== null &&
102
+ typeof modelEnvelope.id === "string"
103
+ ? modelEnvelope.id
104
+ : typeof env.model_id === "string"
105
+ ? env.model_id
106
+ : undefined;
107
+ const eventName = typeof env.hook_event_name === "string" ? env.hook_event_name : "SessionStart";
108
+ if (!sessionId || !transcriptPath) {
109
+ // A degraded envelope: cannot mint an honest reference record. Log + no-op
110
+ // rather than write a half-record or guess a transcript path. LEVEL depends
111
+ // on the event: a degraded SessionStart / CwdChanged means the session FAILED
112
+ // to become (or refresh) a garden citizen — that is the silent registration
113
+ // miss the doctor must catch (blocker #2), so ERROR. UserPromptSubmit only
114
+ // ever does a best-effort record backfill, so a degraded one is just WARN.
115
+ const degradedLevel = eventName === "UserPromptSubmit" ? "WARN" : "ERROR";
116
+ logLine(degradedLevel, `degraded envelope (event=${eventName}, session_id=${sessionId ? "set" : "MISSING"}, transcript_path=${transcriptPath ? "set" : "MISSING"})`);
117
+ emit({});
118
+ }
119
+ let gardenId;
120
+ try {
121
+ const result = upsertMetaSession({
122
+ input: { backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
123
+ onSkip: (filename, e) => logLine("WARN", `scan skipped ${filename}: ${e.message}`),
124
+ });
125
+ gardenId = result.record.gardenId;
126
+ logLine("INFO", `${result.action} record ${path.basename(result.path)} (event=${eventName}, native=${sessionId})`);
127
+ }
128
+ catch (err) {
129
+ // Best-effort: a broken record store must surface via the doctor, not by
130
+ // breaking the user's session open. Log and continue with no arm. This is
131
+ // the silent-registration-miss (blocker #2): the session opened fine but is
132
+ // NOT a garden citizen — the doctor catches it via this ERROR line.
133
+ logLine("ERROR", `upsert failed (event=${eventName}, native=${sessionId}): ${err instanceof Error ? err.message : String(err)}`);
134
+ emit({});
135
+ }
136
+ // Sender marker, keyed by the shared Claude Code parent pid: the user-scope
137
+ // MCP child (same parent) reads it at entwurf_v2 send time to promote this
138
+ // session from anonymous external-mcp to a REPLYABLE meta-session sender —
139
+ // process ancestry, not cwd inference (same repo + multiple sessions would be
140
+ // ambiguous). Best-effort: a failed marker only costs reply-addressability
141
+ // (WARN), it does not break the session or the receiver path.
142
+ //
143
+ // SE-1/SE-2 (dual-owner fix): write ONLY for the direct parent (process.ppid =
144
+ // the Claude CLI that ran this hook, verified the native tree is direct — the
145
+ // plugin host is not in between). The old code ALSO wrote a marker for the
146
+ // grandparent. That grandparent is the login shell (e.g. bash under ghostty/i3),
147
+ // which OUTLIVES the Claude session: when Claude exits, the grandparent marker's
148
+ // ownerStartKey still matches a live pid, so it passes readMetaSenderMarker's
149
+ // reuse guard and the dead session keeps looking like a live, replyable receiver
150
+ // — a false-positive "active receiver" leak. The owner must be the watchPaths
151
+ // subscriber (the Claude CLI), nothing higher. If a stray topology means the MCP
152
+ // child's shared ancestor is not process.ppid, that resolves to "no marker"
153
+ // (fail-closed, honest) rather than a wrong-but-live grandparent identity.
154
+ const ownerPid = process.ppid;
155
+ if (typeof ownerPid === "number" && ownerPid > 0) {
156
+ try {
157
+ writeMetaSenderMarker({ backend: "claude-code", gardenId, nativeSessionId: sessionId, cwd, ownerPid });
158
+ logLine("INFO", `sender marker ${ownerPid} -> ${gardenId} (event=${eventName})`);
159
+ }
160
+ catch (err) {
161
+ logLine("WARN", `sender marker write failed (event=${eventName}, pid=${ownerPid}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
162
+ }
163
+ }
164
+ // watchPaths is emittable only from SessionStart / CwdChanged / FileChanged.
165
+ // UserPromptSubmit reaches here only to backfill the record above; it must NOT
166
+ // claim to arm a watch it cannot (decided).
167
+ if (eventName === "UserPromptSubmit") {
168
+ emit({});
169
+ }
170
+ try {
171
+ const mailbox = path.join(defaultMetaMailboxDir(), gardenId);
172
+ fs.mkdirSync(mailbox, { recursive: true });
173
+ const signal = path.join(mailbox, "inbox.signal");
174
+ if (!fs.existsSync(signal))
175
+ fs.writeFileSync(signal, "", { mode: 0o600 });
176
+ logLine("INFO", `armed watch ${signal}`);
177
+ // Receiver presence marker (SE-2): written on the arm-capable hook path that
178
+ // emits watchPaths, keyed by garden id with the watch owner pid (= the Claude
179
+ // CLI, process.ppid — same single owner as the sender marker, never the
180
+ // grandparent). It records that a LIVE owner reached the watch-arm emit; it is
181
+ // not proof the host ack'd the watch registration. This is what lets a sender
182
+ // tell a live receiver from a terminated one whose record still lingers.
183
+ // Best-effort: a failed/skipped marker only costs deliverability detection
184
+ // (WARN), it does not break the arm. An unknown event maps to null provenance →
185
+ // no marker (fail-closed: never claim an active receiver we cannot back).
186
+ const ownerPid = process.ppid;
187
+ const armProvenance = armProvenanceFor(eventName);
188
+ if (armProvenance === null) {
189
+ logLine("WARN", `receiver marker skipped — non-arm event ${eventName} (garden=${gardenId})`);
190
+ }
191
+ else if (typeof ownerPid === "number" && ownerPid > 0) {
192
+ try {
193
+ writeMetaReceiverMarker({
194
+ gardenId,
195
+ backend: "claude-code",
196
+ nativeSessionId: sessionId,
197
+ ownerPid,
198
+ armProvenance,
199
+ });
200
+ logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName}`);
201
+ }
202
+ catch (err) {
203
+ logLine("WARN", `receiver marker write failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
204
+ }
205
+ }
206
+ emit({
207
+ hookSpecificOutput: {
208
+ hookEventName: eventName,
209
+ watchPaths: [signal],
210
+ },
211
+ });
212
+ }
213
+ catch (err) {
214
+ logLine("ERROR", `arm failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
215
+ emit({}); // record landed; only the arm failed — the doctor will flag the missing watch.
216
+ }
217
+ }
218
+ main();
@@ -46,5 +46,22 @@
46
46
  // emits here, so it adds ONE leaf JS (dist/scripts/meta-bridge-store-doctor.js)
47
47
  // and ZERO new deps. It is NOT in index.ts's boot closure, so
48
48
  // check-entwurf-bridge-pi-free (which walks the index eager closure) is unaffected.
49
- "include": ["./src/index.ts", "../../protocol.js", "../../scripts/meta-bridge-store-doctor.ts"]
49
+ //
50
+ // WHY meta-bridge-hook rides THIS build (0.12.5): the SessionStart/UserPromptSubmit
51
+ // plugin hook ran as raw `.ts` (`node ${CLAUDE_PLUGIN_ROOT}/meta-bridge-hook.ts`).
52
+ // Under an installed marketplace source below node_modules that is
53
+ // ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — the SAME strip-types fence
54
+ // start.sh (0.12.1) and the store-doctor (0.12.4) already cross by shipping JS.
55
+ // The hook was the last `.ts`-at-runtime surface; emitting it here closes that
56
+ // class for good (installer copies the dist JS when installed, keeps `.ts` for a
57
+ // dev clone). Its only repo import is meta-session.ts, ALREADY emitted for the
58
+ // store-doctor, so this adds ONE leaf JS (dist/pi-extensions/meta-bridge-hook.js)
59
+ // and ZERO new deps. It is a plugin hook, never in index.ts's boot closure, so
60
+ // check-entwurf-bridge-pi-free is unaffected.
61
+ "include": [
62
+ "./src/index.ts",
63
+ "../../protocol.js",
64
+ "../../scripts/meta-bridge-store-doctor.ts",
65
+ "../../pi-extensions/meta-bridge-hook.ts"
66
+ ]
50
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -65,7 +65,6 @@
65
65
  "engines": {
66
66
  "node": ">=22.6.0"
67
67
  },
68
- "packageManager": "pnpm@10.33.0",
69
68
  "dependencies": {
70
69
  "@agentclientprotocol/claude-agent-acp": "0.54.1",
71
70
  "@agentclientprotocol/sdk": "1.1.0",
@@ -120,7 +119,7 @@
120
119
  "format": "biome check --write .",
121
120
  "check-meta-manifest-schema": "./run.sh check-meta-manifest-schema",
122
121
  "check-package-source-routing": "./run.sh check-package-source-routing",
123
- "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./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-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./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-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
122
+ "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-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-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./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-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./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-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
124
123
  },
125
124
  "pi": {
126
125
  "extensions": [
@@ -132,7 +131,8 @@
132
131
  },
133
132
  "bin": {
134
133
  "entwurf": "./run.sh",
135
- "entwurf-bridge": "./mcp/entwurf-bridge/start.sh"
134
+ "entwurf-bridge": "./mcp/entwurf-bridge/start.sh",
135
+ "entwurf-statusline": "./scripts/meta-bridge-statusline.sh"
136
136
  },
137
137
  "peerDependenciesMeta": {
138
138
  "@earendil-works/pi-ai": {
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/meta-bridge-hook.ts"
9
+ "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
10
10
  }
11
11
  ]
12
12
  }
@@ -17,7 +17,7 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/meta-bridge-hook.ts"
20
+ "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
21
21
  }
22
22
  ]
23
23
  }
@@ -28,7 +28,7 @@
28
28
  "hooks": [
29
29
  {
30
30
  "type": "command",
31
- "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/meta-bridge-hook.ts"
31
+ "command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
32
32
  }
33
33
  ]
34
34
  }
@@ -28,9 +28,14 @@
28
28
  * a UserPromptSubmit fire does a degraded RECORD backfill (upsert) but cannot
29
29
  * re-arm the idle watch — the record's address is restored, the wake is not.
30
30
  *
31
- * Run: `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
32
- * Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); the installer copies
33
- * that lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
31
+ * Run (dev clone): `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
32
+ * Run (installed): `<node> <plugin-root>/meta-bridge-hook.js` the tsc-emitted
33
+ * closure (build-bridge dist), because Node REFUSES strip-types on a `.ts` below
34
+ * node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
35
+ * the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
36
+ * Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); `rewriteRelativeImportExtensions`
37
+ * rewrites that to `./lib/meta-session.js` in the emitted closure. The installer
38
+ * copies the lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
34
39
  */
35
40
 
36
41
  import * as fs from "node:fs";