@junghanacs/entwurf 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +79 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +55 -4
  5. package/VERIFY.md +2 -2
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +3 -3
  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 +334 -16
  33. package/scripts/check-copilot-birth-hook.ts +497 -0
  34. package/scripts/check-copilot-launch.ts +395 -0
  35. package/scripts/check-copilot-receive-arm.ts +734 -0
  36. package/scripts/check-copilot-statusline.ts +122 -0
  37. package/scripts/check-entwurf-capabilities.ts +25 -6
  38. package/scripts/check-fresh-cut-gate.sh +92 -23
  39. package/scripts/check-gate-qualification.ts +7 -1
  40. package/scripts/check-install-container.sh +2 -2
  41. package/scripts/check-install-surface.ts +1 -1
  42. package/scripts/check-meta-capability-source.ts +89 -5
  43. package/scripts/check-meta-doctor-oracle.sh +26 -0
  44. package/scripts/check-meta-manifest-schema.py +38 -0
  45. package/scripts/check-meta-receiver-marker.ts +25 -2
  46. package/scripts/check-meta-session.ts +32 -0
  47. package/scripts/copilot-bridge-doctor.sh +209 -0
  48. package/scripts/copilot-bridge-install.sh +224 -0
  49. package/scripts/copilot-launch.sh +236 -0
  50. package/scripts/copilot-mcp-bridge.sh +177 -0
  51. package/scripts/copilot-mcp-config.py +221 -0
  52. package/scripts/copilot-receive-bridge.sh +483 -0
  53. package/scripts/copilot-statusline-bridge.sh +149 -0
  54. package/scripts/copilot-statusline-config.py +201 -0
  55. package/scripts/copilot-statusline.sh +68 -0
  56. package/scripts/dev-bin.sh +4 -1
  57. package/scripts/meta-bridge-hook-log.sh +17 -5
  58. package/scripts/mutants/capability-cache.json +64 -0
  59. package/scripts/mutants/copilot-birth.json +155 -0
  60. package/scripts/mutants/copilot-launch.json +187 -0
  61. package/scripts/mutants/copilot-receive.json +263 -0
  62. package/scripts/mutants/fresh-cut.json +17 -0
  63. package/scripts/mutants/mux-fresh-call.json +216 -2
  64. package/scripts/mutants/pack-install.json +17 -0
  65. package/scripts/raw-async-delivery/README.md +170 -125
  66. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  67. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  68. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  69. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  70. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  71. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  72. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Deliver an async message INTO one already-running, IDLE Copilot CLI session.
3
+ #
4
+ # Addressed by sessionId: only the named session's receiver sees the poke, so
5
+ # siblings stay idle. Free — it is a file write plus a continuation of a session
6
+ # that is already running; no `copilot -p` spawn is involved.
7
+ #
8
+ # ./copilot-enqueue-addressed.sh <session_id> "your async message"
9
+ #
10
+ # The receiving session must be running the copilot-extension-receive extension
11
+ # and must agree on COPILOT_MAILBOX_ROOT. `ready.json` is raw-probe discovery,
12
+ # not production liveness authority: this script does not close stale pid/PID-reuse.
13
+ # Inspect armed candidates with:
14
+ # ls "${COPILOT_MAILBOX_ROOT:-$HOME/.copilot/mailbox}"/*/ready.json
15
+ set -euo pipefail
16
+
17
+ root="${COPILOT_MAILBOX_ROOT:-$HOME/.copilot/mailbox}"
18
+ session_id="${1:?usage: copilot-enqueue-addressed.sh <session_id> <message>}"
19
+ shift
20
+ message="${*:?usage: copilot-enqueue-addressed.sh <session_id> <message>}"
21
+
22
+ box="$root/$session_id"
23
+ # Refuse an address that never armed. A stale marker remains possible in this raw
24
+ # probe; the managed product must add record-backed pid + start-key certification.
25
+ if [[ ! -f "$box/ready.json" ]]; then
26
+ echo "no receiver marker at $box (no ready.json) — not queued" >&2
27
+ exit 1
28
+ fi
29
+
30
+ msg="$box/$(date +%s%N).msg"
31
+ printf '%s\n' "$message" > "$msg"
32
+ # The body lands before the doorbell rings, so the receiver never wakes to an
33
+ # empty box.
34
+ printf 'poke\n' >> "$box/inbox.signal"
35
+ echo "queued-and-poked $msg (raw probe; extension log is the delivery receipt)"
@@ -0,0 +1,123 @@
1
+ /**
2
+ * entwurf mailbox receiver — GitHub Copilot CLI extension.
3
+ *
4
+ * WHAT THIS REPLACES. The earlier Copilot probe reached the TUI through the
5
+ * hidden `--ui-server` loopback port, and that rail was refused admission for a
6
+ * reason no amount of care could fix from outside: its authentication was not
7
+ * established (an unauthenticated SDK client connected; a token-bearing one got
8
+ * AUTHENTICATION_NOT_CONFIGURED). This rail does not answer that objection — it
9
+ * removes it. An extension is forked by the CLI itself and speaks JSON-RPC over
10
+ * its own stdio, so there is no port, no token, and no listener to authenticate.
11
+ * The trust boundary is the process fork.
12
+ *
13
+ * WHAT IT DOES. `joinSession()` attaches to the CLI's foreground session; the
14
+ * extension then watches ONE per-session signal file and calls `session.send()`
15
+ * when it changes. `fs.watch` -> `session.send()` is the pattern the bundled SDK
16
+ * documents itself (copilot-sdk/docs/examples.md, "Detecting when the plan file
17
+ * is created or edited"), not a mechanism discovered by inspection.
18
+ *
19
+ * ADDRESSED, NEVER BROADCAST. Each receiver owns `<root>/<sessionId>/` and
20
+ * watches only its own signal, so a sender pokes exactly one session rather than
21
+ * broadcasting. A two-process isolation run was observed, but its decisive B log
22
+ * was not preserved; managed admission must rerun that D3 control.
23
+ *
24
+ * LAUNCH CONTRACT. Extensions sit behind an experimental feature flag; without
25
+ * it the CLI never scans for them and this file is inert with no error:
26
+ *
27
+ * COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS copilot --model auto
28
+ *
29
+ * `--experimental` is NOT required (measured separately). Discovery scopes are
30
+ * user (`~/.copilot/extensions/`), plugin, session, and — interactive mode only
31
+ * — project (`.github/extensions/`).
32
+ */
33
+
34
+ import {
35
+ appendFileSync,
36
+ existsSync,
37
+ mkdirSync,
38
+ readdirSync,
39
+ readFileSync,
40
+ renameSync,
41
+ watch,
42
+ writeFileSync,
43
+ } from "node:fs";
44
+ import { homedir } from "node:os";
45
+ import { join } from "node:path";
46
+ import { joinSession } from "@github/copilot-sdk/extension";
47
+
48
+ const root = process.env.COPILOT_MAILBOX_ROOT ?? join(homedir(), ".copilot", "mailbox");
49
+
50
+ const session = await joinSession();
51
+ const box = join(root, session.sessionId);
52
+ mkdirSync(box, { recursive: true });
53
+ const logPath = join(box, "ext.log");
54
+ const log = (line) => appendFileSync(logPath, `${new Date().toISOString()} ${line}\n`);
55
+
56
+ // The sender needs an address, and scraping ~/.copilot storage for one is what
57
+ // the old command-hook probe failed at (its sessionStart input carries no
58
+ // sessionId). The receiver publishes its own identity instead, so discovery is
59
+ // a file this process wrote about itself rather than an inference about it.
60
+ writeFileSync(
61
+ join(box, "ready.json"),
62
+ JSON.stringify(
63
+ {
64
+ sessionId: session.sessionId,
65
+ pid: process.pid,
66
+ cwd: process.cwd(),
67
+ armedAt: new Date().toISOString(),
68
+ },
69
+ null,
70
+ 2,
71
+ ),
72
+ );
73
+
74
+ const signal = join(box, "inbox.signal");
75
+ if (!existsSync(signal)) writeFileSync(signal, "");
76
+ log(`ARMED sessionId=${session.sessionId} signal=${signal}`);
77
+
78
+ for (const type of ["user.message", "assistant.turn_start", "assistant.message", "session.idle"]) {
79
+ session.on(type, (event) => {
80
+ const d = event?.data ?? {};
81
+ log(
82
+ `EVENT ${type} ${JSON.stringify({
83
+ turnId: d.turnId,
84
+ content: typeof d.content === "string" ? d.content.slice(0, 200) : undefined,
85
+ source: d.source,
86
+ })}`,
87
+ );
88
+ });
89
+ }
90
+
91
+ // Serialized because a burst of writes to one signal file is normal and two
92
+ // overlapping drains would rename the same `.msg` twice.
93
+ let busy = false;
94
+ async function drain(why) {
95
+ if (busy) return;
96
+ busy = true;
97
+ try {
98
+ const pending = readdirSync(box)
99
+ .filter((f) => f.endsWith(".msg"))
100
+ .sort();
101
+ for (const name of pending) {
102
+ const from = join(box, name);
103
+ const to = `${from}.delivered`;
104
+ // Rename BEFORE sending: a crash between the two loses one delivery,
105
+ // but sending before renaming would replay it on every later poke.
106
+ renameSync(from, to);
107
+ const body = readFileSync(to, "utf-8");
108
+ log(`DELIVER (${why}) ${name} bytes=${body.length}`);
109
+ await session.send({ prompt: body, mode: "enqueue" });
110
+ log(`SENT ${name}`);
111
+ }
112
+ } catch (err) {
113
+ log(`ERROR ${String(err)}`);
114
+ } finally {
115
+ busy = false;
116
+ }
117
+ }
118
+
119
+ watch(signal, () => {
120
+ void drain("signal");
121
+ });
122
+ // Messages that arrived while nothing was armed are still owed a delivery.
123
+ void drain("startup");
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-copilot-mcp-state — hermetic install/doctor/inverse contract for
3
+ # the Copilot MCP-config adapter. No Copilot process or model turn.
4
+ set -euo pipefail
5
+
6
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ RUN="$REPO_DIR/run.sh"
8
+ pass=0
9
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
10
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
11
+ want() { eval "$2" && ok "$1" || die "$1"; }
12
+
13
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
14
+ SB="$(mktemp -d)"
15
+ trap 'rm -rf "$SB"' EXIT
16
+ export HOME="$SB/home"
17
+ export XDG_DATA_HOME="$SB/xdg"
18
+ export COPILOT_MCP_CONFIG="$HOME/.copilot/mcp-config.json"
19
+ export COPILOT_MCP_COMMAND="entwurf-bridge"
20
+ STATE="$XDG_DATA_HOME/entwurf/copilot-mcp/install-state.json"
21
+ mkdir -p "$(dirname "$COPILOT_MCP_CONFIG")" "$SB/bin"
22
+
23
+ write_mcp_fake() {
24
+ cat > "$1" <<'FAKE'
25
+ #!/usr/bin/env bash
26
+ while IFS= read -r line; do
27
+ case "$line" in
28
+ *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
29
+ *'"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"}]}}' ;;
30
+ esac
31
+ done
32
+ FAKE
33
+ chmod +x "$1"
34
+ }
35
+ write_dead_fake() {
36
+ cat > "$1" <<'FAKE'
37
+ #!/usr/bin/env bash
38
+ echo "bash: /nonexistent/entwurf-bridge: No such file or directory" >&2
39
+ exit 127
40
+ FAKE
41
+ chmod +x "$1"
42
+ }
43
+ write_mcp_fake "$SB/bin/$COPILOT_MCP_COMMAND"
44
+
45
+ _clean_path=""
46
+ while IFS= read -r dir; do
47
+ [ -n "$dir" ] || continue
48
+ [ -e "$dir/$COPILOT_MCP_COMMAND" ] || [ -L "$dir/$COPILOT_MCP_COMMAND" ] && continue
49
+ _clean_path="${_clean_path:+$_clean_path:}$dir"
50
+ done < <(printf '%s\n' "$PATH" | tr ':' '\n')
51
+ export PATH="$SB/bin:$_clean_path"
52
+
53
+ write_config() {
54
+ printf '{\n "mcpServers": {\n "other": { "type": "local", "command": "keep-me" },\n "entwurf-bridge": { "type": "local", "command": "manual-bridge", "args": [] }\n }\n}\n' > "$COPILOT_MCP_CONFIG"
55
+ }
56
+ json() { python3 - "$COPILOT_MCP_CONFIG" "$@" <<'PY'
57
+ import json, sys
58
+ data = json.load(open(sys.argv[1]))
59
+ mode = sys.argv[2]
60
+ servers = data["mcpServers"]
61
+ ours = {"type": "local", "command": "entwurf-bridge", "args": []}
62
+ if mode == "installed":
63
+ assert servers["entwurf-bridge"] == ours
64
+ assert servers["other"] == {"type": "local", "command": "keep-me"}
65
+ assert set(data.keys()) == {"mcpServers"}
66
+ elif mode == "manual":
67
+ assert servers["entwurf-bridge"] == {"type": "local", "command": "manual-bridge", "args": []}
68
+ assert servers["other"] == {"type": "local", "command": "keep-me"}
69
+ elif mode == "created":
70
+ assert data == {"mcpServers": {"entwurf-bridge": ours}}
71
+ else:
72
+ raise AssertionError(mode)
73
+ PY
74
+ }
75
+
76
+ # Existing matching/manual config is adopted, not treated as a fresh file.
77
+ write_config
78
+ "$RUN" doctor-copilot-mcp >/dev/null
79
+ want "unowned manual config doctor is an honest note" "[ $? -eq 0 ]"
80
+ "$RUN" install-copilot-mcp >/dev/null
81
+ json installed
82
+ want "install adopts regular config and preserves unrelated servers" "[ $? -eq 0 ]"
83
+ want "install writes state" "[ -f '$STATE' ]"
84
+ "$RUN" doctor-copilot-mcp >/dev/null
85
+ want "owned configured doctor is green" "[ $? -eq 0 ]"
86
+ python3 - "$STATE" <<'PY'
87
+ import json, sys
88
+ state = json.load(open(sys.argv[1]))
89
+ assert state["preimage"] == {"type": "local", "command": "manual-bridge", "args": []}
90
+ assert state["serverKey"] == "entwurf-bridge"
91
+ PY
92
+ ok "first install captures manual entwurf-bridge preimage"
93
+ "$RUN" install-copilot-mcp >/dev/null
94
+ "$RUN" uninstall-copilot-mcp >/dev/null
95
+ json manual
96
+ want "reinstall keeps the first preimage for inverse" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
97
+
98
+ # The live-host shape is already our command with no state: adopt that exact preimage too.
99
+ write_config
100
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"local","command":"entwurf-bridge","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
101
+ "$RUN" install-copilot-mcp >/dev/null
102
+ python3 - "$STATE" <<'PY'
103
+ import json, sys
104
+ state = json.load(open(sys.argv[1]))
105
+ assert state["preimage"] == {"type": "local", "command": "entwurf-bridge", "args": []}
106
+ PY
107
+ "$RUN" uninstall-copilot-mcp >/dev/null
108
+ json installed
109
+ want "matching manual Copilot MCP config is adopted and restored unchanged" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
110
+
111
+ # State turns a later config drift into a red doctor rather than a silent pass.
112
+ write_config
113
+ "$RUN" install-copilot-mcp >/dev/null
114
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"stdio","command":"entwurf-bridge","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
115
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "type=stdio doctor should fail"; fi
116
+ ok "state-present type=stdio entry is red"
117
+ "$RUN" install-copilot-mcp >/dev/null
118
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"local","command":"other","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
119
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "command drift doctor should fail"; fi
120
+ ok "state-present command drift is red"
121
+ "$RUN" install-copilot-mcp >/dev/null
122
+
123
+ # A stable-bin command that boots then dies is red without touching config.
124
+ write_dead_fake "$SB/bin/$COPILOT_MCP_COMMAND"
125
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "dead command doctor should fail"; fi
126
+ ok "state-present command that does not serve MCP is red"
127
+ write_mcp_fake "$SB/bin/$COPILOT_MCP_COMMAND"
128
+ "$RUN" uninstall-copilot-mcp >/dev/null
129
+ json manual
130
+ want "inverse restores manual config after drift repairs" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
131
+
132
+ # Created-new is removed only when it still contains only the adapter's key.
133
+ rm -f "$COPILOT_MCP_CONFIG"
134
+ "$RUN" install-copilot-mcp >/dev/null
135
+ json created
136
+ "$RUN" uninstall-copilot-mcp >/dev/null
137
+ want "created-new empty mcp-config is removed by inverse" "[ ! -e '$COPILOT_MCP_CONFIG' ] && [ ! -f '$STATE' ]"
138
+
139
+ # A foreign/corrupt config file is never adopted or given a state record.
140
+ printf '{"mcpServers":{}}\n' > "$SB/foreign.json"
141
+ ln -s "$SB/foreign.json" "$COPILOT_MCP_CONFIG"
142
+ if "$RUN" install-copilot-mcp >/dev/null 2>&1; then die "symlink install should refuse"; fi
143
+ ok "symlink config is refused"
144
+ want "symlink refusal leaves foreign config and no state" "! grep -q entwurf-bridge '$SB/foreign.json' && [ ! -f '$STATE' ]"
145
+ rm -f "$COPILOT_MCP_CONFIG"
146
+ printf 'not-json{{' > "$COPILOT_MCP_CONFIG"
147
+ if "$RUN" install-copilot-mcp >/dev/null 2>&1; then die "corrupt install should fail"; fi
148
+ ok "corrupt config fails loud"
149
+ want "corrupt refusal writes no state" "[ ! -f '$STATE' ]"
150
+
151
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
152
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "smoke changed the checkout"
153
+ printf '\nsmoke-copilot-mcp-state: %d checks passed\n' "$pass"
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-copilot-statusline-state — hermetic install/doctor/inverse contract for
3
+ # the Copilot custom-footer settings adapter. No Copilot process or model turn.
4
+ set -euo pipefail
5
+
6
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ RUN="$REPO_DIR/run.sh"
8
+ pass=0
9
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
10
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
11
+ want() { eval "$2" && ok "$1" || die "$1"; }
12
+
13
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
14
+ SB="$(mktemp -d)"
15
+ trap 'rm -rf "$SB"' EXIT
16
+ export HOME="$SB/home"
17
+ export XDG_DATA_HOME="$SB/xdg"
18
+ export COPILOT_SETTINGS_CONFIG="$HOME/.copilot/settings.json"
19
+ export COPILOT_STATUSLINE_COMMAND="entwurf-copilot-statusline"
20
+ STATE="$XDG_DATA_HOME/entwurf/copilot-statusline/install-state.json"
21
+ mkdir -p "$(dirname "$COPILOT_SETTINGS_CONFIG")" "$SB/bin"
22
+ printf '#!/usr/bin/env bash\necho fake-copilot-statusline\n' > "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
23
+ chmod +x "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
24
+
25
+ _clean_path=""
26
+ while IFS= read -r dir; do
27
+ [ -n "$dir" ] || continue
28
+ [ -e "$dir/$COPILOT_STATUSLINE_COMMAND" ] || [ -L "$dir/$COPILOT_STATUSLINE_COMMAND" ] && continue
29
+ _clean_path="${_clean_path:+$_clean_path:}$dir"
30
+ done < <(printf '%s\n' "$PATH" | tr ':' '\n')
31
+ export PATH="$SB/bin:$_clean_path"
32
+
33
+ write_settings() {
34
+ printf '{\n "enabledPlugins": { "other@marketplace": true },\n "footer": { "showCustom": true, "showDirectory": true },\n "statusLine": { "command": "manual-statusline" }\n}\n' > "$COPILOT_SETTINGS_CONFIG"
35
+ }
36
+ json() { python3 - "$COPILOT_SETTINGS_CONFIG" "$@" <<'PY'
37
+ import json, sys
38
+ settings = json.load(open(sys.argv[1]))
39
+ mode = sys.argv[2]
40
+ if mode == "installed":
41
+ assert settings["statusLine"] == {"command": "entwurf-copilot-statusline"}
42
+ assert settings["footer"] == {"showCustom": True, "showDirectory": True}
43
+ assert settings["enabledPlugins"] == {"other@marketplace": True}
44
+ elif mode == "manual":
45
+ assert settings["statusLine"] == {"command": "manual-statusline"}
46
+ assert settings["footer"] == {"showCustom": True, "showDirectory": True}
47
+ elif mode == "created":
48
+ assert settings == {"statusLine": {"command": "entwurf-copilot-statusline"}, "footer": {"showCustom": True}}
49
+ else:
50
+ raise AssertionError(mode)
51
+ PY
52
+ }
53
+
54
+ # Existing matching/manual settings are adopted, not treated as a fresh file.
55
+ write_settings
56
+ "$RUN" doctor-copilot-statusline >/dev/null
57
+ want "unowned manual settings doctor is an honest note" "[ $? -eq 0 ]"
58
+ "$RUN" install-copilot-statusline >/dev/null
59
+ json installed
60
+ want "install adopts regular settings and preserves unrelated keys" "[ $? -eq 0 ]"
61
+ want "install writes state" "[ -f '$STATE' ]"
62
+ python3 - "$STATE" <<'PY'
63
+ import json, sys
64
+ state = json.load(open(sys.argv[1]))
65
+ assert state["statusLinePreimage"] == {"command": "manual-statusline"}
66
+ assert state["showCustomPreimage"] is True
67
+ PY
68
+ ok "first install captures manual statusLine preimage"
69
+ "$RUN" install-copilot-statusline >/dev/null
70
+ "$RUN" uninstall-copilot-statusline >/dev/null
71
+ json manual
72
+ want "reinstall keeps the first preimage for inverse" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
73
+
74
+ # The live-host shape is already our command with no state: adopt that exact preimage too.
75
+ write_settings
76
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["statusLine"]={"command":"entwurf-copilot-statusline"}; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
77
+ "$RUN" install-copilot-statusline >/dev/null
78
+ python3 - "$STATE" <<'PY'
79
+ import json, sys
80
+ state = json.load(open(sys.argv[1]))
81
+ assert state["statusLinePreimage"] == {"command": "entwurf-copilot-statusline"}
82
+ assert state["showCustomPreimage"] is True
83
+ PY
84
+ "$RUN" uninstall-copilot-statusline >/dev/null
85
+ json installed
86
+ want "matching manual Copilot config is adopted and restored unchanged" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
87
+
88
+ # State turns a later setting drift into a red doctor rather than a silent pass.
89
+ write_settings
90
+ "$RUN" install-copilot-statusline >/dev/null
91
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["footer"]["showCustom"]=False; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
92
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "custom-disabled doctor should fail"; fi
93
+ ok "state-present custom-disabled footer is red"
94
+ "$RUN" install-copilot-statusline >/dev/null
95
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["statusLine"]={"command":"other"}; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
96
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "statusLine drift doctor should fail"; fi
97
+ ok "state-present statusLine drift is red"
98
+ "$RUN" install-copilot-statusline >/dev/null
99
+
100
+ # A stable-bin command that stops resolving is red without touching settings.
101
+ rm -f "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
102
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "dangling command doctor should fail"; fi
103
+ ok "dangling bare command is red"
104
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
105
+ chmod +x "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
106
+ "$RUN" uninstall-copilot-statusline >/dev/null
107
+ json manual
108
+ want "inverse restores manual settings after drift repairs" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
109
+
110
+ # Created-new is removed only when it still contains only the adapter's keys.
111
+ rm -f "$COPILOT_SETTINGS_CONFIG"
112
+ "$RUN" install-copilot-statusline >/dev/null
113
+ json created
114
+ "$RUN" uninstall-copilot-statusline >/dev/null
115
+ want "created-new empty settings file is removed by inverse" "[ ! -e '$COPILOT_SETTINGS_CONFIG' ] && [ ! -f '$STATE' ]"
116
+
117
+ # A foreign/corrupt settings file is never adopted or given a state record.
118
+ printf '{"footer":{}}\n' > "$SB/foreign.json"
119
+ ln -s "$SB/foreign.json" "$COPILOT_SETTINGS_CONFIG"
120
+ if "$RUN" install-copilot-statusline >/dev/null 2>&1; then die "symlink install should refuse"; fi
121
+ ok "symlink settings are refused"
122
+ want "symlink refusal leaves foreign settings and no state" "! grep -q statusLine '$SB/foreign.json' && [ ! -f '$STATE' ]"
123
+ rm -f "$COPILOT_SETTINGS_CONFIG"
124
+ printf 'not-json{{' > "$COPILOT_SETTINGS_CONFIG"
125
+ if "$RUN" install-copilot-statusline >/dev/null 2>&1; then die "corrupt install should fail"; fi
126
+ ok "corrupt settings fail loud"
127
+ want "corrupt refusal writes no state" "[ ! -f '$STATE' ]"
128
+
129
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
130
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "smoke changed the checkout"
131
+ printf '\nsmoke-copilot-statusline-state: %d checks passed\n' "$pass"
@@ -213,6 +213,8 @@ async function main(): Promise<void> {
213
213
  // onboarding instead of exercising the configured runtime. Separate servers preserve
214
214
  // those backend-native environments without adding an env carrier to the product.
215
215
  const nonces = new Map<string, string>();
216
+ // Intentional: pi + claude-code only. Copilot clause-7 LIVE is operator-metered
217
+ // and is not a release MUST — do not add `copilot` to this loop (VERIFY.md).
216
218
  for (const backend of ["pi", "claude-code"] as const) {
217
219
  const socket = path.join(root, `${backend}.sock`);
218
220
  privateSockets.add(socket);
@@ -557,7 +557,9 @@ async function main(): Promise<void> {
557
557
  return sender;
558
558
  }
559
559
 
560
- /** One private tmux server per CELL, plus the client env that anchors into it. */
560
+ /** One private tmux server per CELL, plus the client env that anchors into it.
561
+ * Intentional: pi + claude-code only. Copilot clause-7 LIVE is operator-metered
562
+ * and is not a release MUST — do not widen this type (VERIFY.md). */
561
563
  function startPrivateServer(
562
564
  cell: string,
563
565
  backend: "pi" | "claude-code",
@@ -30,6 +30,7 @@
30
30
  "../pi-extensions/lib/**/*.ts",
31
31
  "../pi-extensions/model-lock.ts",
32
32
  "../pi-extensions/meta-bridge-hook.ts",
33
+ "../pi-extensions/meta-bridge-hook-copilot.ts",
33
34
  "../mcp/entwurf-bridge/src/resume-mode.ts"
34
35
  ]
35
36
  }