@junghanacs/entwurf 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +149 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +120 -20
  5. package/VERIFY.md +10 -7
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +118 -18
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
  17. package/mcp/entwurf-bridge/src/index.ts +10 -6
  18. package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
  19. package/package.json +22 -11
  20. package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
  21. package/pi/entwurf-capabilities.json +1 -0
  22. package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
  23. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
  24. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
  25. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
  26. package/pi-extensions/entwurf-control.ts +17 -8
  27. package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
  28. package/pi-extensions/lib/meta-sender-identity.ts +10 -2
  29. package/pi-extensions/lib/meta-session.ts +102 -29
  30. package/pi-extensions/lib/mux-fresh-call.ts +82 -11
  31. package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
  32. package/run.sh +1079 -186
  33. package/scripts/agy-bridge.sh +4 -2
  34. package/scripts/check-copilot-birth-hook.ts +1174 -0
  35. package/scripts/check-copilot-launch.ts +395 -0
  36. package/scripts/check-copilot-receive-arm.ts +734 -0
  37. package/scripts/check-copilot-statusline.ts +122 -0
  38. package/scripts/check-entwurf-capabilities.ts +25 -6
  39. package/scripts/check-fresh-cut-gate.sh +104 -27
  40. package/scripts/check-gate-qualification.ts +10 -1
  41. package/scripts/check-install-container.sh +2 -2
  42. package/scripts/check-install-surface.ts +1 -1
  43. package/scripts/check-meta-capability-source.ts +89 -5
  44. package/scripts/check-meta-doctor-oracle.sh +26 -0
  45. package/scripts/check-meta-manifest-schema.py +38 -0
  46. package/scripts/check-meta-receiver-marker.ts +25 -2
  47. package/scripts/check-meta-session.ts +32 -0
  48. package/scripts/check-setup-qualification.sh +126 -0
  49. package/scripts/copilot-bridge-doctor.sh +242 -0
  50. package/scripts/copilot-bridge-install.sh +384 -0
  51. package/scripts/copilot-bridge-oracle.sh +223 -0
  52. package/scripts/copilot-bridge-uninstall.sh +137 -0
  53. package/scripts/copilot-launch.sh +236 -0
  54. package/scripts/copilot-mcp-bridge.sh +177 -0
  55. package/scripts/copilot-mcp-config.py +221 -0
  56. package/scripts/copilot-receive-bridge.sh +483 -0
  57. package/scripts/copilot-statusline-bridge.sh +149 -0
  58. package/scripts/copilot-statusline-config.py +201 -0
  59. package/scripts/copilot-statusline.sh +68 -0
  60. package/scripts/dev-bin.sh +53 -9
  61. package/scripts/fake-copilot-vendor.sh +67 -0
  62. package/scripts/meta-bridge-hook-log.sh +17 -5
  63. package/scripts/mutants/capability-cache.json +64 -0
  64. package/scripts/mutants/copilot-birth.json +244 -0
  65. package/scripts/mutants/copilot-launch.json +187 -0
  66. package/scripts/mutants/copilot-receive.json +263 -0
  67. package/scripts/mutants/fresh-cut.json +17 -0
  68. package/scripts/mutants/mux-fresh-call.json +216 -2
  69. package/scripts/mutants/pack-install.json +17 -0
  70. package/scripts/mutants/pi-package-ownership.json +92 -0
  71. package/scripts/mutants/setup-verdict.json +148 -0
  72. package/scripts/mutants/source-install.json +32 -0
  73. package/scripts/pi_settings_io.py +22 -0
  74. package/scripts/raw-async-delivery/README.md +170 -125
  75. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  76. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  77. package/scripts/register-pi-package.py +525 -7
  78. package/scripts/register-pi-provider.py +162 -12
  79. package/scripts/smoke-agy-install-state.sh +86 -30
  80. package/scripts/smoke-agy-statusline-state.sh +9 -6
  81. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  82. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  83. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  84. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  85. package/scripts/smoke-setup-verdict.sh +291 -0
  86. package/scripts/smoke-user-scope-citizen.sh +388 -1
  87. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * check-copilot-statusline — #82 Copilot footer renderer, without Copilot.
3
+ *
4
+ * Subject: scripts/copilot-statusline.sh. Cells: record → exact gid; duplicate
5
+ * nativeSessionId → `!`; no record → `?`; no session_id / empty / malformed
6
+ * → `ready`; every path exit 0.
7
+ * Copilot blanks the slot on nonzero (bundle Fxi). Isolated HOME+store.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import { spawnSync } from "node:child_process";
12
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import * as path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const SCRIPT = path.join(REPO, "scripts", "copilot-statusline.sh");
19
+ const GID_RE = /\d{8}T\d{6}-[0-9a-f]{6}/;
20
+ const GID = "20260821T075400-c0ffee";
21
+ const SID = "4269fad4-d5f5-4281-969f-bbeb211f0d7c";
22
+
23
+ let passed = 0;
24
+ function ok(label: string, cond: boolean): void {
25
+ assert.ok(cond, label);
26
+ console.log(` ok ${label}`);
27
+ passed++;
28
+ }
29
+
30
+ const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-statusline."));
31
+ const home = path.join(root, "home");
32
+ const store = path.join(root, "meta-sessions");
33
+ mkdirSync(home);
34
+ mkdirSync(store);
35
+
36
+ function run(stdin: string, extraEnv: NodeJS.ProcessEnv = {}): { status: number; stdout: string } {
37
+ const result = spawnSync("bash", [SCRIPT], {
38
+ cwd: root,
39
+ env: {
40
+ ...process.env,
41
+ ...extraEnv,
42
+ HOME: home,
43
+ ENTWURF_META_SESSIONS_DIR: store,
44
+ PI_CODING_AGENT_DIR: "",
45
+ },
46
+ encoding: "utf8",
47
+ input: stdin,
48
+ timeout: 10_000,
49
+ });
50
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
51
+ }
52
+
53
+ function env(sessionId: unknown): string {
54
+ return JSON.stringify({ session_id: sessionId });
55
+ }
56
+
57
+ const noRecord = run(env(SID));
58
+ ok("no-record exits 0", noRecord.status === 0);
59
+ ok("no-record is ?", noRecord.stdout === "🪛 ? cop");
60
+ ok("no-record plants no gid", !GID_RE.test(noRecord.stdout));
61
+
62
+ writeFileSync(
63
+ path.join(store, `${GID}.meta.json`),
64
+ `${JSON.stringify({
65
+ schemaVersion: 3,
66
+ gardenId: GID,
67
+ backend: "copilot",
68
+ nativeSessionId: SID,
69
+ })}\n`,
70
+ );
71
+ const withRecord = run(env(SID));
72
+ ok("record-present exits 0", withRecord.status === 0);
73
+ ok("record-present is the exact gid", withRecord.stdout === `🪛 ${GID} cop`);
74
+
75
+ const duplicateGid = "20260821T075401-deadbe";
76
+ writeFileSync(
77
+ path.join(store, `${duplicateGid}.meta.json`),
78
+ `${JSON.stringify({
79
+ schemaVersion: 3,
80
+ gardenId: duplicateGid,
81
+ backend: "copilot",
82
+ nativeSessionId: SID,
83
+ })}\n`,
84
+ );
85
+ const duplicate = run(env(SID));
86
+ ok("duplicate nativeSessionId exits 0", duplicate.status === 0);
87
+ ok("duplicate nativeSessionId is !", duplicate.stdout === "🪛 ! cop");
88
+
89
+ const empty = run("");
90
+ ok("empty stdin exits 0", empty.status === 0);
91
+ ok("empty stdin is ready", empty.stdout === "🪛 ready cop");
92
+ ok("empty stdin plants no gid", !GID_RE.test(empty.stdout));
93
+
94
+ const malformed = run("{this is not json");
95
+ ok("malformed stdin exits 0", malformed.status === 0);
96
+ ok("malformed stdin is ready", malformed.stdout === "🪛 ready cop");
97
+ ok("malformed stdin plants no gid", !GID_RE.test(malformed.stdout));
98
+
99
+ const missing = run("{}");
100
+ ok("missing session_id exits 0", missing.status === 0);
101
+ ok("missing session_id is ready", missing.stdout === "🪛 ready cop");
102
+
103
+ const nullId = run(env(null));
104
+ ok("null session_id exits 0", nullId.status === 0);
105
+ ok("null session_id is ready — no leftover lookup", nullId.stdout === "🪛 ready cop");
106
+ ok("null session_id plants no gid", !GID_RE.test(nullId.stdout));
107
+
108
+ const catBin = path.join(root, "cat-bin");
109
+ mkdirSync(catBin);
110
+ writeFileSync(path.join(catBin, "cat"), "#!/bin/sh\nexit 73\n", { mode: 0o755 });
111
+ const catFailure = run(env(SID), { PATH: `${catBin}${path.delimiter}${process.env.PATH ?? ""}` });
112
+ ok("stdin read failure exits 0", catFailure.status === 0);
113
+ ok("stdin read failure is ready, not blank", catFailure.stdout === "🪛 ready cop");
114
+
115
+ const pythonBin = path.join(root, "python-bin");
116
+ mkdirSync(pythonBin);
117
+ writeFileSync(path.join(pythonBin, "python3"), "#!/bin/sh\nprintf partial\nexit 12\n", { mode: 0o755 });
118
+ const pythonFailure = run(env(SID), { PATH: `${pythonBin}${path.delimiter}${process.env.PATH ?? ""}` });
119
+ ok("partial interpreter failure exits 0", pythonFailure.status === 0);
120
+ ok("partial interpreter failure is ?, not partial", pythonFailure.stdout === "🪛 ? cop");
121
+
122
+ console.log(`\ncheck-copilot-statusline: ${passed} assertions ok`);
@@ -21,7 +21,9 @@ import assert from "node:assert/strict";
21
21
  import fs from "node:fs";
22
22
  import {
23
23
  META_BACKEND_DESCRIPTORS,
24
+ META_BACKENDS,
24
25
  META_CITIZEN_BACKENDS,
26
+ type MetaBackend,
25
27
  MetaRecordError,
26
28
  metaCapabilitiesFilePath,
27
29
  parseMetaCapabilityRegistry,
@@ -50,12 +52,29 @@ ok(
50
52
  );
51
53
  ok("shipped registry includes the pi backend", reg.backends.pi !== undefined);
52
54
 
53
- // 2. drift guard — the JSON must agree field-for-field with the const for the
54
- // three backends it still describes. 3D-3 already cut the live consumer over
55
- // to the registry, so this is no longer a pre-cut-over guard: the const now
56
- // exists ONLY as this reference, and the assertion is what keeps the live
57
- // source from silently diverging from the shape it was cut over from.
58
- for (const backend of ["claude-code", "antigravity", "codex"] as const) {
55
+ // 2. drift guard — the JSON must agree field-for-field with the const for every
56
+ // backend the const describes. 3D-3 already cut the live consumer over to the
57
+ // registry, so this is no longer a pre-cut-over guard: the const now exists
58
+ // ONLY as this reference, and the assertion is what keeps the live source from
59
+ // silently diverging from the shape it was cut over from.
60
+ //
61
+ // IT MUST ITERATE `META_BACKENDS`, NEVER A LITERAL (#82, 2026-08-20). This loop
62
+ // used to spell out `["claude-code", "antigravity", "codex"]`, so a backend added
63
+ // to the const AND to the shipped JSON was compared against nothing: the coverage
64
+ // cell above passed (the keysets still matched) and the drift cell never looked at
65
+ // it. A green gate that does not read the row it is supposed to guard is worse than
66
+ // an absent one. Driving it from the const is what makes the next backend
67
+ // automatically in scope.
68
+ const driftScope: readonly MetaBackend[] = META_BACKENDS;
69
+ ok(
70
+ "[QK:CAPABILITY-DRIFT-SCOPE-FROM-CONST] drift scope = every shipped backend except pi (pi has no live const to compare)",
71
+ [...driftScope].sort().join(",") ===
72
+ Object.keys(reg.backends)
73
+ .filter((b) => b !== "pi")
74
+ .sort()
75
+ .join(","),
76
+ );
77
+ for (const backend of driftScope) {
59
78
  const live = META_BACKEND_DESCRIPTORS[backend];
60
79
  const cap = reg.backends[backend];
61
80
  ok(
@@ -47,6 +47,16 @@ case "$REPO" in
47
47
  ;;
48
48
  esac
49
49
 
50
+ # This gate reaches run.sh paths that import `scripts/pi_settings_io.py`, and CPython
51
+ # writes `scripts/__pycache__/*.pyc` next to the source it imports. In a normal clone
52
+ # that file already exists and nothing looks different; inside the gate-qualification
53
+ # SNAPSHOT — which copies TRACKED files only — it is a brand new file, and the harness
54
+ # hashes the whole tree, so the run ends IMPURE with every mutant killed. Measured
55
+ # 2026-08-20: a tracked-only copy gained exactly `scripts/__pycache__/pi_settings_io.
56
+ # cpython-313.pyc` after this gate ran. `smoke-pi-provider-state.sh` and
57
+ # `smoke-meta-install-state.sh` already carry this line for the same reason.
58
+ export PYTHONDONTWRITEBYTECODE=1
59
+
50
60
  SANDBOX="$(mktemp -d -t entwurf-fresh-cut-gate.XXXXXX)"
51
61
  trap 'rm -rf "$SANDBOX"' EXIT
52
62
  export HOME="$SANDBOX/home"
@@ -384,11 +394,19 @@ esac
384
394
  # Static order: the gate precedes the first writer in each entrypoint, and the
385
395
  # preflight never runs the cut on the operator's behalf.
386
396
  gate_ln=$(grep -n 'preflight_v3_store setup' "$REPO/run.sh" | head -1 | cut -d: -f1) || true
387
- auth_ln=$(awk '/^setup_all\(\)/,/^}/{ if ($0 ~ /^ sync_auth$/) { print NR; exit } }' "$REPO/run.sh")
388
- if [ -n "$gate_ln" ] && [ -n "$auth_ln" ] && [ "$gate_ln" -lt "$auth_ln" ]; then
389
- ok "D4 setup_all calls the gate before sync_auth (line $gate_ln < $auth_ln)"
397
+ boot_ln=$(awk '/^setup_all\(\)/,/^}/{ if ($0 ~ /pnpm install --frozen-lockfile/) { print NR; exit } }' "$REPO/run.sh")
398
+ if [ -n "$gate_ln" ] && [ -n "$boot_ln" ] && [ "$gate_ln" -lt "$boot_ln" ]; then
399
+ ok "D4 setup_all calls the gate before the source bootstrap (line $gate_ln < $boot_ln)"
400
+ else
401
+ bad "D4 setup_all's gate is missing or sits after the source bootstrap (gate=$gate_ln bootstrap=$boot_ln)"
402
+ fi
403
+ # #86 A5 credential tripwire: the retired sync_auth mutation (OAuth alias copy +
404
+ # auth.json.bak) must never resurrect as an invocable surface in run.sh. Comments
405
+ # documenting the removal are allowed; a function definition or call is not.
406
+ if awk '!/^[[:space:]]*#/' "$REPO/run.sh" | grep -q 'sync_auth'; then
407
+ bad "D4b the retired sync_auth credential mutation reappeared as code in run.sh"
390
408
  else
391
- bad "D4 setup_all's gate is missing or sits after sync_auth (gate=$gate_ln sync_auth=$auth_ln)"
409
+ ok "D4b no sync_auth credential surface remains in run.sh (comments only)"
392
410
  fi
393
411
 
394
412
  ilp_gate=$(awk '/^install_local_package\(\)/,/^}/{ if ($0 ~ /preflight_v3_store install/) { print NR; exit } }' "$REPO/run.sh")
@@ -973,34 +991,93 @@ fi
973
991
 
974
992
  # ── G. an archive collision is a NO-OP, never a half-cut ─────────────────────
975
993
  echo "[check-fresh-cut-gate] G. archive-destination preflight"
976
- reset_world prevgen
977
- mkdir -p "$ENTWURF_META_MAILBOX_DIR/20260305T000000-dddd05"
978
- printf 'hello\n' > "$ENTWURF_META_MAILBOX_DIR/20260305T000000-dddd05/0001.msg"
979
- store_before="$(store_bytes)"
980
- # Occupy the MAILBOX destination — the second move in the plan — for this second
981
- # and the next two, so whichever stamp the cut computes, its first move looks free
982
- # and its second collides. That ordering is the whole point: checking each
983
- # destination just before its own rename archived the store, then refused.
984
- for off in 0 1 2; do
985
- mkdir -p "$SANDBOX/mailbox.archive-$(date -d "+$off seconds" +%Y%m%dT%H%M%S)"
994
+ # Occupy the MAILBOX destination — the second move in the plan — so whichever
995
+ # stamp the cut computes, its first move looks free and its second collides.
996
+ # That ordering is the whole point: checking each destination just before its
997
+ # own rename archived the store, then refused.
998
+ #
999
+ # WHY THE BAND IS WIDE. `stamp()` takes no injection point, so the only way to
1000
+ # occupy the destination the cut will pick is to occupy every second it could
1001
+ # pick. The band used to be three seconds, which held on a dev box and lost on a
1002
+ # loaded runner: on 2026-08-20 (CI run 32370770123) node reached `stamp()` after
1003
+ # the band had passed, no collision happened, the cut correctly succeeded — and
1004
+ # G1/G2/G3 all reported a product defect that did not exist. A gate that can go
1005
+ # red with no product change is worse than the hole it guards, so the band is now
1006
+ # far longer than any plausible interpreter start, and a miss is reported AS a
1007
+ # miss rather than as a refusal that never happened.
1008
+ #
1009
+ # THE BAND MUST BE CONTIGUOUS, which is why every stamp comes off ONE base epoch.
1010
+ # Reading the clock again inside the loop looks equivalent and is not: iteration k
1011
+ # would occupy `now(k) + k`, so a second boundary crossed mid-loop skips a value and
1012
+ # punches a HOLE in the band. A cut landing in that hole finds its destination free,
1013
+ # and the hole sits INSIDE the range, so the miss test below would not catch it either.
1014
+ #
1015
+ # AND WHY A MISS RETRIES ONCE. A wider band is still a race, and "60s is enough"
1016
+ # is not a claim this file can prove. What it CAN do is make the race cost
1017
+ # nothing: stage the band, and if the cut lands past it, rebuild the band and run
1018
+ # the cut again. Two independent misses is the square of one, and the retry costs
1019
+ # a second on the happy path because it never fires there. This matters most
1020
+ # where the cell is cheapest to overlook — `check-gate-qualification` runs this
1021
+ # gate as both control and mutant pass, so a single miss there does not cost one
1022
+ # red cell, it costs the whole 22-minute inventory and re-runs it.
1023
+ G_BAND_SECONDS=60
1024
+ g_miss=0
1025
+ for g_attempt in 1 2; do
1026
+ reset_world prevgen
1027
+ mkdir -p "$ENTWURF_META_MAILBOX_DIR/20260305T000000-dddd05"
1028
+ printf 'hello\n' > "$ENTWURF_META_MAILBOX_DIR/20260305T000000-dddd05/0001.msg"
1029
+ store_before="$(store_bytes)"
1030
+ g_band_base="$(date +%s)"
1031
+ for off in $(seq 0 "$G_BAND_SECONDS"); do
1032
+ mkdir -p "$SANDBOX/mailbox.archive-$(date -d "@$((g_band_base + off))" +%Y%m%dT%H%M%S)"
1033
+ done
1034
+ g_band_end="$(date -d "@$((g_band_base + G_BAND_SECONDS))" +%Y%m%dT%H%M%S)"
1035
+ set +e
1036
+ out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcg=$?
1037
+ set -e
1038
+ g_cut_stamp="$(printf '%s\n' "$out" | sed -n 's|.*/mailbox\.archive-\([0-9T]*\).*|\1|p' | head -1)"
1039
+ g_miss=0
1040
+ [ "$rcg" != 1 ] && [ -n "$g_cut_stamp" ] && [ "$g_cut_stamp" \> "$g_band_end" ] && g_miss=1
1041
+ [ "$g_miss" = 0 ] && break
1042
+ echo " .. G fixture missed its band (cut stamped $g_cut_stamp > $g_band_end) — restaging once"
986
1043
  done
987
- set +e
988
- out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcg=$?
989
- set -e
990
- if [ "$rcg" = 1 ]; then
991
- ok "G1 an occupied archive destination REFUSES the cut with the NO-MOVE status (exit 1)"
1044
+ if [ "$rcg" = 1 ] && [ -n "$(printf '%s\n' "$out" | sed -n 's|^ARCHIVE EXISTS: .*/\(mailbox\.archive-[0-9T]*\)$|\1|p')" ]; then
1045
+ ok "G1 an occupied archive destination REFUSES the cut with the NO-MOVE status (exit 1), naming the MAILBOX destination — the second move in the plan"
1046
+ elif [ "$rcg" = 1 ]; then
1047
+ # Refusing for some other reason is not this cell's contract. Without this arm any
1048
+ # unrelated no-move regression that prints the generic "Nothing was moved" line would
1049
+ # keep G1-G3 green while the second-move collision went unproven.
1050
+ bad "G1 the cut refused with NO-MOVE but never named a mailbox.archive-* destination — this is not the second-move collision G claims to prove" "$out"
1051
+ elif [ "$g_miss" = 1 ]; then
1052
+ bad "G1 FIXTURE MISS, not a product verdict: the cut stamped $g_cut_stamp, past the occupied band ending $g_band_end, so no collision was ever staged. Widen G_BAND_SECONDS; do not read this as the cut ignoring a collision" "$out"
992
1053
  else
993
- bad "G1 the cut proceeded into an occupied destination, or refused with the wrong status (rc=$rcg, want 1)" "$out"
1054
+ bad "G1 the cut proceeded into an occupied destination, or refused with the wrong status (rc=$rcg, want 1) [QK:FRESH-CUT-COLLISION-NO-MOVE]" "$out"
994
1055
  fi
995
- if [ -z "$(find "$SANDBOX" -maxdepth 1 -type d -name 'store.archive-*')" ] && [ "$store_before" = "$(store_bytes)" ]; then
996
- ok "G2 the collision refusal moved NOTHING the store never left its place (no half-cut generation)"
1056
+ # G2/G3 only mean something once a collision was actually staged. On a fixture
1057
+ # miss the cut was SUPPOSED to archive and SUPPOSED not to print the no-op line,
1058
+ # so scoring them would turn one missed fixture into three red cells that read
1059
+ # like a product defect — the exact cascade that sent a reader hunting a bug in
1060
+ # the cut on 2026-08-20.
1061
+ if [ "$g_miss" = 1 ]; then
1062
+ bad "G2/G3 not scored — the G fixture missed its band (see G1); this says nothing about the cut"
997
1063
  else
998
- bad "G2 half-cut generation: the store was archived before the mailbox collision was seen"
1064
+ # NOTE for whoever audits kill reasons here: the mutant carried by
1065
+ # `scripts/mutants/fresh-cut.json` plants "preflight only the FIRST destination",
1066
+ # which lets the cut succeed outright and dies at G1. The ORIGINAL defect this
1067
+ # section was written for — checking each destination just before its own rename —
1068
+ # produces exit 1, an `ARCHIVE EXISTS` line and the no-op sentence, so it passes
1069
+ # G1 and G3 and dies HERE, at G2, which carries no [QK:] label of its own. If that
1070
+ # path ever needs qualifying, this is the cell to label.
1071
+ if [ -z "$(find "$SANDBOX" -maxdepth 1 -type d -name 'store.archive-*')" ] && [ "$store_before" = "$(store_bytes)" ]; then
1072
+ ok "G2 the collision refusal moved NOTHING — the store never left its place (no half-cut generation)"
1073
+ else
1074
+ bad "G2 half-cut generation: the store was archived before the mailbox collision was seen"
1075
+ fi
1076
+ case "$out" in
1077
+ *"Nothing was moved"*) ok "G3 the refusal states its own no-op guarantee" ;;
1078
+ *) bad "G3 the refusal never claimed the no-op" "$out" ;;
1079
+ esac
999
1080
  fi
1000
- case "$out" in
1001
- *"Nothing was moved"*) ok "G3 the refusal states its own no-op guarantee" ;;
1002
- *) bad "G3 the refusal never claimed the no-op" "$out" ;;
1003
- esac
1004
1081
 
1005
1082
  # ── H. the MARKER-LESS live surface: native-push conversations ───────────────
1006
1083
  # A socket+marker walk is not the whole world. `entwurf_register_native` proves an
@@ -806,19 +806,28 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
806
806
  "agy-permission": 6,
807
807
  "bridge-boot-resume": 3,
808
808
  "bridge-command-boot": 9,
809
+ "capability-cache": 3,
810
+ "copilot-birth": 19,
811
+ "copilot-launch": 14,
812
+ "copilot-receive": 18,
813
+ "fresh-cut": 1,
809
814
  "meta-facts": 4,
810
815
  "meta-identity": 4,
811
816
  "meta-retire": 3,
812
817
  "mux-boundary": 14,
813
- "mux-fresh-call": 19,
818
+ "mux-fresh-call": 37,
814
819
  "mux-launcher-fence": 7,
815
820
  "mux-parent-artifact": 3,
821
+ "pack-install": 1,
822
+ "pi-package-ownership": 6,
816
823
  "mux-resume-call": 12,
817
824
  "probe-ordering": 1,
818
825
  "release-gate": 12,
819
826
  "resume-args": 6,
820
827
  "resume-launch-identity": 6,
821
828
  "self-address": 3,
829
+ "setup-verdict": 10,
830
+ "source-install": 2,
822
831
  "v2-surface": 7,
823
832
  "v2-visible-resume": 17,
824
833
  };
@@ -225,9 +225,9 @@ ok "non-root 'npm install -g' succeeded into the writable isolated prefix $npm_c
225
225
  PKG="$npm_config_prefix/lib/node_modules/@junghanacs/entwurf"
226
226
  [ -d "$PKG" ] || die "installed package not at $PKG"
227
227
 
228
- # ── 3. the five bins, resolved THROUGH the global PATH shim ──────────────────
228
+ # ── 3. the six bins, resolved THROUGH the global PATH shim ───────────────────
229
229
  echo "[global bins via PATH shim]"
230
- for b in entwurf entwurf-bridge entwurf-statusline entwurf-agy-statusline entwurf-agy-imprint; do
230
+ for b in entwurf entwurf-bridge entwurf-statusline entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline; do
231
231
  RESOLVED="$(command -v "$b" || true)"
232
232
  if [ -z "$RESOLVED" ]; then bad "bin '$b' is not on PATH after a global install"
233
233
  elif [ "$RESOLVED" != "$npm_config_prefix/bin/$b" ]; then bad "bin '$b' resolved to $RESOLVED, not the global shim $npm_config_prefix/bin/$b"
@@ -236,7 +236,7 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
236
236
  // themselves: a mutation whose target is a literal $HOME/~ config path is operating on
237
237
  // the operator's own bridge, whatever the rest of the file does. Reads ($HOME/.current-device,
238
238
  // probing whether a real bundle exists) stay legal — they cannot uninstall anything.
239
- const LIVE_ROOTS = String.raw`(?:\$HOME|\$\{HOME\}|~)/\.(?:claude|gemini|pi|config/pi|local/share/(?:claude|entwurf))`;
239
+ const LIVE_ROOTS = String.raw`(?:\$HOME|\$\{HOME\}|~)/\.(?:claude|copilot|gemini|pi|config/pi|local/share/(?:claude|entwurf))`;
240
240
  const MUTATORS = String.raw`rm\s+-\w+|rm|mv|cp|install|mkdir\s+-p|mkdir|touch|tee|ln\s+-s\w*|truncate`;
241
241
 
242
242
  // A mutating run.sh drive is shell code, not prose. `bad "… re-run ./run.sh install-meta-bridge"`
@@ -12,14 +12,20 @@
12
12
  * Proves:
13
13
  * - the lookup seam is registry-DRIVEN — fed a doctored registry,
14
14
  * metaCapabilityFor follows it (so the value is read from the registry, not
15
- * hardcoded off the const); the default still reads the shipped file and
16
- * memoizes.
15
+ * hardcoded off the const); the default still reads the shipped file.
16
+ * - the default load is not stale-able: in a temp package closure it follows an
17
+ * atomic on-disk replacement within one process, while a missing or corrupt
18
+ * registry still throws.
17
19
  * - behaviour preserved: registry ≡ META_BACKEND_DESCRIPTORS for the 3 native
18
20
  * backends (the const survives only as the drift-guard reference; the
19
21
  * registry ≡ const drift guard proper is check-entwurf-capabilities).
20
22
  */
21
23
 
22
24
  import assert from "node:assert/strict";
25
+ import { copyFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import path from "node:path";
28
+ import { pathToFileURL } from "node:url";
23
29
  import {
24
30
  loadMetaCapabilityRegistry,
25
31
  META_BACKEND_DESCRIPTORS,
@@ -28,6 +34,9 @@ import {
28
34
  metaCapabilityFor,
29
35
  } from "../pi-extensions/lib/meta-session.ts";
30
36
 
37
+ /** Repo root, from this gate's own location (`scripts/` -> `..`). */
38
+ const REPO = path.join(import.meta.dirname, "..");
39
+
31
40
  let passed = 0;
32
41
  function ok(label: string, cond: boolean): void {
33
42
  assert.ok(cond, label);
@@ -57,9 +66,84 @@ ok(
57
66
  "seam default reads the shipped registry, not the doctored copy",
58
67
  metaCapabilityFor("claude-code").wakeMode === shipped.backends["claude-code"].wakeMode,
59
68
  );
60
- const load1 = loadMetaCapabilityRegistry();
61
- const load2 = loadMetaCapabilityRegistry();
62
- ok("loadMetaCapabilityRegistry memoizes (same object on repeat load)", load1 === load2);
69
+
70
+ // --- the default load is not stale-able (#82 RAIL 7) -----------------------
71
+ // This block replaces an assertion that the loader MEMOIZES. That memo was a real
72
+ // defect, not a design: the entwurf-bridge MCP child outlives many installs, so a
73
+ // grade or wakeMode moved on disk stayed invisible to every already-running
74
+ // dispatcher, with no symptom but a citizen answering out of last week's registry.
75
+ //
76
+ // The oracle has to be independent of the production module's own idea of freshness,
77
+ // so it does not poke at the shipped file — mutating the real checkout registry would
78
+ // make this gate a host-mutating test and could leave the tree dirty on a failure.
79
+ // Instead it builds a TEMP PACKAGE CLOSURE: the module resolves its registry by
80
+ // arithmetic on `import.meta.dirname` (see metaCapabilitiesFilePath), so a copy of
81
+ // meta-session.ts placed at `<tmp>/pi-extensions/lib/` reads `<tmp>/pi/…json` and
82
+ // nothing else. Two files are enough — meta-session imports only node builtins and
83
+ // its sibling session-id.js.
84
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "entwurf-capability-source."));
85
+ try {
86
+ const libDir = path.join(tmpRoot, "pi-extensions", "lib");
87
+ const regDir = path.join(tmpRoot, "pi");
88
+ mkdirSync(libDir, { recursive: true });
89
+ mkdirSync(regDir, { recursive: true });
90
+ const srcLib = path.join(REPO, "pi-extensions", "lib");
91
+ copyFileSync(path.join(srcLib, "meta-session.ts"), path.join(libDir, "meta-session.ts"));
92
+ copyFileSync(path.join(srcLib, "session-id.js"), path.join(libDir, "session-id.js"));
93
+
94
+ const registryPath = path.join(regDir, "entwurf-capabilities.json");
95
+ const withGrade = (level: string): string =>
96
+ JSON.stringify({
97
+ schemaVersion: shipped.schemaVersion,
98
+ backends: Object.fromEntries(
99
+ Object.entries(shipped.backends).map(([b, cap]) => [
100
+ b,
101
+ b === "copilot" ? { ...cap, deliveryLevel: level } : cap,
102
+ ]),
103
+ ),
104
+ });
105
+
106
+ writeFileSync(registryPath, withGrade("D0"));
107
+ const mod = await import(pathToFileURL(path.join(libDir, "meta-session.ts")).href);
108
+ const before = mod.metaCapabilityFor("copilot").deliveryLevel;
109
+
110
+ // Replace the way an installer does — write a sibling and rename over the target —
111
+ // so the old inode is gone rather than truncated. A stat-keyed cache can survive
112
+ // this; only actually reading the file cannot.
113
+ const swap = `${registryPath}.next`;
114
+ writeFileSync(swap, withGrade("D6"));
115
+ renameSync(swap, registryPath);
116
+ const after = mod.metaCapabilityFor("copilot").deliveryLevel;
117
+
118
+ ok(
119
+ `[QK:CAPABILITY-REGISTRY-REREAD-SEES-REPLACEMENT] the default load follows an atomic on-disk replacement in the SAME process (${before} -> ${after})`,
120
+ before === "D0" && after === "D6",
121
+ );
122
+
123
+ // Losing the registry must stay loud. A loader that "kept the last good copy" to
124
+ // survive a replacement would pass the cell above and hide this one.
125
+ rmSync(registryPath);
126
+ let threwMissing = false;
127
+ try {
128
+ mod.loadMetaCapabilityRegistry();
129
+ } catch {
130
+ threwMissing = true;
131
+ }
132
+ ok("[QK:CAPABILITY-REGISTRY-MISSING-STILL-THROWS] a missing registry still throws on the default load", threwMissing);
133
+
134
+ writeFileSync(registryPath, "{ not json");
135
+ let threwCorrupt = false;
136
+ try {
137
+ mod.loadMetaCapabilityRegistry();
138
+ } catch {
139
+ threwCorrupt = true;
140
+ }
141
+ ok("[QK:CAPABILITY-REGISTRY-CORRUPT-STILL-THROWS] a corrupt registry still throws on the default load", threwCorrupt);
142
+ } finally {
143
+ // Reap unconditionally: a gate that leaves fixtures behind on failure is how a
144
+ // checkout accumulates temp roots nobody owns.
145
+ rmSync(tmpRoot, { recursive: true, force: true });
146
+ }
63
147
 
64
148
  // --- cut-over preserves behaviour: registry ≡ const for the 3 backends ------
65
149
  for (const backend of META_BACKENDS) {
@@ -500,6 +500,32 @@ mv "$AGENT/meta-bridge-hook.log" "$TMP/hook-log.bak"
500
500
  expect_red "hook log absent" "NOT CERTIFIED — no hook log"
501
501
  mv "$TMP/hook-log.bak" "$AGENT/meta-bridge-hook.log"
502
502
 
503
+ # M9b — a COPILOT line in the shared hook log must not move this doctor AT ALL (#82).
504
+ # The Copilot birth unit appends to the same file, tagged `[copilot]`, and its ERROR
505
+ # lines are frequently CORRECT fail-closed refusals. Worse, their recovery token cannot
506
+ # exist here: `armed watch` is written by CLAUDE's arm. Copilot does have a doorbell
507
+ # since RAIL 5, but a different process arms it (the forked extension) into a different
508
+ # log (`meta-bridge-receive-copilot.log`), so this file still never carries a Copilot
509
+ # recovery line. An untagged grep would redden the Claude doctor forever, for a refusal
510
+ # on another rail, and prescribe a Claude wake failure that never happened. This case is the kill-proof
511
+ # for that separation — it is an expect-GREEN, since the defect it guards makes a
512
+ # HEALTHY host go red.
513
+ # The [QK:] token lives on the `bad` line ONLY: the qualifier requires it exactly
514
+ # once in this file, and its classifier does not count a token printed on an `ok`
515
+ # line — a later assertion must never be able to self-certify a red run.
516
+ cp "$AGENT/meta-bridge-hook.log" "$TMP/hook-log-precopilot.bak"
517
+ {
518
+ echo "2026-08-21T00:00:02.000Z ERROR [copilot] degraded envelope: cwd missing or not a non-empty string (keys=sessionId)"
519
+ echo "2026-08-21T00:00:03.000Z INFO [copilot] create record 20260821T000003-aaaaaa.meta.json (event=native(source=new), shape=copilot-native, native=cop-1)"
520
+ } >> "$AGENT/meta-bridge-hook.log"
521
+ run_doctor
522
+ if [ "$DOC_RC" -eq 0 ]; then
523
+ ok "a copilot ERROR in the shared hook log leaves the CLAUDE doctor green (rails stay separate)"
524
+ else
525
+ bad "[QK:HOOK-LOG-RAIL-SCOPED] a copilot ERROR in the shared hook log turned the CLAUDE doctor red — the hook-log judgement is not rail-scoped:"$'\n'"$(printf '%s\n' "$DOC_OUT" | grep -E '^ (FAIL|WARN)' | sed 's/^/ /')"
526
+ fi
527
+ cp "$TMP/hook-log-precopilot.bak" "$AGENT/meta-bridge-hook.log"
528
+
503
529
  # M10 — deployed writer bundle missing: staleness becomes unknowable.
504
530
  mv "$PLANTED/lib/meta-session.ts" "$TMP/meta-session.bak"
505
531
  expect_red "installed writer bundle missing" "deployed writer version is UNKNOWN"
@@ -138,6 +138,44 @@ if hooks is not None:
138
138
  f"want command={expected_owner_command!r} args={expected_owner_args!r}"
139
139
  )
140
140
 
141
+ # --- the Copilot unit's manifests (#82) --------------------------------------
142
+ # The Copilot BIRTH unit is a SECOND marketplace root, not a second plugin inside the
143
+ # Claude one: the Claude installer assembles one root and copies exactly one plugin out
144
+ # of it, so a shared marketplace.json would publish a `source` the assembly does not
145
+ # contain and `claude plugin validate` would run over it. Its hook FORM is pinned by
146
+ # check-copilot-birth-hook (which drives the real assembler); what is pinned HERE is the
147
+ # same closed-schema keyset lesson that produced this gate — a decorative key in a
148
+ # marketplace/plugin manifest is a future rejection on some other host's CLI version.
149
+ MBC = REPO / "pi" / "meta-bridge-copilot"
150
+ PLUGIN_C = "entwurf-meta-receive-copilot"
151
+
152
+ mkt_c = load(MBC / ".claude-plugin" / "marketplace.json")
153
+ if mkt_c is not None:
154
+ subset("copilot marketplace root", mkt_c, {"name", "owner", "plugins"})
155
+ subset("copilot marketplace.owner", mkt_c.get("owner", {}), {"name"})
156
+ plugins_c = mkt_c.get("plugins")
157
+ if not isinstance(plugins_c, list) or len(plugins_c) != 1:
158
+ bad("copilot marketplace.plugins must hold exactly the one birth plugin")
159
+ else:
160
+ subset("copilot marketplace.plugins[0]", plugins_c[0], {"name", "source", "description"})
161
+ if plugins_c[0].get("name") != PLUGIN_C or plugins_c[0].get("source") != f"./{PLUGIN_C}":
162
+ bad(f"copilot marketplace.plugins[0] must name/source {PLUGIN_C}; got "
163
+ f"{plugins_c[0].get('name')!r}/{plugins_c[0].get('source')!r}")
164
+ else:
165
+ ok("copilot marketplace.plugins[0] names the shipped unit directory")
166
+
167
+ plug_c = load(MBC / PLUGIN_C / ".claude-plugin" / "plugin.json")
168
+ if plug_c is not None:
169
+ subset("copilot plugin.json", plug_c, {"name", "version", "description"})
170
+
171
+ # The two roots must stay DISJOINT. One marketplace name serving both would make an
172
+ # install of either one silently replace the other's registration.
173
+ if mkt is not None and mkt_c is not None:
174
+ if mkt.get("name") == mkt_c.get("name"):
175
+ bad(f"both marketplaces are named {mkt.get('name')!r} — installing one would replace the other")
176
+ else:
177
+ ok("the Claude and Copilot marketplaces carry distinct names")
178
+
141
179
  # --- desired_mcp() installed-vs-clone dual-mode ------------------------------
142
180
  def desired_mcp(repo: str):
143
181
  out = subprocess.run(
@@ -163,10 +163,33 @@ ok(
163
163
 
164
164
  // ── armProvenance constraint ────────────────────────────────────────────────
165
165
  ok(
166
- "arm provenances are exactly the arm-capable events",
166
+ "arm provenances are exactly the arm-capable events, across every backend that arms one",
167
167
  JSON.stringify([...META_RECEIVER_ARM_PROVENANCES].sort()) ===
168
- JSON.stringify(["cwd-changed", "file-changed", "session-start"]),
168
+ JSON.stringify(["cwd-changed", "extension-join", "file-changed", "session-start"]),
169
169
  );
170
+ // The Copilot arm is not a hook event, and the marker schema carries that difference
171
+ // rather than flattening it: a different provenance AND a different ownerKind, because
172
+ // the process that holds Copilot's watch is the forked extension, not the CLI. A reader
173
+ // that sees `copilot-extension` is looking at a watcher it can verify by pid; one that
174
+ // saw `claude-code-cli` there would be verifying the wrong process (#82 RAIL 5).
175
+ {
176
+ const copilotGarden = "20260823T120000-dddddd";
177
+ writeMetaReceiverMarker({
178
+ gardenId: copilotGarden,
179
+ backend: "copilot",
180
+ nativeSessionId: "cop-native-1",
181
+ ownerPid: process.pid,
182
+ ownerKind: "copilot-extension",
183
+ armProvenance: "extension-join",
184
+ receiversDir: DIR,
185
+ });
186
+ const cop = readMetaReceiverMarker({ gardenId: copilotGarden, receiversDir: DIR });
187
+ ok(
188
+ "a Copilot extension arm round-trips with its own provenance and owner kind",
189
+ cop?.backend === "copilot" && cop?.armProvenance === "extension-join" && cop?.ownerKind === "copilot-extension",
190
+ );
191
+ ok("and it is subject to the SAME liveness rule as every other marker", cop?.ownerPid === process.pid);
192
+ }
170
193
  let upsRejected = false;
171
194
  try {
172
195
  writeMetaReceiverMarker({