@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.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +79 -0
- package/DELIVERY.md +72 -59
- package/README.md +55 -4
- package/VERIFY.md +2 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +26 -4
- package/docs/setup-clean-host.md +3 -3
- 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 +334 -16
- package/scripts/check-copilot-birth-hook.ts +497 -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 +92 -23
- package/scripts/check-gate-qualification.ts +7 -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/copilot-bridge-doctor.sh +209 -0
- package/scripts/copilot-bridge-install.sh +224 -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 +4 -1
- package/scripts/meta-bridge-hook-log.sh +17 -5
- package/scripts/mutants/capability-cache.json +64 -0
- package/scripts/mutants/copilot-birth.json +155 -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/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/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/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",
|