@junghanacs/entwurf 0.12.6 → 0.12.7

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 (92) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +22 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +18 -13
  7. package/docs/setup-clean-host.md +80 -22
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
  30. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
  31. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
  33. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
  34. package/mcp/entwurf-bridge/src/index.ts +101 -67
  35. package/mcp/entwurf-bridge/test.sh +1 -1
  36. package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
  37. package/package.json +10 -5
  38. package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
  39. package/pi-extensions/lib/entwurf-self-address.ts +58 -15
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
  41. package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
  42. package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
  43. package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
  44. package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
  45. package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
  46. package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
  48. package/pi-extensions/lib/meta-sender-identity.ts +154 -0
  49. package/pi-extensions/lib/native-push/adapter.ts +255 -0
  50. package/pi-extensions/lib/native-push/register.ts +99 -0
  51. package/run.sh +756 -214
  52. package/scripts/agy-bridge-config.py +446 -0
  53. package/scripts/agy-bridge.sh +359 -0
  54. package/scripts/agy-hooks-bridge.sh +193 -0
  55. package/scripts/agy-hooks-config.py +257 -0
  56. package/scripts/agy-imprint.sh +28 -0
  57. package/scripts/agy-imprint.ts +193 -0
  58. package/scripts/agy-statusline-bridge.sh +176 -0
  59. package/scripts/agy-statusline-config.py +213 -0
  60. package/scripts/agy-statusline.sh +256 -0
  61. package/scripts/build-bridge.sh +20 -0
  62. package/scripts/check-agy-sender-identity.ts +364 -0
  63. package/scripts/check-entwurf-bridge-boot.ts +8 -2
  64. package/scripts/check-entwurf-deliverability.ts +34 -0
  65. package/scripts/check-entwurf-self-address.ts +78 -11
  66. package/scripts/check-entwurf-v2-contract.ts +136 -1
  67. package/scripts/check-entwurf-v2-decider.ts +95 -1
  68. package/scripts/check-entwurf-v2-matrix.ts +14 -3
  69. package/scripts/check-entwurf-v2-native-push.ts +193 -0
  70. package/scripts/check-entwurf-v2-production.ts +68 -1
  71. package/scripts/check-entwurf-v2-runner.ts +58 -0
  72. package/scripts/check-entwurf-v2-surface.ts +35 -0
  73. package/scripts/check-install-surface.ts +357 -0
  74. package/scripts/check-native-push-adapter.ts +319 -0
  75. package/scripts/check-native-push-register.ts +130 -0
  76. package/scripts/dev-bin.sh +195 -0
  77. package/scripts/doctor-pi-provider.ts +140 -0
  78. package/scripts/meta-bridge-doctor.sh +36 -2
  79. package/scripts/register-pi-package.py +37 -3
  80. package/scripts/register-pi-provider.py +287 -0
  81. package/scripts/smoke-agy-hooks-state.sh +172 -0
  82. package/scripts/smoke-agy-install-state.sh +660 -0
  83. package/scripts/smoke-agy-native-push-live.ts +243 -0
  84. package/scripts/smoke-agy-statusline-state.sh +300 -0
  85. package/scripts/smoke-meta-async-drift.sh +9 -2
  86. package/scripts/smoke-meta-install-state.sh +20 -0
  87. package/scripts/smoke-pi-provider-state.sh +182 -0
  88. package/scripts/smoke-user-scope-citizen.sh +62 -0
  89. package/scripts/with-dist-lock.sh +81 -0
  90. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
  91. package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
  92. package/scripts/__pycache__/register-pi-package.cpython-313.pyc +0 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * smoke-agy-native-push-live — the 봉인 8 LIVE acceptance gate for the native-push (agy /
3
+ * Antigravity) delivery rail. It drives the PRODUCTION code path — the real antigravity
4
+ * adapter (pgrep/ss/`agentapi get-conversation-metadata`+`send-message`), the register core,
5
+ * and `runEntwurfV2` with production deps — against a REAL, already-running agy conversation,
6
+ * and asserts the rail end to end: probe finds the volatile route, register create/re-attach is
7
+ * idempotent, a fire-and-forget dispatches over native-push (delivered), an owned-outcome is
8
+ * rejected (no resume authority), and a bogus conversation is rejected (probe-indeterminate).
9
+ *
10
+ * This automates what the GPT manual L1~L5 pass proved by hand (register create/attach, v2
11
+ * native-push delivered ×2, owned reject, mailbox/marker/pi-domain non-intrusion) — same
12
+ * production MCP surface, now a repeatable gate.
13
+ *
14
+ * STORE ISOLATION (페블-approved): only the agy CONVERSATION round-trip is real (that is the
15
+ * LIVE essence). The meta-record register + v2 write to a TEMP store via the production
16
+ * dir-resolution env (`ENTWURF_META_SESSIONS_DIR` → `defaultMetaSessionsDir()`) + explicit
17
+ * `sessionsDir`/`mailboxDir`/`lockDir` opts — NO test-only fork in the register/decider code,
18
+ * just the natural env/opt seam. The temp store is removed in `finally`, so no real-store
19
+ * meta-record residue is ever left (this structurally closes NEXT ⑥'s "leftover meta-record"
20
+ * class; the one pre-existing record 20260704T201811-071ba8 is a separate one-shot prune).
21
+ *
22
+ * REQUIRES `AGY_CONVERSATION_ID` — a live agy conversation the operator points at (the proven
23
+ * L1~L5 path). Optional `AGY_BIN`, `AGY_SMOKE_CWD` (a scratch cwd to record; default a temp).
24
+ * LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
25
+ * Honest SKIP when LIVE!=1 (touches a real agy → out of `pnpm check`).
26
+ *
27
+ * SEQUENCING: the doctor-static preflight runs FIRST — before ③ (agent-config drops the agy
28
+ * mcp_config symlink so install-agy-bridge records the bare `entwurf-bridge`) the agy config
29
+ * still points at a dangling path, so this smoke honestly FAILs at preflight. That is the gate:
30
+ * the canonical run lands only after ③ wires the bridge. (The deterministic non-LIVE checks —
31
+ * SKIP, typecheck, and an isolated preflight-FAIL via AGY_MCP_CONFIG — validate the scaffold now.)
32
+ *
33
+ * CANONICAL-GATED (deferred to the post-③ live run, marked honestly rather than faked here):
34
+ * - a smoke-OWNED agy launched via tmux + conversationId extraction (so `AGY_CONVERSATION_ID`
35
+ * could be optional) — v1 requires the operator's conversation instead of killing theirs;
36
+ * - the `native-push-target-dead` reject (needs the agy process ABSENT — i.e. a smoke-owned
37
+ * agy we may kill; v1 covers the sibling `native-push-probe-indeterminate` instead, which
38
+ * needs only a bogus conv while agy is alive);
39
+ * - full CONTENT receipt (that the delivered text is visible IN the conversation transcript) —
40
+ * v1 asserts delivery via the production send + a post-send liveness re-probe (D7 partial,
41
+ * matching DELIVERY.md's honest label); the manual tmux capture-pane observation stands as
42
+ * the D7 evidence until an `agentapi`-level transcript read is wired.
43
+ */
44
+
45
+ import { spawnSync } from "node:child_process";
46
+ import * as fsp from "node:fs/promises";
47
+ import * as os from "node:os";
48
+ import * as path from "node:path";
49
+ import { fileURLToPath } from "node:url";
50
+ import type { SenderEnvelope } from "../pi-extensions/lib/entwurf-control-rpc.ts";
51
+ import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
52
+ import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
53
+ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
54
+ import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
55
+ import { antigravityAdapter } from "../pi-extensions/lib/native-push/adapter.ts";
56
+ import { registerNativeConversation } from "../pi-extensions/lib/native-push/register.ts";
57
+
58
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
59
+
60
+ let passed = 0;
61
+ const artifacts: Record<string, string> = {};
62
+
63
+ function ok(label: string, cond: boolean): void {
64
+ if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
65
+ console.log(` ok ${label}`);
66
+ passed++;
67
+ }
68
+
69
+ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
70
+ return {
71
+ sessionId: gardenId,
72
+ agentId: "smoke/agy-native-push-live",
73
+ cwd,
74
+ timestamp: new Date(0).toISOString(),
75
+ origin: "pi-session",
76
+ replyable: false,
77
+ };
78
+ }
79
+
80
+ async function main(): Promise<void> {
81
+ if (process.env.LIVE !== "1") {
82
+ console.log(
83
+ "[smoke-agy-native-push-live] skipped — set LIVE=1 (+ AGY_CONVERSATION_ID) to run (sends into a real agy conversation).",
84
+ );
85
+ return;
86
+ }
87
+
88
+ // ── doctor-static preflight (runs FIRST; a dangling agy config FAILs here — the ③ gate) ──
89
+ // Uses the SAME env the operator set (AGY_MCP_CONFIG[_ALT] pin the candidates), so an
90
+ // isolated dangling config reproduces the FAIL deterministically without touching real agy.
91
+ const doctor = spawnSync("bash", [path.join(REPO_ROOT, "run.sh"), "doctor-agy-bridge"], {
92
+ encoding: "utf8",
93
+ env: process.env,
94
+ });
95
+ if (doctor.status !== 0) {
96
+ console.error("[smoke-agy-native-push-live] doctor-static preflight FAILED — the agy bridge is not wired.");
97
+ console.error(" This is expected before ③ (agent-config drops the mcp_config symlink so install-agy-bridge");
98
+ console.error(" can record the bare `entwurf-bridge`). Wire it, then re-run. doctor output:");
99
+ console.error((doctor.stdout ?? "").replace(/^/gm, " "));
100
+ console.error((doctor.stderr ?? "").replace(/^/gm, " "));
101
+ throw new Error("smoke-agy-native-push-live: doctor-static preflight FAILED (agy bridge not wired — see above).");
102
+ }
103
+ console.log(" ok doctor-static preflight PASS (agy bridge wired)");
104
+ passed++;
105
+
106
+ const conversationId = process.env.AGY_CONVERSATION_ID?.trim();
107
+ if (!conversationId) {
108
+ throw new Error(
109
+ "smoke-agy-native-push-live: AGY_CONVERSATION_ID is required — point it at a LIVE agy conversation " +
110
+ "(open agy, take its conversationId). A smoke-owned agy launch is a canonical follow-up.",
111
+ );
112
+ }
113
+ artifacts["conversationId"] = conversationId;
114
+
115
+ // ── temp world: the meta-record store is isolated; only the agy round-trip is real ──
116
+ const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "agy-np-"));
117
+ const sessionsDir = path.join(tmp, "meta-sessions");
118
+ const mailboxDir = path.join(tmp, "mailbox");
119
+ const lockDir = path.join(tmp, "locks");
120
+ const controlDir = path.join(tmp, "control"); // native-push never touches it; kept off the real dir for hygiene
121
+ for (const d of [sessionsDir, mailboxDir, lockDir, controlDir]) await fsp.mkdir(d, { recursive: true });
122
+ process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
123
+ process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
124
+ const recordCwd = process.env.AGY_SMOKE_CWD?.trim() || tmp;
125
+ let succeeded = false;
126
+
127
+ try {
128
+ // ── 1. probe the REAL conversation directly (route diagnostics; proves the live host) ──
129
+ const probe = await antigravityAdapter.probe(conversationId);
130
+ if (probe.status === "alive") {
131
+ artifacts["route"] = probe.route.lsAddress;
132
+ console.log(` .. probe: alive via ${probe.route.lsAddress}`);
133
+ } else {
134
+ artifacts["probe"] = `${probe.status}: ${probe.reason}`;
135
+ }
136
+ ok("probe finds the conversation LIVE (agy host + LS port serve it)", probe.status === "alive");
137
+
138
+ // ── 2. register create → gardenId; re-register → attach the SAME gardenId (idempotent) ──
139
+ const reg1 = await registerNativeConversation(
140
+ { backend: "antigravity", nativeSessionId: conversationId, cwd: recordCwd },
141
+ { sessionsDir },
142
+ );
143
+ artifacts["gardenId"] = reg1.gardenId;
144
+ ok("register create: new garden citizen minted", reg1.action === "create");
145
+ ok("register create: backend is antigravity", reg1.backend === "antigravity");
146
+ const reg2 = await registerNativeConversation(
147
+ { backend: "antigravity", nativeSessionId: conversationId, cwd: recordCwd },
148
+ { sessionsDir },
149
+ );
150
+ ok("re-register: attaches (does not duplicate)", reg2.action === "attach");
151
+ ok("re-register: same gardenId (idempotent identity)", reg2.gardenId === reg1.gardenId);
152
+
153
+ // ── production v2 deps: the REAL native-push adapter drives decide-probe AND executor send ──
154
+ const prodDeps = makeProductionEntwurfV2Deps({
155
+ senderProvider: () => smokeSender(reg1.gardenId, recordCwd),
156
+ sessionsDir,
157
+ mailboxDir,
158
+ lockDir,
159
+ controlSocketDir: controlDir,
160
+ });
161
+
162
+ // ── 3. fire-and-forget → native-push delivered (production send into the live conv) ──
163
+ const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
164
+ artifacts["nonce"] = nonce;
165
+ const fire: EntwurfV2RunResult = await runEntwurfV2(
166
+ {
167
+ target: reg1.gardenId,
168
+ intent: "fire-and-forget",
169
+ message: `[entwurf smoke-agy-native-push-live] delivery probe ${nonce} — no reply needed.`,
170
+ },
171
+ prodDeps,
172
+ );
173
+ ok(
174
+ "fire-and-forget executed over native-push (delivered)",
175
+ fire.kind === "executed" && fire.transport === "native-push",
176
+ );
177
+ if (fire.kind === "executed" && fire.outcome.transport === "native-push") {
178
+ ok(
179
+ "native-push delivery is lock-free/first-try (retried flag present)",
180
+ typeof fire.outcome.retried === "boolean",
181
+ );
182
+ }
183
+
184
+ // ── 4. receipt (D7 partial): re-probe shows the target still reachable post-send ──
185
+ // Full CONTENT receipt (the text visible in the transcript) is the canonical follow-up —
186
+ // here we assert the delivery target stayed live through the send, matching DELIVERY.md's
187
+ // honest D7-partial label (the manual capture-pane observation is the standing D7 evidence).
188
+ const post = await antigravityAdapter.probe(conversationId);
189
+ ok("post-send re-probe still alive (delivery target reachable, D7 partial)", post.status === "alive");
190
+
191
+ // ── 5. owned-outcome → rejected: a native-push backend has no resume authority ──
192
+ const owned: EntwurfV2RunResult = await runEntwurfV2(
193
+ { target: reg1.gardenId, intent: "owned-outcome", message: "should be rejected" },
194
+ prodDeps,
195
+ );
196
+ ok(
197
+ "owned-outcome rejected as native-push-no-resume-authority",
198
+ owned.kind === "rejected" && owned.receipt.reason === "native-push-no-resume-authority",
199
+ );
200
+ ok(
201
+ "owned reject stamped observedLiveness (post-probe, non-null)",
202
+ owned.kind === "rejected" && owned.receipt.observedLiveness !== null,
203
+ );
204
+
205
+ // ── 6. bogus conversation → rejected native-push-probe-indeterminate ──
206
+ // agy is alive but no LS port serves this fabricated conv id → INDETERMINATE (never coerced
207
+ // to dead). A meta-record is upserted DIRECTLY (register would refuse a non-live probe), so
208
+ // the decider/executor still run the production path over a real (indeterminate) probe.
209
+ const bogusConv = `entwurf-smoke-bogus-${nonce}`;
210
+ const bogus = upsertMetaSession({
211
+ input: { backend: "antigravity", nativeSessionId: bogusConv, cwd: recordCwd, model: null, transcriptPath: null },
212
+ dir: sessionsDir,
213
+ });
214
+ const indeterminate: EntwurfV2RunResult = await runEntwurfV2(
215
+ { target: bogus.record.gardenId, intent: "fire-and-forget", message: "into the void" },
216
+ prodDeps,
217
+ );
218
+ ok(
219
+ "bogus-conv fire rejected as native-push-probe-indeterminate (agy live, no port serves it)",
220
+ indeterminate.kind === "rejected" && indeterminate.receipt.reason === "native-push-probe-indeterminate",
221
+ );
222
+ ok(
223
+ "indeterminate reject stamped observedLiveness=indeterminate",
224
+ indeterminate.kind === "rejected" && indeterminate.receipt.observedLiveness === "indeterminate",
225
+ );
226
+
227
+ succeeded = true;
228
+ console.log(`\nsmoke-agy-native-push-live: ${passed} checks passed (real agy round-trip; isolated meta-store)`);
229
+ } catch (err) {
230
+ console.error("\n[smoke-agy-native-push-live] FAILED — diagnostic artifacts:");
231
+ for (const [k, v] of Object.entries(artifacts)) console.error(` ${k} = ${v}`);
232
+ throw err;
233
+ } finally {
234
+ // Remove the ENTIRE temp meta-store — no real-store residue (NEXT ⑥ structural close).
235
+ await fsp.rm(tmp, { recursive: true, force: true }).catch(() => {});
236
+ if (!succeeded) console.error(`[smoke-agy-native-push-live] (temp store ${tmp} removed)`);
237
+ }
238
+ }
239
+
240
+ main().catch((err) => {
241
+ console.error(err);
242
+ process.exit(1);
243
+ });
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-agy-statusline-state — regression gate for the agy statusLine install adapter (#46 Task
3
+ # 1). Runs install → doctor → uninstall in an ISOLATED HOME + XDG_DATA_HOME with a fake stable
4
+ # bin (entwurf-agy-statusline) + fake `pgrep` (no real ~/.gemini, no real agy). Asserts:
5
+ # - adopt a regular file: statusLine subtree OWNED WHOLE ({type:custom, command:bare bin,
6
+ # enabled:true}), UNRELATED settings keys (model/permissions/…) preserved, state written with
7
+ # the STABLE command (never a repo/checkout path), prior subtree captured as preimage.
8
+ # - doctor STATIC clean + LIVE SKIP with no agy; LIVE consistent (not overclaimed) with a fake agy.
9
+ # - state-drift (state present, statusLine changed away OR removed from existing settings) → doctor FAILS.
10
+ # - uninstall honest-inverse: statusLine preimage restored, unrelated keys survive, state removed.
11
+ # - SYMLINK target → install REFUSES + writes NO state (someone else's SSOT).
12
+ # - DANGLING SYMLINK (departed owner) → install REFUSES the same, NO state, link left intact.
13
+ # - DANGLING command (bin not on PATH) → doctor FAILS (the stable-bin resolvability gate).
14
+ # - NOT-OURS (statusLine still the prior/agent-config command, no state) → doctor is an honest
15
+ # "never installed" note, NOT a pass-in-disguise and NOT a hard fail.
16
+ # - CREATE-NEW → uninstall removes the file it created.
17
+ # - SETUP INTEGRATION: the wire-agy-statusline wrapper — agy absent → honest skip + NO state;
18
+ # agy present + regular → idempotent install + state; agy present + symlink/corrupt →
19
+ # NON-FATAL WARN + continue (exit 0, reason-specific, no clobber, no state).
20
+ # - checkout stays byte-identical (nothing written under $REPO).
21
+ # Offline + deterministic (deps: bash + python3).
22
+ set -euo pipefail
23
+
24
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
25
+ BRIDGE="$REPO_DIR/scripts/agy-statusline-bridge.sh"
26
+
27
+ pass=0
28
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
29
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
30
+ want() { eval "$2" && ok "$1" || die "$1"; }
31
+
32
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
33
+
34
+ SB="$(mktemp -d)"
35
+ trap 'rm -rf "$SB"' EXIT
36
+ export HOME="$SB/home"
37
+ export XDG_DATA_HOME="$SB/xdg"
38
+ SET="$HOME/.gemini/antigravity-cli/settings.json"
39
+ STATE="$XDG_DATA_HOME/entwurf/agy-statusline/install-state.json"
40
+ mkdir -p "$(dirname "$SET")" "$SB/bin"
41
+
42
+ # fake stable bin (on PATH) — the renderer the statusLine command resolves to.
43
+ printf '#!/usr/bin/env bash\necho fake-agy-statusline\n' > "$SB/bin/entwurf-agy-statusline"
44
+ chmod +x "$SB/bin/entwurf-agy-statusline"
45
+ # Keep THIS fake authoritative: drop the dir holding a REAL entwurf-agy-statusline (a dev host
46
+ # that ran setup exposes one at ~/.local/bin) so the dangling-command test is not masked by it.
47
+ _real_sl="$(command -v entwurf-agy-statusline 2>/dev/null || true)"
48
+ _real_sl_dir="${_real_sl%/*}"
49
+ if [ -n "$_real_sl_dir" ]; then
50
+ PATH="$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx "$_real_sl_dir" | paste -sd: -)"
51
+ fi
52
+ export PATH="$SB/bin:$PATH"
53
+ export AGY_SETTINGS_CONFIG="$SET"
54
+
55
+ fake_agy() { # install/remove a fake `pgrep` that reports (or not) a live agy
56
+ if [ "$1" = "on" ]; then
57
+ printf '#!/usr/bin/env bash\n[ "$2" = agy ] && { echo 4242; exit 0; }\nexit 1\n' > "$SB/bin/pgrep"
58
+ else
59
+ printf '#!/usr/bin/env bash\nexit 1\n' > "$SB/bin/pgrep"
60
+ fi
61
+ chmod +x "$SB/bin/pgrep"
62
+ }
63
+ fake_agy off
64
+
65
+ # The prior (agent-config) statusLine we must capture + restore, alongside unrelated keys.
66
+ PRIOR='"/home/junghan/repos/gh/agent-config/antigravity/statusline.sh"'
67
+ write_settings() { # $1 = statusLine command (or "" to omit statusLine)
68
+ if [ -z "$1" ]; then
69
+ printf '{\n "model": "Gemini 3.1 Pro (Low)",\n "permissions": { "allow": ["mcp(*)"] }\n}\n' > "$SET"
70
+ else
71
+ printf '{\n "model": "Gemini 3.1 Pro (Low)",\n "permissions": { "allow": ["mcp(*)"] },\n "statusLine": { "type": "custom", "command": %s, "enabled": true }\n}\n' "$1" > "$SET"
72
+ fi
73
+ }
74
+
75
+ # ── NOT-OURS: statusLine still the prior command, no state → doctor note, not a fail ──
76
+ write_settings "$PRIOR"
77
+ DOC_OUT="$(bash "$BRIDGE" doctor)"; DOC_RC=$?
78
+ want "not-ours: doctor exits 0 (statusLine present but not ours is not a hard fail)" "[ '$DOC_RC' -eq 0 ]"
79
+ want "not-ours: doctor says NOT entwurf-owned (honest, not a pass)" "printf '%s' \"\$DOC_OUT\" | grep -q 'NOT entwurf-owned'"
80
+ want "not-ours: doctor names the '?' never-installed case" "printf '%s' \"\$DOC_OUT\" | grep -q 'never installed'"
81
+
82
+ # ── A: adopt a regular file — own the subtree WHOLE + preserve unrelated + capture preimage ──
83
+ bash "$BRIDGE" install >/dev/null
84
+ want "install: statusLine command is our stable bin" \
85
+ "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d[\"statusLine\"][\"command\"]==\"entwurf-agy-statusline\" else 1)' '$SET'"
86
+ want "install: statusLine subtree owned whole (type=custom, enabled=true)" \
87
+ "python3 -c 'import json,sys; s=json.load(open(sys.argv[1]))[\"statusLine\"]; sys.exit(0 if s[\"type\"]==\"custom\" and s[\"enabled\"] is True else 1)' '$SET'"
88
+ want "install: unrelated keys preserved (model + permissions)" \
89
+ "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get(\"model\") and d.get(\"permissions\") else 1)' '$SET'"
90
+ want "install: state written under XDG" "[ -f '$STATE' ]"
91
+ want "install: state records the STABLE command (not a repo/checkout path)" \
92
+ "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"command\"])' '$STATE' | grep -qx entwurf-agy-statusline"
93
+ want "install: settings has NO repo path in statusLine" "! grep -q '$REPO_DIR' '$SET'"
94
+ want "install: preimage captured the prior (agent-config) subtree" \
95
+ "python3 -c 'import json,sys; p=json.load(open(sys.argv[1]))[\"preimage\"]; sys.exit(0 if p and p.get(\"command\")==\"/home/junghan/repos/gh/agent-config/antigravity/statusline.sh\" else 1)' '$STATE'"
96
+
97
+ # ── B: doctor — static clean, live SKIP (no agy), state-evidence confirms ──
98
+ DOC_OUT="$(bash "$BRIDGE" doctor)"; DOC_RC=$?
99
+ want "doctor(no-agy): exits 0 (static clean)" "[ '$DOC_RC' -eq 0 ]"
100
+ want "doctor(no-agy): configured → resolvable" "printf '%s' \"\$DOC_OUT\" | grep -q \"configured → 'entwurf-agy-statusline' (resolvable)\""
101
+ want "doctor(no-agy): live tier is an honest SKIP" "printf '%s' \"\$DOC_OUT\" | grep -q 'live: SKIP'"
102
+ want "doctor(no-agy): SKIP is not disguised as a pass" "! printf '%s' \"\$DOC_OUT\" | grep -q 'consistent with runtime wiring'"
103
+ want "doctor(installed): state-evidence confirms still configured" \
104
+ "printf '%s' \"\$DOC_OUT\" | grep -q 'still configure entwurf-agy-statusline'"
105
+
106
+ # ── C: doctor with a fake agy present → live is CONSISTENT (honest, not overclaimed) ──
107
+ fake_agy on
108
+ DOC_OUT="$(bash "$BRIDGE" doctor)"; DOC_RC=$?
109
+ want "doctor(agy-live): exits 0" "[ '$DOC_RC' -eq 0 ]"
110
+ want "doctor(agy-live): live says consistent-with-wiring (not SKIP)" \
111
+ "printf '%s' \"\$DOC_OUT\" | grep -q 'consistent with runtime wiring'"
112
+ want "doctor(agy-live): does NOT overclaim statusline-read as proven" \
113
+ "printf '%s' \"\$DOC_OUT\" | grep -q 'statusline read NOT proven'"
114
+ fake_agy off
115
+
116
+ # ── C2 (drift): install-state present but statusLine changed away → doctor FAILS ──
117
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["statusLine"]={"type":"custom","command":"someone-elses","enabled":True}; json.dump(d,open(p,"w"))' "$SET"
118
+ if bash "$BRIDGE" doctor >/dev/null 2>&1; then die "drift: doctor should FAIL (state present, statusLine changed)"; fi
119
+ ok "drift: doctor FAILS on state-present + statusLine-changed (installed-then-loosened)"
120
+ bash "$BRIDGE" install >/dev/null # restore our command
121
+ want "drift: re-install restores our command" \
122
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"statusLine\"][\"command\"]==\"entwurf-agy-statusline\" else 1)' '$SET'"
123
+
124
+ # ── C2b (drift): settings file exists but statusLine was removed → FAIL, NOT orphan auto-clean ──
125
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d.pop("statusLine", None); json.dump(d,open(p,"w"))' "$SET"
126
+ if bash "$BRIDGE" doctor >/dev/null 2>&1; then die "drift-absent-key: doctor should FAIL (state present, settings exists, statusLine removed)"; fi
127
+ ok "drift-absent-key: doctor FAILS on state-present + settings-exists + statusLine-removed"
128
+ want "drift-absent-key: state is NOT auto-cleaned" "[ -f '$STATE' ]"
129
+ bash "$BRIDGE" install >/dev/null # restore our command/state for the orphan case
130
+ want "drift-absent-key: re-install restores our command" \
131
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"statusLine\"][\"command\"]==\"entwurf-agy-statusline\" else 1)' '$SET'"
132
+
133
+ # ── C3 (ORPHANED): install-state present but managed config is completely ABSENT → Auto-clean ──
134
+ rm -f "$SET"
135
+ DOC_OUT="$(bash "$BRIDGE" doctor 2>&1)"; DOC_RC=$?
136
+ want "orphan: doctor exits 0 when config is completely absent (HOME wiped)" "[ '$DOC_RC' -eq 0 ]"
137
+ want "orphan: doctor logs ORPHANED and auto-cleans" "printf '%s' \"\$DOC_OUT\" | grep -q 'ORPHANED'"
138
+ want "orphan: state file is removed automatically" "[ ! -f '$STATE' ]"
139
+ bash "$BRIDGE" uninstall >/dev/null # clear the drift-polluted preimage before the honest-inverse check
140
+
141
+
142
+ # ── D: uninstall — honest inverse (FRESH install so preimage is the true prior, not C2's drift) ──
143
+ write_settings "$PRIOR"
144
+ bash "$BRIDGE" install >/dev/null
145
+ bash "$BRIDGE" uninstall >/dev/null
146
+ want "uninstall: statusLine restored to the prior (agent-config) command" \
147
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"statusLine\"][\"command\"]==\"/home/junghan/repos/gh/agent-config/antigravity/statusline.sh\" else 1)' '$SET'"
148
+ want "uninstall: unrelated keys survived (model)" \
149
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1])).get(\"model\") else 1)' '$SET'"
150
+ want "uninstall: state file removed" "[ ! -f '$STATE' ]"
151
+
152
+ # ── E: SYMLINK target → install REFUSES + writes NO state ──
153
+ rm -f "$SET"
154
+ printf '{"model":"x"}\n' > "$SB/real_settings.json"
155
+ ln -s "$SB/real_settings.json" "$SET"
156
+ if bash "$BRIDGE" install >/dev/null 2>&1; then die "symlink: install should have REFUSED"; fi
157
+ ok "symlink: install refused (nonzero exit)"
158
+ want "symlink: NO state written on refusal" "[ ! -f '$STATE' ]"
159
+ want "symlink: the linked SSOT was NOT clobbered (no statusLine written into it)" \
160
+ "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if \"statusLine\" not in d else 1)' '$SB/real_settings.json'"
161
+ rm -f "$SET"
162
+
163
+ # ── E2: DANGLING SYMLINK (departed owner) → install REFUSES + NO state, link intact ──
164
+ DEPARTED="$SB/departed-owner/settings.json" # target does NOT exist (departed owner)
165
+ ln -s "$DEPARTED" "$SET"
166
+ want "dangling-symlink: precondition — link is dangling (target absent)" "[ -L '$SET' ] && [ ! -e '$SET' ]"
167
+ set +e; OUT="$(bash "$BRIDGE" install 2>&1)"; RC=$?; set -e
168
+ want "dangling-symlink: install exits nonzero (refused)" "[ '$RC' -ne 0 ]"
169
+ want "dangling-symlink: refusal is the SYMLINK reason (not invalid-json / other)" \
170
+ "printf '%s' \"\$OUT\" | grep -qi 'refused (symlink)'"
171
+ want "dangling-symlink: NO state written" "[ ! -f '$STATE' ]"
172
+ want "dangling-symlink: link NOT followed — departed target still absent" "[ ! -e '$DEPARTED' ]"
173
+ want "dangling-symlink: the dangling link left intact (specimen, not silently removed)" "[ -L '$SET' ]"
174
+ rm -f "$SET"
175
+
176
+ # ── F: DANGLING command (bin not on PATH) → doctor FAILS ──
177
+ write_settings "" # start clean
178
+ bash "$BRIDGE" install >/dev/null
179
+ mv "$SB/bin/entwurf-agy-statusline" "$SB/bin/entwurf-agy-statusline.hidden" # bin no longer resolvable
180
+ if bash "$BRIDGE" doctor >/dev/null 2>&1; then die "dangling-cmd: doctor should FAIL (our command not resolvable)"; fi
181
+ ok "dangling-cmd: doctor FAILS when our stable bin is not on PATH (resolvability gate)"
182
+ mv "$SB/bin/entwurf-agy-statusline.hidden" "$SB/bin/entwurf-agy-statusline"
183
+ bash "$BRIDGE" uninstall >/dev/null
184
+
185
+ # ── G: CREATE-NEW → uninstall removes the file it created ──
186
+ rm -f "$SET" "$STATE"
187
+ bash "$BRIDGE" install >/dev/null
188
+ want "create-new: file created" "[ -f '$SET' ]"
189
+ want "create-new: state detectMode is created-new" \
190
+ "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"detectMode\"])' '$STATE' | grep -qx created-new"
191
+ bash "$BRIDGE" uninstall >/dev/null
192
+ want "create-new: uninstall removed the file it created (nothing else remained)" "[ ! -f '$SET' ]"
193
+ want "create-new: state removed" "[ ! -f '$STATE' ]"
194
+
195
+ # ── H: uninstall with no state is idempotent (a note, not a failure) ──
196
+ bash "$BRIDGE" uninstall >/dev/null 2>&1
197
+ ok "idempotent: uninstall with no state exits 0 (nothing to undo)"
198
+
199
+ # ── I: setup integration — wire-agy-statusline (detection-gated, NON-FATAL) ──
200
+ rm -f "$SET" "$STATE"
201
+ printf '#!/usr/bin/env bash\necho fake-agy\n' > "$SB/bin/agy"; chmod +x "$SB/bin/agy"
202
+
203
+ # I-1: agy ABSENT → honest skip, no state, exit 0
204
+ set +e; OUT="$(AGY_BIN="$SB/no-such-agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
205
+ want "wire(no-agy): exits 0 (non-fatal skip)" "[ '$RC' -eq 0 ]"
206
+ want "wire(no-agy): honest skip message" "printf '%s' \"\$OUT\" | grep -q 'skipping agy statusLine wiring'"
207
+ want "wire(no-agy): NO state written" "[ ! -f '$STATE' ]"
208
+
209
+ # I-2: agy PRESENT + regular settings → idempotent install + state, exit 0
210
+ write_settings "$PRIOR"
211
+ set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
212
+ want "wire(agy+regular): exits 0" "[ '$RC' -eq 0 ]"
213
+ want "wire(agy+regular): statusLine now our command" \
214
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"statusLine\"][\"command\"]==\"entwurf-agy-statusline\" else 1)' '$SET'"
215
+ want "wire(agy+regular): state written" "[ -f '$STATE' ]"
216
+ set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
217
+ want "wire(agy+regular, re-run): idempotent exit 0" "[ '$RC' -eq 0 ]"
218
+ bash "$BRIDGE" uninstall >/dev/null; rm -f "$SET"
219
+
220
+ # I-3: agy PRESENT + SYMLINK settings → NON-FATAL WARN + continue (exit 0), no clobber, no state
221
+ printf '{"model":"x"}\n' > "$SB/real_wire_set.json"
222
+ ln -s "$SB/real_wire_set.json" "$SET"
223
+ set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
224
+ want "wire(agy+symlink): exits 0 (NON-FATAL — setup not bricked)" "[ '$RC' -eq 0 ]"
225
+ want "wire(agy+symlink): reason-specific WARN names the symlink" "printf '%s' \"\$OUT\" | grep -qi 'symlink'"
226
+ want "wire(agy+symlink): linked SSOT NOT clobbered" \
227
+ "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if \"statusLine\" not in d else 1)' '$SB/real_wire_set.json'"
228
+ want "wire(agy+symlink): NO state written" "[ ! -f '$STATE' ]"
229
+ rm -f "$SET"
230
+
231
+ # I-4: agy PRESENT + CORRUPT settings (invalid JSON) → NON-FATAL WARN + continue
232
+ printf 'not json{{{' > "$SET"
233
+ set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
234
+ want "wire(agy+corrupt): exits 0 (NON-FATAL)" "[ '$RC' -eq 0 ]"
235
+ want "wire(agy+corrupt): reason-specific WARN flags invalid JSON" "printf '%s' \"\$OUT\" | grep -qi 'invalid JSON'"
236
+ want "wire(agy+corrupt): NO state written" "[ ! -f '$STATE' ]"
237
+ rm -f "$SET" "$SB/bin/agy"
238
+
239
+ # ── checkout purity: the working tree is byte-identical (nothing under $REPO) ──
240
+ # ── RE-INSTALL PROVENANCE: an installer is re-run on every upgrade ────────────
241
+ # The preimage answers "what was here before US" — a fact about a moment that has already passed.
242
+ # Re-capturing it on each install would record OUR OWN previous subtree as the operator's, and the
243
+ # honest inverse would then faithfully restore us: uninstall leaves behind the very thing it exists
244
+ # to remove. Measured before the fix: install×2 → uninstall left statusLine in the file.
245
+ rm -f "$SET" "$STATE"
246
+ printf '{"model":"x"}\n' > "$SET"
247
+ bash "$BRIDGE" install >/dev/null 2>&1
248
+ bash "$BRIDGE" install >/dev/null 2>&1
249
+ bash "$BRIDGE" install >/dev/null 2>&1
250
+ want "re-install: provenance stays the FIRST install's (preimage still null, not our own subtree)" \
251
+ "python3 -c \"import json,sys;sys.exit(0 if json.load(open('$STATE'))['preimage'] is None else 1)\""
252
+ bash "$BRIDGE" uninstall >/dev/null 2>&1
253
+ want "re-install: uninstall after install×3 still removes our statusLine (no self-restore)" \
254
+ "! python3 -c \"import json,sys;sys.exit(0 if 'statusLine' in json.load(open('$SET')) else 1)\""
255
+ want "re-install: the operator's unrelated keys survive the whole cycle" \
256
+ "[ \"\$(python3 -c \"import json;print(json.load(open('$SET')).get('model'))\")\" = x ]"
257
+
258
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
259
+ want "purity: checkout unchanged (all writes stayed in the sandbox HOME+XDG)" \
260
+ "[ \"\$REPO_BEFORE\" = \"\$REPO_AFTER\" ]"
261
+
262
+ # ── FOREIGN TARGET: state describes a settings file this host does not read ───────────
263
+ # The recorded file is perfectly configured — it is just not OURS. A doctor that only inspects the
264
+ # recorded path blesses a host it owns nothing on, and the damage here is specific: the LIVE
265
+ # statusLine has no recorded preimage, so uninstall would DROP the key instead of restoring the
266
+ # operator's own command. This exact shape was produced on a real host by a verification run that
267
+ # isolated HOME but SHARED XDG_DATA_HOME — sandbox settings, real state. Isolation must move HOME
268
+ # and XDG_DATA_HOME together.
269
+ bash "$BRIDGE" install >/dev/null # the uninstall cases above left no state; re-own this host first
270
+ FOREIGN_SET="$SB/foreign-settings.json"
271
+ cp "$SET" "$FOREIGN_SET"
272
+ python3 -c "
273
+ import json,sys
274
+ p=sys.argv[1]; d=json.load(open(p)); d['managedSettingsPath']=sys.argv[2]
275
+ json.dump(d, open(p,'w'))" "$STATE" "$FOREIGN_SET"
276
+ set +e; FT_OUT="$(bash "$BRIDGE" doctor 2>&1)"; FT_RC=$?; set -e
277
+ want "foreign-target: doctor FAILS when install-state manages settings this host does not read" "[ '$FT_RC' -ne 0 ]"
278
+ want "foreign-target: the report names both the recorded and the live settings path" \
279
+ "printf '%s' \"\$FT_OUT\" | grep -q 'FOREIGN TARGET' && printf '%s' \"\$FT_OUT\" | grep -qF '$FOREIGN_SET' && printf '%s' \"\$FT_OUT\" | grep -qF '$SET'"
280
+ want "foreign-target: it names the real damage — the live statusLine has no recorded preimage" \
281
+ "printf '%s' \"\$FT_OUT\" | grep -qi 'preimage'"
282
+ want "foreign-target: neither the foreign settings nor the state is auto-deleted" "[ -f '$FOREIGN_SET' ] && [ -f '$STATE' ]"
283
+ rm -f "$FOREIGN_SET"
284
+
285
+ # A present state with invalid JSON or no managed target is corrupt evidence, not "no evidence".
286
+ # The doctor must fail loud instead of parsing to an empty string and skipping the ownership check.
287
+ printf 'not-json{{{' > "$STATE"
288
+ set +e; FT_OUT="$(bash "$BRIDGE" doctor 2>&1)"; FT_RC=$?; set -e
289
+ want "corrupt-state: doctor FAILS on unreadable install-state" "[ '$FT_RC' -ne 0 ]"
290
+ want "corrupt-state: report names CORRUPT instead of silently skipping ownership" \
291
+ "printf '%s' \"\$FT_OUT\" | grep -q 'state: CORRUPT'"
292
+
293
+ # A RELATIVE managed path is corrupt too — install records absolute paths only, and resolving a
294
+ # relative one against the doctor's cwd could bless whatever directory it happens to run from.
295
+ printf '{"managedSettingsPath":".gemini/antigravity-cli/settings.json"}' > "$STATE"
296
+ set +e; FT_OUT="$(bash "$BRIDGE" doctor 2>&1)"; FT_RC=$?; set -e
297
+ want "corrupt-state: a relative managedSettingsPath is CORRUPT, never resolved against the doctor's cwd" \
298
+ "[ '$FT_RC' -ne 0 ] && printf '%s' \"\$FT_OUT\" | grep -q 'state: CORRUPT'"
299
+
300
+ printf '\nsmoke-agy-statusline-state: %d checks passed\n' "$pass"
@@ -51,8 +51,15 @@ RAW_DIR="$HERE/raw-async-delivery"
51
51
  # 1.0 -> 1.1) is the real "re-verify the markers + Gotchas + raw/LIVE probes"
52
52
  # trigger and DOES scream.
53
53
  PIN_CLAUDE_MINOR="2.1"
54
- PIN_CODEX_MINOR="0.136"
55
- PIN_AGY_MINOR="1.0"
54
+ # codex 0.144 (2026-07-14): OBSERVED bump, not a re-verification. Codex is not a shipped
55
+ # native-citizen lane in 0.12.x; the probe evidence in DELIVERY.md §Codex was measured at
56
+ # 0.136.0 and has NOT been re-run on 0.144 — that explicit verdict lives there. Re-run the
57
+ # raw probes before building any codex adapter on this line.
58
+ PIN_CODEX_MINOR="0.144"
59
+ # agy 1.1 (2026-07-14): re-verified live — entwurf_self called without a permission prompt,
60
+ # bidirectional native-push reply on the same gid, LIVE=1 smoke-agy-native-push-live 13/13
61
+ # at agy 1.1.0 (evidence recorded in DELIVERY.md §Antigravity).
62
+ PIN_AGY_MINOR="1.1"
56
63
 
57
64
  # --- undocumented-behavior identifiers that the delivery path rides on ---------
58
65
  # Each MUST stay present in the installed Claude binary. Zero = drift = SCREAM.
@@ -519,6 +519,14 @@ valid_record "20260606T120000-eeeeee" "doctor-native" > "$DOC_STORE/20260606T120
519
519
  # Fake claude: a healthy toolchain so the doctor sails past every other section.
520
520
  cat > "$DOC_BIN/claude" <<'SH'
521
521
  #!/usr/bin/env bash
522
+ if [ "${1:-} ${2:-} ${3:-}" = "plugin list --json" ]; then
523
+ if [ "${FAKE_PLUGIN_CACHE_MISS:-0}" = 1 ]; then
524
+ printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true,"errors":["Marketplace meta-bridge-local failed to load: cache-miss"]}]'
525
+ else
526
+ printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true}]'
527
+ fi
528
+ exit 0
529
+ fi
522
530
  case "$1${2:+ $2}" in
523
531
  "--version") echo "2.1.167 (Claude Code)" ;;
524
532
  "plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
@@ -549,6 +557,18 @@ if printf '%s\n' "$DOC_OUT" | grep -q 'user MCP entwurf-bridge'; then ok "doctor
549
557
  if printf '%s\n' "$DOC_OUT" | grep -q '\[plugin install'; then ok "doctor continues to a later section after the drift (no early set -e death)"; else bad "doctor did not reach a later section header after the drift:"$'\n'"$DOC_OUT"; fi
550
558
  if printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: FAIL'; then ok "doctor runs the whole chain to its final summary line"; else bad "doctor did not reach its final summary line (mid-run death):"$'\n'"$DOC_OUT"; fi
551
559
 
560
+ # A plugin can be configured enabled while Claude refuses to load its missing marketplace
561
+ # source. Text-only `grep enabled` called that healthy and let `🪛 ? cc` look mysterious.
562
+ set +e
563
+ DOC_CACHE_OUT="$(env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" PI_CODING_AGENT_DIR="$DOC_AGENT" ENTWURF_META_SESSIONS_DIR="$DOC_STORE" FAKE_PLUGIN_CACHE_MISS=1 PATH="$DOC_BIN:$PATH" bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
564
+ DOC_CACHE_CODE=$?
565
+ set -e
566
+ if [ "$DOC_CACHE_CODE" -eq 1 ] && printf '%s\n' "$DOC_CACHE_OUT" | grep -q 'enabled=true but FAILED TO LOAD:.*cache-miss'; then
567
+ ok "doctor distinguishes configured-enabled from loadable (cache-miss is a hard failure)"
568
+ else
569
+ bad "doctor did not surface enabled-but-cache-miss as a hard load failure:"$'\n'"$DOC_CACHE_OUT"
570
+ fi
571
+
552
572
  # ⓪ 경계 종료 단언: 이 smoke 전체(install 조립 + state + wrapper-uninstall + doctor)가
553
573
  # 끝난 뒤에도 checkout 안에는 live marketplace source가 생기지 않았다 — source
554
574
  # origin(repo)과 live artifact(XDG)가 구조적으로 분리됐다는 최종 증명. 미래 회귀가