@junghanacs/entwurf 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -5
  3. package/CHANGELOG.md +220 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +202 -60
  6. package/README.md +54 -26
  7. package/VERIFY.md +37 -7
  8. package/docs/acp-backend-rail.md +31 -15
  9. package/docs/external-mcp-host.md +58 -35
  10. package/docs/fresh-cut-policy.md +3 -2
  11. package/docs/setup-clean-host.md +110 -17
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  30. package/mcp/entwurf-bridge/src/index.ts +115 -111
  31. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  32. package/package.json +14 -5
  33. package/pi-extensions/entwurf-control.ts +71 -19
  34. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  35. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  36. package/pi-extensions/lib/acp/backend.ts +3 -3
  37. package/pi-extensions/lib/acp/event-mapper.ts +4 -4
  38. package/pi-extensions/lib/acp/tool-surface.ts +6 -0
  39. package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
  40. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  41. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  42. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  43. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  44. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  45. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  46. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  47. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  48. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  49. package/pi-extensions/lib/mux-fresh-call.ts +64 -19
  50. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  51. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  52. package/pi-extensions/lib/native-push/register.ts +7 -9
  53. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  54. package/run.sh +251 -35
  55. package/scripts/check-acp-usage-accounting.ts +9 -9
  56. package/scripts/check-agy-sender-identity.ts +1 -1
  57. package/scripts/check-codex-birth-hook.ts +264 -0
  58. package/scripts/check-codex-bridge-identity.ts +179 -0
  59. package/scripts/check-codex-native-push.ts +386 -0
  60. package/scripts/check-codex-sender-identity.ts +495 -0
  61. package/scripts/check-compaction-send-guard.ts +130 -0
  62. package/scripts/check-copilot-receive-arm.ts +4 -1
  63. package/scripts/check-entwurf-fact-provider.ts +38 -0
  64. package/scripts/check-entwurf-peers-surface.ts +13 -1
  65. package/scripts/check-entwurf-self-address.ts +15 -16
  66. package/scripts/check-entwurf-v2-contract.ts +4 -3
  67. package/scripts/check-entwurf-v2-decider.ts +7 -5
  68. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  69. package/scripts/check-entwurf-v2-production.ts +203 -11
  70. package/scripts/check-entwurf-v2-runner.ts +1 -1
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +7 -4
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-install-surface.ts +23 -7
  75. package/scripts/check-mux-launch-tmux.ts +47 -2
  76. package/scripts/check-native-push-adapter.ts +20 -16
  77. package/scripts/check-native-push-register.ts +5 -1
  78. package/scripts/check-release-gate-outcomes.ts +47 -1
  79. package/scripts/check-setup-qualification.sh +3 -1
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-statusline-config.py +434 -0
  85. package/scripts/codex_toml_io.py +532 -0
  86. package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
  87. package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
  88. package/scripts/lib/launch-receipt-windows.ts +46 -0
  89. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  90. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  91. package/scripts/mutants/codex-native.json +838 -0
  92. package/scripts/mutants/compaction-send-guard.json +103 -0
  93. package/scripts/mutants/entwurf-peers.json +19 -0
  94. package/scripts/mutants/mux-fresh-call.json +19 -8
  95. package/scripts/mutants/omp-fresh.json +6 -4
  96. package/scripts/mutants/pi-package-ownership.json +26 -0
  97. package/scripts/mutants/release-gate.json +13 -0
  98. package/scripts/mutants/v2-surface.json +53 -1
  99. package/scripts/pi_settings_io.py +3 -1
  100. package/scripts/raw-async-delivery/README.md +2 -1
  101. package/scripts/raw-codex-measure/README.md +114 -46
  102. package/scripts/register-pi-package.py +38 -30
  103. package/scripts/register-pi-provider.py +3 -2
  104. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  105. package/scripts/smoke-agy-native-push-live.ts +3 -1
  106. package/scripts/smoke-codex-birth.sh +347 -0
  107. package/scripts/smoke-codex-config-state.sh +511 -0
  108. package/scripts/smoke-codex-fresh-live.ts +1186 -0
  109. package/scripts/smoke-codex-native-push-live.ts +75 -0
  110. package/scripts/smoke-setup-verdict.sh +123 -10
  111. package/scripts/smoke-user-scope-citizen.sh +199 -71
  112. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
6
+ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
7
+ import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
8
+ import { probeCodexThread } from "../pi-extensions/lib/native-push/codex-ws-client.ts";
9
+ import { skipLive } from "./lib/live-skip.ts";
10
+
11
+ if (process.env.LIVE !== "1") {
12
+ skipLive(
13
+ "smoke-codex-native-push-live",
14
+ "set LIVE=1 + CODEX_LIVE_THREAD_ID to run (sends a real turn to an already-loaded Codex thread).",
15
+ );
16
+ }
17
+
18
+ const threadId = process.env.CODEX_LIVE_THREAD_ID?.trim();
19
+ if (!threadId) {
20
+ skipLive(
21
+ "smoke-codex-native-push-live",
22
+ "set CODEX_LIVE_THREAD_ID to a thread loaded by a TUI attached to the default Codex app-server.",
23
+ );
24
+ }
25
+
26
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-codex-live-"));
27
+ process.on("exit", () => fs.rmSync(root, { recursive: true, force: true }));
28
+ const sessionsDir = path.join(root, "meta-sessions");
29
+ const born = upsertMetaSession({
30
+ dir: sessionsDir,
31
+ input: {
32
+ backend: "codex",
33
+ nativeSessionId: threadId,
34
+ cwd: process.cwd(),
35
+ model: process.env.CODEX_LIVE_MODEL?.trim() || null,
36
+ transcriptPath: path.join(root, "codex-live-transcript.jsonl"),
37
+ },
38
+ });
39
+
40
+ const before = await probeCodexThread(threadId);
41
+ assert.equal(before.status, "alive", `[QK:CODEX-LIVE-PROBE] ${before.status === "alive" ? "" : before.reason}`);
42
+
43
+ const token = `CODEX-NATIVE-LIVE-${Date.now()}`;
44
+ const result = await runEntwurfV2(
45
+ {
46
+ target: born.record.gardenId,
47
+ intent: "fire-and-forget",
48
+ wantsReply: true,
49
+ message: `Entwurf native-push live acceptance. Reply with exactly ${token}`,
50
+ },
51
+ makeProductionEntwurfV2Deps({
52
+ senderProvider: () => ({
53
+ sessionId: born.record.gardenId,
54
+ agentId: "meta-session/codex",
55
+ cwd: process.cwd(),
56
+ timestamp: new Date().toISOString(),
57
+ origin: "meta-session",
58
+ replyable: true,
59
+ }),
60
+ sessionsDir,
61
+ mailboxDir: path.join(root, "mailbox"),
62
+ controlSocketDir: path.join(root, "control"),
63
+ lockDir: path.join(root, "locks"),
64
+ }),
65
+ );
66
+ assert.equal(result.kind, "executed", `[QK:CODEX-LIVE-PUBLIC-DISPATCH] ${JSON.stringify(result)}`);
67
+ assert.equal(result.kind === "executed" ? result.transport : null, "native-push");
68
+ const after = await probeCodexThread(threadId);
69
+ assert.equal(after.status, "alive", "Codex thread remains loaded after the one-shot queue acceptance");
70
+
71
+ console.log(`smoke-codex-native-push-live: delivered ${token}`);
72
+ console.log(` gardenId: ${born.record.gardenId}`);
73
+ console.log(` threadId: ${threadId}`);
74
+ console.log(` socket: ${before.status === "alive" ? before.route.socketPath : "unreachable"}`);
75
+ console.log(" verify the exact token appears in that visible Codex TUI; the delivery receipt is not the model result");
@@ -16,6 +16,9 @@
16
16
  # core and later components still attempted
17
17
  # S-8 omp present → the four omp units compose (birth/MCP/tools.xdev
18
18
  # setting/receiver), states exist, second run idempotent
19
+ # S-8c codex present → birth publishes but stays a named FAIL until the vendor trust
20
+ # receipt exists; all three atoms compose twice, preserve foreign
21
+ # config, stay idempotent, and go green on the trusted rerun
19
22
  # S-5 installed mode → the installed-vs-source branch is a NAMED verdict
20
23
  # printed before anything else, and never reaches
21
24
  # the source pnpm bootstrap
@@ -27,12 +30,15 @@
27
30
  # below-floor and false-green shapes as living end-to-end evidence.
28
31
  #
29
32
  # Deterministic: no model, no network, no cost. Presence probes are pinned via
30
- # PI_BIN / CLAUDE_BIN / AGY_BIN / COPILOT_BIN / OMP_BIN (the same hermetic seam
33
+ # PI_BIN / CLAUDE_BIN / AGY_BIN / COPILOT_BIN / OMP_BIN / CODEX_BIN (the same hermetic seam
31
34
  # smoke-agy-install-state
32
35
  # uses), and every write root is sandboxed: HOME, XDG roots, the pi agent dir,
33
- # and the dev-bin dir. The source bootstrap (`pnpm install --frozen-lockfile`)
34
- # runs against the ALREADY-INSTALLED checkout, which is a verified no-op under a
35
- # sandbox HOME (measured 2026-08-26).
36
+ # and the dev-bin dir. Source-mode cells put a strict fixture `pnpm` first on PATH:
37
+ # it accepts the no-op `install --frozen-lockfile` and the source-owned `run build-bridge`
38
+ # emit only. Package-manager behavior belongs to the package gates. This is load-bearing
39
+ # under mutation qualification, whose snapshot shares node_modules read-only — a real pnpm
40
+ # follows that symlink and attempts to rewrite the origin checkout. Any dist this gate had
41
+ # to build is removed on exit, restoring the snapshot's pre-gate surface.
36
42
  set -euo pipefail
37
43
 
38
44
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -51,7 +57,13 @@ esac
51
57
  export PYTHONDONTWRITEBYTECODE=1
52
58
 
53
59
  SB="$(mktemp -d -t entwurf-setup-verdict.XXXXXX)"
54
- cleanup() { rm -rf "$SB"; }
60
+ DIST_ROOT="$REPO_DIR/mcp/entwurf-bridge/dist"
61
+ DIST_PREEXISTED=0
62
+ [ -d "$DIST_ROOT" ] && DIST_PREEXISTED=1
63
+ cleanup() {
64
+ [ "$DIST_PREEXISTED" -eq 1 ] || rm -rf "$DIST_ROOT"
65
+ rm -rf "$SB"
66
+ }
55
67
  trap cleanup EXIT
56
68
 
57
69
  export HOME="$SB/home"
@@ -62,12 +74,27 @@ export XDG_CONFIG_HOME="$SB/home/.config"
62
74
  export PI_CODING_AGENT_DIR="$SB/home/.pi/agent"
63
75
  export ENTWURF_DEV_BIN_DIR="$SB/bin"
64
76
  export PATH="$SB/bin:$PATH"
65
- mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness"
77
+ mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness" "$SB/bootstrap-bin"
78
+ cat > "$SB/bootstrap-bin/pnpm" <<'SH'
79
+ #!/bin/sh
80
+ if [ "$#" -eq 2 ] && [ "$1" = install ] && [ "$2" = --frozen-lockfile ]; then
81
+ exit 0
82
+ fi
83
+ if [ "$#" -eq 2 ] && [ "$1" = run ] && [ "$2" = build-bridge ]; then
84
+ # The qualification snapshot is process-exclusive. Invoke the source-owned emit
85
+ # directly so the production lock wrapper cannot leave its ignored parent dir.
86
+ exec bash scripts/build-bridge.sh
87
+ fi
88
+ echo "fixture pnpm accepts only: install --frozen-lockfile | run build-bridge" >&2
89
+ exit 64
90
+ SH
91
+ chmod +x "$SB/bootstrap-bin/pnpm"
92
+ export PATH="$SB/bootstrap-bin:$PATH"
66
93
 
67
94
  # Presence pins: default every harness to a definitely-absent path; each cell
68
95
  # re-pins what it needs. Production leaves these unset.
69
96
  ABSENT="$SB/harness/definitely-absent"
70
- export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT" OMP_BIN="$ABSENT"
97
+ export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT" OMP_BIN="$ABSENT" CODEX_BIN="$ABSENT"
71
98
 
72
99
  PASS=0
73
100
  ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$*"; }
@@ -94,7 +121,8 @@ echo "[smoke-setup-verdict] S-1 all-harness-absent setup"
94
121
  PROJ1="$SB/proj1"; mkdir -p "$PROJ1"
95
122
  seed_auth
96
123
  set +e; OUT="$(bash "$REPO_DIR/run.sh" setup "$PROJ1" 2>&1)"; RC=$?; set -e
97
- want "S-1: all-absent setup exits 0 (core green is still green)" "[ '$RC' -eq 0 ]"
124
+ want "S-1: all-absent setup exits 0 with a Codex SKIP" \
125
+ "[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
98
126
  want "S-1: mode is a named source-checkout branch, printed first" \
99
127
  "printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: source checkout'"
100
128
  want "S-1: pi absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP'"
@@ -112,6 +140,7 @@ want "S-1: no user-scope pi settings were created" "[ ! -e '$PI_CODING_AGENT_DIR
112
140
  want "S-1: no agy config was created" "[ ! -e '$HOME/.gemini' ]"
113
141
  want "S-1: no Copilot config/units were created" "[ ! -e '$HOME/.copilot' ]"
114
142
  want "S-1: no OMP config/units were created" "[ ! -e '$HOME/.omp' ]"
143
+ want "S-1: no Codex config/units were created" "[ ! -e '$HOME/.codex' ]"
115
144
  want_auth_untouched "S-1"
116
145
 
117
146
  # ── S-2: pi resolvable but BELOW floor → detected FAIL, never SKIP, no writes ──
@@ -287,6 +316,90 @@ rm -rf "$HOME/.omp" "$XDG_DATA_HOME/entwurf/omp-bridge" "$XDG_DATA_HOME/entwurf/
287
316
  "$XDG_DATA_HOME/entwurf/omp-config" "$XDG_DATA_HOME/entwurf/omp-receive" \
288
317
  "$XDG_DATA_HOME/entwurf/meta-bridge-omp" "$XDG_DATA_HOME/entwurf/omp-receive"
289
318
 
319
+ # ── S-8c: codex PRESENT → setup publishes birth, and stays NON-GREEN until the vendor agrees ──
320
+ # The birth unit's paths are the operator's own, so setup installs it here for real, inside the
321
+ # fixture's HOME/CODEX_HOME/XDG. What setup CANNOT do is answer the vendor's one-time trust
322
+ # prompt: that is the operator's, in their own visible Codex. So the first run writes every
323
+ # byte and is honestly non-green, the two config atoms compose anyway, and the run after a
324
+ # vendor receipt appears is green and byte-idempotent. Nothing here ever writes [hooks.state] —
325
+ # the receipt is forged by the FIXTURE to stand in for the vendor, never by setup.
326
+ echo "[smoke-setup-verdict] S-8c codex present — incomplete three-unit composition"
327
+ PROJ8C="$SB/proj8c"; mkdir -p "$PROJ8C"
328
+ FAKE_CODEX="$SB/harness/codex-fake"; mkdir -p "$FAKE_CODEX"
329
+ printf '#!/usr/bin/env bash\necho "codex-cli 0.153.4"\n' > "$FAKE_CODEX/codex"
330
+ chmod +x "$FAKE_CODEX/codex"
331
+ CODEX_HOME_SANDBOX="$HOME/.codex"
332
+ mkdir -p "$CODEX_HOME_SANDBOX"
333
+ cat > "$CODEX_HOME_SANDBOX/config.toml" <<'TOML'
334
+ model = "operator-model"
335
+
336
+ [projects."/operator/keep"]
337
+ trust_level = "trusted"
338
+
339
+ [mcp_servers.operator-owned]
340
+ command = "/operator/bin/server"
341
+ args = ["--keep"]
342
+
343
+ [tui]
344
+ status_line = ["model-with-reasoning"]
345
+ TOML
346
+ REAL_BASH="$(command -v bash)"
347
+ CODEX_BIRTH_UNIT_ROOT="$XDG_DATA_HOME/entwurf/codex-birth"
348
+ seed_auth
349
+ set +e
350
+ OUT="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
351
+ RC=$?
352
+ set -e
353
+ want "[QK:CODEX-SETUP-NEVER-PRESEEDS-TRUST] S-8c: setup wrote NO vendor trust receipt of its own" \
354
+ "! grep -q 'hooks.state' '$CODEX_HOME_SANDBOX/config.toml' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
355
+ want "S-8c: detected-incomplete Codex is a named nonzero result, never an absent SKIP [QK:CODEX-SETUP-BIRTH-COSMETIC-PASS]" \
356
+ "[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'codex-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
357
+ want "S-8c: both user-owned Codex atoms still compose independently as PASS [QK:CODEX-SETUP-BIRTH-INDEPENDENT]" \
358
+ "printf '%s' \"\$OUT\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'codex-statusline: PASS'"
359
+ want "S-8c: summary is NON-GREEN and attributes it to the untrusted birth unit" \
360
+ "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: codex-birth)' && ! printf '%s' \"\$OUT\" | grep -q 'result: green'"
361
+ want "S-8c: the reason names the operator's one-time answer, not a repair setup could have made" \
362
+ "printf '%s' \"\$OUT\" | grep -q 'no trust receipt' && printf '%s' \"\$OUT\" | grep -q 'Trust all and continue'"
363
+ want "S-8c: setup PUBLISHED the birth unit — declaration, launcher closure and ownership state" \
364
+ "[ -f '$CODEX_HOME_SANDBOX/hooks.json' ] && [ -x '$CODEX_BIRTH_UNIT_ROOT/helper/codex-birth-launch.sh' ] && [ -f '$CODEX_BIRTH_UNIT_ROOT/install-state.json' ]"
365
+ CODEX_MCP_STATE="$XDG_DATA_HOME/entwurf/codex-mcp/install-state.json"
366
+ CODEX_STATUSLINE_STATE="$XDG_DATA_HOME/entwurf/codex-statusline/install-state.json"
367
+ want "S-8c: both user atoms wrote their package-owned install states inside the sandbox" \
368
+ "[ -f '$CODEX_MCP_STATE' ] && [ -f '$CODEX_STATUSLINE_STATE' ] && grep -q '\"atom\": \"codex-mcp\"' '$CODEX_MCP_STATE' && grep -q '\"atom\": \"codex-statusline\"' '$CODEX_STATUSLINE_STATE'"
369
+ want "S-8c: MCP and visible-identity atoms landed in the sandbox Codex config" \
370
+ "grep -q '\\[mcp_servers\\.entwurf-bridge\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'status_line = \\[\"thread-title\", \"model-with-reasoning\"\\]' '$CODEX_HOME_SANDBOX/config.toml'"
371
+ want "S-8c: unrelated operator config survives both atom writers" \
372
+ "grep -q 'model = \"operator-model\"' '$CODEX_HOME_SANDBOX/config.toml' && grep -q '\\[mcp_servers\\.operator-owned\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'command = \"/operator/bin/server\"' '$CODEX_HOME_SANDBOX/config.toml'"
373
+ CODEX_CONFIG_AFTER="$(sha256sum "$CODEX_HOME_SANDBOX/config.toml" | cut -d' ' -f1)"
374
+ CODEX_MCP_STATE_AFTER="$(sha256sum "$CODEX_MCP_STATE" | cut -d' ' -f1)"
375
+ CODEX_STATUSLINE_STATE_AFTER="$(sha256sum "$CODEX_STATUSLINE_STATE" | cut -d' ' -f1)"
376
+ set +e
377
+ OUT2="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
378
+ RC2=$?
379
+ set -e
380
+ want "S-8c: second setup still reports the untrusted birth FAIL and both user atoms PASS" \
381
+ "[ '$RC2' -ne 0 ] && printf '%s' \"\$OUT2\" | grep -q 'codex-birth: FAIL' && printf '%s' \"\$OUT2\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT2\" | grep -q 'codex-statusline: PASS'"
382
+ want "S-8c: second setup is byte-idempotent for config and both ownership receipts" \
383
+ "[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_AFTER' ] && [ \"\$(sha256sum '$CODEX_MCP_STATE' | cut -d' ' -f1)\" = '$CODEX_MCP_STATE_AFTER' ] && [ \"\$(sha256sum '$CODEX_STATUSLINE_STATE' | cut -d' ' -f1)\" = '$CODEX_STATUSLINE_STATE_AFTER' ]"
384
+ want "S-8c: second setup rechecks rather than faking the missing receipt" \
385
+ "! printf '%s' \"\$OUT2\" | grep -q 'codex-birth: PASS' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
386
+
387
+ # The vendor answers. Written by the FIXTURE, exactly as the vendor writes it after one
388
+ # 'Trust all': the receipt keyed to this declaration, carrying its own hash.
389
+ printf '\n[hooks.state."%s/hooks.json:session_start:0:0"]\ntrusted_hash = "sha256:%s"\n' \
390
+ "$CODEX_HOME_SANDBOX" "$(printf '4%.0s' $(seq 64))" >> "$CODEX_HOME_SANDBOX/config.toml"
391
+ CODEX_CONFIG_TRUSTED="$(sha256sum "$CODEX_HOME_SANDBOX/config.toml" | cut -d' ' -f1)"
392
+ set +e
393
+ OUT3="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
394
+ RC3=$?
395
+ set -e
396
+ want "[QK:CODEX-SETUP-TRUSTED-RERUN-GREEN] S-8c: once the vendor receipt exists the same setup is GREEN" \
397
+ "[ '$RC3' -eq 0 ] && printf '%s' \"\$OUT3\" | grep -q 'codex-birth: PASS' && printf '%s' \"\$OUT3\" | grep -q 'result: green'"
398
+ want "S-8c: the trusted rerun is byte-idempotent — it neither rewrote the config nor touched the receipt" \
399
+ "[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_TRUSTED' ]"
400
+ want_auth_untouched "S-8c"
401
+ rm -rf "$HOME/.codex" "$XDG_DATA_HOME/entwurf/codex-mcp" "$XDG_DATA_HOME/entwurf/codex-statusline" "$CODEX_BIRTH_UNIT_ROOT"
402
+
290
403
  # ── S-9: rail certification is a SEPARATE axis from install success (#78 D1) ──
291
404
  # The 0.20.0 macOS lane opened the installers to Darwin (meta-bridge-install.sh's
292
405
  # platform gate became its own python3/node dependency instead of the platform
@@ -379,8 +492,8 @@ PROJ9C="$SB/proj9c"; mkdir -p "$PROJ9C"
379
492
  seed_auth
380
493
  set +e; OUT="$(PATH="$FAKE_UNAME:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ9C" 2>&1)"; RC=$?; set -e
381
494
  want "S-9c: a harness-free uncertified host still exits 0" "[ '$RC' -eq 0 ]"
382
- want "S-9c: all five harness probes are still zero-state SKIPs" \
383
- "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP'"
495
+ want "S-9c: all six harness probes are still zero-state SKIPs" \
496
+ "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
384
497
  want "S-9c: core still PASSes and the computed summary is still green" \
385
498
  "printf '%s' \"\$OUT\" | grep -q 'core: PASS' && printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
386
499
  want "S-9c: an absent harness is never given a rail verdict it did not earn" \
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # smoke-user-scope-citizen — deterministic gate for pi packages[] citizen
3
- # registration/removal (register-pi-package.py). Offline/hermetic: a fake
4
- # settings file + a fake REPO_DIR under $TMP, no pi, no network, no ~/.pi touched.
3
+ # registration/removal (register-pi-package.py). Offline/hermetic: fake settings
4
+ # files and fake REPO_DIRs under $TMP, sandboxed HOME/PI_CODING_AGENT_DIR/XDG,
5
+ # no network, nothing of the operator's ~/.pi read or written.
5
6
  #
6
7
  # Guards the wiring that dropped when `pi install` was removed from setup
7
8
  # (2026-07-03: `--entwurf-control` / `--emacs-agent-socket` unknown in a foreign
@@ -10,10 +11,14 @@
10
11
  # in another repo. This gate is that missing tripwire: if the registration ever
11
12
  # regresses, `pnpm run check:full` goes red.
12
13
  #
13
- # Cells 11–13 close the second omission (#53 B): every case here drove a fake
14
- # settings file with ABSOLUTE entries, so the portable, settings-relative form this
15
- # repo actually commits was never registered against — and `setup` duplicated and
16
- # restyled the tracked file for four cuts without a single gate seeing it.
14
+ # Cells 11–13 close the second omission (#53 B): every case before them drove a fake
15
+ # settings file with ABSOLUTE entries, so the portable, settings-relative form was
16
+ # never registered against — and `setup` duplicated and restyled a tracked file for
17
+ # four cuts without a single gate seeing it.
18
+ #
19
+ # Cell 15 closes the third (#110), and it is the one cell here that SPAWNS pi: the
20
+ # defect is not a byte, it is which of two checkouts wins `--entwurf-control`, and
21
+ # only the real loader can say. It uses the pinned devDep pi, never PATH.
17
22
  set -euo pipefail
18
23
  export PYTHONDONTWRITEBYTECODE=1 # snapshot purity: no ignored scripts/__pycache__ writes under qualification
19
24
 
@@ -193,69 +198,64 @@ assert d['defaultProvider']=='openai-codex', 'run.sh remove-user-scope dropped a
193
198
  # idempotent: a second remove-user-scope is a clean no-op (no crash on absent entry)
194
199
  if XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1; then ok "run.sh remove-user-scope is idempotent (no-op second run)"; else bad "run.sh remove-user-scope second run crashed"; fi
195
200
 
196
- # ── 11–13. #53 B: install must not dirty a tracked, formatter-governed settings file
201
+ # ── 11–13. #53 B: a settings-relative self entry is source, never install state
197
202
  # The cell whose ABSENCE let this ship. Every case above drove a fake settings file
198
- # with absolute entries, so the one shape this repo actually commits — the portable,
199
- # settings-relative `".."` that check-install-surface S7c pins was never registered
200
- # against. `setup` therefore appended the absolute path BESIDE it and rewrote the
201
- # tracked, biome-owned bytes at indent=2, and `pnpm check` died at step 1 reading
202
- # "format error" instead of "install wrote this".
203
+ # with ABSOLUTE entries, so the portable, settings-relative form — the one shape
204
+ # register never WRITES, and must therefore never duplicate, absolutize or restyle —
205
+ # was never registered against. `setup` appended the absolute path BESIDE it and
206
+ # rewrote the file at indent=2, and `pnpm check` died at step 1 reading "format
207
+ # error" instead of "install wrote this".
203
208
  #
204
- # The seed is the repo's OWN committed settings, copied into a stand-in checkout so
205
- # `".."` resolves to that clone rather than to the real repo (nothing here touches
206
- # $REPO). Setup landing is PROVEN before any product verdict: if the seed is not the
207
- # portable form, this says SETUP MISS in its own name instead of blaming the code.
209
+ # The seed is a FIXTURE. It used to be this repo's OWN committed `.pi/settings.json`,
210
+ # because the repo shipped `packages: [".."]`; #110 retired that entry (entwurf's own
211
+ # checkout self-registers through user scope alone, so a second checkout can no
212
+ # longer register a rival copy of pi-extensions). A witness that depends on us still
213
+ # SHIPPING the shape it tests dies with the shape — and the shape it tests did not
214
+ # die. An operator may hand-write a settings-relative entry, and
215
+ # `is_settings_relative_self` is also what stops an unrelated relative package
216
+ # (`../../repos/gh/andenken`) from being read as ours. Dropping the fixture to a
217
+ # literal here makes the reason honest: the predicate is the subject, not our bytes.
208
218
  CLONE="$TMP/selfclone"; mkdir -p "$CLONE/.pi"
209
219
  CS="$CLONE/.pi/settings.json"
210
- seed_src=""
211
- if (cd "$REPO" && git show :.pi/settings.json) > "$CS" 2>/dev/null && [ -s "$CS" ]; then
212
- seed_src="candidate index"
213
- elif cp "$REPO/.pi/settings.json" "$CS" 2>/dev/null; then
214
- seed_src="worktree"
220
+ # Tab-indented with a compact array on purpose: cell 13 pins that a genuine rewrite
221
+ # preserves the file's own indent unit, and 11b below pins that this file is not
222
+ # rewritten at all. A seed written in the writer's default style could not tell the
223
+ # two apart.
224
+ printf '{\n\t"skills": ["../.claude/skills"],\n\t"packages": [".."]\n}\n' > "$CS"
225
+ BEFORE="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_B="$(stat -c %Y "$CS")"; sleep 1
226
+ OUT_SELF="$(python3 "$REG" "$CS" "$CLONE")"
227
+ AFTER="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_A="$(stat -c %Y "$CS")"
228
+ if printf '%s' "$OUT_SELF" | grep -q 'no-op'; then
229
+ ok "11 register against a settings-relative self entry is a no-op"
230
+ else
231
+ bad "11 register duplicated/absolutized the portable '..' entry" "$OUT_SELF"
215
232
  fi
216
- if [ ! -s "$CS" ]; then
217
- bad "11 SETUP MISS: could not seed this repo's own .pi/settings.json (no git object, no worktree file)"
218
- elif ! python3 -c "
219
- import json,sys
220
- p=json.load(open(sys.argv[1])).get('packages')
221
- sys.exit(0 if p==['..'] else 1)" "$CS"; then
222
- bad "11 SETUP MISS: the seed ($seed_src) is not the committed portable form (packages != ['..']) — product verdict withheld"
233
+ if [ "$BEFORE" = "$AFTER" ] && [ "$MT_B" = "$MT_A" ]; then
234
+ ok "11b the formatter-governed bytes are UNCHANGED (sha256 + mtime)"
223
235
  else
224
- BEFORE="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_B="$(stat -c %Y "$CS")"; sleep 1
225
- OUT_SELF="$(python3 "$REG" "$CS" "$CLONE")"
226
- AFTER="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_A="$(stat -c %Y "$CS")"
227
- if printf '%s' "$OUT_SELF" | grep -q 'no-op'; then
228
- ok "11 register against this repo's OWN committed settings is a no-op (seed: $seed_src)"
229
- else
230
- bad "11 register duplicated/absolutized the portable '..' entry" "$OUT_SELF"
231
- fi
232
- if [ "$BEFORE" = "$AFTER" ] && [ "$MT_B" = "$MT_A" ]; then
233
- ok "11b the tracked, formatter-governed bytes are UNCHANGED (sha256 + mtime)"
234
- else
235
- bad "11b install rewrote the tracked settings file (sha $BEFORE -> $AFTER, mtime $MT_B -> $MT_A)"
236
- fi
237
- # The inverse direction of the same asymmetry: the shared matcher now RECOGNIZES
238
- # `".."`, so an uninstall that deleted it would edit committed source — the same
239
- # defect pointed the other way. remove leaves it and says so; dry-run agrees,
240
- # because both ask one predicate.
241
- BEFORE_R="$(sha256sum "$CS" | cut -d' ' -f1)"
242
- OUT_RM="$(python3 "$REG" "$CS" "$CLONE" --remove)"
243
- OUT_DRY="$(python3 "$REG" "$CS" "$CLONE" --remove --dry-run)"
244
- if [ "$BEFORE_R" = "$(sha256sum "$CS" | cut -d' ' -f1)" ]; then
245
- ok "11c --remove does NOT delete the committed portable entry (bytes unchanged)"
246
- else
247
- bad "11c --remove edited the repo's committed settings source" "$OUT_RM"
248
- fi
249
- if printf '%s' "$OUT_RM" | grep -q 'kept 1 settings-relative'; then
250
- ok "11d the inverse REPORTS what it deliberately left behind (never a silent partial uninstall)"
251
- else
252
- bad "11d --remove left the entry without saying so" "$OUT_RM"
253
- fi
254
- if printf '%s' "$OUT_DRY" | grep -q 'no entwurf packages\[\] entry to remove'; then
255
- ok "11e --dry-run agrees with remove (one predicate, no over-report)"
256
- else
257
- bad "11e --dry-run disagreed with what remove actually does" "$OUT_DRY"
258
- fi
236
+ bad "11b register rewrote a settings file it only recognizes (sha $BEFORE -> $AFTER, mtime $MT_B -> $MT_A)"
237
+ fi
238
+ # The inverse direction of the same asymmetry: the shared matcher RECOGNIZES `".."`,
239
+ # so an uninstall that deleted it would edit source it never wrote — the same defect
240
+ # pointed the other way. remove leaves it and says so; dry-run agrees, because both
241
+ # ask one predicate.
242
+ BEFORE_R="$(sha256sum "$CS" | cut -d' ' -f1)"
243
+ OUT_RM="$(python3 "$REG" "$CS" "$CLONE" --remove)"
244
+ OUT_DRY="$(python3 "$REG" "$CS" "$CLONE" --remove --dry-run)"
245
+ if [ "$BEFORE_R" = "$(sha256sum "$CS" | cut -d' ' -f1)" ]; then
246
+ ok "11c --remove does NOT delete a settings-relative self entry (bytes unchanged)"
247
+ else
248
+ bad "11c --remove edited settings source it never authored" "$OUT_RM"
249
+ fi
250
+ if printf '%s' "$OUT_RM" | grep -q 'kept 1 settings-relative'; then
251
+ ok "11d the inverse REPORTS what it deliberately left behind (never a silent partial uninstall)"
252
+ else
253
+ bad "11d --remove left the entry without saying so" "$OUT_RM"
254
+ fi
255
+ if printf '%s' "$OUT_DRY" | grep -q 'no entwurf packages\[\] entry to remove'; then
256
+ ok "11e --dry-run agrees with remove (one predicate, no over-report)"
257
+ else
258
+ bad "11e --dry-run disagreed with what remove actually does" "$OUT_DRY"
259
259
  fi
260
260
 
261
261
  # 12. the state a pre-fix `setup` already left on real hosts: the portable entry AND
@@ -291,12 +291,20 @@ if grep -q $'^\t"packages"' "$S"; then ok "13 a rewrite preserves the file's tab
291
291
  # indent=2 — semantically a no-op, byte-wise a RED `pnpm check` diagnosed as "formatting".
292
292
  # A per-writer cell can never see that; only the real drive can. So this one runs
293
293
  # `run.sh install <checkout>` for real and demands sha256 + mtime invariance.
294
- #
295
294
  # The checkout is a stand-in: run.sh resolves its OWN symlinks to find REPO_DIR, so
296
295
  # run.sh is COPIED (a link would point the drive back at the operator's real repo) while
297
296
  # the trees it only reads are linked. That makes REPO_DIR == the stand-in, which is the
298
- # shape that matters — `".."` in <checkout>/.pi/settings.json resolves to the very repo
299
- # being registered, exactly as it does in a dev clone.
297
+ # shape that matters — the drive installs entwurf INTO entwurf's own checkout, exactly
298
+ # as `./run.sh setup` does from a dev clone (PROJECT_DIR_DEFAULT=$(pwd)).
299
+ #
300
+ # #110 raised what this cell demands. "install is a no-op here" used to hold because
301
+ # the committed `packages: [".."]` already satisfied the writer. That entry is gone,
302
+ # so the writer must instead RECOGNIZE that the project it is installing into IS this
303
+ # root and skip project scope entirely — user scope already registers it, and a second
304
+ # entry naming a different checkout is what made pi load two rival copies of
305
+ # pi-extensions/entwurf-control.ts. The invariant, tested end to end below: entwurf's
306
+ # own checkout carries no project-scope self-registration, in tracked bytes OR in
307
+ # bytes a writer produced.
300
308
  CK="$TMP/checkout"
301
309
  mkdir -p "$CK/.pi"
302
310
  cp "$REPO/run.sh" "$CK/run.sh"
@@ -317,8 +325,8 @@ if [ ! -s "$CKS" ]; then
317
325
  elif ! python3 -c "
318
326
  import json,sys
319
327
  d=json.load(open(sys.argv[1]))
320
- sys.exit(0 if d.get('packages')==['..'] and isinstance(d.get('entwurfProvider'),dict) else 1)" "$CKS"; then
321
- bad "14 SETUP MISS: the seed ($ck_seed) is not the committed portable form (packages ['..'] + entwurfProvider)"
328
+ sys.exit(0 if d.get('packages') is None and isinstance(d.get('entwurfProvider'),dict) else 1)" "$CKS"; then
329
+ bad "14 SETUP MISS: the seed ($ck_seed) is not the committed form (no packages key + entwurfProvider)"
322
330
  else
323
331
  CK_BEFORE="$(sha256sum "$CKS" | cut -d' ' -f1)"; CK_MT="$(stat -c %Y "$CKS")"
324
332
  sleep 1
@@ -359,6 +367,124 @@ else
359
367
  else
360
368
  bad "14d the user-scope citizen was not written under the sandboxed PI_CODING_AGENT_DIR" "$ck_out"
361
369
  fi
370
+ # The invariant, read off the file the drive just wrote through: no writer put a
371
+ # project-scope self-registration back. 14b already pins the bytes, but a writer
372
+ # that ADDED `packages` to a file with no `packages` key would fail 14b for a
373
+ # reason a reader could mistake for formatting; this says the thing #110 is about,
374
+ # in its own words.
375
+ L14E="14e install into entwurf's OWN checkout wrote no project-scope packages[] self-registration [QK:SELF-CHECKOUT-PROJECT-REGISTRATION]"
376
+ if python3 -c "
377
+ import json,sys
378
+ sys.exit(0 if json.load(open(sys.argv[1])).get('packages') is None else 1)" "$CKS"; then
379
+ ok "$L14E"
380
+ else
381
+ bad "$L14E — violated: install self-registered this checkout, so a second checkout will load a rival copy of pi-extensions" "$(cat "$CKS")"
382
+ fi
383
+ case "$ck_out" in
384
+ *"project-scope packages[] skipped"*) ok "14f the drive REPORTS the skip (a silent skip and a silent write read alike)" ;;
385
+ *) bad "14f install skipped project scope without saying so — 14e cannot tell that apart from a writer that never ran" "$ck_out" ;;
386
+ esac
387
+ fi
388
+ fi
389
+
390
+ # ── 15. #110: the operator-observable this whole contract exists for ───────────
391
+ # Cells 11–14 read settings bytes. Bytes are the mechanism; the SYMPTOM is what pi
392
+ # does with them, and that is what nobody could see: a second checkout booted pi,
393
+ # printed all seven `entwurf` model rows, and looked fine, while `--entwurf-control`
394
+ # — the flag that makes a pi session a garden citizen — was being served by the
395
+ # branch copy, and the INSTALLED extension failed to load with an error naming the
396
+ # installed path as the loser. Not silent. Designed to be misread.
397
+ #
398
+ # pi's package identity is `local:<resolved path>` and dedupePackages collapses only
399
+ # EQUAL identities (package-manager.js getPackageIdentity/dedupePackages), so two
400
+ # real checkouts can never merge — no realpath rule, ours or pi's, would fix this.
401
+ # The only lever is not registering the second one.
402
+ #
403
+ # The cell is control-then-assert on purpose. A sandbox where pi loads no project
404
+ # settings at all (untrusted cwd, missing extension deps, a probe that dies early)
405
+ # would report "no conflicts" for reasons that have nothing to do with the contract,
406
+ # and that green would be a lie. So it first PLANTS the retired entry and demands the
407
+ # conflict APPEAR; only a cell that has just watched the defect happen is allowed to
408
+ # assert its absence.
409
+ # Two different absences, and only one of them is allowed to be quiet. A tree with no
410
+ # `node_modules` at all is a genuinely reduced checkout and the property is untestable
411
+ # there — a named skip. A tree that HAS dependencies but no pinned pi is a broken dev
412
+ # checkout, and skipping it would hide the breakage behind an exit-0 gate, so that one
413
+ # fails loud. (The qualification snapshot is the first case only in principle: it
414
+ # symlinks the origin's node_modules in after its baseline commit — mutation-qualify.ts
415
+ # createRepoSnapshot — so cell 15 really runs there, which is what lets the two mutants
416
+ # below cover this skip. A skip nobody can reach with a planted defect would be exactly
417
+ # the quiet green this cell is built to refuse.)
418
+ PI_BIN_PINNED="$REPO/node_modules/.bin/pi"
419
+ if [ ! -d "$REPO/node_modules" ]; then
420
+ echo " skip 15 second-checkout probe skipped: this tree has no node_modules (reduced checkout); the property is untestable here, not violated"
421
+ elif [ ! -x "$PI_BIN_PINNED" ]; then
422
+ bad "15 the pinned pi is missing from a checkout that HAS dependencies ($PI_BIN_PINNED) — a broken dev checkout, not a reduced one; re-run pnpm install"
423
+ else
424
+ A15="$TMP/dual/installed"; B15="$TMP/dual/second"
425
+ AG15="$TMP/agent15"
426
+ mkdir -p "$A15/.pi" "$B15/.pi" "$AG15"
427
+ # pi-extensions is COPIED into each root, not linked: the defect is two DISTINCT
428
+ # real paths declaring the same flag, and a shared symlink target would be one path
429
+ # wearing two names — a fixture that cannot reproduce what two checkouts do.
430
+ for root in "$A15" "$B15"; do
431
+ cp -R "$REPO/pi-extensions" "$root/pi-extensions"
432
+ for entry in node_modules package.json protocol.js; do
433
+ [ -e "$REPO/$entry" ] && ln -s "$REPO/$entry" "$root/$entry"
434
+ done
435
+ done
436
+ # user scope registers the INSTALLED root, absolutely — what `register_user_scope_citizen`
437
+ # writes on a real host. Trust is seeded for the second checkout because `.pi/` is a
438
+ # trust-gated input: an untrusted cwd loads no project settings, which would silence
439
+ # the control below and make the assertion vacuous.
440
+ printf '{"packages": ["%s"]}\n' "$A15" > "$AG15/settings.json"
441
+ printf '{"%s": true}\n' "$B15" > "$AG15/trust.json"
442
+ # Two fences on the probe child, and BOTH are load-bearing.
443
+ #
444
+ # `< /dev/null`: `pi --print` reads a non-tty stdin as piped prompt input, so it
445
+ # inherits whatever the CALLER handed this gate. Under an interactive shell or a
446
+ # /dev/null stdin that ends immediately and the probe returns in ~2s; under a
447
+ # supervised runner — which hands the process an open socket — it blocks forever.
448
+ # Measured 2026-09-10: a release-gate run sat 1h56m on this line with `pi` alive
449
+ # and no output, and the gate has no timeout of its own to catch it. A gate must
450
+ # read the loader, never the stdin of whoever started it.
451
+ #
452
+ # `timeout 60`: even so, a probe that cannot finish must go RED rather than hang.
453
+ # 124 lands in the captured output, the conflict grep misses, and the cell fails
454
+ # with its own name — the honest outcome for "the loader never answered".
455
+ #
456
+ # Ambient identity carriers are stripped for the same reason: a live pi/ACP session
457
+ # exports PI_SESSION_ID/PI_AGENT_ID and CLAUDE_CONFIG_DIR into children.
458
+ probe15() {
459
+ (cd "$B15" && env -u PI_SESSION_ID -u PI_AGENT_ID -u CLAUDE_CONFIG_DIR \
460
+ HOME="$TMP/home" PI_CODING_AGENT_DIR="$AG15" \
461
+ XDG_DATA_HOME="$TMP/xdg" XDG_STATE_HOME="$TMP/state" XDG_CACHE_HOME="$TMP/cache" \
462
+ timeout 60 "$PI_BIN_PINNED" --model zzz-no-such-provider/zzz-no-such-model --print x \
463
+ </dev/null 2>&1 || true)
464
+ }
465
+ # CONTROL: the retired shape, re-planted. `".."` resolves against <B>/.pi, so the
466
+ # project entry names B while user scope names A — two identities, both loaded.
467
+ printf '{"packages": [".."]}\n' > "$B15/.pi/settings.json"
468
+ OUT15_CTL="$(probe15)"
469
+ if printf '%s' "$OUT15_CTL" | grep -q 'conflicts with'; then
470
+ ok "15 control: a second checkout that registers itself DOES collide on the extension flags"
471
+ else
472
+ bad "15 SETUP MISS: the planted second registration produced no conflict — this sandbox cannot observe the defect, so 15b would be vacuous" "$OUT15_CTL"
473
+ fi
474
+ # ASSERT: this repo's real `.pi/settings.json`, read from the WORKING TREE — not
475
+ # from the candidate index the way cells 11/14 read theirs. The subject here is
476
+ # what pi's loader does with the file that exists ON DISK when a session starts;
477
+ # the index is a different question and check-install-surface S7c already owns it.
478
+ # Reading the index here also made this cell blind to its own replant: a mutation
479
+ # writes the file, git show reads the staged bytes, and the gate stayed green over
480
+ # a planted defect (measured — qualification reported SURVIVED, 2026-09-10).
481
+ cp "$REPO/.pi/settings.json" "$B15/.pi/settings.json"
482
+ OUT15="$(probe15)"
483
+ L15B="15b a second checkout of this repo boots pi with zero extension conflicts [QK:SECOND-CHECKOUT-EXTENSION-COLLISION]"
484
+ if printf '%s' "$OUT15" | grep -q 'conflicts with'; then
485
+ bad "$L15B — violated: the branch copy wins --entwurf-control and the INSTALLED extension fails, printing the installed path as the loser" "$OUT15"
486
+ else
487
+ ok "$L15B"
362
488
  fi
363
489
  fi
364
490
 
@@ -740,14 +866,16 @@ if [ "$RC34B" -ne 0 ] && printf '%s' "$OUT34B" | grep -q 'CORRUPT'; then
740
866
  ok "34b doctor coupling FAILs on a wrong-type provider installerRoot (named CORRUPT) $QK34"
741
867
  else bad "34b doctor stayed green/silent over a corrupt installerRoot (rc=$RC34B): $OUT34B $QK34"; fi
742
868
 
743
- # 15. WIRING: both writers of this one file must share the serializer, not copy it.
869
+ # 35. WIRING: both writers of this one file must share the serializer, not copy it.
744
870
  # A duplicated indent-detector is how the provider writer stayed open after the
745
871
  # package writer was closed; a parity check is cheaper than a third round.
872
+ # (Numbered 35, after the ownership block: it used to be a second "15", which
873
+ # stopped being merely untidy once #110 added the real cell 15 above.)
746
874
  for w in register-pi-package.py register-pi-provider.py; do
747
875
  if grep -q "from pi_settings_io import" "$REPO/scripts/$w"; then
748
- ok "15 $w routes through the shared pi_settings_io serializer"
876
+ ok "35 $w routes through the shared pi_settings_io serializer"
749
877
  else
750
- bad "15 $w serializes settings on its own again (copied rule = the #53 B shape)"
878
+ bad "35 $w serializes settings on its own again (copied rule = the #53 B shape)"
751
879
  fi
752
880
  done
753
881
 
@@ -31,6 +31,7 @@
31
31
  "../pi-extensions/model-lock.ts",
32
32
  "../pi-extensions/meta-bridge-hook.ts",
33
33
  "../pi-extensions/meta-bridge-hook-copilot.ts",
34
+ "../pi-extensions/meta-bridge-hook-codex.ts",
34
35
  "../pi-extensions/meta-bridge-omp.ts",
35
36
  "../pi-extensions/meta-bridge-receive-omp.ts",
36
37
  "../mcp/entwurf-bridge/src/resume-mode.ts"