@junghanacs/entwurf 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +13 -2
  2. package/CHANGELOG.md +63 -0
  3. package/DELIVERY.md +57 -0
  4. package/README.md +16 -7
  5. package/VERIFY.md +4 -4
  6. package/demo/README.md +3 -1
  7. package/demo/demo-baseline.sh +12 -1
  8. package/demo/demo.sh +9 -1
  9. package/docs/acp-backend-rail.md +103 -4
  10. package/docs/external-mcp-host.md +1 -1
  11. package/docs/setup-clean-host.md +3 -3
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +12 -5
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +45 -3
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -47
  16. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +139 -47
  17. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +294 -0
  18. package/mcp/entwurf-bridge/src/index.ts +14 -5
  19. package/mcp/entwurf-bridge/tsconfig.build.json +15 -5
  20. package/package.json +9 -9
  21. package/pi-extensions/entwurf-control.ts +13 -4
  22. package/pi-extensions/lib/acp/backend.ts +229 -9
  23. package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
  24. package/pi-extensions/lib/mux-fresh-call.ts +57 -4
  25. package/pi-extensions/lib/mux-resume-call.ts +21 -53
  26. package/run.sh +70 -25
  27. package/scripts/agy-bridge-config.py +47 -13
  28. package/scripts/agy-bridge.sh +73 -23
  29. package/scripts/check-acp-prompt-lifecycle.ts +221 -9
  30. package/scripts/check-entwurf-bridge-boot.ts +28 -0
  31. package/scripts/check-gate-qualification.ts +5 -3
  32. package/scripts/check-mux-resume-call.ts +11 -10
  33. package/scripts/check-probe-bridge-command.ts +201 -0
  34. package/scripts/check-release-gate-outcomes.ts +54 -1
  35. package/scripts/doctor-pi-provider.ts +155 -51
  36. package/scripts/meta-bridge-state.py +75 -1
  37. package/scripts/mutants/acp-prompt-lifecycle.json +25 -3
  38. package/scripts/mutants/bridge-command-boot.json +107 -0
  39. package/scripts/mutants/meta-retire.json +47 -0
  40. package/scripts/mutants/mux-fresh-call.json +48 -4
  41. package/scripts/mutants/mux-resume-call.json +3 -3
  42. package/scripts/mutants/release-gate.json +13 -0
  43. package/scripts/probe-bridge-command.ts +330 -0
  44. package/scripts/raw-async-delivery/README.md +158 -1
  45. package/scripts/raw-async-delivery/copilot-ui-server-probe.mjs +337 -0
  46. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  47. package/scripts/smoke-agy-install-state.sh +76 -2
  48. package/scripts/smoke-entwurf-chain-live.ts +12 -4
  49. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  50. package/scripts/smoke-meta-install-state.sh +169 -3
  51. package/scripts/smoke-mux-fresh-call-live.ts +1 -1
  52. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  53. package/scripts/smoke-pi-provider-state.sh +135 -6
  54. package/scripts/smoke-resident-garden-guard.sh +2 -2
@@ -63,7 +63,7 @@ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
63
63
  const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
64
64
  const SOCKET_SUFFIX = ".sock";
65
65
 
66
- const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.4";
66
+ const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.6-luna";
67
67
  const ACP_TARGET = process.env.ENTWURF_CHAIN_ACP_TARGET?.trim() || "entwurf/claude-sonnet-5";
68
68
  const BOOT_TIMEOUT_MS = 45_000;
69
69
  const CLAUDE_TURN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_CLAUDE_TIMEOUT_MS) || 240_000;
@@ -161,6 +161,14 @@ async function main(): Promise<void> {
161
161
  ENTWURF_META_MAILBOX_DIR: mailboxDir,
162
162
  ENTWURF_META_RECEIVERS_DIR: receiversDir,
163
163
  };
164
+ // The MCP bridge prefers PI_SESSION_ID+PI_AGENT_ID over a meta-sender marker when both
165
+ // are present. A runner that is itself a pi --entwurf-control session (release-gate,
166
+ // an agent preparing a cut) would otherwise stamp every Claude hop with the RUNNER's
167
+ // garden id — hop 1 then fails "B saw A's real garden id" while the payload still
168
+ // traverses. Strip the carriers so A's SessionStart marker is the only authority.
169
+ const childEnv: NodeJS.ProcessEnv = { ...process.env, ...worldEnv };
170
+ delete childEnv.PI_SESSION_ID;
171
+ delete childEnv.PI_AGENT_ID;
164
172
 
165
173
  const nonce = `ENTWURF-CHAIN-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
166
174
  console.error(`[smoke-entwurf-chain-live] repo: ${REPO_ROOT}`);
@@ -194,7 +202,7 @@ async function main(): Promise<void> {
194
202
  const c = spawn(
195
203
  "pi",
196
204
  [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", acpProvider, "--model", acpModel, "--mode", "rpc"],
197
- { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
205
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: childEnv },
198
206
  );
199
207
  children.push(c);
200
208
  c.stdout?.on("data", (b: Buffer) => {
@@ -215,7 +223,7 @@ async function main(): Promise<void> {
215
223
  const b = spawn(
216
224
  "pi",
217
225
  [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", gptProvider, "--model", gptModel, "--mode", "rpc"],
218
- { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
226
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: childEnv },
219
227
  );
220
228
  children.push(b);
221
229
  b.stdout?.on("data", (x: Buffer) => {
@@ -265,7 +273,7 @@ async function main(): Promise<void> {
265
273
  cwd: world,
266
274
  encoding: "utf8",
267
275
  timeout: CLAUDE_TURN_TIMEOUT_MS,
268
- env: { ...process.env, ...worldEnv },
276
+ env: childEnv,
269
277
  });
270
278
  const claudeOut = `${claudeRun.stdout ?? ""}\n${claudeRun.stderr ?? ""}`;
271
279
  ok("A's native Claude Code turn ran", claudeRun.status === 0);
@@ -36,7 +36,7 @@
36
36
  *
37
37
  * LIVE-only (spawns a real pi, opens a real socket) — kept OUT of `pnpm check`; honest skip when
38
38
  * LIVE!=1 (a release-gate that hard-fails without auth/model is unrunnable unattended). Model:
39
- * ENTWURF_LIVE_TARGET = "<provider>/<model>" (default "openai-codex/gpt-5.4")
39
+ * ENTWURF_LIVE_TARGET = "<provider>/<model>" (default "openai-codex/gpt-5.6-luna")
40
40
  * (or split: ENTWURF_LIVE_PROVIDER + ENTWURF_LIVE_MODEL)
41
41
  * LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
42
42
  *
@@ -101,7 +101,7 @@ function resolveTarget(): { provider: string; model: string } {
101
101
  }
102
102
  return {
103
103
  provider: process.env.ENTWURF_LIVE_PROVIDER?.trim() || "openai-codex",
104
- model: process.env.ENTWURF_LIVE_MODEL?.trim() || "gpt-5.4",
104
+ model: process.env.ENTWURF_LIVE_MODEL?.trim() || "gpt-5.6-luna",
105
105
  };
106
106
  }
107
107
 
@@ -6,6 +6,7 @@ set -euo pipefail
6
6
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  REPO="$(cd "$HERE/.." && pwd)"
8
8
  export REPO
9
+ export PYTHONDONTWRITEBYTECODE=1
9
10
  STATE="$REPO/scripts/meta-bridge-state.py"
10
11
  STORE_DOCTOR="$REPO/scripts/meta-bridge-store-doctor.ts"
11
12
  # shellcheck source=scripts/meta-bridge-hook-log.sh
@@ -192,6 +193,136 @@ JSON
192
193
 
193
194
  py() { python3 "$STATE" "$@" --repo "$REPO" --asm "$ASM"; }
194
195
 
196
+ # #71 retirement cells: drive the production leaf directly so JSON scalar type,
197
+ # malformed provenance and one-shot convergence cannot hide inside the larger
198
+ # install fixture below. Three cells carry [QK:] claims backed by exact-once
199
+ # mutants in scripts/mutants/meta-retire.json; each prints its token on the
200
+ # FAILURE line, because signatureOnFailureLine ignores any line starting with
201
+ # `ok`. Cells keep running after a failure so one planted defect reddens exactly
202
+ # its own claim instead of masking the ones behind it.
203
+ if python3 - "$STATE" <<'PY'
204
+ import copy, importlib.util, sys
205
+
206
+ spec = importlib.util.spec_from_file_location("meta_bridge_state", sys.argv[1])
207
+ assert spec and spec.loader
208
+ m = importlib.util.module_from_spec(spec)
209
+ spec.loader.exec_module(m)
210
+ NAME = "skipDangerousModePermissionPrompt"
211
+ PATH = [NAME]
212
+ MISSING = object()
213
+
214
+ def entry(existed, value):
215
+ return {"kind": "scalar", "path": PATH, "original": {"existed": existed, "value": value}}
216
+
217
+ def fixture(current=MISSING, old_entry=MISSING):
218
+ settings = {} if current is MISSING else {NAME: current}
219
+ keys = {} if old_entry is MISSING else {NAME: copy.deepcopy(old_entry)}
220
+ state = {"files": {"settings": {"keys": keys}}}
221
+ return state, settings
222
+
223
+ def run(current=MISSING, old_entry=MISSING):
224
+ state, settings = fixture(current, old_entry)
225
+ m.relinquish_retired_scalar(state, settings, NAME, PATH, True)
226
+ return state, settings
227
+
228
+ failed = 0
229
+
230
+ def cell(sig, label, fn):
231
+ global failed
232
+ text = f"{sig} {label}".strip()
233
+ try:
234
+ fn()
235
+ except Exception as exc:
236
+ print(f" FAIL {text}: {exc!r}")
237
+ failed = 1
238
+ return
239
+ print(f" ok {text}")
240
+
241
+ def compare_type_cells():
242
+ assert NAME not in run(True, entry(False, None))[1]
243
+ assert run(True, entry(True, False))[1][NAME] is False
244
+ assert run(True, entry(True, True))[1][NAME] is True
245
+ assert run(False, entry(True, False))[1][NAME] is False
246
+ assert NAME not in run(MISSING, entry(True, False))[1]
247
+ value = run(1, entry(True, False))[1][NAME]
248
+ assert type(value) is int and value == 1
249
+
250
+ cell(
251
+ "[QK:META-RETIRE-COMPARE-TYPE]",
252
+ "only an exact bool true with proven provenance is restored; 1 is not true",
253
+ compare_type_cells,
254
+ )
255
+
256
+ def malformed_cells():
257
+ for bad_entry in [
258
+ {"kind": "map-entry", "path": PATH, "original": {"existed": False, "value": None}},
259
+ {"kind": "scalar", "path": PATH, "original": {"existed": "no", "value": None}},
260
+ ]:
261
+ state, settings = fixture(True, bad_entry)
262
+ before = copy.deepcopy((state, settings))
263
+ try:
264
+ m.relinquish_retired_scalar(state, settings, NAME, PATH, True)
265
+ except m.StateError:
266
+ pass
267
+ else:
268
+ raise AssertionError("malformed retired provenance was accepted")
269
+ assert (state, settings) == before
270
+
271
+ cell(
272
+ "[QK:META-RETIRE-MALFORMED-SILENT]",
273
+ "malformed provenance fails loud and mutates neither settings nor state",
274
+ malformed_cells,
275
+ )
276
+
277
+ def fresh_cells():
278
+ for current in [True, False, 1, MISSING]:
279
+ state, settings = run(current)
280
+ expected = {} if current is MISSING else {NAME: current}
281
+ assert settings == expected and NAME not in state["files"]["settings"]["keys"]
282
+
283
+ cell(
284
+ "[QK:META-RETIRE-FRESH-TOUCH]",
285
+ "a provenance-less value is the operator's and is never touched",
286
+ fresh_cells,
287
+ )
288
+
289
+ def one_shot_cells():
290
+ state, settings = fixture(True, entry(True, False))
291
+ m.relinquish_retired_scalar(state, settings, NAME, PATH, True)
292
+ assert settings[NAME] is False and NAME not in state["files"]["settings"]["keys"]
293
+ before = copy.deepcopy((state, settings))
294
+ m.relinquish_retired_scalar(state, settings, NAME, PATH, True)
295
+ assert (state, settings) == before
296
+
297
+ # Recommended, not claimed: one-shot convergence and the disjoint-set guard ride
298
+ # along as ordinary cells so the proof budget stays at the three claims above.
299
+ cell("", "relinquishment consumes the entry once, so a re-run cannot restore again", one_shot_cells)
300
+
301
+ def structural_cells():
302
+ assert not m._managed_scalar_names & m._retired_scalar_names
303
+ # settings_state_keys is the sole reader of the ownership ledger, and
304
+ # load_state validates only the envelope. A corrupt files/settings/keys shape
305
+ # must stop with an operator diagnostic BEFORE any write — never a bare
306
+ # KeyError raised from inside apply(), and never a silent pass that would let
307
+ # relinquishment reason about ownership it cannot actually read.
308
+ for broken in [{}, {"files": {}}, {"files": {"settings": {}}}, {"files": {"settings": {"keys": []}}}]:
309
+ settings = {NAME: True}
310
+ before = copy.deepcopy((broken, settings))
311
+ try:
312
+ m.relinquish_retired_scalar(broken, settings, NAME, PATH, True)
313
+ except m.StateError:
314
+ pass
315
+ else:
316
+ raise AssertionError(f"corrupt state ledger accepted: {broken!r}")
317
+ assert (broken, settings) == before
318
+
319
+ cell("", "retired/managed sets stay disjoint and a corrupt key ledger fails closed", structural_cells)
320
+ sys.exit(failed)
321
+ PY
322
+ then ok "#71 retirement leaf: provenance, bool strictness, malformed refusal, one-shot convergence"
323
+ else bad "#71 retirement leaf cells failed (traceback above)"
324
+ fi
325
+
195
326
  DEV_STATUSLINE="$(python3 "$STATE" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin)["command"])')"
196
327
  if [ "$DEV_STATUSLINE" = "$REPO/scripts/meta-bridge-statusline.sh" ]; then ok "dev statusLine pins the checkout script"; else bad "dev statusLine command drifted: $DEV_STATUSLINE"; fi
197
328
  FAKE_INSTALLED_REPO="$TMP/npmroot/node_modules/@junghanacs/entwurf"
@@ -212,10 +343,26 @@ assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['va
212
343
  assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
213
344
  assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
214
345
  assert s['files']['settings']['keys']['autoCompactEnabled']['original']['existed'] is False
346
+ assert 'skipDangerousModePermissionPrompt' not in s['files']['settings']['keys']
215
347
  assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
216
348
  PY
217
- then ok "state captures original scalar/map values and is mode 0600"; else bad "state did not capture original values / mode 0600"; fi
349
+ then ok "state captures original scalar/map values, excludes the retired scalar, and is mode 0600"; else bad "state did not capture original values / retirement / mode 0600"; fi
218
350
 
351
+ # Simulate an upgrade from 0.14.0: the old state proves entwurf changed false→true.
352
+ # New apply must restore false and consume that ownership exactly once.
353
+ python3 - <<'PY'
354
+ import json, os
355
+ cfg=os.environ['CLAUDE_CONFIG_DIR']
356
+ sp=cfg + '/settings.json'; stp=cfg + '/entwurf.install-state.json'
357
+ settings=json.load(open(sp)); state=json.load(open(stp))
358
+ settings['skipDangerousModePermissionPrompt']=True
359
+ state['files']['settings']['keys']['skipDangerousModePermissionPrompt']={
360
+ 'kind':'scalar', 'path':['skipDangerousModePermissionPrompt'],
361
+ 'original':{'existed':True, 'value':False}
362
+ }
363
+ json.dump(settings, open(sp,'w'), indent=2); open(sp,'a').write('\n')
364
+ json.dump(state, open(stp,'w'), indent=2); open(stp,'a').write('\n')
365
+ PY
219
366
  py apply >/dev/null
220
367
  if python3 - <<'PY'
221
368
  import json, os
@@ -231,7 +378,9 @@ assert settings['env']['KEEP_ME'] == 'yes'
231
378
  assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
232
379
  for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
233
380
  assert settings[key] is False, key
234
- assert settings['skipDangerousModePermissionPrompt'] is True
381
+ assert settings['skipDangerousModePermissionPrompt'] is False
382
+ state=json.load(open(cfg + '/entwurf.install-state.json'))
383
+ assert 'skipDangerousModePermissionPrompt' not in state['files']['settings']['keys']
235
384
  for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
236
385
  assert item in settings['permissions']['allow'], item
237
386
  assert settings['permissions']['allow'].count('Read') == 1
@@ -249,7 +398,24 @@ then ok "apply installs managed keyset without clobbering unrelated keys"; else
249
398
  # (agent-config merge, hand edit) overwrites a pi-owned key, check must fail loud
250
399
  # AND name the drifted key so doctor can surface which one. Adversarial flips
251
400
  # below; restore with apply afterward so the later cases see a clean keyset.
252
- if py check >/dev/null 2>&1; then ok "survival check passes on a freshly applied keyset"; else bad "survival check failed right after apply"; fi
401
+ if py check >/dev/null 2>&1; then ok "survival check passes after relinquishing the retired scalar"; else bad "survival check failed right after retirement apply"; fi
402
+ MANAGED_JSON="$(py managed-keys)"
403
+ if printf '%s' "$MANAGED_JSON" | grep -q 'skipDangerousModePermissionPrompt'; then bad "managed-keys still claims the retired operator key"; else ok "managed-keys returns skipDangerousModePermissionPrompt to the operator"; fi
404
+ # A provenance-less true cannot be changed safely, but doctor/check must make it visible.
405
+ python3 - <<'PY'
406
+ import json, os
407
+ p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
408
+ d=json.load(open(p)); d['skipDangerousModePermissionPrompt']=True
409
+ json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')
410
+ PY
411
+ RETIRED_NOTE="$(py check)"
412
+ if printf '%s' "$RETIRED_NOTE" | grep -q 'operator-owned'; then ok "check advises on a provenance-less true without claiming ownership"; else bad "check hid the provenance-less retired true"; fi
413
+ python3 - <<'PY'
414
+ import json, os
415
+ p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
416
+ d=json.load(open(p)); d['skipDangerousModePermissionPrompt']=False
417
+ json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')
418
+ PY
253
419
  SURVIVAL_SNAP="$TMP/settings-survival-snapshot.json"
254
420
  cp "$CLAUDE_CONFIG_DIR/settings.json" "$SURVIVAL_SNAP" # exact restore point (array-replace below would drop user items)
255
421
  python3 - <<'PY'
@@ -43,7 +43,7 @@ import { skipLive } from "./lib/live-skip.ts";
43
43
  const LABEL = "smoke-mux-fresh-call-live";
44
44
  const CALLBACK_WAIT_MS = 180_000;
45
45
  const LIVE_MODEL = {
46
- pi: "openai-codex/gpt-5.6-terra",
46
+ pi: "openai-codex/gpt-5.6-luna",
47
47
  "claude-code": "claude-sonnet-5",
48
48
  } as const;
49
49
 
@@ -108,7 +108,7 @@ const BRIDGE_LAUNCHER = path.join(REPO, "mcp", "entwurf-bridge", "start.sh");
108
108
 
109
109
  /** The two pi shapes. They differ in exactly one thing that matters here — whether the record
110
110
  * carries `provider=entwurf`, which is what makes the resume argv re-inject the bridge (#29). */
111
- const PI_NATIVE_MODEL = "openai-codex/gpt-5.6-terra";
111
+ const PI_NATIVE_MODEL = "openai-codex/gpt-5.6-luna";
112
112
  const PI_ACP_MODEL = "entwurf/claude-sonnet-5";
113
113
  const CLAUDE_MODEL = "claude-sonnet-5";
114
114
 
@@ -31,6 +31,14 @@ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
31
31
 
32
32
  SB="$(mktemp -d)"
33
33
  trap 'rm -rf "$SB"' EXIT
34
+ # This smoke drives register-pi-provider.py, which imports the sibling pi_settings_io module — and
35
+ # CPython writes scripts/__pycache__/*.pyc for that import, INSIDE the checkout. The purity cell
36
+ # below never caught it because those bytes are gitignored, so `git status --porcelain` stays clean
37
+ # while the working tree is not. That difference is invisible here and load-bearing under gate
38
+ # qualification, whose snapshot judges tree cleanliness rather than porcelain output. Write no
39
+ # bytecode instead of cleaning it up afterwards: a smoke that leaves artifacts and then sweeps them
40
+ # is one early `exit` away from leaving them for real.
41
+ export PYTHONDONTWRITEBYTECODE=1
34
42
  export HOME="$SB/home"
35
43
  export XDG_DATA_HOME="$SB/xdg"
36
44
  GLOBAL="$SB/global.json"
@@ -46,8 +54,52 @@ LEGACY_CMD="$FAKE_REPO/mcp/entwurf-bridge/start.sh"
46
54
  # authoritative: drop EVERY original PATH dir that carries a real entwurf-bridge. A maintainer
47
55
  # can have both the managed dev link and the registry package shim; filtering only command -v's
48
56
  # first hit lets the second one mask the dangling-command cell.
49
- printf '#!/usr/bin/env bash\necho fake-bridge\n' > "$SB/bin/entwurf-bridge"
50
- chmod +x "$SB/bin/entwurf-bridge"
57
+ #
58
+ # #81: the doctor no longer stops at resolvability — it BOOTS the effective command and requires
59
+ # the entwurf MCP tool surface back. So the healthy fake must actually speak MCP: answer the
60
+ # `tools/list` frame (id 2) with a tools array carrying entwurf_v2. A fake that only echoed a word
61
+ # would now (correctly) read as "resolves but does not serve MCP", which is the NEGATIVE cell
62
+ # below — keeping the two fakes distinct is what makes this an A/B rather than a rename.
63
+ write_mcp_fake() { # $1 = path
64
+ cat > "$1" <<'FAKE'
65
+ #!/usr/bin/env bash
66
+ # minimal MCP stdio server: enough frames for the boot probe, nothing else.
67
+ while IFS= read -r line; do
68
+ case "$line" in
69
+ *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
70
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
71
+ esac
72
+ done
73
+ FAKE
74
+ chmod +x "$1"
75
+ }
76
+ # A launcher that RESOLVES and dies on exec — the shape of a relocated package-manager shim whose
77
+ # $0-derived target is gone (observed: exit 127 with a "No such file or directory" on stderr).
78
+ write_dead_fake() { # $1 = path
79
+ cat > "$1" <<'FAKE'
80
+ #!/usr/bin/env bash
81
+ echo "bash: /nonexistent/../global/v11/deadbeef/node_modules/@junghanacs/entwurf/mcp/entwurf-bridge/start.sh: No such file or directory" >&2
82
+ exit 127
83
+ FAKE
84
+ chmod +x "$1"
85
+ }
86
+ # A launcher that boots and speaks MCP but is NOT this bridge: it serves entwurf_v2 while missing
87
+ # entwurf_self. This is the shape of the observed #81 session (its schema had no entwurf_self) and
88
+ # of a stale build or a foreign binary that happens to own the name — a probe keyed on one tool
89
+ # would call it healthy, which is why identity is the exact verb SET.
90
+ write_stale_fake() { # $1 = path
91
+ cat > "$1" <<'FAKE'
92
+ #!/usr/bin/env bash
93
+ while IFS= read -r line; do
94
+ case "$line" in
95
+ *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"stale-bridge","version":"0"}}}' ;;
96
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
97
+ esac
98
+ done
99
+ FAKE
100
+ chmod +x "$1"
101
+ }
102
+ write_mcp_fake "$SB/bin/entwurf-bridge"
51
103
  _clean_path=""
52
104
  while IFS= read -r _dir; do
53
105
  [ -n "$_dir" ] || continue
@@ -136,14 +188,91 @@ printf '{}\n' > "$PROJECT"; rm -f "$STATE"
136
188
  OUT="$(doctor)"; RC=$?
137
189
  want "doctor(user-only bare): exits 0" "[ '$RC' -eq 0 ]"
138
190
  want "doctor(user-only bare): ok — bare bin resolves" "printf '%s' \"\$OUT\" | grep -q 'bare stable bin'"
191
+ # The green must be about BOOTING, not about resolving — anchored on the verdict word so a doctor
192
+ # that regressed to the resolvability-only ok cannot keep this cell green.
193
+ want "doctor(user-only bare): green says the bin BOOTS (#81), not merely resolves" "printf '%s' \"\$OUT\" | grep -q 'and it BOOTS'"
194
+
195
+ # ── H-merge: the doctor normalizes what PRODUCTION normalizes, not what each FILE holds ──
196
+ # resolveProviderConfig merges the two mcpServers maps per-name and normalizes the RESULT once. A
197
+ # doctor that normalized each file separately would validate entries production never sees: a
198
+ # malformed global entry the project map shadows is gone before pi ever looks, yet the per-file
199
+ # doctor throws and calls a healthy session red. Both directions are pinned here.
200
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":123}}}}, open(sys.argv[1],"w"))' "$GLOBAL"
201
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$PROJECT"
202
+ set +e
203
+ OUT="$(doctor 2>&1)"; RC=$?
204
+ set -e
205
+ want "doctor(invalid global SHADOWED by valid project): green — production never sees the bad entry" "[ '$RC' -eq 0 ]"
206
+ want "doctor(invalid global shadowed): EFFECTIVE scope is project" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
207
+ # An unrelated malformed server in the global map is likewise production's problem only if it
208
+ # survives the merge — here the project map does not shadow it, so it MUST stay red.
209
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"},"other":{"command":[]}}}}, open(sys.argv[1],"w"))' "$GLOBAL"
210
+ printf '{}\n' > "$PROJECT"
211
+ if doctor >/dev/null 2>&1; then die "doctor(unshadowed malformed sibling): should FAIL — the merged map is what production normalizes"; fi
212
+ ok "doctor(unshadowed malformed sibling): FAILS — an invalid entry that SURVIVES the merge is red"
213
+ # The effective entry itself malformed → red regardless of which scope wrote it.
214
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
215
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":123}}}}, open(sys.argv[1],"w"))' "$PROJECT"
216
+ if doctor >/dev/null 2>&1; then die "doctor(effective malformed): should FAIL"; fi
217
+ ok "doctor(effective malformed in project): FAILS — shadowing cannot rescue the EFFECTIVE entry"
218
+ # restore the H-1 baseline for the cells below
219
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
220
+ printf '{}\n' > "$PROJECT"
221
+
222
+ # ── H-boot: #81 A/B — a bare bin that RESOLVES but does not serve MCP ─────────
223
+ # This is the whole defect in one cell: before #81 the doctor printed ok here, and a host in this
224
+ # exact state ran Pi ACP turns with NO mcp__entwurf-bridge__* tool at all. PATH/HOME/XDG are the
225
+ # sandbox's, so the operator's real launcher is never touched by this A/B.
226
+ write_dead_fake "$SB/bin/entwurf-bridge"
227
+ # `set -e` is on: a doctor that (correctly) exits non-zero here would kill the smoke before it could
228
+ # judge the failure. Fence just the drive, never the assertions — a silently swallowed rc is how a
229
+ # negative cell turns into decoration.
230
+ set +e
231
+ OUT="$(doctor 2>&1)"; RC=$?
232
+ set -e
233
+ want "[QK:PI-DOCTOR-BOOT-NEGATIVE] doctor(bare resolves, does not boot): FAILS instead of printing ok" "[ '$RC' -ne 0 ]"
234
+ want "doctor(bare resolves, does not boot): names the boot failure reason" "printf '%s' \"\$OUT\" | grep -q 'does NOT serve MCP \[exited-before-tools-list\]'"
235
+ want "doctor(bare resolves, does not boot): carries the launcher's own stderr" "printf '%s' \"\$OUT\" | grep -q 'No such file or directory'"
236
+ # Remediation must NOT be a fix-all: entwurf may not clobber a launcher it does not own, so the
237
+ # foreign case has to be named as the operator's repair.
238
+ want "doctor(bare resolves, does not boot): refuses to clobber a foreign launcher" "printf '%s' \"\$OUT\" | grep -q 'will not overwrite'"
239
+ want "doctor(bare resolves, does not boot): still distinguishes 'resolves' from 'boots'" "printf '%s' \"\$OUT\" | grep -q 'RESOLVES — probing'"
240
+ # A bridge that BOOTS and speaks MCP but is not this one — the #81 symptom exactly.
241
+ write_stale_fake "$SB/bin/entwurf-bridge"
242
+ set +e
243
+ OUT="$(doctor 2>&1)"; RC=$?
244
+ set -e
245
+ want "doctor(boots, wrong verb set): FAILS — booting an MCP server is not being THIS bridge" "[ '$RC' -ne 0 ]"
246
+ want "doctor(boots, wrong verb set): names the missing verb (#81 symptom was entwurf_self)" "printf '%s' \"\$OUT\" | grep -q 'missing entwurf_self'"
247
+ want "doctor(boots, wrong verb set): reason is the tool-set mismatch, not a boot failure" "printf '%s' \"\$OUT\" | grep -q 'tool-set-mismatch'"
139
248
 
140
- # H-2: project STALE (user bare, project legacy) → effective=project=legacy note (green-runtime-red guard)
249
+ write_mcp_fake "$SB/bin/entwurf-bridge"
250
+ OUT="$(doctor)"; RC=$?
251
+ want "doctor(after repair): the SAME unchanged doctor goes green once the launcher boots" "[ '$RC' -eq 0 ]"
252
+ want "doctor(after repair): green names the exact verb set as the evidence" "printf '%s' \"\$OUT\" | grep -q 'exact entwurf verb set'"
253
+
254
+ # The review A/B: an unowned/non-bare override shadows the healthy user bridge in production.
255
+ # Ownership remains the operator's, but runtime truth cannot become a green note merely because
256
+ # entwurf will not overwrite it. Keep this QK cell before the healthy legacy override: a mutant
257
+ # that skips all non-bare runtime probes must be attributed to THIS failure, not the later positive.
258
+ write_dead_fake "$SB/bin/dead-override"
141
259
  printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
260
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2],"args":[]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$SB/bin/dead-override"
261
+ set +e
262
+ OUT="$(doctor 2>&1)"; RC=$?
263
+ set -e
264
+ want "[QK:PI-DOCTOR-PROBES-OVERRIDE] doctor(dead unowned override): FAILS runtime independently of ownership" "[ '$RC' -ne 0 ]"
265
+ want "doctor(dead unowned override): names the exact invocation failure" "printf '%s' \"\$OUT\" | grep -q 'effective invocation does NOT serve MCP'"
266
+
267
+ # H-2: project legacy override still gets an independent runtime verdict before its ownership note.
268
+ mkdir -p "$(dirname "$LEGACY_CMD")"
269
+ write_mcp_fake "$LEGACY_CMD"
142
270
  python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
143
271
  OUT="$(doctor)"; RC=$?
144
- want "doctor(project-stale): exits 0 (honest note, not a crash)" "[ '$RC' -eq 0 ]"
145
- want "doctor(project-stale): EFFECTIVE is project (shadows user)" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
146
- want "doctor(project-stale): flags legacy managed path (not adopted)" "printf '%s' \"\$OUT\" | grep -q 'LEGACY managed repo path'"
272
+ want "doctor(project-legacy): exits 0 when the exact override boots" "[ '$RC' -eq 0 ]"
273
+ want "doctor(project-legacy): EFFECTIVE is project (shadows user)" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
274
+ want "doctor(project-legacy): runtime BOOTS before the ownership note" "printf '%s' \"\$OUT\" | grep -q 'configured override BOOTS'"
275
+ want "doctor(project-legacy): flags legacy managed path (not adopted)" "printf '%s' \"\$OUT\" | grep -q 'LEGACY managed repo path'"
147
276
 
148
277
  # H-3: state-owned DRIFT → FAIL (state says managed but effective is foreign)
149
278
  printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/foreign/thing"}}}}\n' > "$GLOBAL"
@@ -70,7 +70,7 @@ fi
70
70
  SESSIONS_BASE="$RGG_AGENT_DIR/sessions"
71
71
  cleanup() { rm -rf "$RGG_AGENT_DIR"; }
72
72
  trap cleanup EXIT
73
- # Default target stays pi-native (openai-codex/gpt-5.4), but honor the SAME target
73
+ # Default target stays pi-native (openai-codex/gpt-5.6-luna), but honor the SAME target
74
74
  # knob as the other live smokes: ENTWURF_LIVE_TARGET="<provider>/<model>". On
75
75
  # acp-on-v2 the ACP `entwurf` provider is back, and `./run.sh smoke-acp-rgg-live`
76
76
  # drives THIS runner against it (deterministic half) — the citizen logic (record +
@@ -83,7 +83,7 @@ if [ -n "${ENTWURF_LIVE_TARGET:-}" ]; then
83
83
  esac
84
84
  else
85
85
  _rgg_provider="${ENTWURF_LIVE_PROVIDER:-openai-codex}"
86
- _rgg_model="${ENTWURF_LIVE_MODEL:-gpt-5.4}"
86
+ _rgg_model="${ENTWURF_LIVE_MODEL:-gpt-5.6-luna}"
87
87
  fi
88
88
  PROVIDER="${SMOKE_RGG_PROVIDER:-$_rgg_provider}"
89
89
  MODEL="${SMOKE_RGG_MODEL:-$_rgg_model}"