@junghanacs/entwurf 0.34.1 → 0.34.2

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,108 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.34.2 - 2026-10-11
8
+
9
+ ### Release coordination
10
+
11
+ - **A patch cut for #136.** Coordinated by the Claude Code Fable citizen `20261011T132613-9a46b6`,
12
+ implemented by Claude Code Opus `20261008T094901-5d295e` as sole writer on the thinkpad that
13
+ holds the failing installation, and independently reviewed by a pi `gpt-6.1-sol` sibling. GLG
14
+ authorized the commits, main and this release. This section is written at prepare time: the
15
+ `check:full` and exact-SHA CI results will be recorded at make time in the GitHub release and
16
+ #136, not claimed here.
17
+ - **GLG waived the LIVE `release-gate --cut` for this cut** (doctor-only change, no LIVE surface
18
+ touched). The deterministic floor `check:full` and the exact-SHA CI body are this release's
19
+ evidence, and acceptance 4 (host rebind + new Claude session) is owed after publication. This is
20
+ not a PASS and not a standing waiver.
21
+ - **The plugin runtime lock rides `herdr-checkout` for this window.** `certifyLockCoherence`
22
+ (`scripts/herdr-runtime.mjs:353-361`) refuses an npm lock whose version differs from the
23
+ checkout's package, so `plugins/herdr/runtime-lock.json` leaves npm 0.34.1 for the closed
24
+ `herdr-checkout` source from this prep until the post-publish npm 0.34.2 re-pin; the plugin
25
+ manifest moves with that re-pin. A host already on npm 0.34.1 is unaffected until then and
26
+ upgrades same-source; a host that installs the plugin from `main` inside the window gets a
27
+ checkout-source runtime and needs `herdr-plugin-deactivate` before the npm reinstall.
28
+
29
+ ### Fixed
30
+
31
+ - **`doctor-meta-bridge` certifies a Herdr plugin installation (#136).** The plugin activation
32
+ writes Claude's MCP and statusLine as absolute commands under the stable runtime, but the doctor
33
+ computed its three expectations (`meta-bridge-state.py check`, `desired-statusline`,
34
+ `desired-mcp`) without that mode. It expected bare bins and reported correct wiring as drift, so
35
+ no plugin host could reach a green doctor. The doctor now learns its mode from the certified
36
+ activation ledger through one narrow call (`resolveDoctorPluginRuntime`,
37
+ `scripts/herdr-doctor-mode.mjs`). Derived plugin mode needs all four clauses:
38
+ - the ledger phase is `active`;
39
+ - `claude-code` was activated;
40
+ - the ledger's runtimeRoot is this host's stable root;
41
+ - the doctor runs from that runtime's package.
42
+ A flagless checkout doctor never opens the ledger. The explicit `--plugin-runtime <root>`
43
+ override bypasses only the repo clause and still reads and certifies the ledger, which it must
44
+ agree with. A refusal reports each expectation as UNKNOWN, never a bare-bin fallback, and
45
+ the runtime sections still run. The doctor prints the mode it used on one line.
46
+ - **`run.sh doctor-meta-bridge` forwards its arguments.** The verb was never shifted, so it rode
47
+ into the doctor's argv; the override was unreachable through the operator entrypoint.
48
+
49
+ ### Docs
50
+
51
+ - **CONTRIBUTING matches the scheduling rule.** A gate, mutant or matrix change runs only its
52
+ affected exact-argv groups as PARTIAL receipts; the whole qualification body runs once per cut,
53
+ in the prepared SHA's CI. CONTRIBUTING still told a gate-changing PR to run the body itself.
54
+
55
+ ### Verification
56
+
57
+ - New cells: `check-herdr-activation` D1-D6 and `check-meta-doctor-oracle` P1-P3. The existing
58
+ clone control is unchanged (P4). Six mutants joined the `herdr-activation` lane (now 34).
59
+ PARTIAL receipts: herdr-activation group 33/33 and doctor-oracle group 2/2 killed, control ok.
60
+ No whole body ran in development.
61
+ - **Host acceptance before publication (thinkpad, live plugin 0.34.1 wiring).** The patched
62
+ checkout doctor with `--plugin-runtime <stable root>` named `mode: plugin-runtime … (activation
63
+ ledger)`. Its only FAIL was writer-version parity, the declared non-goal of a checkout doctor on
64
+ a plugin host. The seven drift lines the shipped doctor printed are gone.
65
+ - **Owed after publication.** After the plugin is re-pinned to 0.34.2 and rebound on that host, a
66
+ flagless runtime-absolute `doctor-meta-bridge` must exit 0 and a new Claude session must call
67
+ `entwurf_self`. That is what closes #136.
68
+
69
+ ## v2026.10.9-herdr.0.34.1 — Herdr Entwurf plugin 0.34.1
70
+
71
+ A Herdr plugin snapshot, not a core release: no package version, npm artifact or core tag moves.
72
+
73
+ ### Herdr plugin
74
+
75
+ - **Runtime re-pinned to the published 0.34.1 (#135).** `plugins/herdr/runtime-lock.json` returns
76
+ from the `herdr-checkout` verification carrier the 0.34.1 preparation rode to the npm production
77
+ source: `@junghanacs/entwurf@0.34.1` with the registry's measured sha512
78
+ (`sha512-AiB88Ext…GDxA==`). The manifest version moves 0.34.0 → 0.34.1; `min_herdr_version`
79
+ stays 0.9.0. A plugin host now receives the #135 fixes listed under 0.34.1: owner preflight
80
+ before acquisition, the no-ledger deactivate, and the regular-file-or-nothing ledger address.
81
+ - **Upgrading.** A host on the npm 0.34.0 snapshot stays on the npm source, so its next
82
+ `herdr plugin install` is a same-source rebind under the plugin README's conditions. A host that
83
+ installed while the lock named `herdr-checkout` — the 0.30.1 plugin, or `main` during the 0.34.1
84
+ preparation window — holds a checkout-source runtime and ledger, and npm is a different
85
+ acquisition source, refused rather than inherited (`activation-artifact-source-drifted`). Run
86
+ `herdr-plugin-deactivate` by its absolute runtime path, then `herdr plugin install` again.
87
+ - **The READMEs no longer state a current release.** The root README's hardcoded release block and
88
+ the plugin README's snapshot and carrier prose went stale with every version bump; they now point
89
+ at `runtime-lock.json`, `herdr-plugin.toml`, Releases and this file. The source-switch rules and
90
+ the 0.34.0 runtime's no-ledger limit stay, the latter as a historical fact.
91
+
92
+ ### Verification
93
+
94
+ - **Registry, measured before the pin.** One scheduled observation, after a 300 s delay:
95
+ version 0.34.1 published at 2026-10-09T14:29:48.184Z with `latest` → 0.34.1, and the downloaded
96
+ tarball re-hashed to the artifact accepted as 0.34.1 (sha256 `73407ba8…`, 13587297 bytes), with
97
+ `dist.integrity` re-derived equal. Dist-tags before publication were not captured, so no
98
+ before/after claim is made about them.
99
+ - **VERIFY's 0.34.0 public-ref row is closed with its measured receipts:** `--ref main` PASS at
100
+ `b5d15d8` and `8ed4d1a`, both acquiring npm 0.34.0. That is a main-ref receipt, not a tag-ref
101
+ or native claim.
102
+ - No gate, mutant or matrix changed, so no qualification PARTIAL is owed. The npm-source re-proof
103
+ for this pin — the focused Herdr gates, a real-npm 0.34.0 → 0.34.1 swap and torn-swap recovery,
104
+ an installed registry consumer, the registry-file container consumer, the public `main`
105
+ first-user path and the exact-SHA CI run — is recorded in the GitHub release, not claimed here.
106
+ A `--ref <tag>` install, a citizen drawn as a ROW in a real Herdr pane, and native hosts stay
107
+ unmeasured.
108
+
7
109
  ## 0.34.1 - 2026-10-09
8
110
 
9
111
  ### Release coordination
package/CONTRIBUTING.md CHANGED
@@ -30,7 +30,7 @@ pnpm check # everyday core (prints wall time; <=60s on the referenc
30
30
  pnpm run check:full # full deterministic floor — the required PR gate
31
31
  ```
32
32
 
33
- The deterministic floor is tiered (#70). `pnpm check` is the everyday core (biome, tsc, the vitest lanes, and the fast contract gates); `pnpm run check:full` adds the hermetic-integration and package/install tiers — including `check-gate-manifests`, the qualification HEAD (runner self-test, manifest-set validation, declared lane inventory; zero COMMITTED mutants executed — the self-test runs only its synthetic fixture mutants in tmp fixture repos — ~20s), but not the separately scheduled mutant-EXECUTING body `check-gate-qualification`, which CI runs on a branch push whose diff touched the qualification surface (and unconditionally on dispatch or the weekly schedule) and a gate-changing PR must run once itself. Exact membership is the named `check:*` scripts in `package.json`. Run `check:full` once on your frozen commit candidate — the pre-commit hook (`.husky/pre-commit`) carries only fast static checks (whitespace, lint, typecheck), not the full floor, so a green `pnpm run check:full` before commit is the evidence that your change holds (scheduling contract: AGENTS.md "Verification scheduling"). No install or build lifecycle script installs git hooks: a global `core.hooksPath` guard that chains to repository-local hooks runs its own checks, then delegates — to `.husky/pre-commit` here, unless an executable `.git/hooks/pre-commit` takes precedence.
33
+ The deterministic floor is tiered (#70). `pnpm check` is the everyday core (biome, tsc, the vitest lanes, and the fast contract gates); `pnpm run check:full` adds the hermetic-integration and package/install tiers — including `check-gate-manifests`, the qualification HEAD (runner self-test, manifest-set validation, declared lane inventory; zero COMMITTED mutants executed — the self-test runs only its synthetic fixture mutants in tmp fixture repos — ~20s), but not the separately scheduled mutant-EXECUTING body `check-gate-qualification`, which CI runs on a branch push whose diff touched the qualification surface (and unconditionally on dispatch or the weekly schedule). Development never runs that whole body: a gate, mutant or matrix change runs only its affected exact-argv groups once, as PARTIAL receipts (`./run.sh check-gate-qualification --group '<argv JSON>' --receipt <absolute path outside the repo>`), and the whole body runs once per cut, in the prepared SHA's CI. Exact membership is the named `check:*` scripts in `package.json`. Run `check:full` once on your frozen commit candidate — the pre-commit hook (`.husky/pre-commit`) carries only fast static checks (whitespace, lint, typecheck), not the full floor, so a green `pnpm run check:full` before commit is the evidence that your change holds (scheduling contract: AGENTS.md "Verification scheduling"). No install or build lifecycle script installs git hooks: a global `core.hooksPath` guard that chains to repository-local hooks runs its own checks, then delegates — to `.husky/pre-commit` here, unless an executable `.git/hooks/pre-commit` takes precedence.
34
34
 
35
35
  For changes that touch backend launch, session lifecycle, or `_meta` shape, also run
36
36
  the live ACP smokes that cover the touched rail — at minimum:
package/README.md CHANGED
@@ -27,11 +27,8 @@ Claude and Cortex Code are ACP backends. Codex and Antigravity stay native so th
27
27
  retain their own tools and work context. Capabilities differ by rail: a shipped
28
28
  citizen is not a promise of every lifecycle operation.
29
29
 
30
- > **Current release: [0.33.0](https://github.com/junghan0611/entwurf/releases/tag/v0.33.0)**
31
- > ([`fea9b3d`](https://github.com/junghan0611/entwurf/commit/fea9b3dc4779ac5deb34e239abc8e0b5e3e95382)).
32
- > Includes the pinned Pi 1.1.0 native durable app/TUI and one explicit native-module ingress;
33
- > `npm` `latest` is `0.33.0`. What that cut ran and did not run is in its CHANGELOG section.
34
- > Release history and detailed evidence belong in [CHANGELOG](./CHANGELOG.md).
30
+ [Releases](https://github.com/junghan0611/entwurf/releases) contain each cut's notes and acceptance
31
+ receipts; [CHANGELOG](./CHANGELOG.md) records source history.
35
32
 
36
33
  ## Install
37
34
 
@@ -88,8 +85,8 @@ models/auth, queues, SQLite, transcripts and recovery remain upstream-owned:
88
85
  [Hard Rule 17](./AGENTS.md#hard-rules), **supply the missing surface; do not become the harness**.
89
86
 
90
87
  Optional `--native-module <absolute file>` initializes trusted ESM before runtime/TUI
91
- imports, then installs its default native Extension after the contact. After 0.33.0 (#134):
92
- `--native-module-dir <absolute directory>` loads the top-level `*.extension.mjs` files of that
88
+ imports, then installs its default native Extension after the contact.
89
+ `--native-module-dir <absolute directory>` (#134) loads the top-level `*.extension.mjs` files of that
93
90
  one named directory in byte-wise order instead, and the durable TUI keeps a live
94
91
  background-task badge on screen at any terminal size. No automatic module discovery or
95
92
  classic Pi API shim; guards are not a sandbox or rollback.
package/VERIFY.md CHANGED
@@ -96,7 +96,7 @@ that moved out of `scripts/` and into a file beside its subject would otherwise
96
96
 
97
97
  > **The herdr rail is an on-demand axis until it has run.** `smoke-herdr-fresh-call-live` opens a real caller inside a real herdr pane, has it invoke the PUBLIC `entwurf_fresh_call`, and reads the child's nonce callback — the one axis no deterministic gate reaches, since the rail is selected from the caller's own process environment. **The release aggregate does not yet require the herdr rail**: herdr is optional (a cut on a host without it must not block), and first evidence precedes promotion — a rail whose first acceptance run is days old has not earned a place in the gate that blocks every cut. Run it directly with `LIVE=1 ./run.sh smoke-herdr-fresh-call-live` whenever a cut changes herdr rail code, and promote it to a MUST only after the axis has a run history to point at. Without `LIVE=1`, or without herdr/pi/claude on PATH, it reports protocol SKIP — never a pass.
98
98
 
99
- > **The plugin's FIRST USER PATH is its own on-demand axis, and an npm acquisition receipt holds only for the ref it ran on (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. npm has been the production authority class since `dd84ac0`; the 0.30.0 follow-up re-pinned `@junghanacs/entwurf@0.30.0` with its published sha512 after the candidate windows rode `herdr-checkout` (`8b92e09`), and the 0.30.1 preparation rode that closed verification carrier again without inventing a registry integrity. The last published plugin snapshot `v2026.10.9-herdr.0.34.0` pins the published `@junghanacs/entwurf@0.34.0` with its measured sha512 (#132); 0.31–0.33 were never pinned. This checkout's unpublished 0.34.1 preparation rides the closed `herdr-checkout` verification carrier again, without inventing a registry integrity: it has no published npm SRI and is not a plugin pair. The source re-proof has separate dated npm rows: 2026-09-23 public `main=113ca53`, plugin 0.5.0, runtime 0.25.0 (CHANGELOG 0.25.1), and the 0.30.0 re-pin accepted at public `506c46f` in the [#126 thread](https://github.com/junghan0611/entwurf/issues/126). Neither certifies the 0.34.0 registry acquisition. That pin's real-npm swap / torn-swap recovery was measured locally on 2026-10-09 by driving the registry-shipped `scripts/herdr-runtime.mjs` (byte-equal to this checkout's) with its own npm acquisition: a 0.33.0 → 0.34.0 swap, and the same upgrade torn between its two renames then resumed, where the last-good 0.33.0 tree was restored before 0.34.0 was acquired again. Both ended `runtime-ready` at observed digest `sha256-e9d995ff…`, the accepted 0.34.0 artifact. That local receipt is recorded in the [#132 thread](https://github.com/junghan0611/entwurf/issues/132); it covers runtime recovery, not this smoke's cells. The public-ref acceptance of this smoke is PENDING the prepared push, and its actual result will be recorded there; checkout candidate evidence does not transfer. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server. The 2026-09-23 PASS installed with no server on the 0.9.1 binary `113ca53`'s `herdr-supply.json` pins, so that install path worked on 0.9.1 — inferred from the PASS, not a direct observation of the fallback branch; a later "no" on another herdr is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact eight-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
99
+ > **The plugin's FIRST USER PATH is its own on-demand axis, and an npm acquisition receipt holds only for the ref it ran on (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. npm has been the production authority class since `dd84ac0`; the 0.30.0 follow-up re-pinned `@junghanacs/entwurf@0.30.0` with its published sha512 after the candidate windows rode `herdr-checkout` (`8b92e09`), and the 0.30.1 preparation rode that closed verification carrier again without inventing a registry integrity. The plugin snapshot `v2026.10.9-herdr.0.34.0` pinned the published `@junghanacs/entwurf@0.34.0` with its measured sha512 (#132); 0.31–0.33 were never pinned. The 0.34.1 preparation rode the closed `herdr-checkout` verification carrier again without inventing a registry integrity, and the snapshot `v2026.10.9-herdr.0.34.1` re-pins the published `@junghanacs/entwurf@0.34.1` with its measured sha512 (#135). The source re-proof has separate dated npm rows: 2026-09-23 public `main=113ca53`, plugin 0.5.0, runtime 0.25.0 (CHANGELOG 0.25.1), and the 0.30.0 re-pin accepted at public `506c46f` in the [#126 thread](https://github.com/junghan0611/entwurf/issues/126). Neither certifies the 0.34.0 registry acquisition. That pin's real-npm swap / torn-swap recovery was measured locally on 2026-10-09 by driving the registry-shipped `scripts/herdr-runtime.mjs` (byte-equal to this checkout's) with its own npm acquisition: a 0.33.0 → 0.34.0 swap, and the same upgrade torn between its two renames then resumed, where the last-good 0.33.0 tree was restored before 0.34.0 was acquired again. Both ended `runtime-ready` at observed digest `sha256-e9d995ff…`, the accepted 0.34.0 artifact. That local receipt is recorded in the [#132 thread](https://github.com/junghan0611/entwurf/issues/132); it covers runtime recovery, not this smoke's cells. This smoke's public-ref acceptance of the 0.34.0 pin was measured locally at `--ref main`: PASS at resolved `b5d15d8` and again at the docs follow-up `8ed4d1a`, each acquiring `kind=npm @junghanacs/entwurf@0.34.0` — a main-ref receipt, not a tag-ref, native or full-Herdr claim. The 0.34.1 re-pin's npm-source rows — swap / torn-swap recovery, this smoke at a public ref, and the package consumer — are its own, recorded with that pin and never inherited from 0.34.0; checkout candidate evidence does not transfer. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server. The 2026-09-23 PASS installed with no server on the 0.9.1 binary `113ca53`'s `herdr-supply.json` pins, so that install path worked on 0.9.1 — inferred from the PASS, not a direct observation of the fallback branch; a later "no" on another herdr is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact eight-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
100
100
 
101
101
  > **The Herdr plugin's install-time build has two axes, and one of them needs the network (#116 M3-b3).** `check-herdr-plugin-build` is deterministic and rides `check:hermetic`: the listing reaches the runner through its own spawn seam (there is no fake `herdr` binary in this repo), and the one cell that drives the REAL binary reports a named SKIP on a host without it — `ENTWURF_REQUIRE_HERDR=1` makes that absence red. What it cannot have is the real acquisition: `npm pack git+https://github.com/junghan0611/entwurf.git#<full sha>` builds the bridge through `prepare`, which installs devDependencies from the registry. So `LIVE=1 ./run.sh smoke-herdr-plugin-build-live` owns that axis, and its cells are **two kinds of evidence, labelled as such**: three are the real Herdr journey — a real `herdr plugin install` driving the real `[[build]]`, with the UNCHANGED product remote redirected to a local bare clone by git `insteadOf` (the one substitution, which is what makes an unpushed candidate testable) — covering remote-commit available, the `{pi}` → `{pi, claude-code}` reinstall with OpenCode as a zero-write negative control and exactly ONE counted vendor MCP owner entry, and the post-build Herdr-commit gap (where the ledger has already rebound to the gap commit, so the honest assertion is that transition and not unchanged bytes). The fourth cell — remote-commit **unavailable** — calls the shipped runtime leaf directly with the fixed product argv and is therefore **acquisition-leaf evidence, not a journey**; routing it through Herdr would add a second explanation for one red. Its oracle is the honest one too: active runtime, ledger and harness bytes preserved, while the journal is left as the certified `installing` retry authority the transaction deliberately writes before acquiring, and a normal install afterwards is green. Its Claude cell is where the vendor question the deterministic gates refuse to claim gets answered — whether Claude Code accepts an ABSOLUTE executable as an MCP command — and it reports a named skip on a host without `claude`, never a pass. Like the herdr fresh-call rail, this smoke is **on-demand and not an aggregate MUST**. The release aggregate does not require the plugin build smoke: herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion.
102
102
 
@@ -195,8 +195,8 @@ root-bound — an install from another root rewrites it rather than refusing; no
195
195
  `PI_CODING_AGENT_DIR`, Claude config) changed between runs or a host that changes between the plugin's
196
196
  early owner check and its installed recheck, which can still refuse after the download; and the plugin's
197
197
  deactivate, finding no activation ledger, reports what is left at its addresses and neither reclaims nor
198
- reuses it. The no-ledger diagnostic is not in the published 0.34.0 runtime; a plugin host receives it only
199
- from a later published runtime the plugin pins.
198
+ reuses it. The 0.34.0 runtime predates that no-ledger diagnostic; a plugin host runs the runtime its
199
+ `runtime-lock.json` pins.
200
200
 
201
201
  ## 2. Optional pi adapter / ACP plugin
202
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
5
5
  "license": "MIT",
6
6
  "author": {
package/run.sh CHANGED
@@ -7634,6 +7634,8 @@ case "$cmd" in
7634
7634
  # global plugin install, USER MCP reach, meta-record store integrity, hook log
7635
7635
  # no-ERROR, and actual SessionStart creation evidence. A plugin present with
7636
7636
  # zero claude-code meta-records is a SILENT MISS -> non-zero exit.
7637
+ # #136: the only argument is `--plugin-runtime <root>`; shift the verb so the doctor sees it.
7638
+ shift || true
7637
7639
  (cd "$REPO_DIR" && bash scripts/meta-bridge-doctor.sh "$@")
7638
7640
  ;;
7639
7641
  install-copilot-bridge)
@@ -3471,7 +3471,7 @@ let manifestCount: number;
3471
3471
  "herdr-placement": 13,
3472
3472
  "herdr-plugin": 25,
3473
3473
  "herdr-plugin-profile": 14,
3474
- "herdr-activation": 28,
3474
+ "herdr-activation": 34,
3475
3475
  "herdr-plugin-build": 20,
3476
3476
  "herdr-runtime-bootstrap": 31,
3477
3477
  "herdr-fresh-call": 38,
@@ -183,6 +183,7 @@ interface Mod {
183
183
  resolveEntwurfDataRoot: (env: NodeJS.ProcessEnv) => string;
184
184
  LEDGER_PHASES: string[];
185
185
  COMPONENT_STATES: string[];
186
+ resolveDoctorPluginRuntime: (env: NodeJS.ProcessEnv, opts: { repo: string; override?: string }) => string | null;
186
187
  }
187
188
 
188
189
  const mod = (await import(pathToFileURL(ACTIVATION).href)) as Mod;
@@ -204,6 +205,7 @@ const {
204
205
  resolveEntwurfDataRoot,
205
206
  LEDGER_PHASES,
206
207
  COMPONENT_STATES,
208
+ resolveDoctorPluginRuntime,
207
209
  } = mod;
208
210
 
209
211
  /** The pi pin package.json declares (an exact production dependency since #130) — the SSOT
@@ -1484,4 +1486,82 @@ function ledgerWith(
1484
1486
  );
1485
1487
  }
1486
1488
 
1489
+ // ── 29. the doctor's expectation mode comes from the activation ledger (#136) ────
1490
+ // `doctor-meta-bridge` must compute the plugin activation's ABSOLUTE expectations exactly when the
1491
+ // ledger authorised that wiring, and never adopt it from a checkout. The oracle for the returned
1492
+ // root is `runtimeRootOf`, computed here independently of the resolver.
1493
+ {
1494
+ const env = world("doctor-mode");
1495
+ seedRuntime(env);
1496
+ const active = runtimeRootOf(env);
1497
+ const runtimeRepo = path.join(active, "node_modules", "@junghanacs", "entwurf");
1498
+ const layout = resolveActivationLayout(env);
1499
+ const mode = (opts: { repo: string; override?: string }): string | null => {
1500
+ try {
1501
+ return resolveDoctorPluginRuntime(env, opts);
1502
+ } catch (err) {
1503
+ return `refused:${(err as { code?: string }).code ?? String(err)}`;
1504
+ }
1505
+ };
1506
+ // D2 — no ledger: a doctor running from the runtime package is simply not in plugin mode.
1507
+ const absent = mode({ repo: runtimeRepo });
1508
+ writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
1509
+ // D1 — runtime package + an active ledger that activated claude-code at THIS stable root.
1510
+ const derived = mode({ repo: runtimeRepo });
1511
+ ok(
1512
+ "[QK:DOCTOR-MODE-DERIVED] a doctor running from the plugin runtime's package derives plugin-runtime mode from " +
1513
+ `an active ledger that activated claude-code at this host's stable root, and from nothing else (no-ledger=${absent} derived=${derived})`,
1514
+ absent === null && derived === active,
1515
+ );
1516
+ // D3 — the checkout never derives, and never even opens the ledger: an uncertified body is
1517
+ // planted first, so reading it would throw instead of returning null.
1518
+ fs.writeFileSync(layout.ledgerPath, "not json\n");
1519
+ const checkout = mode({ repo: REPO });
1520
+ ok(
1521
+ "[QK:DOCTOR-MODE-CHECKOUT-NEVER-DERIVES] a doctor whose repo is not the runtime package returns no plugin mode " +
1522
+ `without opening the ledger, so a checkout never adopts plugin wiring (checkout=${checkout})`,
1523
+ checkout === null,
1524
+ );
1525
+ // D5 — on a reading path an uncertified ledger dies with the reader's own refusal, never null.
1526
+ fs.unlinkSync(layout.ledgerPath);
1527
+ fs.symlinkSync(path.join(env.HOME as string, "nowhere.json"), layout.ledgerPath);
1528
+ const viaRuntime = mode({ repo: runtimeRepo });
1529
+ const viaOverride = mode({ repo: REPO, override: active });
1530
+ fs.unlinkSync(layout.ledgerPath);
1531
+ ok(
1532
+ "[QK:DOCTOR-MODE-UNCERTIFIED-DIES] an uncertified ledger on either reading path (runtime-repo derivation, explicit " +
1533
+ `override) refuses with the reader's own code, never 'not plugin mode' (runtime=${viaRuntime} override=${viaOverride})`,
1534
+ viaRuntime === "refused:activation-ledger-uncertified" && viaOverride === "refused:activation-ledger-uncertified",
1535
+ );
1536
+ // D4 — the admission clauses on a reading path, each on a CERTIFIED ledger so only the clause can
1537
+ // refuse: a phase other than active, a ledger that never activated claude-code, and a runtimeRoot
1538
+ // that is not this host's stable root. Each is refused by name, never read as 'not plugin mode'.
1539
+ writeLedger(layout, ledgerWith(env, "activating", { pi: "active", "claude-code": "pending" }));
1540
+ const notActive = mode({ repo: runtimeRepo });
1541
+ writeLedger(layout, ledgerFor(env, ["pi"]));
1542
+ const piOnly = mode({ repo: runtimeRepo });
1543
+ writeLedger(layout, {
1544
+ ...ledgerFor(env, ["pi", "claude-code"]),
1545
+ runtimeRoot: path.join(env.HOME as string, "another-runtime", "active"),
1546
+ });
1547
+ const foreignRoot = mode({ repo: runtimeRepo });
1548
+ ok(
1549
+ "[QK:DOCTOR-MODE-ADMISSION-CLAUSES] on the runtime repo a certified ledger still admits plugin mode only when " +
1550
+ "phase is active, claude-code was activated and its runtimeRoot is this host's stable root — each other case " +
1551
+ `is refused by name (activating=${notActive} pi-only=${piOnly} foreign-root=${foreignRoot})`,
1552
+ notActive === "refused:doctor-plugin-runtime-unadmitted" &&
1553
+ piOnly === "refused:doctor-plugin-runtime-unadmitted" &&
1554
+ foreignRoot === "refused:doctor-plugin-runtime-unadmitted",
1555
+ );
1556
+ // D6 — the override must agree with the ledger's root; agreeing, it is admitted from a checkout.
1557
+ writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
1558
+ const elsewhere = mode({ repo: REPO, override: path.join(env.HOME as string, "elsewhere") });
1559
+ const agreeing = mode({ repo: REPO, override: active });
1560
+ ok(
1561
+ "[QK:DOCTOR-MODE-OVERRIDE-AGREES] an explicit --plugin-runtime is admitted only when it names the ledger's own " +
1562
+ `stable root (elsewhere=${elsewhere} agreeing=${agreeing})`,
1563
+ elsewhere === "refused:doctor-plugin-runtime-unadmitted" && agreeing === active,
1564
+ );
1565
+ }
1566
+
1487
1567
  console.log(`\ncheck-herdr-activation: ${passed} assertions passed`);
@@ -227,6 +227,8 @@ ok "fake live entwurf MCP bridge running as a child of the stand-in Claude pid (
227
227
  # --- doctor driver -----------------------------------------------------------
228
228
  DOC_OUT=""
229
229
  DOC_RC=0
230
+ # #136: arguments for the plugin-runtime block at the end; empty for every clone cell.
231
+ DOC_ARGS=()
230
232
  run_doctor() {
231
233
  set +e
232
234
  # Deliberate cross-harness poison. Before the doctor scrubbed these carriers, the
@@ -237,7 +239,7 @@ run_doctor() {
237
239
  DOC_OUT="$(PI_SESSION_ID=ambient-pi-session-must-not-win \
238
240
  PI_AGENT_ID=ambient-pi-agent/must-not-win \
239
241
  ENTWURF_META_SENDER_MARKER="$TMP/ambient-sender-marker-must-not-win.json" \
240
- bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
242
+ bash "$REPO/scripts/meta-bridge-doctor.sh" ${DOC_ARGS[@]+"${DOC_ARGS[@]}"} 2>&1)"
241
243
  DOC_RC=$?
242
244
  set -e
243
245
  }
@@ -713,5 +715,88 @@ else
713
715
  bad "doctor stayed red after reverting every mutation (a restore is incomplete):"$'\n'"$(printf '%s\n' "$DOC_OUT" | grep -E '^ FAIL' | sed 's/^/ /')"
714
716
  fi
715
717
 
718
+ # --- 6. #136: a Herdr plugin activation's ABSOLUTE wiring ---------------------
719
+ # The plugin activation runs the same installer with ENTWURF_PLUGIN_RUNTIME, so Claude's MCP and
720
+ # statusLine name absolute bins under the stable runtime. The doctor must learn that mode from the
721
+ # activation ledger and certify the wiring, not call it drift. FIXTURE HONESTY: the two runtime
722
+ # bins are wrappers onto this checkout's own launcher and statusline, and the ledger is written by
723
+ # the shipped writer with a fixture artifact identity; no npm runtime is installed here.
724
+ echo "[plugin-runtime] the installer's absolute wiring, certified through the activation ledger"
725
+ ACTIVE="$XDG_DATA_HOME/entwurf/herdr-plugin/runtime/active"
726
+ mkdir -p "$ACTIVE/node_modules/.bin" "$ACTIVE/node_modules/@junghanacs/entwurf"
727
+ printf '#!/usr/bin/env bash\nexec bash "%s/mcp/entwurf-bridge/start.sh" "$@"\n' "$REPO" > "$ACTIVE/node_modules/.bin/entwurf-bridge"
728
+ printf '#!/usr/bin/env bash\nexec "%s/scripts/meta-bridge-statusline.sh" "$@"\n' "$REPO" > "$ACTIVE/node_modules/.bin/entwurf-statusline"
729
+ chmod +x "$ACTIVE/node_modules/.bin/entwurf-bridge" "$ACTIVE/node_modules/.bin/entwurf-statusline"
730
+ if ENTWURF_PLUGIN_RUNTIME="$ACTIVE" bash "$REPO/scripts/meta-bridge-install.sh" >"$TMP/install-plugin.log" 2>&1; then
731
+ ok "real meta-bridge-install.sh wires the plugin-runtime mode (absolute bins under the sandbox stable root)"
732
+ else
733
+ bad "plugin-runtime install-meta-bridge failed in the sandbox:"$'\n'"$(sed 's/^/ install| /' "$TMP/install-plugin.log")"
734
+ fi
735
+ rm -rf "$PLANTED"
736
+ mkdir -p "$PLANTED"
737
+ cp -r "$ASM/$PLUGIN/." "$PLANTED/"
738
+ write_ledger() {
739
+ node --input-type=module - "$REPO/scripts/herdr-activation.mjs" "$ACTIVE" "$1" <<'JS'
740
+ import { pathToFileURL } from "node:url";
741
+ const [modPath, active, backends] = process.argv.slice(2);
742
+ const m = await import(pathToFileURL(modPath).href);
743
+ const states = Object.fromEntries(backends.split(",").map((b) => [b, "active"]));
744
+ m.writeLedger(m.resolveActivationLayout(process.env), m.ledgerBody({
745
+ phase: "active",
746
+ runtimeRoot: active,
747
+ artifactIdentity: { kind: "npm", name: "@junghanacs/entwurf", version: "0.34.1", expectedIntegrity: "sha512-fixture", observedDigest: `sha256-${"c".repeat(64)}` },
748
+ roots: m.resolveComponentRoots(process.env),
749
+ states,
750
+ }));
751
+ JS
752
+ }
753
+ write_ledger "pi,claude-code"
754
+ DOC_ARGS=(--plugin-runtime "$ACTIVE")
755
+
756
+ # P1 — the plugin wiring the installer wrote is certified: PASS, and the doctor names its mode.
757
+ # The [QK:] token lives on the `bad` line only (see M9b).
758
+ run_doctor
759
+ if [ "$DOC_RC" -eq 0 ] && printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: PASS' \
760
+ && printf '%s\n' "$DOC_OUT" | grep -qF "mode: plugin-runtime $ACTIVE (activation ledger)" \
761
+ && printf '%s\n' "$DOC_OUT" | grep -qF 'live mcpServers.entwurf-bridge equals desired_mcp()'; then
762
+ ok "plugin-runtime wiring reaches doctor PASS through the activation ledger, and the doctor names that mode"
763
+ else
764
+ bad "[QK:DOCTOR-PLUGIN-MODE-GREEN] the plugin activation's absolute wiring did NOT reach PASS (exit $DOC_RC):"$'\n'"$(printf '%s\n' "$DOC_OUT" | grep -E '^ (FAIL|WARN)|mode:' | sed 's/^/ /')"
765
+ fi
766
+
767
+ # P2 — genuine drift stays red in plugin mode: an absolute statusLine under the same root, but not ours.
768
+ cp "$CLAUDE_CONFIG_DIR/settings.json" "$TMP/settings-p2.bak"
769
+ python3 - "$CLAUDE_CONFIG_DIR/settings.json" "$ACTIVE/node_modules/.bin/evil" <<'PY'
770
+ import json, sys
771
+ p = sys.argv[1]
772
+ d = json.load(open(p, encoding="utf-8"))
773
+ d["statusLine"]["command"] = sys.argv[2]
774
+ json.dump(d, open(p, "w", encoding="utf-8"), indent=2)
775
+ PY
776
+ expect_red "plugin-mode statusLine repointed under the same runtime root" "statusLine.command drifted"
777
+ cp "$TMP/settings-p2.bak" "$CLAUDE_CONFIG_DIR/settings.json"
778
+
779
+ # P3 — a ledger that did not activate claude-code admits no plugin mode. The refusal is named,
780
+ # and it is NOT an early exit: the store and live-owner sections still run (Hard Rule 13).
781
+ write_ledger "pi"
782
+ expect_red "override against a ledger that never activated claude-code" "doctor-plugin-runtime-unadmitted"
783
+ # Each of the three expectation sites says UNKNOWN for that reason (never a bare-bin fallback),
784
+ # and the runtime axes still run: the live command still DELIVERS, the store and owner join report.
785
+ for claim in \
786
+ 'managed settings/MCP keyset is UNKNOWN: the expectation mode was not established' \
787
+ 'statusLine expectation is UNKNOWN: the expectation mode was not established' \
788
+ 'live-vs-desired MCP ownership is UNKNOWN: the expectation mode was not established' \
789
+ 'live bridge command DELIVERS' \
790
+ '[meta-record store]' \
791
+ '[live Claude MCP owner join]'
792
+ do
793
+ if printf '%s\n' "$DOC_OUT" | sed -n '/expectation mode refused: doctor-plugin-runtime-unadmitted/,$p' | grep -qF "$claim"; then
794
+ ok "after the mode refusal the doctor still reports: $claim"
795
+ else
796
+ bad "after the mode refusal the doctor never reported: $claim"
797
+ fi
798
+ done
799
+ write_ledger "pi,claude-code"
800
+
716
801
  echo
717
802
  if [ "$fail" -eq 0 ]; then echo "check-meta-doctor-oracle: PASS"; else echo "check-meta-doctor-oracle: FAIL (see above)"; exit 1; fi
@@ -28,7 +28,7 @@
28
28
 
29
29
  import fs from "node:fs";
30
30
  import path from "node:path";
31
- import { artifactStage, certifyArtifactIdentity, sameArtifactRequest } from "./herdr-runtime.mjs";
31
+ import { artifactStage, certifyArtifactIdentity, resolveRuntimeLayout, sameArtifactRequest } from "./herdr-runtime.mjs";
32
32
 
33
33
  /** Ledger format. Bump only with a reader that understands both. */
34
34
  export const ACTIVATION_SCHEMA_VERSION = 2;
@@ -502,6 +502,55 @@ export function piStatePaths(env) {
502
502
  });
503
503
  }
504
504
 
505
+ /**
506
+ * Which expectations `doctor-meta-bridge` must compute (#136): the stable active root when this
507
+ * host's Claude wiring was written by a plugin activation, otherwise null (clone / installed).
508
+ *
509
+ * The plugin activation writes Claude's MCP and statusLine as ABSOLUTE commands under the stable
510
+ * runtime. A doctor that does not know that expects bare bins and calls correct wiring drifted. The
511
+ * mode is read from the record that authorised the wiring, the certified activation ledger, never
512
+ * from settings, environment or a path guess:
513
+ * - phase `active`, `claude-code` activated, and the ledger's runtimeRoot is THIS host's stable root;
514
+ * - without an override, only a doctor running FROM that runtime's package derives the mode. A
515
+ * checkout's doctor returns null without opening the ledger, so it never adopts plugin wiring;
516
+ * - an explicit override must agree with the ledger, or it is refused.
517
+ * An absent ledger is "not plugin mode"; an uncertified one throws the reader's own refusal.
518
+ */
519
+ export function resolveDoctorPluginRuntime(env, { repo, override } = {}) {
520
+ const activeDir = resolveRuntimeLayout(env).activeDir;
521
+ if (override === undefined) {
522
+ const runtimePackage = path.join(activeDir, "node_modules", "@junghanacs", "entwurf");
523
+ let same = false;
524
+ try {
525
+ same = fs.realpathSync(repo) === fs.realpathSync(runtimePackage);
526
+ } catch {
527
+ // Bounded probe: no runtime package at the stable address means this repo is not it.
528
+ same = false;
529
+ }
530
+ if (!same) return null;
531
+ }
532
+ const ledger = readCertifiedLedger(resolveActivationLayout(env));
533
+ if (ledger === null) {
534
+ if (override === undefined) return null;
535
+ throw new ActivationError(
536
+ "doctor-plugin-runtime-unadmitted",
537
+ "--plugin-runtime was given but no activation ledger exists",
538
+ );
539
+ }
540
+ const refuse = (clause) => {
541
+ throw new ActivationError("doctor-plugin-runtime-unadmitted", clause);
542
+ };
543
+ if (ledger.phase !== "active") refuse(`activation ledger phase is ${ledger.phase}, not active`);
544
+ if (!ledger.activatedBackends.includes("claude-code")) refuse("the activation ledger did not activate claude-code");
545
+ if (ledger.runtimeRoot !== activeDir) {
546
+ refuse(`the ledger's runtimeRoot ${ledger.runtimeRoot} is not this host's stable runtime root ${activeDir}`);
547
+ }
548
+ if (override !== undefined && override !== activeDir) {
549
+ refuse(`--plugin-runtime ${override} does not name the ledger's runtime root ${activeDir}`);
550
+ }
551
+ return activeDir;
552
+ }
553
+
505
554
  /** A component row, in the one shape the certifier accepts. */
506
555
  export function componentRow(backend, state) {
507
556
  return Object.freeze({ backend, state });
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * herdr-doctor-mode — the one call `meta-bridge-doctor.sh` makes to learn its expectation mode (#136).
4
+ *
5
+ * node herdr-doctor-mode.mjs <repo> [<plugin-runtime override>]
6
+ *
7
+ * Prints the stable active root (plugin-runtime mode) or nothing (clone / installed), exit 0.
8
+ * A refusal prints `code: detail` to stderr and exits 3. Any other exit is a crash the doctor
9
+ * reports as UNKNOWN. All judgement lives in `resolveDoctorPluginRuntime`; nothing is decided here.
10
+ */
11
+ import { resolveDoctorPluginRuntime } from "./herdr-activation.mjs";
12
+
13
+ const [repo, override] = process.argv.slice(2);
14
+ if (!repo) {
15
+ process.stderr.write("usage: herdr-doctor-mode.mjs <repo> [<plugin-runtime>]\n");
16
+ process.exit(2);
17
+ }
18
+ let root;
19
+ try {
20
+ root = resolveDoctorPluginRuntime(process.env, { repo, override });
21
+ } catch (err) {
22
+ if (err?.name !== "ActivationError") throw err;
23
+ process.stderr.write(`${err.message}\n`);
24
+ process.exit(3);
25
+ }
26
+ if (root !== null) process.stdout.write(`${root}\n`);
@@ -17,6 +17,18 @@ MKT_NAME="meta-bridge-local"
17
17
  PLUGIN="entwurf-meta-receive"
18
18
  CLAUDE_CFG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
19
19
  REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ # #136: the one argument. A Herdr plugin activation wires Claude with ABSOLUTE commands under the
21
+ # stable runtime; the mode is normally derived from the activation ledger (see [expectation mode]),
22
+ # and this override exists so a checkout's doctor can name that root. It must agree with the ledger.
23
+ PLUGIN_RUNTIME_OVERRIDE=""
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ --plugin-runtime)
27
+ [ -n "${2:-}" ] || { echo "meta-bridge-doctor: --plugin-runtime requires a value" >&2; exit 2; }
28
+ PLUGIN_RUNTIME_OVERRIDE="$2"; shift 2 ;;
29
+ *) echo "usage: meta-bridge-doctor.sh [--plugin-runtime <stable active runtime root>]" >&2; exit 2 ;;
30
+ esac
31
+ done
20
32
  # The live artifact lives under the XDG data dir (dev clone and installed package
21
33
  # alike — never the checkout). Prefer the path RECORDED in install-state so a doctor
22
34
  # run under a different XDG_DATA_HOME than install still hashes the REAL assembled
@@ -110,6 +122,44 @@ if command -v node >/dev/null; then
110
122
  if [ "${MJ:-0}" -ge 24 ]; then ok "node $NV (>= 24, supported axis)"; else bad "node $NV too old (need >= 24; there is no Node 22 lane)"; fi
111
123
  else bad "node not on PATH"; fi
112
124
 
125
+ echo "[expectation mode]"
126
+ # #136: which expectations the three desired-state calls below compute. The judgement is the
127
+ # activation owner's (`resolveDoctorPluginRuntime`, herdr-activation.mjs) reached through one narrow
128
+ # call, so no second reader of the ledger exists. A refusal is NOT a fallback to bare expectations:
129
+ # each call site then reports UNKNOWN, and every other section still runs (Hard Rule 13).
130
+ PLUGIN_ARGS=()
131
+ MODE_ERR=""
132
+ if ! command -v node >/dev/null; then
133
+ MODE_ERR="node not on PATH, so the activation ledger could not be read"
134
+ bad "expectation mode is UNKNOWN: $MODE_ERR"
135
+ else
136
+ MODE_STDERR="$(mktemp -t entwurf-doctor-mode.XXXXXX)"
137
+ set +e
138
+ if [ -n "$PLUGIN_RUNTIME_OVERRIDE" ]; then
139
+ MODE_ROOT="$(node "$REPO/scripts/herdr-doctor-mode.mjs" "$REPO" "$PLUGIN_RUNTIME_OVERRIDE" 2>"$MODE_STDERR")"
140
+ else
141
+ MODE_ROOT="$(node "$REPO/scripts/herdr-doctor-mode.mjs" "$REPO" 2>"$MODE_STDERR")"
142
+ fi
143
+ MODE_RC=$?
144
+ set -e
145
+ MODE_DETAIL="$(tr '\n' ' ' < "$MODE_STDERR" | cut -c1-400)"
146
+ rm -f "$MODE_STDERR"
147
+ if [ "$MODE_RC" -eq 3 ]; then
148
+ MODE_ERR="$MODE_DETAIL"
149
+ bad "expectation mode refused: $MODE_ERR"
150
+ elif [ "$MODE_RC" -ne 0 ]; then
151
+ MODE_ERR="herdr-doctor-mode.mjs exited $MODE_RC: $MODE_DETAIL"
152
+ bad "expectation mode is UNKNOWN: $MODE_ERR"
153
+ elif [ -n "$MODE_ROOT" ]; then
154
+ PLUGIN_ARGS=(--plugin-runtime "$MODE_ROOT")
155
+ ok "mode: plugin-runtime $MODE_ROOT (activation ledger)"
156
+ elif [ "$LIB_EXT" = "js" ]; then
157
+ ok "mode: installed (no activation ledger, or this package is not the plugin runtime)"
158
+ else
159
+ ok "mode: clone (this checkout is not the plugin runtime)"
160
+ fi
161
+ fi
162
+
113
163
  echo "[managed config state]"
114
164
  if command -v python3 >/dev/null; then
115
165
  # state.py check is the effect-based keyset-survival guard: it asserts every
@@ -123,7 +173,9 @@ if command -v python3 >/dev/null; then
123
173
  # which key drifted — silent instead of fail-loud. As an `if` condition the
124
174
  # substitution's nonzero status is consumed (set -e suspended), so we always
125
175
  # reach the detail. stderr (drift detail) is captured via `2>&1 >/dev/null`.
126
- if CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" 2>&1 >/dev/null)"; then
176
+ if [ -n "$MODE_ERR" ]; then
177
+ bad "managed settings/MCP keyset is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
178
+ elif CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} 2>&1 >/dev/null)"; then
127
179
  ok "state file present and managed settings/MCP keyset survives intact"
128
180
  else
129
181
  bad "managed settings/MCP keyset missing or overwritten — another consumer may have clobbered a pi-owned key. Re-run ./run.sh install-meta-bridge. Drift detail:"
@@ -565,25 +617,29 @@ except Exception:
565
617
  print('')
566
618
  PY
567
619
  )"
568
- EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
569
- if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command matches expected (dev: checkout path; installed: entwurf-statusline bin shim): $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
570
- if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
571
- if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
572
- elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
573
- ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
620
+ if [ -n "$MODE_ERR" ]; then
621
+ bad "statusLine expectation is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
574
622
  else
575
- bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
623
+ EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
624
+ if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command matches expected (dev: checkout path; installed: entwurf-statusline bin shim): $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
625
+ if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
626
+ if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
627
+ elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
628
+ ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
629
+ else
630
+ bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
631
+ fi
632
+ SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
633
+ # The identity row may carry the #98 B unread badge between `cc` and the model letter:
634
+ # ` ✉N` when mail is waiting, nothing when the mailbox is empty, ` ✉?` when the count
635
+ # could not be taken. All three are healthy renders, so the shape test admits any of them.
636
+ if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -qE ' cc( ✉([0-9]+|\?))? \| s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
637
+ # The synthetic feeds a session_id with no meta-record, so the garden id does not
638
+ # resolve and there is no mailbox to count. `✉?` is the required answer: a statusline
639
+ # that drew nothing there would be claiming "no mail" about a mailbox it never opened —
640
+ # the exact silence #98 was opened to remove.
641
+ if printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '✉?'; then ok "statusline badge distinguishes 'could not count' (✉?) from zero on the no-record synthetic"; else bad "statusline badge omitted ✉? for the no-record synthetic (a false zero would be indistinguishable from an empty mailbox)"; fi
576
642
  fi
577
- SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
578
- # The identity row may carry the #98 B unread badge between `cc` and the model letter:
579
- # ` ✉N` when mail is waiting, nothing when the mailbox is empty, ` ✉?` when the count
580
- # could not be taken. All three are healthy renders, so the shape test admits any of them.
581
- if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -qE ' cc( ✉([0-9]+|\?))? \| s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
582
- # The synthetic feeds a session_id with no meta-record, so the garden id does not
583
- # resolve and there is no mailbox to count. `✉?` is the required answer: a statusline
584
- # that drew nothing there would be claiming "no mail" about a mailbox it never opened —
585
- # the exact silence #98 was opened to remove.
586
- if printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '✉?'; then ok "statusline badge distinguishes 'could not count' (✉?) from zero on the no-record synthetic"; else bad "statusline badge omitted ✉? for the no-record synthetic (a false zero would be indistinguishable from an empty mailbox)"; fi
587
643
  else
588
644
  bad "cannot validate statusline without python3"
589
645
  fi
@@ -669,9 +725,12 @@ PY
669
725
  [ -n "$LIVE_MCP_DESC" ] || LIVE_MCP_DESC="(unprintable live command)"
670
726
  # Ownership axis. `|| true` on the pipeline only: a state.py failure must read as
671
727
  # UNKNOWN-and-loud below, never abort the doctor before the delivery drive.
672
- DESIRED_MCP="$( { python3 "$REPO/scripts/meta-bridge-state.py" desired-mcp --repo "$REPO" \
728
+ DESIRED_MCP=""
729
+ [ -n "$MODE_ERR" ] || DESIRED_MCP="$( { python3 "$REPO/scripts/meta-bridge-state.py" desired-mcp --repo "$REPO" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} \
673
730
  | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin), sort_keys=True))'; } 2>/dev/null || true)"
674
- if [ -z "$DESIRED_MCP" ]; then
731
+ if [ -n "$MODE_ERR" ]; then
732
+ bad "live-vs-desired MCP ownership is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
733
+ elif [ -z "$DESIRED_MCP" ]; then
675
734
  bad "could not compute the desired MCP entry (meta-bridge-state.py desired-mcp failed) — live-vs-desired ownership is UNKNOWN"
676
735
  elif [ "$LIVE_MCP" = "$DESIRED_MCP" ]; then
677
736
  ok "live mcpServers.entwurf-bridge equals desired_mcp() for this mode (installed: bin shim; clone: this checkout's start.sh)"
@@ -351,6 +351,77 @@
351
351
  "timeoutSeconds": 300,
352
352
  "signature": "[QK:HAC-LEDGER-PATH-KIND]",
353
353
  "signatureSource": "scripts/check-herdr-activation.ts"
354
+ },
355
+ {
356
+ "claim": "DOCTOR-MODE-DERIVED",
357
+ "title": "a resolver that never answers plugin mode leaves the doctor computing bare-bin expectations on a plugin-activated host, so the activation's correct absolute wiring is reported as drift and the supported route can never be certified (#136)",
358
+ "subject": "scripts/herdr-activation.mjs",
359
+ "find": ["\treturn activeDir;\n}"],
360
+ "replace": ["\treturn null;\n}"],
361
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
362
+ "timeoutSeconds": 300,
363
+ "signature": "[QK:DOCTOR-MODE-DERIVED]",
364
+ "signatureSource": "scripts/check-herdr-activation.ts"
365
+ },
366
+ {
367
+ "claim": "DOCTOR-MODE-CHECKOUT-NEVER-DERIVES",
368
+ "title": "dropping the runtime-package precondition lets a checkout's doctor read the plugin ledger and adopt its absolute wiring as its own expectations, so the checkout certifies a runtime it does not own (#136)",
369
+ "subject": "scripts/herdr-activation.mjs",
370
+ "find": ["\t\tif (!same) return null;"],
371
+ "replace": ["\t\tvoid same;"],
372
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
373
+ "timeoutSeconds": 300,
374
+ "signature": "[QK:DOCTOR-MODE-CHECKOUT-NEVER-DERIVES]",
375
+ "signatureSource": "scripts/check-herdr-activation.ts"
376
+ },
377
+ {
378
+ "claim": "DOCTOR-MODE-UNCERTIFIED-DIES",
379
+ "title": "swallowing the reader's refusal turns an uncertified ledger into 'not plugin mode', so a doctor silently falls back to bare expectations over a record nobody can vouch for (#136)",
380
+ "subject": "scripts/herdr-activation.mjs",
381
+ "find": [
382
+ "\tconst ledger = readCertifiedLedger(resolveActivationLayout(env));\n\tif (ledger === null) {\n\t\tif (override === undefined) return null;"
383
+ ],
384
+ "replace": [
385
+ "\tlet ledger = null;\n\ttry {\n\t\tledger = readCertifiedLedger(resolveActivationLayout(env));\n\t} catch {\n\t\tledger = null;\n\t}\n\tif (ledger === null) {\n\t\tif (override === undefined || override !== undefined) return null;"
386
+ ],
387
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
388
+ "timeoutSeconds": 300,
389
+ "signature": "[QK:DOCTOR-MODE-UNCERTIFIED-DIES]",
390
+ "signatureSource": "scripts/check-herdr-activation.ts"
391
+ },
392
+ {
393
+ "claim": "DOCTOR-MODE-OVERRIDE-AGREES",
394
+ "title": "an override that need not agree with the ledger lets any caller-named root become the doctor's expectation, so the doctor would certify absolute commands pointing anywhere (#136)",
395
+ "subject": "scripts/herdr-activation.mjs",
396
+ "find": ["\tif (override !== undefined && override !== activeDir) {"],
397
+ "replace": ["\tif (false) {"],
398
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
399
+ "timeoutSeconds": 300,
400
+ "signature": "[QK:DOCTOR-MODE-OVERRIDE-AGREES]",
401
+ "signatureSource": "scripts/check-herdr-activation.ts"
402
+ },
403
+ {
404
+ "claim": "DOCTOR-PLUGIN-MODE-GREEN",
405
+ "title": "a doctor that resolves plugin mode but never passes it to its desired-state calls still expects bare bins, so the plugin activation's absolute wiring stays falsely red (#136). One mutant for the shared wire on purpose: omitting the mode at any single one of the three call sites also turns P1 red, but no per-site mutant proves that",
406
+ "subject": "scripts/meta-bridge-doctor.sh",
407
+ "find": [" PLUGIN_ARGS=(--plugin-runtime \"$MODE_ROOT\")"],
408
+ "replace": [" : \"$MODE_ROOT\""],
409
+ "gate": ["bash", "run.sh", "check-meta-doctor-oracle"],
410
+ "timeoutSeconds": 600,
411
+ "signature": "[QK:DOCTOR-PLUGIN-MODE-GREEN]",
412
+ "signatureSource": "scripts/check-meta-doctor-oracle.sh",
413
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
414
+ },
415
+ {
416
+ "claim": "DOCTOR-MODE-ADMISSION-CLAUSES",
417
+ "title": "dropping the phase clause lets a ledger caught mid-activation or mid-teardown admit plugin mode, so the doctor certifies absolute wiring the activation has not finished writing or is already removing (#136)",
418
+ "subject": "scripts/herdr-activation.mjs",
419
+ "find": ["\tif (ledger.phase !== \"active\") refuse(`activation ledger phase is ${ledger.phase}, not active`);"],
420
+ "replace": ["\tvoid refuse;"],
421
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
422
+ "timeoutSeconds": 300,
423
+ "signature": "[QK:DOCTOR-MODE-ADMISSION-CLAUSES]",
424
+ "signatureSource": "scripts/check-herdr-activation.ts"
354
425
  }
355
426
  ]
356
427
  }