@junghanacs/entwurf 0.14.0 → 0.14.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.
Files changed (54) hide show
  1. package/AGENTS.md +13 -2
  2. package/CHANGELOG.md +63 -0
  3. package/DELIVERY.md +57 -0
  4. package/README.md +16 -7
  5. package/VERIFY.md +4 -4
  6. package/demo/README.md +3 -1
  7. package/demo/demo-baseline.sh +12 -1
  8. package/demo/demo.sh +9 -1
  9. package/docs/acp-backend-rail.md +103 -4
  10. package/docs/external-mcp-host.md +1 -1
  11. package/docs/setup-clean-host.md +3 -3
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +12 -5
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +45 -3
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -47
  16. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +139 -47
  17. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +294 -0
  18. package/mcp/entwurf-bridge/src/index.ts +14 -5
  19. package/mcp/entwurf-bridge/tsconfig.build.json +15 -5
  20. package/package.json +9 -9
  21. package/pi-extensions/entwurf-control.ts +13 -4
  22. package/pi-extensions/lib/acp/backend.ts +229 -9
  23. package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
  24. package/pi-extensions/lib/mux-fresh-call.ts +57 -4
  25. package/pi-extensions/lib/mux-resume-call.ts +21 -53
  26. package/run.sh +70 -25
  27. package/scripts/agy-bridge-config.py +47 -13
  28. package/scripts/agy-bridge.sh +73 -23
  29. package/scripts/check-acp-prompt-lifecycle.ts +221 -9
  30. package/scripts/check-entwurf-bridge-boot.ts +28 -0
  31. package/scripts/check-gate-qualification.ts +5 -3
  32. package/scripts/check-mux-resume-call.ts +11 -10
  33. package/scripts/check-probe-bridge-command.ts +201 -0
  34. package/scripts/check-release-gate-outcomes.ts +54 -1
  35. package/scripts/doctor-pi-provider.ts +155 -51
  36. package/scripts/meta-bridge-state.py +75 -1
  37. package/scripts/mutants/acp-prompt-lifecycle.json +25 -3
  38. package/scripts/mutants/bridge-command-boot.json +107 -0
  39. package/scripts/mutants/meta-retire.json +47 -0
  40. package/scripts/mutants/mux-fresh-call.json +48 -4
  41. package/scripts/mutants/mux-resume-call.json +3 -3
  42. package/scripts/mutants/release-gate.json +13 -0
  43. package/scripts/probe-bridge-command.ts +330 -0
  44. package/scripts/raw-async-delivery/README.md +158 -1
  45. package/scripts/raw-async-delivery/copilot-ui-server-probe.mjs +337 -0
  46. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  47. package/scripts/smoke-agy-install-state.sh +76 -2
  48. package/scripts/smoke-entwurf-chain-live.ts +12 -4
  49. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  50. package/scripts/smoke-meta-install-state.sh +169 -3
  51. package/scripts/smoke-mux-fresh-call-live.ts +1 -1
  52. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  53. package/scripts/smoke-pi-provider-state.sh +135 -6
  54. package/scripts/smoke-resident-garden-guard.sh +2 -2
package/run.sh CHANGED
@@ -120,7 +120,7 @@ usage() {
120
120
  cat <<'EOF'
121
121
  Usage:
122
122
  ./run.sh setup [project-dir] # ONE confident install: pnpm install + install + meta-bridge (if native harness) + v2 install smoke (LIVE substrate = release-gate)
123
- ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
123
+ ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
124
124
  ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
125
125
  ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and entwurf_resume_call registered/schema + the tools/list surface is EXACTLY the seven shipped garden verbs; tools/list only, no auth/side-effect
126
126
  ./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
@@ -156,8 +156,8 @@ Usage:
156
156
  ./run.sh check-mux-launcher-fence # deterministic gate for the shared operator-launcher fence (issue #67): scripts/lib/claude-launcher-fence.ts + its wiring into BOTH mux LIVE smokes. Replants the observed install-destruction shape (real HOME + fixture XDG_DATA_HOME → self-update retargets the real `claude` launcher into the fixture tree, teardown deletes it) wholly inside disposable mkdtemp roots — the real launcher is never inspected. Pins fail-closed preflight, retarget/content-change detection before cleanup, removal BLOCKED on fixture reference / unproven safety / surviving tracked panes, exact operator-parity XDG restore (absent = DELETED, not canonical defaults), the lifecycle cell-branch topology, and one shared helper consumed by both smokes
157
157
  ./run.sh check-entwurf-v2-visible-resume # deterministic gate: S1 visible-resume COMPOSITION (entwurf-v2-visible-resume.ts) with every seam injected — the whole state machine incl. the timeout branch runs with no tmux/lock/socket/clock. Pins lock BEFORE liveness, identity under the lock and before any window (no-transcript citizen fails loud, opens nothing), live/indeterminate/address-conflict refused unlaunched, observation as a BOUNDED WAIT (measured: socket answers ~2–4s after launch, so one immediate probe would call a successful resume unobserved), exactly ONE launch on every path, timeout → lock released + window left open + nothing retried/killed, failed release throws, and the two receipts staying separate in type and text
158
158
  ./run.sh check-resume-launch-identity # deterministic gate for resume-launch-identity.ts, the record-authoritative launch-identity leaf preserved through the visible-first cut (spawn-bg and all its callers are gone; this leaf answers "which being is this, and which conversation is theirs"). Temp meta-store fixture: gardenId→record.transcriptPath happy path with header cwd/provider/model; C3 integrity (header id ≠ record.nativeSessionId → refused, never resumed); #52 ADDRESSABLE read (a gid that no longer holds its nativeSessionId alone is refused from EITHER side — the plain targeted read would resume one transcript twice under two locks); cause fidelity per impossible resume incl. the F7 pin (recorded-but-deleted transcript → MISSING, not "no recorded model"); header↔gate SSOT. No spawn/socket/timer
159
- ./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b record-less socket (#50 C4: live record-less pi → EVERY intent rejected pre-probe record-less-socket, no lock, rendered hint names record authority + fresh-cut), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
160
- ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
159
+ ./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b record-less socket (#50 C4: live record-less pi → EVERY intent rejected pre-probe record-less-socket, no lock, rendered hint names record authority + fresh-cut), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.6-luna). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
160
+ ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
161
161
  ./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-not-pi, no window opened and no lock residue. LIVE=1 ./run.sh smoke-mux-lifecycle-live
162
162
  ./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b socket-domain 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, out-of-socket-domain+socket fail-loud; pure, no IO
163
163
  ./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
@@ -197,6 +197,7 @@ Usage:
197
197
  ./run.sh doctor-meta-bridge # THE RELEASE ORACLE (#51, Linux-certified repair axis). exit 0 = every required layer was MEASURED on this Linux host: toolchain + state + plugin/MCP + resolved-artifact launch-form classification (all 3 owner hooks + doorbell static contract) + synthetic owner join + store scan + hook errors + SessionStart evidence + REQUIRED live MCP↔marker join + writer-version parity. Missing live evidence is NOT CERTIFIED (open a Claude session and re-run), never a pass; Darwin is not yet verified/certified and stays nonzero for this cut (future validation may reopen it). Detection power is held by check-meta-doctor-oracle
198
198
  ./run.sh install-agy-bridge # 봉인 7: agy MCP install adapter — register ONE entwurf-bridge server in the agy mcp_config (adopt file / create / REFUSE symlink), stable bin command, install-state under $XDG_DATA_HOME/entwurf/agy-bridge/
199
199
  ./run.sh uninstall-agy-bridge # 봉인 7: honest inverse of install-agy-bridge from install-state (restore preimage / remove key; refuse if config became a symlink)
200
+ ./run.sh probe-bridge-command <cmd> [args...] # #81: BOOT the given bridge invocation and require the entwurf MCP tool surface back. `--invocation-json '{"command":"…","args":[],"env":{}}'` preserves a harness config exactly. It waits for a valid initialize response, then sends initialized + tools/list only (no tools/call, lock, record, or delivery). exit 0 = it serves the bridge; 1 = it does not. The pi/agy doctors use this leaf.
200
201
  ./run.sh doctor-agy-bridge # fail-loud doctor: MCP config + exact permission rule + state + live probe label
201
202
  ./run.sh install-agy-statusline # own the agy statusLine subtree with bare entwurf-agy-statusline; preserve unrelated settings
202
203
  ./run.sh uninstall-agy-statusline # honest inverse from statusline install-state
@@ -1356,7 +1357,7 @@ smoke_acp_cortex_live() {
1356
1357
  # (ACP rail Cortex verification boundary). Drives one real
1357
1358
  # cortex ACP turn through the entwurf provider path (outbound entwurf_v2 +
1358
1359
  # dual-HOME overlay facts + process-group reclaim — CP2).
1359
- # HONEST-SKIP (exit 0) when LIVE!=1 OR `cortex` is not on PATH OR no
1360
+ # HONEST-SKIP (protocol exit 97, never 0) when LIVE!=1 OR `cortex` is not on PATH OR no
1360
1361
  # connection is pinned — the live turn needs `cortex` installed with the
1361
1362
  # operator's own web-login auth already present (there is no `cortex auth`
1362
1363
  # subcommand — CP0 D6), reached through the overlay's narrow credential
@@ -1389,7 +1390,7 @@ smoke_entwurf_v2_matrix_live() {
1389
1390
  # reject, no garbage). Model-in-loop is OUT (GPT Q2): "does the sender model call entwurf_v2"
1390
1391
  # is a separate behavior test — this is a transport/lock/enqueue gate. Negative/timeout/contention
1391
1392
  # stay deterministic. Honest skip when LIVE!=1 so the release-gate is runnable unattended.
1392
- # Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4).
1393
+ # Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.6-luna).
1393
1394
  # LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
1394
1395
  entwurf_require_step_outcome
1395
1396
  if [ "${LIVE:-}" != "1" ]; then
@@ -1628,13 +1629,13 @@ assert.equal(peerTui, piAi,
1628
1629
  // floor tracks the devDep pin so a consumer can't install against a pi lacking
1629
1630
  // the public trust exports the bridge imports at the pinned minor, AND an upper
1630
1631
  // bound at the next minor stops a fresh install from silently pulling a future
1631
- // pi (past the declared ceiling — 0.85+ at the current 0.84.1 pin) whose
1632
+ // pi (past the declared ceiling — 0.85+ at the current 0.84.2 pin) whose
1632
1633
  // internal export surface has drifted from the one we typecheck against.
1633
1634
  // pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
1634
1635
  // factory churn is exactly this), so an open `>=` floor is exactly how the next
1635
1636
  // installer re-acquires the drift. The floor is also the HARD MINIMUM a consumer
1636
- // install resolves: at `>=0.84.1` an existing 0.83.x host is upgraded, not kept.
1637
- // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.84.1 <0.85`).
1637
+ // install resolves: at `>=0.84.2` an existing 0.83.x host is upgraded, not kept.
1638
+ // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.84.2 <0.85`).
1638
1639
  const [piMaj, piMin] = piAi.split('.').map(Number);
1639
1640
  assert.equal(piMaj, 0,
1640
1641
  `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
@@ -2741,6 +2742,9 @@ check_pack() {
2741
2742
  "mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js"
2742
2743
  "mcp/entwurf-bridge/dist/scripts/new-session-id.js"
2743
2744
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js"
2745
+ # #81 — the boot probe doctor-pi-provider imports (and `entwurf probe-bridge-command` runs).
2746
+ # Without the twin the installed doctor dies on the import before printing a verdict.
2747
+ "mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js"
2744
2748
  # The generation verb. The hosts that need it are installed hosts on a
2745
2749
  # previous-generation store; without this twin the prescription every
2746
2750
  # v3-only rejection names would be dead exactly where it matters.
@@ -2940,6 +2944,8 @@ _check_pack_install_impl() {
2940
2944
  "mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js"
2941
2945
  "mcp/entwurf-bridge/dist/scripts/new-session-id.js"
2942
2946
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js"
2947
+ # #81 — the boot probe the installed doctor imports (see check-pack).
2948
+ "mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js"
2943
2949
  # The generation verb (see check-pack). The installed-command regression below
2944
2950
  # opens a fresh generation on a 0-record sandbox through the real bin.
2945
2951
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js"
@@ -3021,7 +3027,7 @@ _check_pack_install_impl() {
3021
3027
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
3022
3028
 
3023
3029
  # pi-agent-core is pinned even though we never import it: pi-coding-agent depends
3024
- # on it by CARET (`^0.84.1`), so with no lockfile in this fresh temp project it
3030
+ # on it by CARET (`^0.84.2`), so with no lockfile in this fresh temp project it
3025
3031
  # floats to whatever pi published last — and that newer core then drags a NESTED
3026
3032
  # pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
3027
3033
  # pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
@@ -3040,12 +3046,12 @@ _check_pack_install_impl() {
3040
3046
  local install_log
3041
3047
  install_log=$(cd "$tmp" && pnpm add \
3042
3048
  "$tgz_path" \
3043
- "@earendil-works/pi-ai@0.84.1" \
3044
- "@earendil-works/pi-coding-agent@0.84.1" \
3045
- "@earendil-works/pi-tui@0.84.1" \
3046
- "@earendil-works/pi-agent-core@0.84.1" \
3047
- "@earendil-works/pi-client@0.84.1" \
3048
- "@earendil-works/pi-protocol@0.84.1" \
3049
+ "@earendil-works/pi-ai@0.84.2" \
3050
+ "@earendil-works/pi-coding-agent@0.84.2" \
3051
+ "@earendil-works/pi-tui@0.84.2" \
3052
+ "@earendil-works/pi-agent-core@0.84.2" \
3053
+ "@earendil-works/pi-client@0.84.2" \
3054
+ "@earendil-works/pi-protocol@0.84.2" \
3049
3055
  "typebox@latest" \
3050
3056
  --ignore-workspace --ignore-scripts 2>&1) || {
3051
3057
  fail "[check-pack-install] pnpm add failed:"
@@ -3055,17 +3061,17 @@ _check_pack_install_impl() {
3055
3061
 
3056
3062
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
3057
3063
  # @earendil-works pi package present — direct or transitive, top level or nested —
3058
- # must be the pinned 0.84.1. Anything else means an unpinned caret floated and the
3064
+ # must be the pinned 0.84.2. Anything else means an unpinned caret floated and the
3059
3065
  # rest of this gate would be exercising a runtime nobody verified, while still
3060
- # printing "pinned pi 0.84.1". Fail loud instead of proving the wrong floor.
3066
+ # printing "pinned pi 0.84.2". Fail loud instead of proving the wrong floor.
3061
3067
  local leaked_pi
3062
- leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.84\.1' || true)
3068
+ leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.84\.2' || true)
3063
3069
  if [ -n "$leaked_pi" ]; then
3064
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.84.1):"
3070
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.84.2):"
3065
3071
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
3066
3072
  return 1
3067
3073
  fi
3068
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.84.1"
3074
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.84.2"
3069
3075
 
3070
3076
  # Resolve the installed package.json and confirm pi.extensions
3071
3077
  # arrived intact. If pi.extensions is empty or missing, the
@@ -4463,6 +4469,23 @@ release_gate() {
4463
4469
  # is gated behind SMOKE_RGG_POSITIVE=1 in the BEHAVIOR lane below.
4464
4470
  run_step "smoke-resident-garden-guard (3c citizen: record birth / record-keyed socket / attach-on-reopen, 0-token)" gate env SMOKE_RGG_POSITIVE=0 bash "$self" smoke-resident-garden-guard
4465
4471
  run_step "check-bridge" gate bash "$self" check-bridge
4472
+ # check-bridge booted the launcher THIS CHECKOUT ships; this boots the invocation the operator's
4473
+ # pi provider actually EXECS. Two different strings, and only the second one reaches a live ACP
4474
+ # session. 2026-08-19 measured the gap at full price: `~/.local/bin/entwurf-bridge` had been
4475
+ # relocated onto a pnpm cmd-shim (basedir derived from $0, so not relocatable), it exited 127, the
4476
+ # bundled bridge never booted, and the model had no `mcp__entwurf-bridge__*` tool — while
4477
+ # `command -v` answered yes throughout. #81's probe already settles that in under a second and
4478
+ # doctor-pi-provider consumes it; it was simply never a step, so the verdict first surfaced at
4479
+ # smoke-acp-bundled-mcp-live, sixteen LIVE steps and ~20 minutes of real model spend later.
4480
+ # `run_step`, not `run_live_step`: this doctor is not LIVE-gated and never emits 97, so it has no
4481
+ # prerequisite to decline. It exits 1 for a configured invocation that will not boot and for
4482
+ # state-owned drift; a host that never installed the circuit keeps its existing exit 0 note, and
4483
+ # the bundled smokes below still fail loud there. So this tightens nothing — it only moves an
4484
+ # existing red earlier. cwd is REPO_DIR regardless of `gate` (run_ts cds there), so the scope
4485
+ # judged is this checkout's `.pi/settings.json` shadowing the operator's global: deterministic,
4486
+ # but not in principle the smokes' global-only subject if the two ever diverge.
4487
+ # Position and classifier arm are pinned by [QK:PI-DOCTOR-IS-RELEASE-MUST].
4488
+ run_step "doctor-pi-provider (#81: the operator's CONFIGURED bridge invocation actually boots)" gate bash "$self" doctor-pi-provider
4466
4489
  # D4-c: the v2 dispatch substrate sentinel (5d-5). A SINGLE run (NOT backend-looped — it proves
4467
4490
  # production runEntwurfV2 deps + real pi control-socket RPC + real mailbox enqueue + v2 lock, not
4468
4491
  # per-backend model behavior). Placed right after check-bridge: the MCP/protocol substrate must be
@@ -5103,12 +5126,34 @@ case "$cmd" in
5103
5126
  # Fail-loud doctor for agy hooks.json imprint wiring.
5104
5127
  (cd "$REPO_DIR" && bash scripts/agy-hooks-bridge.sh doctor "$@")
5105
5128
  ;;
5129
+ check-probe-bridge-command)
5130
+ # #81: contract gate for the boot probe both doctors stake their verdict on — the reason
5131
+ # taxonomy (each value = a different operator repair) plus the one side effect the probe owns,
5132
+ # reaping the child it spawned. Hermetic stubs only; boots no bridge of ours.
5133
+ run_ts scripts/check-probe-bridge-command.ts
5134
+ ;;
5135
+ probe-bridge-command)
5136
+ # #81: does a configured bridge command actually BOOT and serve MCP? `command -v` answering
5137
+ # yes is not that claim — a relocated launcher can resolve and still exit 127, which is how a
5138
+ # host ran with NO bridge in its ACP turns while every doctor printed ok. It waits for the
5139
+ # initialize RESPONSE before it sends notifications/initialized + `tools/list` (no tools/call),
5140
+ # so it takes no lock, writes no record, and delivers nothing. Both doctors route their boot
5141
+ # cell here so one leaf owns the verdict — agy for every configured {command,args,env}, pi for
5142
+ # every effective stdio invocation.
5143
+ shift || true
5144
+ run_ts scripts/probe-bridge-command.ts "$@"
5145
+ ;;
5106
5146
  doctor-pi-provider)
5107
- # #46 Task 2: read-only fail-loud doctor for the pi provider ownership (entwurfProvider.
5108
- # mcpServers.entwurf-bridge). Uses config.ts readProviderSettingsFile SSOT for the EFFECTIVE
5109
- # (project-shadows-user) command never a re-implemented merge. Reports user/project/effective,
5110
- # gates on stable-bin resolvability, and distinguishes state-owned drift (FAIL) from an
5111
- # unowned user override (honest note). No agy/pi process needed — pure settings inspection.
5147
+ # #46 Task 2 + #81: fail-loud doctor for the pi provider ownership (entwurfProvider.
5148
+ # mcpServers.entwurf-bridge). Uses the config.ts SSOT for the EFFECTIVE (project-shadows-user)
5149
+ # entry — per-name merge then ONE normalize, exactly as resolveProviderConfig does; never a
5150
+ # re-implemented merge. Reports user/project/effective and distinguishes state-owned drift
5151
+ # (FAIL) from an unowned user override (honest note).
5152
+ # This doctor no longer merely reads files: it BOOTS every effective stdio invocation and
5153
+ # requires the entwurf verb set back, because `command -v` succeeding was never evidence that
5154
+ # pi gets a bridge. Runtime evidence and ownership are separate: an unowned override is not
5155
+ # repaired, but a dead one is still red. It writes no operator state, but it does exec the
5156
+ # configured command on this host. No agy/pi process is needed.
5112
5157
  run_ts scripts/doctor-pi-provider.ts "$@"
5113
5158
  ;;
5114
5159
  wire-agy-statusline)
@@ -28,8 +28,12 @@ Subcommands (argv[1]):
28
28
  This is NOT tracked for an honest inverse: the legacy entry was wrong and stays gone.
29
29
 
30
30
  doctor-static <config_path>
31
- Print one line describing the candidate for the shell doctor: `absent` / `symlink ->
32
- <target>` prefix / `invalid-json` / `not-configured` / `command <cmd>`. Never mutates.
31
+ Print one line describing the candidate for the shell doctor: `absent` / `invalid-json` /
32
+ `invalid-entry` / `not-configured` / `configured <cmd>`. Never mutates.
33
+
34
+ doctor-invocation <config_path>
35
+ Print the configured server's exact `{command,args,env}` as compact JSON for the boot probe.
36
+ Invalid or absent entries fail rather than silently dropping argv/environment.
33
37
 
34
38
  permission-install <settings_path> <state_path>
35
39
  The OTHER half of "agy can call our bridge": registering the server (above) makes the tools
@@ -349,25 +353,51 @@ def cmd_clean_legacy(config_path: str) -> None:
349
353
  sys.stdout.write(f"cleaned-kept {config_path}\n")
350
354
 
351
355
 
352
- def cmd_doctor_static(config_path: str) -> None:
353
- # Report the RESOLVED path's config status in one shell-parseable token line. Symlink
354
- # detection/reporting is the shell's job (realpath here just follows any link).
356
+ def _doctor_invocation(config_path: str):
357
+ """Return the exact stdio invocation agy reads, or a status token.
358
+
359
+ Command-only inspection is a false-success surface: configured argv/env can make a launcher
360
+ fail even while the same command boots with defaults. Display and execution share this parser
361
+ so they cannot disagree about whether an entry is valid.
362
+ """
355
363
  real = os.path.realpath(config_path)
356
364
  if not os.path.exists(real):
357
- sys.stdout.write("absent\n")
358
- return
365
+ return "absent", None
359
366
  try:
360
367
  with open(real, "r", encoding="utf-8") as fh:
361
368
  data = json.loads(fh.read() or "{}")
362
369
  except (json.JSONDecodeError, OSError):
363
- sys.stdout.write("invalid-json\n")
364
- return
370
+ return "invalid-json", None
365
371
  server = (data.get("mcpServers") or {}).get(SERVER_KEY) if isinstance(data, dict) else None
366
- if not isinstance(server, dict) or not server.get("command"):
367
- sys.stdout.write("not-configured\n")
372
+ if server is None:
373
+ return "not-configured", None
374
+ if not isinstance(server, dict):
375
+ return "invalid-entry", None
376
+ command = server.get("command")
377
+ args = server.get("args", [])
378
+ env = server.get("env", {})
379
+ if not isinstance(command, str) or not command:
380
+ return "invalid-entry", None
381
+ if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
382
+ return "invalid-entry", None
383
+ if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
384
+ return "invalid-entry", None
385
+ return "configured", {"command": command, "args": args, "env": env}
386
+
387
+
388
+ def cmd_doctor_static(config_path: str) -> None:
389
+ status, invocation = _doctor_invocation(config_path)
390
+ if status != "configured":
391
+ sys.stdout.write(f"{status}\n")
368
392
  return
369
- # "configured <command>" — command is the trailing token(s); shell takes field 2..N.
370
- sys.stdout.write(f"configured {server['command']}\n")
393
+ sys.stdout.write(f"configured {invocation['command']}\n")
394
+
395
+
396
+ def cmd_doctor_invocation(config_path: str) -> None:
397
+ status, invocation = _doctor_invocation(config_path)
398
+ if status != "configured":
399
+ _die(4, f"agy-bridge: cannot read configured invocation from {config_path}: {status}")
400
+ sys.stdout.write(json.dumps(invocation, separators=(",", ":")) + "\n")
371
401
 
372
402
 
373
403
  def cmd_permission_install(settings_path: str, state_path: str) -> None:
@@ -619,6 +649,10 @@ def main(argv: list) -> None:
619
649
  if len(argv) != 3:
620
650
  _die(5, "usage: agy-bridge-config.py doctor-static <config_path>")
621
651
  cmd_doctor_static(argv[2])
652
+ elif sub == "doctor-invocation":
653
+ if len(argv) != 3:
654
+ _die(5, "usage: agy-bridge-config.py doctor-invocation <config_path>")
655
+ cmd_doctor_invocation(argv[2])
622
656
  elif sub == "permission-state-doctor":
623
657
  if len(argv) != 3:
624
658
  _die(5, "usage: agy-bridge-config.py permission-state-doctor <state_path>")
@@ -7,9 +7,11 @@
7
7
  # install-state for an honest inverse. The command written is a STABLE bin
8
8
  # (`entwurf-bridge`), NEVER a repo/git-hash path (the oracle dangling lesson).
9
9
  # uninstall honest inverse from the install-state (restore preimage / remove our key).
10
- # doctor 2-tier: STATIC proves both candidate configs (documented + observed) resolve,
11
- # parse, and carry a resolvable command; LIVE proves runtime-effectiveness only
12
- # when an agy process exists, else an honest SKIP (never a PASS in disguise).
10
+ # doctor 2-tier: STATIC proves both candidate configs (documented + observed) resolve, parse,
11
+ # and carry a command that actually BOOTS the entwurf MCP surface (#81 — resolvable
12
+ # is necessary, not sufficient; it execs the configured command); LIVE proves
13
+ # runtime-effectiveness only when an agy process exists, else an honest SKIP (never a
14
+ # PASS in disguise).
13
15
  #
14
16
  # GLOBAL ROOT (the one file that matters): live agy reads its global MCP config from
15
17
  # ~/.gemini/config/mcp_config.json (agy's own builtin doc: mcp_servers.md — "Global Configuration:
@@ -25,6 +27,7 @@
25
27
  set -euo pipefail
26
28
 
27
29
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ REPO_DIR="$(cd "$HERE/.." && pwd)"
28
31
  CONFIG_PY="$HERE/agy-bridge-config.py"
29
32
 
30
33
  GLOBAL_CONFIG="${AGY_MCP_CONFIG:-$HOME/.gemini/config/mcp_config.json}"
@@ -62,6 +65,33 @@ command_resolvable() {
62
65
  esac
63
66
  }
64
67
 
68
+ # Does COMMAND actually BOOT and serve the entwurf MCP surface? (#81) Resolvability is necessary
69
+ # and NOT sufficient: on the reference host the bare name resolved through a relocated pnpm shim
70
+ # whose $0-derived target was gone, so it exited 127 and agy would have had no entwurf tool — while
71
+ # this doctor printed "(resolvable)" and stayed green. The verdict leaf is shared with the pi
72
+ # doctor via run.sh (the ONE strip-types fence crossing), so both lanes judge boot the same way.
73
+ #
74
+ # Cached per exact invocation: the probe spawns a real child, and the doctor asks about the same
75
+ # configured {command,args,env} for two candidate configs plus the live tier. BOOT_DETAIL carries
76
+ # the last probe's one-line verdict for the caller to print.
77
+ BOOT_PROBED_INVOCATION=""
78
+ BOOT_PROBED_RC=1
79
+ BOOT_PROBED_OUT=""
80
+ BOOT_DETAIL=""
81
+ command_boots() {
82
+ local invocation="$1" out rc
83
+ if [ "$invocation" = "$BOOT_PROBED_INVOCATION" ]; then
84
+ BOOT_DETAIL="$BOOT_PROBED_OUT"
85
+ return "$BOOT_PROBED_RC"
86
+ fi
87
+ set +e
88
+ out="$("$REPO_DIR/run.sh" probe-bridge-command --invocation-json "$invocation" 2>&1)"
89
+ rc=$?
90
+ set -e
91
+ BOOT_PROBED_INVOCATION="$invocation"; BOOT_PROBED_RC="$rc"; BOOT_PROBED_OUT="$out"; BOOT_DETAIL="$out"
92
+ return "$rc"
93
+ }
94
+
65
95
  # Prune the agy MCP tool-schema cache for the KNOWN-legacy server keys (LEGACY_CACHE_KEYS). Removes
66
96
  # ONLY the exact-named dirs — a symlink is left intact (not ours), and any OTHER server's cache is
67
97
  # never touched. Idempotent (absent = no-op). One-shot cutover hygiene, not honest-inverse tracked.
@@ -171,8 +201,8 @@ do_uninstall() {
171
201
  }
172
202
 
173
203
  # Static-check ONE candidate config. Prints a status line; returns 1 on a hard failure
174
- # (invalid JSON / configured-but-dangling command), 0 otherwise (absent / not-configured /
175
- # configured+resolvable are not doctor failures — a candidate may legitimately be unused).
204
+ # (invalid JSON / configured invocation that cannot boot), 0 otherwise (absent / not-configured
205
+ # are not doctor failures — a candidate may legitimately be unused).
176
206
  doctor_static_one() {
177
207
  local label="$1" candidate="$2"
178
208
  local link_note=""
@@ -185,13 +215,25 @@ doctor_static_one() {
185
215
  absent) log " $label: absent$link_note"; return 0 ;;
186
216
  not-configured) log " $label: present but entwurf-bridge NOT configured$link_note"; return 0 ;;
187
217
  invalid-json) log " $label: INVALID JSON$link_note"; return 1 ;;
218
+ invalid-entry) log " $label: INVALID entwurf-bridge entry (command/args/env)$link_note"; return 1 ;;
188
219
  configured\ *)
189
- local cmd="${status#configured }"
190
- if command_resolvable "$cmd"; then
191
- log " $label: configured → '$cmd' (resolvable)$link_note"
220
+ local cmd="${status#configured }" invocation
221
+ if ! invocation="$(python3 "$CONFIG_PY" doctor-invocation "$candidate")"; then
222
+ log " $label: configured → '$cmd' but its exact invocation is unreadable$link_note"
223
+ return 1
224
+ fi
225
+ if command_boots "$invocation"; then
226
+ log " $label: configured → '$cmd' (the exact configured invocation boots the entwurf MCP surface)$link_note"
192
227
  return 0
193
228
  fi
194
- log " $label: configured '$cmd' DANGLING (not on PATH / not executable)$link_note"
229
+ # A configured environment may itself carry PATH, so a shell-side `command -v` is not the
230
+ # runtime's subject. The shared probe executes the exact command + args + env instead.
231
+ # entwurf does NOT repair a failed launcher here: it may be owned by a foreign file.
232
+ log " $label: configured → '$cmd' does NOT serve MCP with its configured args/env$link_note"
233
+ log " $BOOT_DETAIL"
234
+ log " Identify the launcher: command -v '$cmd'; readlink -f \"\$(command -v '$cmd')\""
235
+ log " If it is entwurf's managed dev link, restore it with ./run.sh expose-dev-bin (it REFUSES a foreign link)."
236
+ log " If a foreign launcher owns the name, repair/remove it yourself or put a working one earlier on PATH."
195
237
  return 1 ;;
196
238
  *) log " $label: unexpected status '$status'$link_note"; return 1 ;;
197
239
  esac
@@ -283,23 +325,29 @@ sys.exit(0 if same and owned else 1)' "$PERMISSION_STATE_FILE" "$SETTINGS_FILE";
283
325
 
284
326
  do_doctor() {
285
327
  log "[agy-bridge doctor]"
286
- local hard_fail=0 configured_any=0 resolvable_any=0
328
+ local hard_fail=0 configured_any=0 bootable_any=0
287
329
 
288
330
  log "── static (configured candidates)"
289
331
  doctor_static_one "global ($GLOBAL_CONFIG)" "$GLOBAL_CONFIG" || hard_fail=1
290
332
  doctor_static_one "legacy ($LEGACY_CONFIG)" "$LEGACY_CONFIG" || hard_fail=1
291
333
  doctor_permission || hard_fail=1
292
- # Did EITHER candidate carry a configured + resolvable entwurf-bridge? Keep this runtime fact
293
- # separate from ownership-state failures below: a FOREIGN TARGET makes the doctor red, but it
294
- # does not make a visibly configured command disappear.
295
- local c candidate_status candidate_cmd
334
+ # Did EITHER candidate carry a configured entwurf-bridge that actually BOOTS? Keep this runtime
335
+ # fact separate from ownership-state failures below: a FOREIGN TARGET makes the doctor red, but it
336
+ # does not make a visibly configured command disappear. Boot (not mere resolvability) is the fact
337
+ # the live tier reports on — the probe is cached, so this loop re-costs nothing.
338
+ local c candidate_status candidate_cmd candidate_invocation
296
339
  for c in "$GLOBAL_CONFIG" "$LEGACY_CONFIG"; do
297
340
  candidate_status="$(python3 "$CONFIG_PY" doctor-static "$c")"
298
341
  case "$candidate_status" in
299
342
  configured\ *)
300
343
  configured_any=1
301
344
  candidate_cmd="${candidate_status#configured }"
302
- command_resolvable "$candidate_cmd" && resolvable_any=1
345
+ if candidate_invocation="$(python3 "$CONFIG_PY" doctor-invocation "$c")"; then
346
+ command_boots "$candidate_invocation" && bootable_any=1
347
+ else
348
+ log " runtime: configured '$candidate_cmd' has an unreadable exact invocation."
349
+ hard_fail=1
350
+ fi
303
351
  ;;
304
352
  esac
305
353
  done
@@ -373,17 +421,19 @@ do_doctor() {
373
421
 
374
422
  log "── live (runtime wiring)"
375
423
  if command -v pgrep >/dev/null 2>&1 && pgrep -x agy >/dev/null 2>&1; then
376
- if [ "$resolvable_any" -eq 1 ]; then
377
- # HONEST label (N2): a running agy + a resolvable configured candidate is CONSISTENT with
378
- # runtime wiring, but it does NOT prove agy actually read that config — that needs MCP
379
- # tool-listing-grade evidence (deferred). Ownership/state failures remain red independently.
424
+ if [ "$bootable_any" -eq 1 ]; then
425
+ # HONEST label (N2): a running agy + a configured candidate whose command BOOTS and serves the
426
+ # entwurf MCP surface is CONSISTENT with runtime wiring. It still does NOT prove agy actually
427
+ # READ that config — that remains the deferred half. What is no longer deferred (#81) is the
428
+ # tool-listing evidence itself: the static tier above got it from the command, not from the
429
+ # mere fact that a name resolved. Ownership/state failures remain red independently.
380
430
  if [ "$hard_fail" -eq 0 ]; then
381
- log " live: agy is running AND a configured candidate has a resolvable command — consistent with runtime wiring (config-read NOT proven; MCP-tool-listing evidence deferred)."
431
+ log " live: agy is running AND a configured candidate's command boots the entwurf MCP surface — consistent with runtime wiring (agy's own config-read NOT proven)."
382
432
  else
383
- log " live: agy is running and a configured candidate resolves, but ownership/state errors above keep this doctor red (config-read NOT proven)."
433
+ log " live: agy is running and a configured candidate boots, but ownership/state errors above keep this doctor red (agy's own config-read NOT proven)."
384
434
  fi
385
435
  else
386
- log " live: agy is running but no resolvable configured candidate — runtime wiring is broken."
436
+ log " live: agy is running but no configured candidate whose command boots the entwurf MCP surface — runtime wiring is broken."
387
437
  hard_fail=1
388
438
  fi
389
439
  else
@@ -391,7 +441,7 @@ do_doctor() {
391
441
  fi
392
442
 
393
443
  if [ "$hard_fail" -ne 0 ]; then
394
- fail "doctor found a broken candidate (invalid JSON / dangling command / broken live wiring)."
444
+ fail "doctor found a broken candidate (invalid JSON / dangling command / command that does not serve MCP / broken live wiring)."
395
445
  fi
396
446
  log "doctor: ok (static candidates clean)."
397
447
  }