@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.
- package/AGENTS.md +9 -5
- package/CHANGELOG.md +149 -0
- package/DELIVERY.md +72 -59
- package/README.md +120 -20
- package/VERIFY.md +10 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +2 -2
- package/docs/external-mcp-host.md +26 -4
- package/docs/setup-clean-host.md +118 -18
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
- package/mcp/entwurf-bridge/src/index.ts +10 -6
- package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
- package/package.json +22 -11
- package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
- package/pi-extensions/entwurf-control.ts +17 -8
- package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
- package/pi-extensions/lib/meta-sender-identity.ts +10 -2
- package/pi-extensions/lib/meta-session.ts +102 -29
- package/pi-extensions/lib/mux-fresh-call.ts +82 -11
- package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
- package/run.sh +1079 -186
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +1174 -0
- package/scripts/check-copilot-launch.ts +395 -0
- package/scripts/check-copilot-receive-arm.ts +734 -0
- package/scripts/check-copilot-statusline.ts +122 -0
- package/scripts/check-entwurf-capabilities.ts +25 -6
- package/scripts/check-fresh-cut-gate.sh +104 -27
- package/scripts/check-gate-qualification.ts +10 -1
- package/scripts/check-install-container.sh +2 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-meta-capability-source.ts +89 -5
- package/scripts/check-meta-doctor-oracle.sh +26 -0
- package/scripts/check-meta-manifest-schema.py +38 -0
- package/scripts/check-meta-receiver-marker.ts +25 -2
- package/scripts/check-meta-session.ts +32 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +242 -0
- package/scripts/copilot-bridge-install.sh +384 -0
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/copilot-launch.sh +236 -0
- package/scripts/copilot-mcp-bridge.sh +177 -0
- package/scripts/copilot-mcp-config.py +221 -0
- package/scripts/copilot-receive-bridge.sh +483 -0
- package/scripts/copilot-statusline-bridge.sh +149 -0
- package/scripts/copilot-statusline-config.py +201 -0
- package/scripts/copilot-statusline.sh +68 -0
- package/scripts/dev-bin.sh +53 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/meta-bridge-hook-log.sh +17 -5
- package/scripts/mutants/capability-cache.json +64 -0
- package/scripts/mutants/copilot-birth.json +244 -0
- package/scripts/mutants/copilot-launch.json +187 -0
- package/scripts/mutants/copilot-receive.json +263 -0
- package/scripts/mutants/fresh-cut.json +17 -0
- package/scripts/mutants/mux-fresh-call.json +216 -2
- package/scripts/mutants/pack-install.json +17 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-async-delivery/README.md +170 -125
- package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
- package/scripts/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- package/scripts/smoke-copilot-mcp-state.sh +153 -0
- package/scripts/smoke-copilot-statusline-state.sh +131 -0
- package/scripts/smoke-mux-fresh-call-live.ts +2 -0
- package/scripts/smoke-mux-lifecycle-live.ts +3 -1
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- 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
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
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
|
-
|
|
388
|
-
if [ -n "$gate_ln" ] && [ -n "$
|
|
389
|
-
ok "D4 setup_all calls the gate before
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
#
|
|
981
|
-
#
|
|
982
|
-
#
|
|
983
|
-
#
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
|
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
|
|
16
|
-
*
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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({
|