@junghanacs/entwurf 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/run.sh CHANGED
@@ -119,7 +119,7 @@ run_vitest() {
119
119
  usage() {
120
120
  cat <<'EOF'
121
121
  Usage:
122
- ./run.sh setup [project-dir] # ONE confident install: pnpm install + install + meta-bridge (if native harness) + v2 install smoke (LIVE substrate = release-gate)
122
+ ./run.sh setup [project-dir] # ONE presence-driven composition (#86): mode-first (source bootstrap only on a checkout), then per-component PASS/SKIP/FAIL for pi (presence+floor)/claude/agy/copilot (all four units: birth→MCP→receiver→footer, independently) + stable dev bins + v2 install smoke; absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness or touches credentials
123
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
@@ -181,7 +181,8 @@ Usage:
181
181
  ./run.sh smoke-meta-honesty # 1.0.0 meta-bridge: honesty regression gate (#30 blockers) — doorbell counts ALL msgs honestly + hook logs failures as ERROR (best-effort, no scream). Offline/deterministic (deps: bash+node+python3)
182
182
  ./run.sh smoke-meta-install-state # 1.0.0 meta-bridge Phase 2: stateful install/uninstall + store-doctor regression gate. Offline/deterministic (deps: bash+node+python3)
183
183
  ./run.sh check-meta-doctor-oracle # 0.12.8 (#51): detection power of the release ORACLE — healthy fixture must reach `doctor: PASS`, then 21 planted defects (retired shell form, partial hand-patch, launcher bypass/repoint/provenance loss, malformed exec args, owner type drift, extra leaf/group, doorbell asyncRewake/path/timeout, no live bridge, stale receiver, ambiguous/missing cache, missing hook log/writer, failing CLI probes) must each turn it FAIL naming their own cause, plus a positive case pinning that a long-writing CLI is NOT a false negative. Offline/deterministic (deps: bash+node+python3)
184
- ./run.sh smoke-agy-install-state # agy MCP + exact permission ownership regression: isolated HOME+XDG, adopt/state/inverse, symlink refuse, setup degrade. Offline/deterministic
184
+ ./run.sh smoke-agy-install-state # agy MCP + exact permission ownership regression: isolated HOME+XDG, adopt/state/inverse, symlink refuse, truthful setup outcomes. Offline/deterministic
185
+ ./run.sh smoke-setup-verdict # #86 C1+C3b aggregate setup verdict fixture: all-absent SKIP/green, pi presence+floor, detected-FAIL nonzero, copilot four-unit composition (present=independent PASS/FAIL rows, absent=zero-state SKIP), installed-mode named branch, credential store untouched. Offline/deterministic
185
186
  ./run.sh check-agy-permission-matrix # AGY permission CONTRACT SPACE as a literal table (55 cells): parser-state × operation × settings × ownership × precedence with stated exclusion rules; expectations are hand-written literals, never read from the SUT. Offline/deterministic (deps: python3)
186
187
  ./run.sh smoke-entwurf-chain-live # LIVE cross-harness delivery chain: native Claude Code -> pi GPT -> pi ACP Sonnet -> mailbox terminus, proving sender identity/replyable at every hop and a real read receipt at the end. Prerequisites (claude on PATH, pi credentials per backend) report protocol SKIP, never a pass
187
188
  ./run.sh check-release-gate-outcomes # release-gate STEP OUTCOME protocol (P1): one skip exit code shared by the shell + TS halves, classifier never rounds a skip up to a pass, `--cut` refuses a MUST SKIP while a bare diagnostic stays exit 0, no LIVE smoke keeps the old exit-0 skip shape, and both real skip surfaces are INVOKED and observed to propagate the code
@@ -192,7 +193,7 @@ Usage:
192
193
  ./run.sh smoke-copilot-statusline-state # Copilot custom-footer install/doctor/inverse regression. Offline/deterministic
193
194
  ./run.sh smoke-copilot-mcp-state # Copilot MCP install/doctor/inverse regression. Offline/deterministic
194
195
  ./run.sh smoke-agy-hooks-state # agy PreInvocation birth/sender hook install/doctor/inverse + direct stdin→meta-record regression. Offline/deterministic
195
- ./run.sh smoke-user-scope-citizen # 0.12.6 install-boundary: pi packages[] registration SSOT (register-pi-package.py) — idempotent + preserves unrelated + normalizes stale + remove symmetry + fails loud. Offline/hermetic (deps: bash+python3)
196
+ ./run.sh smoke-user-scope-citizen # 0.12.6 install-boundary: pi packages[] registration SSOT (register-pi-package.py) — idempotent + preserves unrelated + remove symmetry + fails loud, and the #86 C2 explicit ownership cells (project scope still normalizes ITS OWN stale entries; user scope refuses other owners and only takeover-user-scope moves the shared entry). Offline/hermetic (deps: bash+python3)
196
197
  ./run.sh smoke-meta-prune # 1.0.0 meta-bridge Phase 4: listing-only store janitor regression gate — classify keep/orphan/stale/ambiguous, delete nothing. Offline/deterministic (deps: bash+node)
197
198
  ./run.sh smoke-meta-keyset-guard # 0.10.0 meta-bridge: keyset-owner guard regression — check-keyset-overlap + managed-keys SSOT (disjoint passes, collisions fail). Offline/hermetic (deps: bash+python3)
198
199
  ./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
@@ -203,7 +204,8 @@ Usage:
203
204
  ./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
204
205
  ./run.sh copilot [args...] # #82 RAIL 7: the MANAGED Copilot launch. exec()s the vendor CLI in THIS terminal (cwd/pid/exit preserved) with COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS set for that one invocation — entwurf owns no part of your shell and writes nothing to it, but it owns the process it is about to become. Without that flag Copilot skips the extension scan SILENTLY. Refuses to launch unless the receiver unit is actually installed. Injects `--model auto` when no --model was given and `--yolo` when no explicit permission/surface policy flag was given, both BEFORE any `--`; every argument you pass is forwarded byte-identical. RUNNING THIS IS YOUR CONSENT to that profile — use plain `copilot` for stock vendor behaviour. Not tmux, not fresh-call, mints no citizen (birth is still the first prompt)
205
206
  ./run.sh install-copilot-bridge # #82: GLOBAL install of the Copilot BIRTH plugin (own marketplace root; node+entry baked into the no-argv exec string). MCP wiring and the RECEIVER extension are separate install surfaces (install-copilot-mcp, install-copilot-receive). Also retires the stale Claude unit (--keep-stale-claude-unit opts out)
206
- ./run.sh doctor-copilot-bridge # #82: fail-loud surface for that unit. Red = a hook that RAN and failed, or a broken/unbaked artifact. "Installed with zero records" is NOT red and is reported as NOT-YET: a Copilot session is born on its FIRST PROMPT, not when the window opens (measured)
207
+ ./run.sh uninstall-copilot-bridge # #86 C3a: honest inverse of install-copilot-bridge from the package-owned install-state (exact qualified id + exact marketplace name/path + recorded assembly only; state deleted LAST; a failing vendor list is UNKNOWN and refuses; never --force, never the stale Claude unit)
208
+ ./run.sh doctor-copilot-bridge # #82/#86: fail-loud surface for that unit — runtime axis (red = a hook that RAN and failed, or a broken/unbaked artifact; "installed with zero records" is NOT red and is reported as NOT-YET: a Copilot session is born on its FIRST PROMPT, not when the window opens, measured) + ownership axis (install-state shape/binding, marketplace path drift, UNKNOWN vendor lists; legacy no-state install = named non-green, repair = install-copilot-bridge adoption). Either axis red = red
207
209
  ./run.sh install-copilot-statusline # own Copilot statusLine + footer.showCustom with an install-state preimage
208
210
  ./run.sh uninstall-copilot-statusline # honest inverse of install-copilot-statusline
209
211
  ./run.sh doctor-copilot-statusline # static ownership/config/bin doctor; never claims a render receipt
@@ -233,12 +235,13 @@ Usage:
233
235
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
234
236
  ./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes (version boundary: @0.84.30 must leak, @0.84.3 bare/peer-hash must pass); snapshot-safe qualification oracle, also run first inside check-pack-install
235
237
  ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
236
- ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (0.82.x) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES)
238
+ ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES)
237
239
  ./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
238
- ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
239
240
  ./run.sh install [project-dir] # INTERNAL part of `setup` (project .pi/settings.json wiring) + npm-consumer entry — prefer `setup`, don't call directly for dev
240
241
  ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
241
- ./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project)
242
+ ./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project). #86 C2: same-owner-only — a LIVE foreign owner refuses; a MISSING owner is removed only when package entry + package state + provider installerRoot all align (reported orphan cleanup)
243
+ ./run.sh takeover-user-scope # #86 C2: operator-EXPLICIT ownership move of the shared user-scope registration to THIS root (old→new reported, packages[] entry + provider installerRoot together). The only writer over another owner — normal install/setup/remove refuse instead. No --force exists
244
+ ./run.sh doctor-pi-package # #86 C2: package-side user-scope ownership verdict — unregistered / owned / owned-by-other(live) / legacy-no-state / mismatch / missing-owner (nonzero on defect verdicts). Provider runtime verdicts stay with doctor-pi-provider
242
245
 
243
246
  Notes:
244
247
  - project-dir defaults to current directory
@@ -268,44 +271,12 @@ print(os.path.abspath(os.path.expanduser(sys.argv[1])))
268
271
  PY
269
272
  }
270
273
 
271
- sync_auth() {
272
- local auth_path="$HOME/.pi/agent/auth.json"
273
- python3 - "$auth_path" "$PROVIDER_ID" <<'PY'
274
- import json, os, sys
275
- from pathlib import Path
276
-
277
- auth_path = Path(sys.argv[1]).expanduser()
278
- provider_id = sys.argv[2]
279
- auth_path.parent.mkdir(parents=True, exist_ok=True)
280
-
281
- if auth_path.exists():
282
- data = json.loads(auth_path.read_text())
283
- if not isinstance(data, dict):
284
- raise SystemExit("auth.json is not an object")
285
- else:
286
- data = {}
287
-
288
- anthropic = data.get("anthropic")
289
- if not isinstance(anthropic, dict):
290
- print("sync-auth: skipped (no anthropic OAuth credentials in ~/.pi/agent/auth.json)")
291
- raise SystemExit(0)
292
-
293
- before = json.dumps(data.get(provider_id), sort_keys=True)
294
- after = json.dumps(anthropic, sort_keys=True)
295
- if before == after:
296
- print(f"sync-auth: already synced ({provider_id})")
297
- raise SystemExit(0)
298
-
299
- data[provider_id] = anthropic
300
- backup = auth_path.with_suffix(auth_path.suffix + ".bak")
301
- if auth_path.exists():
302
- backup.write_text(auth_path.read_text())
303
- auth_path.write_text(json.dumps(data, indent=2) + "\n")
304
- print(f"sync-auth: wrote {provider_id} alias to {auth_path}")
305
- if backup.exists():
306
- print(f"sync-auth: backup -> {backup}")
307
- PY
308
- }
274
+ # The old `sync_auth` credential mutation is GONE (#86 A5): setup composes what
275
+ # the operator already installed and NEVER reads, copies, aliases or backs up a
276
+ # harness credential store — the `entwurf` alias it wrote had no in-repo reader
277
+ # (the ACP provider passes pi's auth check with the no-auth sentinel), and the
278
+ # `auth.json.bak` it left is documented manual-operator-cleanup ONLY. Do not
279
+ # reintroduce credential copying as a convenience helper in any install path.
309
280
 
310
281
  # Repo dependency integrity is a HARD requirement for install — NOT gated on
311
282
  # backend (Claude/ACP) auth. A cloned-but-not-installed or a moved/renamed repo
@@ -514,6 +485,14 @@ PY
514
485
  }
515
486
 
516
487
  install_local_package() {
488
+ # Named interpreter prerequisite BEFORE any preflight/write (#86 E5): the
489
+ # packages[]/provider writers below are python3-backed and `entwurf install`
490
+ # dispatches here directly, so a python3-less host gets this verdict instead
491
+ # of an accidental mid-write failure.
492
+ command -v python3 >/dev/null 2>&1 || {
493
+ echo "[install] entwurf install requires python3 on PATH (settings writers are python3-backed); nothing was written." >&2
494
+ exit 1
495
+ }
517
496
  local project_dir agent_dir
518
497
  project_dir=$(normalize_project_dir "$1")
519
498
  agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
@@ -553,19 +532,79 @@ install_local_package() {
553
532
  # launchers and the npm consumer's "installs → just works" both need the entry
554
533
  # in ~/.pi/agent/settings.json's packages[]. This is the wiring that dropped when
555
534
  # `pi install` was removed from setup (2026-07-03: `--entwurf-control` unknown in
556
- # a foreign cwd). Idempotent: absent → append, present no-op; a stale entwurf
557
- # entry at a different path is normalized to REPO_DIR. Every other package and key
558
- # in the operator's user settings is preserved untouched.
535
+ # a foreign cwd). Idempotent for THIS root: absent → append + owner state, present
536
+ # same-root no-op. #86 C2 retires the silent normalization of other roots'
537
+ # entries: a different owner (live or missing) is a zero-write refusal that names
538
+ # takeover-user-scope. Every other package and key is preserved untouched.
539
+ _pi_package_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-package/install-state.json"; }
540
+ _pi_provider_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"; }
541
+
559
542
  register_user_scope_citizen() {
560
543
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
561
544
  # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
562
- python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR"
545
+ # #86 C2: user scope carries a recorded OWNER (packageRoot in the pi-package
546
+ # install-state). Another owner — live or missing — makes this a zero-write
547
+ # ownership refusal (exit 6) that propagates: install fails, setup records the
548
+ # pi component FAIL. The only writer over another owner is takeover-user-scope.
549
+ #
550
+ # ATOMIC (#86 C2 amendment): BOTH ownership halves are decided READ-ONLY before
551
+ # either writer runs — a provider-side refusal must leave the package side
552
+ # byte-identical (and vice versa), never a half-installed user scope.
553
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
554
+ --scope user --state "$(_pi_package_state)" --preflight >/dev/null
555
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" \
556
+ --scope user --state "$(_pi_provider_state)" --preflight >/dev/null
557
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
558
+ --scope user --state "$(_pi_package_state)"
563
559
  # #46 Task 2: own entwurfProvider.mcpServers.entwurf-bridge as the bare stable bin at USER scope
564
560
  # (GLOBAL/durable →파급s to every cwd), so its inverse needs an install-state honest inverse
565
561
  # under $XDG_DATA_HOME/entwurf/pi-provider/ (Task 0/1 discipline). project scope is checkout-
566
562
  # local and covered by `run.sh remove` (no state) — deliberate, reasoned asymmetry.
567
- local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
568
- python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
563
+ # #86 C2: the provider state records installerRoot, so the same foreign-owner refusal
564
+ # holds here and the package refusal above fires FIRST, before any provider write.
565
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$(_pi_provider_state)"
566
+ }
567
+
568
+ # takeover-user-scope — the operator-explicit ownership move (#86 C2). The ONLY
569
+ # writer that replaces another root's user-scope registration: normal install/
570
+ # setup/remove refuse instead. Reports old→new for both the packages[] entry
571
+ # (packageRoot) and the provider key (installerRoot). No --force variant exists.
572
+ takeover_user_scope() {
573
+ local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" pp_out
574
+ command -v python3 >/dev/null 2>&1 || { echo "[takeover] requires python3 on PATH; nothing was written." >&2; exit 1; }
575
+ section "takeover-user-scope: move the shared user-scope registration to this root"
576
+ # ATOMIC (#86 C2 amendment): both halves decided READ-ONLY first — an exact-entry
577
+ # mismatch on the package side or a refusal on the provider side leaves the other
578
+ # half byte-identical.
579
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
580
+ --scope user --state "$(_pi_package_state)" --takeover --preflight >/dev/null
581
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" \
582
+ --scope user --state "$(_pi_provider_state)" --takeover --preflight >/dev/null
583
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
584
+ --scope user --state "$(_pi_package_state)" --takeover
585
+ pp_out=$(python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" \
586
+ --scope user --state "$(_pi_provider_state)" --takeover)
587
+ printf '%s\n' "$pp_out"
588
+ # Split verdict (#86 C2 amendment): a user-override provider key is preserved and
589
+ # stays UNOWNED — say exactly which half moved instead of a false "both owned".
590
+ if printf '%s' "$pp_out" | grep -q 'provider override preserved'; then
591
+ ok "takeover-user-scope: package owner moved to this root ($REPO_DIR); provider key preserved as the operator's override (unowned — doctor-pi-provider reports it as such)"
592
+ else
593
+ ok "takeover-user-scope: this root now owns the user-scope registration ($REPO_DIR)"
594
+ fi
595
+ }
596
+
597
+ # doctor-pi-package — package-side ownership verdict only (#86 C2): unregistered /
598
+ # owned / owned-by-other(live) / legacy-no-state / mismatch / missing-owner.
599
+ # Deliberately NOT mixed into doctor-pi-provider (provider runtime verdicts stay there).
600
+ doctor_pi_package() {
601
+ local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
602
+ command -v python3 >/dev/null 2>&1 || { echo "[doctor-pi-package] requires python3 on PATH." >&2; exit 1; }
603
+ # #86 C2 amendment: the provider state rides along for the OWNERSHIP coupling
604
+ # verdict only (installerRoot vs packageRoot mismatch = FAIL); provider RUNTIME
605
+ # verdicts stay with doctor-pi-provider.
606
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
607
+ --scope user --state "$(_pi_package_state)" --provider-state "$(_pi_provider_state)" --doctor
569
608
  }
570
609
 
571
610
  # The honest inverse of register_user_scope_citizen: drop entwurf from the GLOBAL
@@ -581,11 +620,54 @@ register_user_scope_citizen() {
581
620
  # reported rather than deleted — install never writes that form (#53 B).
582
621
  remove_user_scope_citizen() {
583
622
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
584
- python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" --remove
623
+ local pkg_state pp_state orphan_args=()
624
+ pkg_state="$(_pi_package_state)"; pp_state="$(_pi_provider_state)"
625
+ # #86 C2 aligned orphan path: --orphan-cleanup is passed ONLY when the package
626
+ # entry, the package state's packageRoot and the provider state's installerRoot
627
+ # all name the SAME root and that root is MISSING on disk. Any live foreign
628
+ # owner, or any mismatch between the three, refuses inside the SSOTs instead.
629
+ if python3 - "$agent_dir/settings.json" "$pkg_state" "$pp_state" <<'PY'
630
+ import json, os, sys
631
+ settings_path, pkg_state_path, pp_state_path = sys.argv[1:4]
632
+ def load(p):
633
+ try:
634
+ with open(p) as fh: return json.load(fh)
635
+ except (OSError, json.JSONDecodeError): return None
636
+ pkg = load(pkg_state_path)
637
+ if not isinstance(pkg, dict) or not isinstance(pkg.get("packageRoot"), str): sys.exit(1)
638
+ owner = pkg["packageRoot"]
639
+ if os.path.isdir(owner): sys.exit(1) # live owner: never orphan
640
+ # Alignment REQUIRES the provider state to exist AND to be bound to the same
641
+ # missing root (#86 C2 amendment): an absent, legacy (no installerRoot) or
642
+ # elsewhere-bound provider state is NOT alignment — both sides refuse instead.
643
+ pp = load(pp_state_path)
644
+ if not isinstance(pp, dict): sys.exit(1)
645
+ if pp.get("installerRoot") != owner: sys.exit(1)
646
+ settings = load(settings_path)
647
+ pkgs = settings.get("packages") if isinstance(settings, dict) else None
648
+ if not isinstance(pkgs, list): sys.exit(1)
649
+ def src(x): return x.get("source") if isinstance(x, dict) else x
650
+ if not any(isinstance(src(x), str) and src(x).rstrip("/") == owner for x in pkgs): sys.exit(1)
651
+ sys.exit(0)
652
+ PY
653
+ then
654
+ orphan_args=(--orphan-cleanup)
655
+ log "remove-user-scope: package entry + package state + provider installerRoot align on a MISSING owner — reported orphan cleanup"
656
+ fi
657
+ # ATOMIC (#86 C2 amendment): both removal halves are decided READ-ONLY first, so
658
+ # a provider-side refusal (live-other, legacy, mismatch) leaves the package
659
+ # entry/state byte-identical instead of a half-removed user scope.
660
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
661
+ --scope user --state "$pkg_state" --remove ${orphan_args[@]+"${orphan_args[@]}"} --preflight >/dev/null
662
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$agent_dir/settings.json" "$REPO_DIR" \
663
+ --scope user --state "$pp_state" ${orphan_args[@]+"${orphan_args[@]}"} --preflight >/dev/null
664
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
665
+ --scope user --state "$pkg_state" --remove ${orphan_args[@]+"${orphan_args[@]}"}
585
666
  # #46 Task 2: honest inverse of the user-scope entwurfProvider ownership — the install-state
586
667
  # drives it (absent/managed-* → remove OUR key; a user-override we never owned is untouched).
587
- local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
588
- python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
668
+ # #86 C2: installerRoot makes this same-owner-only; the aligned orphan flag rides along.
669
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$agent_dir/settings.json" "$REPO_DIR" \
670
+ --scope user --state "$pp_state" ${orphan_args[@]+"${orphan_args[@]}"}
589
671
  }
590
672
 
591
673
  # The ~/.pi/agent/entwurf-targets.json symlink machinery (ensure_agent_dir_symlinks
@@ -2839,6 +2921,8 @@ check_pack() {
2839
2921
  "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh"
2840
2922
  "pi-extensions/meta-bridge-hook-copilot.ts"
2841
2923
  "scripts/copilot-bridge-install.sh"
2924
+ "scripts/copilot-bridge-uninstall.sh"
2925
+ "scripts/copilot-bridge-oracle.sh"
2842
2926
  "scripts/copilot-bridge-doctor.sh"
2843
2927
  # #82 RAIL 5 — the Copilot RECEIVER. A vendor EXTENSION (forked child, stdio
2844
2928
  # JSON-RPC), not a plugin, so it ships as its own unit and installs into the user
@@ -3089,6 +3173,8 @@ _check_pack_install_impl() {
3089
3173
  "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh"
3090
3174
  "pi-extensions/meta-bridge-hook-copilot.ts"
3091
3175
  "scripts/copilot-bridge-install.sh"
3176
+ "scripts/copilot-bridge-uninstall.sh"
3177
+ "scripts/copilot-bridge-oracle.sh"
3092
3178
  "scripts/copilot-bridge-doctor.sh"
3093
3179
  # #82 RAIL 5 — the Copilot RECEIVER. A vendor EXTENSION (forked child, stdio
3094
3180
  # JSON-RPC), not a plugin, so it ships as its own unit and installs into the user
@@ -3144,9 +3230,9 @@ _check_pack_install_impl() {
3144
3230
  # to our pnpm-workspace.yaml; --ignore-scripts blocks the husky
3145
3231
  # prepare hook (and any future install scripts) from running inside
3146
3232
  # the consumer project. Peer deps are pinned to the CURRENT pi release
3147
- # baseline (0.82.x — the pins below are what check-dep-versions binds to
3148
- # the package.json devDep) so the smoke matches the same shape an external
3149
- # pi user would have after `pi install`.
3233
+ # baseline (the package.json devDep pin — the pins below are what
3234
+ # check-dep-versions binds to that devDep) so the smoke matches the same
3235
+ # shape an external pi user would have after `pi install`.
3150
3236
  local tmp npm_tmp
3151
3237
  tmp=$(mktemp -d -t entwurf-install-smoke.XXXXXX)
3152
3238
  # Separate tree for the npm-managed regression below: npm install must NOT be
@@ -3414,6 +3500,230 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
3414
3500
  fi
3415
3501
  echo "[check-pack-install] user-scope citizen regression pass (npm consumer: --entwurf-control loads from a foreign cwd)"
3416
3502
 
3503
+ # Two-root ownership row (#86 C2, L1b extension): a SECOND npm root under the
3504
+ # SAME consumer HOME must not silently steal the user-scope registration the
3505
+ # first root owns — normal install refuses (naming takeover-user-scope) and the
3506
+ # operator-explicit takeover is what moves the shared entry, old→new reported.
3507
+ local npmroot2="$npm_tmp/npmroot2" npmproj2="$npm_tmp/npmproj2" npm2_log two_root_out two_root_rc npm2_pkg
3508
+ mkdir -p "$npmroot2" "$npmproj2"
3509
+ npm2_log=$(cd "$npmroot2" && npm install "$tgz_path" --no-audit --no-fund 2>&1) || {
3510
+ fail "[check-pack-install] second npm root install failed:"
3511
+ echo "$npm2_log" | tail -10 | sed 's/^/ /' >&2
3512
+ return 1
3513
+ }
3514
+ npm2_pkg="$npmroot2/node_modules/@junghanacs/entwurf"
3515
+ set +e
3516
+ two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm2_pkg/run.sh" install "$npmproj2" 2>&1)
3517
+ two_root_rc=$?
3518
+ set -e
3519
+ if [ "$two_root_rc" -eq 0 ] || ! grep -q "takeover-user-scope" <<<"$two_root_out"; then
3520
+ fail "[check-pack-install] a second npm root's normal install must REFUSE the owned user-scope registration and name takeover-user-scope (rc=$two_root_rc):"
3521
+ echo "$two_root_out" | tail -10 | sed 's/^/ /' >&2
3522
+ return 1
3523
+ fi
3524
+ if ! python3 -c "
3525
+ import json,sys
3526
+ p=json.load(open('$npm_user_settings')).get('packages',[])
3527
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
3528
+ ok_a=any(isinstance(s,str) and s.rstrip('/')== '$npm_pkg' for s in srcs)
3529
+ bad_b=any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in srcs)
3530
+ sys.exit(0 if ok_a and not bad_b else 1)
3531
+ "; then
3532
+ fail "[check-pack-install] the refused second-root install still altered the user-scope packages[] entry:"
3533
+ sed 's/^/ /' "$npm_user_settings" >&2
3534
+ return 1
3535
+ fi
3536
+ set +e
3537
+ two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm2_pkg/run.sh" takeover-user-scope 2>&1)
3538
+ two_root_rc=$?
3539
+ set -e
3540
+ if [ "$two_root_rc" -ne 0 ] || ! grep -q "takeover: user-scope entwurf registration moved" <<<"$two_root_out"; then
3541
+ fail "[check-pack-install] explicit takeover-user-scope from the second root failed (rc=$two_root_rc):"
3542
+ echo "$two_root_out" | tail -10 | sed 's/^/ /' >&2
3543
+ return 1
3544
+ fi
3545
+ if ! python3 -c "
3546
+ import json,sys
3547
+ p=json.load(open('$npm_user_settings')).get('packages',[])
3548
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
3549
+ ok_b=any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in srcs)
3550
+ bad_a=any(isinstance(s,str) and s.rstrip('/')== '$npm_pkg' for s in srcs)
3551
+ sys.exit(0 if ok_b and not bad_a else 1)
3552
+ "; then
3553
+ fail "[check-pack-install] takeover did not move the user-scope entry old→new:"
3554
+ sed 's/^/ /' "$npm_user_settings" >&2
3555
+ return 1
3556
+ fi
3557
+ # #86 C2 amendment: BOTH ownership halves moved — the provider installerRoot must
3558
+ # now name the new root, and the OLD root's inverse must refuse (live foreign owner).
3559
+ if ! python3 -c "
3560
+ import json,sys
3561
+ st=json.load(open('$npmhome/.local/share/entwurf/pi-provider/install-state.json'))
3562
+ sys.exit(0 if st.get('installerRoot','').rstrip('/')== '$npm2_pkg' else 1)
3563
+ "; then
3564
+ fail "[check-pack-install] takeover did not rebind the provider installerRoot to the new root:"
3565
+ sed 's/^/ /' "$npmhome/.local/share/entwurf/pi-provider/install-state.json" >&2
3566
+ return 1
3567
+ fi
3568
+ set +e
3569
+ two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm_pkg/run.sh" remove-user-scope 2>&1)
3570
+ two_root_rc=$?
3571
+ set -e
3572
+ if [ "$two_root_rc" -eq 0 ] || ! python3 -c "
3573
+ import json,sys
3574
+ p=json.load(open('$npm_user_settings')).get('packages',[])
3575
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
3576
+ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in srcs) else 1)
3577
+ "; then
3578
+ fail "[check-pack-install] the OLD root's remove-user-scope must refuse after takeover (rc=$two_root_rc) and leave the new owner's entry intact:"
3579
+ echo "$two_root_out" | tail -8 | sed 's/^/ /' >&2
3580
+ return 1
3581
+ fi
3582
+ echo "[check-pack-install] two-root ownership row pass (second root: normal install refused naming takeover-user-scope; explicit takeover moved entry + provider installerRoot old->new; old root's inverse refused)"
3583
+
3584
+ # Installed-package aggregate `setup` row (#86 C1, review blocker 2026-08-26):
3585
+ # the composed public command itself, driven through the consumer bin from the
3586
+ # SAME installed candidate, on a FRESH sandbox home with every harness absent.
3587
+ # This is the first living consumer of package-installed `entwurf setup`:
3588
+ # mode=installed decided by name before anything else, NO pnpm bootstrap inside
3589
+ # node_modules, pi/claude/agy explicit zero-state SKIPs, stable bins PASS as
3590
+ # npm-provided, core bridge boundary PASS, computed green, zero harness/auth
3591
+ # writes. Harness probes are pinned absent via the PI_BIN/CLAUDE_BIN/AGY_BIN/
3592
+ # COPILOT_BIN seams so the gate host's real harnesses never leak in.
3593
+ local setup_home="$npm_tmp/setuphome" setup_proj="$npm_tmp/setupproj" setup_out setup_rc setup_auth
3594
+ mkdir -p "$setup_home/.pi/agent" "$setup_proj"
3595
+ setup_auth="$setup_home/.pi/agent/auth.json"
3596
+ printf '{\n "anthropic": {\n "type": "oauth",\n "access": "consumer-oauth-token"\n }\n}\n' > "$setup_auth"
3597
+ local setup_auth_before
3598
+ setup_auth_before="$(sha256sum "$setup_auth" | cut -d' ' -f1)"
3599
+ set +e
3600
+ setup_out=$(HOME="$setup_home" XDG_DATA_HOME="$setup_home/.local/share" XDG_STATE_HOME="$setup_home/.local/state" \
3601
+ XDG_CACHE_HOME="$setup_home/.cache" XDG_CONFIG_HOME="$setup_home/.config" PI_CODING_AGENT_DIR="$setup_home/.pi/agent" \
3602
+ PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
3603
+ COPILOT_BIN="$npm_tmp/definitely-absent" \
3604
+ "$npmroot/node_modules/.bin/entwurf" setup "$setup_proj" 2>&1)
3605
+ setup_rc=$?
3606
+ set -e
3607
+ if [ "$setup_rc" -ne 0 ]; then
3608
+ fail "[check-pack-install] installed all-absent setup exited $setup_rc — core must stay green with every harness absent, and an installed package must never re-enter the source pnpm bootstrap:"
3609
+ echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
3610
+ return 1
3611
+ fi
3612
+ if ! head -n 1 <<<"$setup_out" | grep -q "mode: installed package"; then
3613
+ fail "[check-pack-install] installed setup did not name its mode FIRST:"
3614
+ echo "$setup_out" | head -5 | sed 's/^/ /' >&2
3615
+ return 1
3616
+ fi
3617
+ if grep -Eqi "pnpm install|Lockfile is up to date|Progress: resolved" <<<"$setup_out"; then
3618
+ fail "[check-pack-install] installed setup reached the source pnpm bootstrap inside node_modules:"
3619
+ echo "$setup_out" | grep -Ei "pnpm|Lockfile|Progress" | sed 's/^/ /' >&2
3620
+ return 1
3621
+ fi
3622
+ local skip_probe
3623
+ for skip_probe in "pi: SKIP" "claude: SKIP" "agy: SKIP" "copilot: SKIP"; do
3624
+ if ! grep -q "$skip_probe" <<<"$setup_out"; then
3625
+ fail "[check-pack-install] installed all-absent setup missing explicit zero-state '$skip_probe':"
3626
+ echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
3627
+ return 1
3628
+ fi
3629
+ done
3630
+ if ! grep -q "bins: PASS — provided by npm bin linking" <<<"$setup_out"; then
3631
+ fail "[check-pack-install] installed setup must report stable bins PASS as npm-provided (capability present, not absent):"
3632
+ echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
3633
+ return 1
3634
+ fi
3635
+ if ! grep -q "core: PASS" <<<"$setup_out" || ! grep -q "result: green (computed from the component outcomes above)" <<<"$setup_out"; then
3636
+ fail "[check-pack-install] installed setup core/computed-green missing:"
3637
+ echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
3638
+ return 1
3639
+ fi
3640
+ if [ -e "$setup_proj/.pi" ] || [ -e "$setup_home/.pi/agent/settings.json" ] || [ -e "$setup_home/.gemini" ] || [ -e "$setup_home/.copilot" ]; then
3641
+ fail "[check-pack-install] installed all-absent setup wrote harness state (must be zero-write)"
3642
+ return 1
3643
+ fi
3644
+ if [ "$(sha256sum "$setup_auth" | cut -d' ' -f1)" != "$setup_auth_before" ] || [ -e "$setup_auth.bak" ]; then
3645
+ fail "[check-pack-install] installed setup touched the credential store (byte drift or .bak)"
3646
+ return 1
3647
+ fi
3648
+ echo "[check-pack-install] installed all-absent setup pass (mode-first, no bootstrap, 4x SKIP, bins npm-provided, core PASS, computed green, zero writes)"
3649
+
3650
+ # Installed copilot-present aggregate `setup` row (#86 C3b): the SAME consumer
3651
+ # bin on a fresh sandbox home, with the shared fake vendor
3652
+ # (scripts/fake-copilot-vendor.sh) on PATH. The presence-driven composition
3653
+ # must run all FOUR Copilot units from the installed package: birth assembles
3654
+ # the COMPILED hook (no raw .ts under node_modules) and drives the real
3655
+ # marketplace-add→plugin-install sequence, MCP + receiver + footer land their
3656
+ # configs/units in the sandbox, each unit keeps its package-owned
3657
+ # install-state (its inverse authority), and the verdict is computed green.
3658
+ local cop_setup_home="$npm_tmp/copsetuphome" cop_setup_proj="$npm_tmp/copsetupproj" cop_fake="$npm_tmp/fake-copilot-vendor"
3659
+ mkdir -p "$cop_setup_home/.pi/agent" "$cop_setup_proj"
3660
+ printf '{\n "anthropic": {\n "type": "oauth",\n "access": "consumer-oauth-token"\n }\n}\n' > "$cop_setup_home/.pi/agent/auth.json"
3661
+ local cop_auth_before
3662
+ cop_auth_before="$(sha256sum "$cop_setup_home/.pi/agent/auth.json" | cut -d' ' -f1)"
3663
+ # EXECUTED, never sourced: this impl holds a `trap … RETURN`, and bash fires a
3664
+ # RETURN trap when a sourced script finishes — a `. factory` here deleted the
3665
+ # whole npm sandbox mid-gate (measured 2026-08-27, C3b review).
3666
+ bash "$REPO_DIR/scripts/fake-copilot-vendor.sh" "$cop_fake" "$npm_pkg/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json" || {
3667
+ fail "[check-pack-install] could not build the fake copilot vendor from the PACKED plugin.json (is pi/meta-bridge-copilot missing from the tarball?)"
3668
+ return 1
3669
+ }
3670
+ set +e
3671
+ setup_out=$(HOME="$cop_setup_home" XDG_DATA_HOME="$cop_setup_home/.local/share" XDG_STATE_HOME="$cop_setup_home/.local/state" \
3672
+ XDG_CACHE_HOME="$cop_setup_home/.cache" XDG_CONFIG_HOME="$cop_setup_home/.config" PI_CODING_AGENT_DIR="$cop_setup_home/.pi/agent" \
3673
+ PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
3674
+ COPILOT_BIN="$cop_fake/copilot" PATH="$cop_fake:$npmroot/node_modules/.bin:$PATH" \
3675
+ "$npmroot/node_modules/.bin/entwurf" setup "$cop_setup_proj" 2>&1)
3676
+ setup_rc=$?
3677
+ set -e
3678
+ if [ "$setup_rc" -ne 0 ]; then
3679
+ fail "[check-pack-install] installed copilot-present setup exited $setup_rc — the four-unit composition must be green against the fake vendor:"
3680
+ echo "$setup_out" | tail -30 | sed 's/^/ /' >&2
3681
+ return 1
3682
+ fi
3683
+ local cop_row
3684
+ for cop_row in "copilot-birth: PASS" "copilot-mcp: PASS" "copilot-receive: PASS" "copilot-statusline: PASS"; do
3685
+ if ! grep -q "$cop_row" <<<"$setup_out"; then
3686
+ fail "[check-pack-install] installed copilot-present setup missing unit verdict '$cop_row':"
3687
+ echo "$setup_out" | tail -30 | sed 's/^/ /' >&2
3688
+ return 1
3689
+ fi
3690
+ done
3691
+ if ! grep -q "result: green (computed from the component outcomes above)" <<<"$setup_out"; then
3692
+ fail "[check-pack-install] installed copilot-present setup did not compute green:"
3693
+ echo "$setup_out" | tail -10 | sed 's/^/ /' >&2
3694
+ return 1
3695
+ fi
3696
+ local cop_setup_asm="$cop_setup_home/.local/share/entwurf/meta-bridge-copilot/.assembled/entwurf-meta-receive-copilot"
3697
+ if [ ! -f "$cop_setup_asm/meta-bridge-hook-copilot.js" ] || [ -f "$cop_setup_asm/meta-bridge-hook-copilot.ts" ]; then
3698
+ fail "[check-pack-install] installed copilot-present setup did not assemble the compiled birth hook (want meta-bridge-hook-copilot.js, no raw .ts) in $cop_setup_asm"
3699
+ return 1
3700
+ fi
3701
+ if ! grep -q "^plugin marketplace add " "$cop_fake/calls.log" || ! grep -q "^plugin install entwurf-meta-receive-copilot@meta-bridge-copilot-local$" "$cop_fake/calls.log"; then
3702
+ fail "[check-pack-install] installed copilot-present setup did not drive the vendor sequence (marketplace add + plugin install) — calls:"
3703
+ sed 's/^/ /' "$cop_fake/calls.log" >&2
3704
+ return 1
3705
+ fi
3706
+ if [ ! -f "$cop_setup_home/.copilot/extensions/entwurf-receive/extension.mjs" ] || [ ! -f "$cop_setup_home/.copilot/extensions/entwurf-receive/lib/meta-session.js" ]; then
3707
+ fail "[check-pack-install] installed copilot-present setup did not deploy the receiver unit from the shipped dist closure"
3708
+ return 1
3709
+ fi
3710
+ if ! grep -q "entwurf-bridge" "$cop_setup_home/.copilot/mcp-config.json" || ! grep -q "entwurf-copilot-statusline" "$cop_setup_home/.copilot/settings.json"; then
3711
+ fail "[check-pack-install] installed copilot-present setup did not land the MCP config / footer settings in the sandbox home"
3712
+ return 1
3713
+ fi
3714
+ local cop_state
3715
+ for cop_state in copilot-bridge copilot-mcp copilot-receive copilot-statusline; do
3716
+ if [ ! -f "$cop_setup_home/.local/share/entwurf/$cop_state/install-state.json" ]; then
3717
+ fail "[check-pack-install] installed copilot-present setup left no package-owned install-state for $cop_state (its inverse authority)"
3718
+ return 1
3719
+ fi
3720
+ done
3721
+ if [ "$(sha256sum "$cop_setup_home/.pi/agent/auth.json" | cut -d' ' -f1)" != "$cop_auth_before" ] || [ -e "$cop_setup_home/.pi/agent/auth.json.bak" ]; then
3722
+ fail "[check-pack-install] installed copilot-present setup touched the credential store (byte drift or .bak)"
3723
+ return 1
3724
+ fi
3725
+ echo "[check-pack-install] installed copilot-present setup pass (four units PASS with install-states, compiled birth hook, vendor sequence driven, computed green, credentials untouched)"
3726
+
3417
3727
  # Installed meta-bridge ownership regression (0.12.5): package upgrades must not
3418
3728
  # bake versioned pnpm-store paths into Claude settings. MCP already uses the
3419
3729
  # stable `entwurf-bridge` bin; statusLine must now use `entwurf-statusline`, and
@@ -4226,24 +4536,109 @@ check_bridge() {
4226
4536
  }
4227
4537
 
4228
4538
 
4229
- # setup_all full entwurf v2 install.
4539
+ # ── setup verdict engine (#86 C1) ────────────────────────────────────────────
4540
+ # setup composes harnesses the operator ALREADY installed. Per component the
4541
+ # verdict is PASS (detected + completed), SKIP (truly absent — zero state
4542
+ # written), or FAIL (detected but incomplete/below-floor/refused). The final
4543
+ # summary and exit status are COMPUTED from these outcomes: core survival never
4544
+ # relabels a detected harness failure as green, and a FAIL owns a nonzero setup
4545
+ # result while every valid earlier write stays installed (re-running setup is
4546
+ # the documented repair action).
4547
+ SETUP_RESULTS=()
4548
+ setup_result() { # $1=component $2=PASS|SKIP|FAIL $3=detail
4549
+ SETUP_RESULTS+=("$1|$2|$3")
4550
+ case "$2" in
4551
+ PASS) ok "$1: PASS — $3" ;;
4552
+ SKIP) log "$1: SKIP — $3" ;;
4553
+ *) fail "$1: FAIL — $3" ;;
4554
+ esac
4555
+ }
4556
+
4557
+ # Installed-vs-source is decided by PACKAGE LOCATION — the same `*/node_modules/*`
4558
+ # seam run_ts and the bridge launcher already branch on — never by which of
4559
+ # pnpm/pi happens to be missing (an accidental failure is not a mode gate).
4560
+ setup_mode() {
4561
+ case "$REPO_DIR" in
4562
+ */node_modules/*) echo installed ;;
4563
+ *) echo source ;;
4564
+ esac
4565
+ }
4566
+
4567
+ # Supported pi range, derived at runtime from the package.json devDependencies
4568
+ # pin (the SSOT check-dep-versions binds: peer range == `>=<pin> <0.<minor+1>`).
4569
+ # Never retyped here as a second literal.
4570
+ pi_supported_range() {
4571
+ node -e '
4572
+ const pkg = require(process.argv[1]);
4573
+ const pin = pkg.devDependencies?.["@earendil-works/pi-coding-agent"];
4574
+ if (!/^0\.\d+\.\d+$/.test(pin ?? "")) { console.error(`unparseable pi pin: ${pin}`); process.exit(1); }
4575
+ const [, min] = pin.split(".").map(Number);
4576
+ console.log(`>=${pin} <0.${min + 1}`);
4577
+ ' "$REPO_DIR/package.json"
4578
+ }
4579
+
4580
+ pi_version_in_range() { # $1=detected version $2=range ">=a.b.c <x.y"; exit 0 in-range
4581
+ node -e '
4582
+ const [ver, range] = process.argv.slice(1);
4583
+ const m = range.match(/^>=(\d+)\.(\d+)\.(\d+) <(\d+)\.(\d+)$/);
4584
+ const v = ver.match(/^(\d+)\.(\d+)\.(\d+)$/);
4585
+ if (!m || !v) process.exit(2);
4586
+ const [maj, min, pat] = v.slice(1).map(Number);
4587
+ const lo = m.slice(1, 4).map(Number);
4588
+ const hi = m.slice(4, 6).map(Number);
4589
+ const geFloor = maj > lo[0] || (maj === lo[0] && (min > lo[1] || (min === lo[1] && pat >= lo[2])));
4590
+ const ltCeil = maj < hi[0] || (maj === hi[0] && min < hi[1]);
4591
+ process.exit(geFloor && ltCeil ? 0 : 1);
4592
+ ' "$1" "$2"
4593
+ }
4594
+
4595
+ # setup_all — one-command presence-driven composition (#86).
4230
4596
  #
4231
- # Installs the v2 dispatch substrate + MCP entwurf-bridge into a target project
4232
- # and verifies the installed bridge boundary. ACP/v1 backend interview gates are
4233
- # deliberately not part of setup on v2-only; the heavier live v2 substrate proof
4234
- # is release-gate's job.
4597
+ # Entwurf installs itself only: this command completes the integration of each
4598
+ # harness the operator already installed and NEVER installs a harness binary,
4599
+ # subscription, credential or login `pi` has no privileged exception. It
4600
+ # verifies the installed bridge boundary; ACP/v1 backend interview gates are
4601
+ # deliberately not part of setup on v2-only (the heavier live v2 substrate
4602
+ # proof is release-gate's job). Copilot's four native units (birth / MCP /
4603
+ # receiver / visible footer) compose presence-driven since the birth inverse
4604
+ # landed (#86 C3a→C3b); the explicit install-copilot-* / uninstall-copilot-*
4605
+ # surfaces remain the operator-selected repair and inverse path.
4235
4606
  #
4236
4607
  # An external harness that consumes entwurf (e.g. agent-config as a
4237
4608
  # pi package + skills set) may still have its own install/setup for its
4238
4609
  # own concerns; those are outside the scope of this script.
4239
4610
  setup_all() {
4611
+ # MODE FIRST (#86 C1): the installed-vs-source branch is a NAMED decision
4612
+ # ahead of every prerequisite check and write, so an installed consumer is
4613
+ # never mode-gated by an accidental missing `pnpm`/`pi`.
4614
+ local mode
4615
+ mode=$(setup_mode)
4616
+ case "$mode" in
4617
+ installed)
4618
+ echo "[setup] mode: installed package — composition only (no source bootstrap; setup never runs npm/pnpm inside node_modules)" ;;
4619
+ *)
4620
+ echo "[setup] mode: source checkout — repo dev bootstrap, then the same composition semantics" ;;
4621
+ esac
4622
+
4623
+ # Named interpreter prerequisite, decided BEFORE any write (#86 C1/E5):
4624
+ # setup's project-path normalization and every pi/agy wiring writer are
4625
+ # python3-backed today. `entwurf --help` / `entwurf check-bridge` stay
4626
+ # python3-free; only the commands that invoke those writers declare it.
4627
+ command -v python3 >/dev/null 2>&1 || {
4628
+ echo "[setup] entwurf setup requires python3 on PATH (project-path normalization + wiring writers)." >&2
4629
+ echo "[setup] Install python3, or use python3-free surfaces (--help, check-bridge)." >&2
4630
+ exit 1
4631
+ }
4632
+ require_cmd node
4633
+
4240
4634
  local project_dir
4241
4635
  project_dir=$(normalize_project_dir "$1")
4242
4636
 
4243
- require_cmd pnpm
4244
- require_cmd python3
4245
- require_cmd pi
4246
- require_cmd node
4637
+ if [ "$mode" = "source" ]; then
4638
+ # Source-only dev fixtures: the pinned repo dependencies are build/test
4639
+ # fixtures, never a product promise that consumers receive them (#86 A4).
4640
+ require_cmd pnpm
4641
+ fi
4247
4642
 
4248
4643
  # Node 24+ is the SINGLE supported axis (GLG, 2026-07-21). The floor is not
4249
4644
  # derived from a feature gate: type-stripping alone would only demand 23.6.
@@ -4264,81 +4659,217 @@ setup_all() {
4264
4659
  echo "[setup] scope: entwurf v2 package + detected native-harness bridges + pi adapter"
4265
4660
  echo "[setup] verification: v2 install smoke (entwurf-bridge; LIVE substrate = release-gate)"
4266
4661
 
4267
- # Store gate FIRST — ahead of pnpm install, sync_auth (which creates and
4268
- # rewrites ~/.pi/agent/auth.json) and every settings writer below. On an
4269
- # existing development host this is the only step standing between a `git pull`
4270
- # and a V3-only activation over a store the live schema cannot read; putting
4271
- # it inside install_local_package would already be one auth.json write too
4272
- # late. The store-doctor imports repo-local libs and node builtins only, so it
4273
- # runs before dependencies exist.
4662
+ # Store gate FIRST — ahead of the source bootstrap and every settings writer
4663
+ # below. On an existing development host this is the only step standing
4664
+ # between a `git pull` and a V3-only activation over a store the live schema
4665
+ # cannot read; putting it inside install_local_package would already be one
4666
+ # write too late. The store-doctor imports repo-local libs and node builtins
4667
+ # only, so it runs before dependencies exist.
4274
4668
  preflight_v3_store setup
4275
4669
 
4276
- (cd "$REPO_DIR" && pnpm install --frozen-lockfile)
4277
- sync_auth
4278
- install_local_package "$project_dir"
4279
-
4280
- # Single confident install command (GLG 2026-06-23): setup ALSO wires the
4281
- # native-harness meta-bridge, so a relocate/clone needs ONE command not the
4282
- # install + install-meta-bridge two-pronged split that froze the Claude
4283
- # statusLine on the old path (the 2026-06-23 relocation gap). Detection-gated:
4284
- # a pi-only host with no native harness skips it cleanly (rail “Shipped adapters” — "있으면 설정,
4285
- # 없으면 담아준다"); meta-bridge-install.sh is itself idempotent.
4286
- if command -v claude >/dev/null 2>&1; then
4670
+ SETUP_RESULTS=()
4671
+
4672
+ if [ "$mode" = "source" ]; then
4673
+ # Source bootstrap is the ONLY install-mode difference (#86): the pinned
4674
+ # repo dev dependencies are fixtures this checkout builds/tests with. A
4675
+ # broken bootstrap is a broken checkout crash, don't degrade.
4676
+ (cd "$REPO_DIR" && pnpm install --frozen-lockfile)
4677
+ fi
4678
+
4679
+ # ── pi (presence + floor, #86 C1/C9) ── pi is OPTIONAL-BY-PRESENCE: absent is
4680
+ # an explicit zero-state SKIP (setup never installs pi and writes no Pi
4681
+ # wiring), below-floor/unreadable is detected-incomplete FAIL — never a SKIP,
4682
+ # so a stale pi is told the truth instead of being silently skipped. PI_BIN
4683
+ # pins the probe for the hermetic smoke (same seam as AGY_BIN); production
4684
+ # leaves it unset.
4685
+ local pi_bin="${PI_BIN:-pi}" pi_range pi_ver
4686
+ if ! command -v "$pi_bin" >/dev/null 2>&1; then
4687
+ setup_result pi SKIP "pi not on PATH — zero Pi wiring written (the operator installs pi; setup never does)"
4688
+ else
4689
+ pi_range=$(pi_supported_range)
4690
+ # Bounded capture: a resolvable pi whose --version crashes or prints garbage
4691
+ # must land in the computed FAIL verdict below, never abort setup via set -e
4692
+ # before the summary (review defect, 2026-08-26).
4693
+ pi_ver="$("$pi_bin" --version 2>/dev/null | head -n 1 | tr -d '[:space:]')" || pi_ver=""
4694
+ if ! pi_version_in_range "${pi_ver:-0}" "$pi_range"; then
4695
+ setup_result pi FAIL "detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range — Pi wiring not written; align pi, then re-run setup"
4696
+ elif (install_local_package "$project_dir"); then
4697
+ setup_result pi PASS "project + user-scope wiring complete (pi $pi_ver in $pi_range) — verify: ./run.sh doctor-pi-provider"
4698
+ else
4699
+ setup_result pi FAIL "pi wiring did not complete (see above) — repair, then re-run setup"
4700
+ fi
4701
+ fi
4702
+
4703
+ # ── claude (meta-bridge) ── single confident install command (GLG 2026-06-23):
4704
+ # setup ALSO wires the native-harness meta-bridge so a relocate/clone needs ONE
4705
+ # command. Detection-gated ("있으면 설정, 없으면 담아준다");
4706
+ # meta-bridge-install.sh is idempotent and enforces the Claude floor + its own
4707
+ # named prerequisites itself, so a below-floor or refused host lands here as a
4708
+ # detected FAIL, never a cosmetic green. CLAUDE_BIN pins the probe for the
4709
+ # hermetic smoke; production leaves it unset.
4710
+ local claude_bin="${CLAUDE_BIN:-claude}"
4711
+ if ! command -v "$claude_bin" >/dev/null 2>&1; then
4712
+ setup_result claude SKIP "claude not on PATH — zero meta-bridge wiring written"
4713
+ else
4287
4714
  section "meta-bridge install (native harness detected: Claude Code)"
4288
- (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh)
4715
+ if (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh); then
4716
+ setup_result claude PASS "meta-bridge wired — verify: ./run.sh doctor-meta-bridge"
4717
+ else
4718
+ setup_result claude FAIL "detected Claude Code, but the meta-bridge integration did not complete (see above) — repair, then re-run setup"
4719
+ fi
4720
+ fi
4721
+
4722
+ # ── stable command exposure ── source mode exposes entwurf's STABLE bins on
4723
+ # PATH for this DEV checkout (막힘 ②) BEFORE wiring agy: the operator command
4724
+ # `entwurf` that Copilot fresh resolves as its managed runtime, plus the bare
4725
+ # bridge/statusline/imprint names native-harness configs record. An installed
4726
+ # consumer already has the same commands from npm bin linking — nothing to
4727
+ # expose there. The core operator stays fail-loud; helper units are attempted
4728
+ # independently and a refused helper is a named FAIL, not a cosmetic green.
4729
+ local bins_rc
4730
+ if [ "$mode" = "installed" ]; then
4731
+ # PASS, not SKIP (review, 2026-08-26): the capability IS present — npm bin
4732
+ # linking provides the same stable commands the source lane must expose.
4733
+ # SKIP stays reserved for a truly absent capability with zero state.
4734
+ setup_result bins PASS "provided by npm bin linking (installed package needs no source exposure)"
4289
4735
  else
4290
- echo "[setup] no native harness (claude) on PATH — skipping meta-bridge wiring (pi-only host)"
4291
- fi
4292
-
4293
- # Expose entwurf's STABLE bins on PATH for this DEV checkout (막힘 ②) BEFORE wiring agy: the
4294
- # agy mcp_config, settings.statusLine, and hooks.json record the bare names `entwurf-bridge` /
4295
- # `entwurf-agy-statusline` / `entwurf-agy-imprint`, so they must resolve for the agy doctors
4296
- # to pass. NON-FATAL a foreign bin already on PATH is left as-is.
4297
- expose_dev_bin
4298
-
4299
- # Fold agy (Antigravity) MCP bridge wiring into setup (막힘 ①, GLG 2026-07-04: install
4300
- # ownership moves to entwurf). Same detection-gated, idempotent, NON-FATAL posture as the
4301
- # meta-bridge block above — agy is an OPTIONAL harness, so a refused/corrupt agy config warns
4302
- # but never bricks a pi/Claude setup. The hard gate stays doctor-agy-bridge.
4303
- wire_agy_bridge
4304
-
4305
- # Fold agy statusLine wiring into setup (#46 Task 1: own the ambient status area so it shows
4306
- # driver + garden id, the claude meta-bridge symmetry). Same detection-gated NON-FATAL posture
4307
- # — an explicit install-agy-statusline is fail-loud, but the setup wrapper WARNs + continues.
4308
- wire_agy_statusline
4309
-
4310
- # Fold agy PreInvocation birth imprint wiring into setup (#46 close criterion: manual register 0).
4311
- # The hook runs the thin `entwurf-agy-imprint` bin and returns {"injectSteps":[]} so agy's loop
4312
- # survives while the meta-record is created/attached by conversationId.
4313
- wire_agy_hooks
4314
-
4315
- # Deterministic preflight lives in `pnpm run check:full`; live substrate acceptance lives
4316
- # in `LIVE=1 ./run.sh release-gate <scratch> --cut`. Setup is the install path, so it
4317
- # verifies the installed MCP bridge boundary only and does NOT run the legacy
4318
- # ACP/v1 session-messaging/sentinel gates.
4319
- section "v2 install smoke: entwurf-bridge (direct MCP protocol)"
4320
- validate_entwurf_bridge
4736
+ bins_rc=0; expose_dev_bin || bins_rc=$?
4737
+ if [ "$bins_rc" -eq 0 ]; then
4738
+ setup_result bins PASS "source operator + helper bins exposed and certified (PATH winner: this checkout)"
4739
+ elif [ "$bins_rc" -eq 3 ]; then
4740
+ setup_result bins FAIL "foreign helper bin(s) refused (named above; every unit was attempted) — repair them, then re-run setup"
4741
+ else
4742
+ setup_result bins FAIL "source operator command not certified (see above) — repair PATH/link, then re-run setup"
4743
+ fi
4744
+ fi
4321
4745
 
4322
- echo ""
4323
- echo "DONE: entwurf setup (pi adapter + detected native bridges + v2 install smoke) green."
4324
- if command -v claude >/dev/null 2>&1; then
4325
- echo "Verify Claude wiring with: ./run.sh doctor-meta-bridge"
4746
+ # ── agy (Antigravity) ── detection-gated (막힘 ①, GLG 2026-07-04: install
4747
+ # ownership moves to entwurf); AGY_BIN pins the probe for the hermetic smoke.
4748
+ # agy absent is one zero-state SKIP. agy PRESENT runs all three leaves
4749
+ # (bridge / statusLine / PreInvocation imprint) independently; a refused or
4750
+ # corrupt config is a named component FAIL that keeps the rest of setup alive
4751
+ # but owns a nonzero final result — the old WARN-then-green posture is retired
4752
+ # (#86 C6/C7). Hard per-leaf verdicts stay with the doctor commands.
4753
+ local agy_rc
4754
+ if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
4755
+ setup_result agy SKIP "agy not on PATH — zero agy wiring written"
4756
+ else
4757
+ agy_rc=0; wire_agy_bridge || agy_rc=$?
4758
+ if [ "$agy_rc" -eq 0 ]; then
4759
+ setup_result agy-bridge PASS "MCP bridge wired — verify: ./run.sh doctor-agy-bridge"
4760
+ else
4761
+ setup_result agy-bridge FAIL "detected agy, but the MCP bridge integration did not complete (reason above) — repair, then re-run setup"
4762
+ fi
4763
+ agy_rc=0; wire_agy_statusline || agy_rc=$?
4764
+ if [ "$agy_rc" -eq 0 ]; then
4765
+ setup_result agy-statusline PASS "statusLine wired — verify: ./run.sh doctor-agy-statusline"
4766
+ else
4767
+ setup_result agy-statusline FAIL "detected agy, but the statusLine integration did not complete (reason above) — repair, then re-run setup"
4768
+ fi
4769
+ agy_rc=0; wire_agy_hooks || agy_rc=$?
4770
+ if [ "$agy_rc" -eq 0 ]; then
4771
+ setup_result agy-hooks PASS "birth imprint wired — verify: ./run.sh doctor-agy-hooks"
4772
+ else
4773
+ setup_result agy-hooks FAIL "detected agy, but the birth-imprint integration did not complete (reason above) — repair, then re-run setup"
4774
+ fi
4326
4775
  fi
4327
- if command -v agy >/dev/null 2>&1; then
4328
- echo "Verify agy wiring with: ./run.sh doctor-agy-bridge"
4329
- echo " ./run.sh doctor-agy-statusline"
4330
- echo " ./run.sh doctor-agy-hooks"
4776
+
4777
+ # ── copilot (GitHub Copilot CLI) ── presence-driven composition (#86 C3b):
4778
+ # the operator's copilot on PATH is the ONLY trigger; absent is one zero-state
4779
+ # SKIP (setup never installs a harness). Present runs all FOUR units the
4780
+ # fresh-call preflight requires — birth → MCP → receiver → visible footer —
4781
+ # INDEPENDENTLY: a failed unit is a named component FAIL that keeps the other
4782
+ # units and the rest of setup alive but owns a nonzero final result. Every
4783
+ # unit is an operator-selectable install with a package-owned inverse (#86
4784
+ # C3a closed the birth inverse), so setup composes only lifecycles it can
4785
+ # also undo. COPILOT_BIN pins the PROBE for the hermetic gates (same seam
4786
+ # spirit as PI_BIN/CLAUDE_BIN/AGY_BIN); the unit scripts address the vendor
4787
+ # as `copilot` on PATH, so production leaves it unset and the two agree.
4788
+ local copilot_rc
4789
+ if ! command -v "${COPILOT_BIN:-copilot}" >/dev/null 2>&1; then
4790
+ setup_result copilot SKIP "copilot not on PATH — zero Copilot wiring written"
4791
+ else
4792
+ section "copilot units (native harness detected: GitHub Copilot CLI)"
4793
+ copilot_rc=0; (cd "$REPO_DIR" && bash scripts/copilot-bridge-install.sh) || copilot_rc=$?
4794
+ if [ "$copilot_rc" -eq 0 ]; then
4795
+ setup_result copilot-birth PASS "birth plugin installed — verify: ./run.sh doctor-copilot-bridge"
4796
+ else
4797
+ setup_result copilot-birth FAIL "detected copilot, but the birth plugin install did not complete (see above) — repair, then re-run setup"
4798
+ fi
4799
+ copilot_rc=0; (cd "$REPO_DIR" && bash scripts/copilot-mcp-bridge.sh install) || copilot_rc=$?
4800
+ if [ "$copilot_rc" -eq 0 ]; then
4801
+ setup_result copilot-mcp PASS "MCP server registered — verify: ./run.sh doctor-copilot-mcp"
4802
+ else
4803
+ setup_result copilot-mcp FAIL "detected copilot, but the MCP registration did not complete (see above) — repair, then re-run setup"
4804
+ fi
4805
+ # The receiver deploys the COMPILED dist closure on every install shape
4806
+ # (Copilot's extension runtime cannot strip-types). The tarball ships dist;
4807
+ # a source checkout may not have built it yet, and those bytes are
4808
+ # entwurf's OWN artifact (Hard Rule 17: the source bootstrap supplies
4809
+ # entwurf's own bytes), so the source lane builds it here once when absent.
4810
+ copilot_rc=0
4811
+ if [ "$mode" = "source" ] && [ ! -f "$REPO_DIR/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js" ]; then
4812
+ (cd "$REPO_DIR" && pnpm run build-bridge) || copilot_rc=$?
4813
+ fi
4814
+ if [ "$copilot_rc" -ne 0 ]; then
4815
+ setup_result copilot-receive FAIL "detected copilot, but the receiver's compiled dist closure failed to build (see above) — repair, then re-run setup"
4816
+ else
4817
+ copilot_rc=0; (cd "$REPO_DIR" && bash scripts/copilot-receive-bridge.sh install) || copilot_rc=$?
4818
+ if [ "$copilot_rc" -eq 0 ]; then
4819
+ setup_result copilot-receive PASS "receiver extension installed — verify: ./run.sh doctor-copilot-receive"
4820
+ else
4821
+ setup_result copilot-receive FAIL "detected copilot, but the receiver extension install did not complete (see above) — repair, then re-run setup"
4822
+ fi
4823
+ fi
4824
+ copilot_rc=0; (cd "$REPO_DIR" && bash scripts/copilot-statusline-bridge.sh install) || copilot_rc=$?
4825
+ if [ "$copilot_rc" -eq 0 ]; then
4826
+ setup_result copilot-statusline PASS "visible footer wired — verify: ./run.sh doctor-copilot-statusline"
4827
+ else
4828
+ setup_result copilot-statusline FAIL "detected copilot, but the visible-footer integration did not complete (see above) — repair, then re-run setup"
4829
+ fi
4331
4830
  fi
4831
+
4832
+ # ── core bridge boundary ── deterministic preflight lives in `pnpm run
4833
+ # check:full`; live substrate acceptance lives in `LIVE=1 ./run.sh
4834
+ # release-gate <scratch> --cut`. Setup is the install path, so it verifies the
4835
+ # installed MCP bridge boundary only and does NOT run the legacy ACP/v1
4836
+ # session-messaging/sentinel gates.
4837
+ section "v2 install smoke: entwurf-bridge (direct MCP protocol)"
4838
+ if (validate_entwurf_bridge); then
4839
+ setup_result core PASS "installed bridge boots and lists the exact v2 tool set"
4840
+ else
4841
+ setup_result core FAIL "installed bridge boundary did not validate (see above)"
4842
+ fi
4843
+
4844
+ # ── computed summary (#86 C7/C8) ── the final verdict is DERIVED from the
4845
+ # component outcomes above, never printed independently of them. A detected
4846
+ # integration FAIL owns a nonzero exit while valid components stay installed;
4847
+ # re-running the same setup is the documented repair action (idempotent).
4848
+ section "setup summary (computed from component outcomes)"
4849
+ local entry rest s_name s_verdict s_fails=""
4850
+ for entry in "${SETUP_RESULTS[@]}"; do
4851
+ s_name="${entry%%|*}"; rest="${entry#*|}"; s_verdict="${rest%%|*}"
4852
+ printf ' %-14s %-4s %s\n' "$s_name" "$s_verdict" "${rest#*|}"
4853
+ if [ "$s_verdict" = "FAIL" ]; then s_fails="$s_fails $s_name"; fi
4854
+ done
4855
+ echo ""
4856
+ if [ -n "$s_fails" ]; then
4857
+ echo "DONE: entwurf setup — result: NON-GREEN (FAIL:$s_fails). Valid components above remain installed; repair the named ones and re-run setup."
4858
+ return 1
4859
+ fi
4860
+ echo "DONE: entwurf setup — result: green (computed from the component outcomes above)."
4332
4861
  echo "Run 'LIVE=1 ./run.sh release-gate <scratch> --cut' for live substrate acceptance (--cut refuses any MUST SKIP)."
4333
4862
  }
4334
4863
 
4335
- # wire_agy_bridge — detection-gated, NON-FATAL agy (Antigravity) MCP bridge wiring, folded into
4864
+ # wire_agy_bridge — detection-gated agy (Antigravity) MCP bridge wiring, folded into
4336
4865
  # setup so a relocate/clone needs ONE idempotent command (막힘 ①). Mirrors the meta-bridge
4337
4866
  # block: agy on PATH → idempotent install-agy-bridge; no agy → honest skip, NO state ("있으면
4338
- # 설정, 없으면 담아준다"). NON-FATAL by contract: agy is an OPTIONAL harness, so a refused/corrupt
4339
- # agy config must NOT brick a pi/Claude setup the hard gate is doctor-agy-bridge (issue #45:
4340
- # the '?' surfaces in doctor, not by killing setup). WARNs are reason-specific so a transitional
4341
- # symlink (someone else's SSOT) and a corrupt config (invalid JSON) are never conflated.
4867
+ # 설정, 없으면 담아준다"). A refused/corrupt agy config still never bricks the rest of setup —
4868
+ # later components are attempted but it RETURNS 1 so the aggregate verdict records a named
4869
+ # component FAIL and setup exits non-green (#86 C6/C7: the old WARN-then-exit-0 posture was the
4870
+ # false-success shape). The hard per-leaf gate stays doctor-agy-bridge (issue #45). WARNs are
4871
+ # reason-specific so a transitional symlink (someone else's SSOT) and a corrupt config
4872
+ # (invalid JSON) are never conflated.
4342
4873
  wire_agy_bridge() {
4343
4874
  # Detection = the agy binary on PATH. AGY_BIN pins the target (default `agy`) so the
4344
4875
  # hermetic smoke can point at a fake agy or a definitely-absent path without depending on
@@ -4359,7 +4890,8 @@ wire_agy_bridge() {
4359
4890
  echo "[setup] agy bridge wired (idempotent). Verify with: ./run.sh doctor-agy-bridge"
4360
4891
  return 0
4361
4892
  fi
4362
- # NON-FATAL: keep setup alive, surface the reason honestly (never a silent pass).
4893
+ # Keep setup alive for the remaining components, surface the reason honestly, and return 1
4894
+ # so the aggregate verdict records this component as FAIL (never a silent pass).
4363
4895
  # ORDER MATTERS: the permission failures carry the same words as the mcp_config ones ("refused
4364
4896
  # (symlink)", "invalid JSON") and MUST be matched first — otherwise a settings.json problem gets
4365
4897
  # reported as an mcp_config problem and sends the operator to repair the wrong file.
@@ -4388,14 +4920,14 @@ wire_agy_bridge() {
4388
4920
  echo "[setup] Bridge NOT wired; verify with ./run.sh doctor-agy-bridge. setup continues." >&2
4389
4921
  ;;
4390
4922
  esac
4391
- return 0
4923
+ return 1
4392
4924
  }
4393
4925
 
4394
- # wire_agy_statusline — detection-gated, NON-FATAL agy statusLine wiring, folded into setup (#46
4926
+ # wire_agy_statusline — detection-gated agy statusLine wiring, folded into setup (#46
4395
4927
  # Task 1). Mirrors wire_agy_bridge: agy on PATH → idempotent install-agy-statusline; no agy →
4396
- # honest skip, NO state. NON-FATAL by contract (agy is OPTIONAL a refused/corrupt settings must
4397
- # not brick a pi/Claude setup); the hard gate is doctor-agy-statusline. The renderer is the
4398
- # stable bin entwurf-agy-statusline, exposed by expose_dev_bin above.
4928
+ # honest skip, NO state; a refused/corrupt settings keeps the rest of setup alive but returns 1
4929
+ # for a named component FAIL (#86 C6/C7). The hard gate is doctor-agy-statusline. The renderer
4930
+ # is the stable bin entwurf-agy-statusline, exposed by expose_dev_bin above.
4399
4931
  wire_agy_statusline() {
4400
4932
  if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
4401
4933
  echo "[setup] no agy on PATH — skipping agy statusLine wiring (no state; this host runs no Antigravity)"
@@ -4428,12 +4960,13 @@ wire_agy_statusline() {
4428
4960
  echo "[setup] statusLine NOT wired; verify with ./run.sh doctor-agy-statusline. setup continues." >&2
4429
4961
  ;;
4430
4962
  esac
4431
- return 0
4963
+ return 1
4432
4964
  }
4433
4965
 
4434
- # wire_agy_hooks — detection-gated, NON-FATAL agy PreInvocation imprint wiring. Same setup posture
4435
- # as the MCP/statusLine adapters, but this one is the birth writer that turns statusLine '?' into a
4436
- # garden id after the first invocation.
4966
+ # wire_agy_hooks — detection-gated agy PreInvocation imprint wiring. Same setup posture
4967
+ # as the MCP/statusLine adapters (skip on absence, named FAIL on detected incompleteness), but
4968
+ # this one is the birth writer that turns statusLine '?' into a garden id after the first
4969
+ # invocation.
4437
4970
  wire_agy_hooks() {
4438
4971
  if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
4439
4972
  echo "[setup] no agy on PATH — skipping agy hooks wiring (no state; this host runs no Antigravity)"
@@ -4464,30 +4997,39 @@ wire_agy_hooks() {
4464
4997
  echo "[setup] Birth imprint NOT wired; verify with ./run.sh doctor-agy-hooks. setup continues." >&2
4465
4998
  ;;
4466
4999
  esac
4467
- return 0
5000
+ return 1
4468
5001
  }
4469
5002
 
4470
- # expose_dev_bin — make the `entwurf-bridge` STABLE bin resolve for a DEV checkout (막힘 ②).
4471
- # setup IS the dev install command (consumers get the bin from npm bin-linking), so setup owns a
4472
- # managed ~/.local/bin/entwurf-bridge symlink into this checkout via scripts/dev-bin.sh, recorded
4473
- # for an honest inverse (remove-dev-bin). NON-FATAL like wire_agy_bridge: a foreign bin already on
4474
- # PATH (a real npm install) is REFUSED not clobbered, and a BIN_DIR off PATH only WARNs — neither
4475
- # bricks setup. The hard gate stays doctor-agy-bridge (a missing/dangling bin FAILs there).
5003
+ # expose_dev_bin — make the package's stable commands resolve for a DEV checkout (막힘 ②).
5004
+ # setup IS the dev install command (consumers get these names from npm bin-linking), so setup owns
5005
+ # managed ~/.local/bin symlinks into this checkout via scripts/dev-bin.sh, each recorded for an
5006
+ # honest inverse. The set starts with `entwurf` → run.sh: Copilot fresh deliberately launches the
5007
+ # managed `entwurf copilot`, never the raw vendor. That operator command is CORE and is certified
5008
+ # after exposure by dev-bin.sh's own target authority: our symlink, our target, and the exact PATH
5009
+ # winner. A foreign/off-PATH/shadowed operator therefore fails setup (return 1). Helper units are
5010
+ # attempted INDEPENDENTLY by dev-bin.sh (#86 C10) and any refusal propagates as return 3 — the
5011
+ # aggregate verdict records it as a named FAIL instead of the old WARN-then-green; the refused
5012
+ # names are already printed truthfully by dev-bin's attempted/ok/refused summary.
4476
5013
  expose_dev_bin() {
4477
- section "dev bin: expose entwurf-bridge on PATH (dev checkout)"
5014
+ section "dev bins: expose stable package commands on PATH (dev checkout)"
4478
5015
  local rc
4479
5016
  set +e
4480
5017
  bash "$REPO_DIR/scripts/dev-bin.sh" expose
4481
5018
  rc=$?
4482
5019
  set -e
5020
+
5021
+ if ! bash "$REPO_DIR/scripts/dev-bin.sh" verify entwurf; then
5022
+ return 1
5023
+ fi
5024
+
4483
5025
  [ "$rc" -eq 0 ] && return 0
4484
5026
  if [ "$rc" -eq 3 ]; then
4485
- echo "[setup] WARN: entwurf-bridge is already on PATH as someone else's bin (not ours) left as-is." >&2
4486
- echo "[setup] If that is a stale/foreign bin, remove it and re-run setup. setup continues." >&2
5027
+ echo "[setup] dev-bin: foreign helper bin path(s) refused, never clobbered the refused names are in the summary above." >&2
5028
+ echo "[setup] Every unit was attempted independently; the source operator itself is certified." >&2
4487
5029
  else
4488
- echo "[setup] WARN: dev bin exposure did not complete (rc=$rc; see above). setup continues." >&2
5030
+ echo "[setup] dev-bin: helper exposure did not complete (rc=$rc; see above)." >&2
4489
5031
  fi
4490
- return 0
5032
+ return "$rc"
4491
5033
  }
4492
5034
 
4493
5035
  # release-gate — the single command that, when GREEN, is sufficient to cut
@@ -5181,6 +5723,16 @@ case "$cmd" in
5181
5723
  # Offline + deterministic (deps: bash+python3).
5182
5724
  (cd "$REPO_DIR" && bash scripts/smoke-agy-install-state.sh)
5183
5725
  ;;
5726
+ smoke-setup-verdict)
5727
+ # #86 C1 aggregate setup verdict fixture — the first automated consumer of
5728
+ # `entwurf setup` itself: all-absent SKIP/green, pi below-floor detected FAIL
5729
+ # (never SKIP), pi-present wiring PASS, agy detected+corrupt named FAIL with
5730
+ # NON-GREEN nonzero exit, installed-mode named branch before the bootstrap,
5731
+ # and the credential store byte-identical in every cell. Sandboxed HOME/XDG/
5732
+ # agent/dev-bin roots; presence pinned via PI_BIN/CLAUDE_BIN/AGY_BIN.
5733
+ # Offline + deterministic (deps: bash+node+python3+pnpm).
5734
+ (cd "$REPO_DIR" && bash scripts/smoke-setup-verdict.sh)
5735
+ ;;
5184
5736
  check-agy-permission-matrix)
5185
5737
  # The agy permission engine's CONTRACT SPACE as a literal table (55 cells + stated
5186
5738
  # exclusions R2/R4-R7): parser-state × operation × settings × ownership × precedence.
@@ -5237,8 +5789,8 @@ case "$cmd" in
5237
5789
  # own the statusLine subtree WHOLE (preserve unrelated keys) + state (stable command, prior
5238
5790
  # subtree as preimage), doctor static-clean/live-SKIP (+live-consistent with a fake agy) /
5239
5791
  # drift-FAIL / dangling-command-FAIL / not-ours note, honest-inverse uninstall, symlink +
5240
- # dangling-symlink refuse, create-new inverse, the NON-FATAL wire wrapper, and checkout
5241
- # impurity 0. Offline + deterministic (deps: bash+python3).
5792
+ # dangling-symlink refuse, create-new inverse, the truthful wire wrapper (detected refusal
5793
+ # → nonzero, #86 C6/C7), and checkout impurity 0. Offline + deterministic (deps: bash+python3).
5242
5794
  (cd "$REPO_DIR" && bash scripts/smoke-agy-statusline-state.sh)
5243
5795
  ;;
5244
5796
  smoke-agy-hooks-state)
@@ -5268,8 +5820,10 @@ case "$cmd" in
5268
5820
  # 0.12.6 install-boundary gate: register-pi-package.py is the shared
5269
5821
  # packages[] SSOT for project/user install and remove; user scope makes
5270
5822
  # --entwurf-control load from any cwd. Idempotent, preserves unrelated
5271
- # packages/keys, normalizes stale entries, remove is symmetric, and corrupt
5272
- # settings fail loud. The tripwire the 2026-07-03 `pi install` removal lacked.
5823
+ # packages/keys, remove is symmetric, corrupt settings fail loud; project
5824
+ # scope normalizes ITS OWN stale entries while user scope carries the #86 C2
5825
+ # explicit ownership contract (other owners refuse; takeover-user-scope moves).
5826
+ # The tripwire the 2026-07-03 `pi install` removal lacked.
5273
5827
  (cd "$REPO_DIR" && bash scripts/smoke-user-scope-citizen.sh)
5274
5828
  ;;
5275
5829
  smoke-claude-native-resume-live)
@@ -5315,6 +5869,15 @@ case "$cmd" in
5315
5869
  shift || true
5316
5870
  (cd "$REPO_DIR" && bash scripts/copilot-bridge-install.sh "$@")
5317
5871
  ;;
5872
+ uninstall-copilot-bridge)
5873
+ # #86 C3a: the package-owned inverse of install-copilot-bridge. The ownership state
5874
+ # is the sole removal authority (exact qualified id, exact marketplace name+path,
5875
+ # recorded assembly only); a failing vendor list is UNKNOWN and refuses; state is
5876
+ # deleted LAST so any partial failure keeps a rerun-repair authority. Never touches
5877
+ # the stale Claude unit and never uses --force.
5878
+ shift || true
5879
+ (cd "$REPO_DIR" && bash scripts/copilot-bridge-uninstall.sh "$@")
5880
+ ;;
5318
5881
  doctor-copilot-bridge)
5319
5882
  # #82: the fail-loud surface for the Copilot unit. Its red conditions differ from
5320
5883
  # the Claude doctor's on purpose: a Copilot session mints on its FIRST PROMPT, not
@@ -5400,11 +5963,12 @@ case "$cmd" in
5400
5963
  (cd "$REPO_DIR" && bash scripts/agy-bridge.sh doctor "$@")
5401
5964
  ;;
5402
5965
  wire-agy-bridge)
5403
- # 막힘 ①: the detection-gated, NON-FATAL setup wrapper around install-agy-bridge (agy on
5404
- # PATH → idempotent install; no agy → honest skip, no state). HIDDEN/internal — setup calls
5405
- # this; it is exposed as a subcommand only so smoke-agy-install-state can drive it
5406
- # deterministically. The hard gate stays doctor-agy-bridge (issue #45: the '?' is a doctor
5407
- # signal, not a setup-killer).
5966
+ # 막힘 ①: the detection-gated, truthful setup wrapper around install-agy-bridge (agy on
5967
+ # PATH → idempotent install; no agy → honest skip, no state; detected refusal/corrupt
5968
+ # reason-specific WARN + nonzero so the aggregate records a named component FAIL while later
5969
+ # components stay attempted, #86 C6/C7). HIDDEN/internal setup calls this; it is exposed
5970
+ # as a subcommand only so smoke-agy-install-state can drive it deterministically. The hard
5971
+ # per-leaf gate stays doctor-agy-bridge (issue #45).
5408
5972
  wire_agy_bridge
5409
5973
  ;;
5410
5974
  install-agy-statusline)
@@ -5472,9 +6036,10 @@ case "$cmd" in
5472
6036
  run_ts scripts/doctor-pi-provider.ts "$@"
5473
6037
  ;;
5474
6038
  wire-agy-statusline)
5475
- # #46 Task 1: the detection-gated, NON-FATAL setup wrapper around install-agy-statusline.
5476
- # HIDDEN/internal setup calls this; exposed so smoke-agy-statusline-state can drive it
5477
- # deterministically. The hard gate stays doctor-agy-statusline.
6039
+ # #46 Task 1: the detection-gated, truthful setup wrapper around install-agy-statusline
6040
+ # (detected refusal/corrupt WARN + nonzero, named component FAIL in the aggregate, #86
6041
+ # C6/C7). HIDDEN/internal setup calls this; exposed so smoke-agy-statusline-state can
6042
+ # drive it deterministically. The hard per-leaf gate stays doctor-agy-statusline.
5478
6043
  wire_agy_statusline
5479
6044
  ;;
5480
6045
  wire-agy-hooks)
@@ -5482,14 +6047,16 @@ case "$cmd" in
5482
6047
  wire_agy_hooks
5483
6048
  ;;
5484
6049
  expose-dev-bin)
5485
- # 막힘 ②: expose the entwurf-bridge STABLE bin on PATH for a DEV checkout (a managed symlink
5486
- # into this checkout, recorded for an honest inverse). HIDDEN/internal — setup calls the
5487
- # NON-FATAL expose_dev_bin wrapper; exposed here so smoke-agy-install-state can drive the
5488
- # foreign-refuse WARN path. The exposure logic lives in scripts/dev-bin.sh.
6050
+ # 막힘 ②: expose the stable package commands on PATH for a DEV checkout, including the
6051
+ # `entwurf` operator command Copilot fresh resolves. HIDDEN/internal — setup requires the
6052
+ # core operator certification and attempts every helper unit independently; a foreign helper
6053
+ # propagates rc=3 as a named bins FAIL (#86 C10). Exposed here so the install smoke can
6054
+ # drive ownership/refusal.
6055
+ # The exposure logic lives in scripts/dev-bin.sh.
5489
6056
  expose_dev_bin
5490
6057
  ;;
5491
6058
  remove-dev-bin)
5492
- # 막힘 ②: honest inverse of expose-dev-bin — remove ONLY our managed link + state (REFUSE if
6059
+ # 막힘 ②: honest inverse of expose-dev-bin — remove ONLY our managed links + states (REFUSE if
5493
6060
  # it became foreign). The raw script (no wrapper) so an operator sees a loud failure.
5494
6061
  shift || true
5495
6062
  (cd "$REPO_DIR" && bash scripts/dev-bin.sh remove "$@")
@@ -5647,9 +6214,6 @@ case "$cmd" in
5647
6214
  # from under node_modules rather than re-packing the installed copy.
5648
6215
  (cd "$REPO_DIR" && bash scripts/check-install-container.sh "$@")
5649
6216
  ;;
5650
- sync-auth)
5651
- sync_auth
5652
- ;;
5653
6217
  install)
5654
6218
  install_local_package "$TARGET_PROJECT_DIR"
5655
6219
  ;;
@@ -5659,6 +6223,17 @@ case "$cmd" in
5659
6223
  remove-user-scope)
5660
6224
  remove_user_scope_citizen
5661
6225
  ;;
6226
+ takeover-user-scope)
6227
+ # #86 C2: operator-explicit ownership move — the ONLY writer allowed to replace
6228
+ # another root's user-scope registration (normal install/setup/remove refuse).
6229
+ takeover_user_scope
6230
+ ;;
6231
+ doctor-pi-package)
6232
+ # #86 C2: package-side ownership verdict (unregistered/owned/owned-by-other/
6233
+ # legacy-no-state/mismatch/missing-owner). Provider runtime stays with
6234
+ # doctor-pi-provider — the two doctors are deliberately not mixed.
6235
+ doctor_pi_package
6236
+ ;;
5662
6237
  -h|--help|help|"")
5663
6238
  usage
5664
6239
  ;;