@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,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stateful Copilot statusLine settings adapter; stdlib only.
|
|
3
|
+
|
|
4
|
+
Owns the whole ``statusLine`` subtree and the one prerequisite leaf
|
|
5
|
+
``footer.showCustom``. It adopts regular settings.json files, refuses symlinks,
|
|
6
|
+
and records first-install preimages for an honest inverse.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
|
|
15
|
+
STATE_SCHEMA_VERSION = 1
|
|
16
|
+
STATUSLINE_KEY = "statusLine"
|
|
17
|
+
FOOTER_KEY = "footer"
|
|
18
|
+
SHOW_CUSTOM_KEY = "showCustom"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def die(code: int, message: str) -> "None":
|
|
22
|
+
sys.stderr.write(message.rstrip("\n") + "\n")
|
|
23
|
+
raise SystemExit(code)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_object(path: str, label: str) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
29
|
+
raw = handle.read()
|
|
30
|
+
except OSError as error:
|
|
31
|
+
die(4, f"copilot-statusline: cannot read {label} {path}: {error}")
|
|
32
|
+
if not raw.strip():
|
|
33
|
+
return {}
|
|
34
|
+
try:
|
|
35
|
+
value = json.loads(raw)
|
|
36
|
+
except json.JSONDecodeError as error:
|
|
37
|
+
die(4, f"copilot-statusline: {label} {path} is not valid JSON: {error}")
|
|
38
|
+
if not isinstance(value, dict):
|
|
39
|
+
die(4, f"copilot-statusline: {label} {path} top-level must be a JSON object")
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def atomic_write(path: str, value: dict) -> None:
|
|
44
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
45
|
+
os.makedirs(directory, exist_ok=True)
|
|
46
|
+
fd, temporary = tempfile.mkstemp(dir=directory, prefix=".copilot-statusline-", suffix=".json")
|
|
47
|
+
try:
|
|
48
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
49
|
+
json.dump(value, handle, indent=2)
|
|
50
|
+
handle.write("\n")
|
|
51
|
+
os.replace(temporary, path)
|
|
52
|
+
except BaseException:
|
|
53
|
+
try:
|
|
54
|
+
os.unlink(temporary)
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
pass
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def now() -> str:
|
|
61
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def checked_state(state_path: str) -> dict:
|
|
65
|
+
state = load_object(state_path, "install-state")
|
|
66
|
+
required = (
|
|
67
|
+
"managedSettingsPath",
|
|
68
|
+
"settingsExistedBefore",
|
|
69
|
+
"statusLineExisted",
|
|
70
|
+
"statusLinePreimage",
|
|
71
|
+
"footerExisted",
|
|
72
|
+
"showCustomExisted",
|
|
73
|
+
"showCustomPreimage",
|
|
74
|
+
)
|
|
75
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or any(key not in state for key in required):
|
|
76
|
+
die(4, f"copilot-statusline: install-state {state_path} has an unsupported shape")
|
|
77
|
+
return state
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def prior_state(state_path: str, settings_path: str) -> dict | None:
|
|
81
|
+
if not os.path.exists(state_path):
|
|
82
|
+
return None
|
|
83
|
+
state = checked_state(state_path)
|
|
84
|
+
return state if state["managedSettingsPath"] == os.path.abspath(settings_path) else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def install(settings_path: str, command: str, state_path: str) -> None:
|
|
88
|
+
if os.path.islink(settings_path):
|
|
89
|
+
die(3, f"copilot-statusline: refusing to adopt {settings_path} — symlink to {os.readlink(settings_path)} (someone else's SSOT)")
|
|
90
|
+
|
|
91
|
+
existed = os.path.exists(settings_path)
|
|
92
|
+
settings = load_object(settings_path, "settings") if existed else {}
|
|
93
|
+
footer_existed = FOOTER_KEY in settings
|
|
94
|
+
footer = settings.get(FOOTER_KEY) if footer_existed else {}
|
|
95
|
+
if not isinstance(footer, dict):
|
|
96
|
+
die(4, f"copilot-statusline: {settings_path} footer must be a JSON object")
|
|
97
|
+
|
|
98
|
+
prior = prior_state(state_path, settings_path)
|
|
99
|
+
if prior is None:
|
|
100
|
+
state = {
|
|
101
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
102
|
+
"managedSettingsPath": os.path.abspath(settings_path),
|
|
103
|
+
"command": command,
|
|
104
|
+
"detectMode": "adopt-regular-file" if existed else "created-new",
|
|
105
|
+
"settingsExistedBefore": existed,
|
|
106
|
+
"statusLineExisted": STATUSLINE_KEY in settings,
|
|
107
|
+
"statusLinePreimage": settings.get(STATUSLINE_KEY),
|
|
108
|
+
"footerExisted": footer_existed,
|
|
109
|
+
"showCustomExisted": SHOW_CUSTOM_KEY in footer,
|
|
110
|
+
"showCustomPreimage": footer.get(SHOW_CUSTOM_KEY),
|
|
111
|
+
"installedAt": now(),
|
|
112
|
+
}
|
|
113
|
+
else:
|
|
114
|
+
state = prior
|
|
115
|
+
|
|
116
|
+
settings[STATUSLINE_KEY] = {"command": command}
|
|
117
|
+
footer[SHOW_CUSTOM_KEY] = True
|
|
118
|
+
settings[FOOTER_KEY] = footer
|
|
119
|
+
atomic_write(settings_path, settings)
|
|
120
|
+
atomic_write(state_path, state)
|
|
121
|
+
sys.stdout.write(f"{state['detectMode']} {os.path.abspath(settings_path)}\n")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def uninstall(state_path: str) -> None:
|
|
125
|
+
if not os.path.exists(state_path):
|
|
126
|
+
die(2, f"copilot-statusline: no install-state at {state_path} — nothing to undo")
|
|
127
|
+
state = checked_state(state_path)
|
|
128
|
+
settings_path = state.get("managedSettingsPath")
|
|
129
|
+
if not isinstance(settings_path, str) or not os.path.isabs(settings_path):
|
|
130
|
+
die(4, f"copilot-statusline: install-state {state_path} has no absolute managedSettingsPath")
|
|
131
|
+
if os.path.islink(settings_path):
|
|
132
|
+
die(3, f"copilot-statusline: refusing to uninstall — {settings_path} became a symlink (someone else's SSOT)")
|
|
133
|
+
|
|
134
|
+
if os.path.exists(settings_path):
|
|
135
|
+
settings = load_object(settings_path, "settings")
|
|
136
|
+
if state.get("statusLineExisted"):
|
|
137
|
+
settings[STATUSLINE_KEY] = state.get("statusLinePreimage")
|
|
138
|
+
else:
|
|
139
|
+
settings.pop(STATUSLINE_KEY, None)
|
|
140
|
+
|
|
141
|
+
footer = settings.get(FOOTER_KEY)
|
|
142
|
+
if not isinstance(footer, dict):
|
|
143
|
+
die(4, f"copilot-statusline: {settings_path} footer changed to a non-object; refusing to guess")
|
|
144
|
+
if state.get("showCustomExisted"):
|
|
145
|
+
footer[SHOW_CUSTOM_KEY] = state.get("showCustomPreimage")
|
|
146
|
+
else:
|
|
147
|
+
footer.pop(SHOW_CUSTOM_KEY, None)
|
|
148
|
+
if footer or state.get("footerExisted"):
|
|
149
|
+
settings[FOOTER_KEY] = footer
|
|
150
|
+
else:
|
|
151
|
+
settings.pop(FOOTER_KEY, None)
|
|
152
|
+
|
|
153
|
+
created = state.get("detectMode") == "created-new" and state.get("settingsExistedBefore") is False
|
|
154
|
+
if created and not settings:
|
|
155
|
+
os.remove(settings_path)
|
|
156
|
+
else:
|
|
157
|
+
atomic_write(settings_path, settings)
|
|
158
|
+
|
|
159
|
+
os.remove(state_path)
|
|
160
|
+
sys.stdout.write(f"uninstalled {settings_path}\n")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def doctor_static(settings_path: str, command: str) -> None:
|
|
164
|
+
if os.path.islink(settings_path):
|
|
165
|
+
sys.stdout.write("symlink\n")
|
|
166
|
+
return
|
|
167
|
+
if not os.path.exists(settings_path):
|
|
168
|
+
sys.stdout.write("file-absent\n")
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
settings = load_object(settings_path, "settings")
|
|
172
|
+
except SystemExit:
|
|
173
|
+
sys.stdout.write("invalid-json\n")
|
|
174
|
+
return
|
|
175
|
+
status_line = settings.get(STATUSLINE_KEY)
|
|
176
|
+
if not isinstance(status_line, dict) or status_line.get("command") != command:
|
|
177
|
+
sys.stdout.write("not-ours\n")
|
|
178
|
+
return
|
|
179
|
+
footer = settings.get(FOOTER_KEY)
|
|
180
|
+
if not isinstance(footer, dict) or footer.get(SHOW_CUSTOM_KEY) is not True:
|
|
181
|
+
sys.stdout.write("custom-disabled\n")
|
|
182
|
+
return
|
|
183
|
+
sys.stdout.write(f"configured {command}\n")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv: list[str]) -> None:
|
|
187
|
+
if len(argv) < 2:
|
|
188
|
+
die(5, "usage: copilot-statusline-config.py <install|uninstall|doctor-static> ...")
|
|
189
|
+
match argv[1]:
|
|
190
|
+
case "install" if len(argv) == 5:
|
|
191
|
+
install(argv[2], argv[3], argv[4])
|
|
192
|
+
case "uninstall" if len(argv) == 3:
|
|
193
|
+
uninstall(argv[2])
|
|
194
|
+
case "doctor-static" if len(argv) == 4:
|
|
195
|
+
doctor_static(argv[2], argv[3])
|
|
196
|
+
case _:
|
|
197
|
+
die(5, "usage: copilot-statusline-config.py <install settings command state|uninstall state|doctor-static settings command>")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main(sys.argv)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# copilot-statusline — Copilot custom footer: garden id only.
|
|
3
|
+
#
|
|
4
|
+
# Copilot already draws directory/branch/model/context/quota. This fills the
|
|
5
|
+
# custom slot with the garden id. Vocabulary matches Claude and agy
|
|
6
|
+
# (ready / ? / !); rail `cop`. Fail-quiet: nonzero blanks the slot (bundle Fxi).
|
|
7
|
+
# Bare bin: entwurf-copilot-statusline. No cache, no git, no ANSI.
|
|
8
|
+
# Reads session_id only — any other envelope key is Copilot's, not ours.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# Copilot silently blanks a nonzero statusline, so a read failure degrades to ready.
|
|
12
|
+
input=$(cat || true)
|
|
13
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
14
|
+
printf '🪛 ? cop'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
output="$(STATUSLINE_INPUT="$input" python3 - <<'PY'
|
|
18
|
+
import json, os, sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
def load_input():
|
|
22
|
+
raw = os.environ.get("STATUSLINE_INPUT", "")
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
25
|
+
except Exception:
|
|
26
|
+
return {}
|
|
27
|
+
return data if isinstance(data, dict) else {}
|
|
28
|
+
|
|
29
|
+
def meta_sessions_dir():
|
|
30
|
+
override = os.environ.get("ENTWURF_META_SESSIONS_DIR")
|
|
31
|
+
if override:
|
|
32
|
+
return Path(override).expanduser().resolve()
|
|
33
|
+
agent = os.environ.get("PI_CODING_AGENT_DIR")
|
|
34
|
+
if agent:
|
|
35
|
+
return Path(agent).expanduser().resolve() / "meta-sessions"
|
|
36
|
+
return Path.home() / ".pi" / "agent" / "meta-sessions"
|
|
37
|
+
|
|
38
|
+
def garden_lookup(native_session_id):
|
|
39
|
+
if not native_session_id:
|
|
40
|
+
return "ready"
|
|
41
|
+
root = meta_sessions_dir()
|
|
42
|
+
if not root.exists():
|
|
43
|
+
return "?"
|
|
44
|
+
matches = []
|
|
45
|
+
try:
|
|
46
|
+
entries = sorted(root.glob("*.meta.json"))
|
|
47
|
+
except Exception:
|
|
48
|
+
return "?"
|
|
49
|
+
for file in entries:
|
|
50
|
+
try:
|
|
51
|
+
record = json.loads(file.read_text(encoding="utf-8"))
|
|
52
|
+
except Exception:
|
|
53
|
+
continue
|
|
54
|
+
if isinstance(record, dict) and record.get("nativeSessionId") == native_session_id:
|
|
55
|
+
garden = record.get("gardenId")
|
|
56
|
+
matches.append(garden if isinstance(garden, str) and garden else "!")
|
|
57
|
+
if len(matches) == 1:
|
|
58
|
+
return matches[0]
|
|
59
|
+
if len(matches) > 1:
|
|
60
|
+
return "!"
|
|
61
|
+
return "?"
|
|
62
|
+
|
|
63
|
+
data = load_input()
|
|
64
|
+
sid = data.get("session_id")
|
|
65
|
+
sys.stdout.write(f"🪛 {garden_lookup(sid if isinstance(sid, str) else '')} cop")
|
|
66
|
+
PY
|
|
67
|
+
)" || output='🪛 ? cop'
|
|
68
|
+
printf '%s' "$output"
|
package/scripts/dev-bin.sh
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# entwurf-bridge -> mcp/entwurf-bridge/start.sh (agy mcp_config command)
|
|
13
13
|
# entwurf-agy-statusline -> scripts/agy-statusline.sh (agy settings.statusLine command)
|
|
14
14
|
# entwurf-agy-imprint -> scripts/agy-imprint.sh (agy PreInvocation birth hook)
|
|
15
|
+
# entwurf-copilot-statusline -> scripts/copilot-statusline.sh (copilot settings.statusLine command)
|
|
15
16
|
#
|
|
16
17
|
# Ownership discipline (mirrors the agy adopt/refuse rule, 봉인 7), applied PER BIN:
|
|
17
18
|
# - create/refresh ONLY a link that is OURS — absent, or a symlink into our target, or the
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
# ENTWURF_BRIDGE_TARGET entwurf-bridge target (default: $REPO/mcp/entwurf-bridge/start.sh)
|
|
36
37
|
# ENTWURF_AGY_STATUSLINE_TARGET entwurf-agy-statusline target (default: $REPO/scripts/agy-statusline.sh)
|
|
37
38
|
# ENTWURF_AGY_IMPRINT_TARGET entwurf-agy-imprint target (default: $REPO/scripts/agy-imprint.sh)
|
|
39
|
+
# ENTWURF_COPILOT_STATUSLINE_TARGET entwurf-copilot-statusline target (default: $REPO/scripts/copilot-statusline.sh)
|
|
38
40
|
# XDG_DATA_HOME state root (states under $XDG_DATA_HOME/entwurf/dev-bin/)
|
|
39
41
|
set -euo pipefail
|
|
40
42
|
|
|
@@ -45,7 +47,7 @@ BIN_DIR="${ENTWURF_DEV_BIN_DIR:-$HOME/.local/bin}"
|
|
|
45
47
|
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/dev-bin"
|
|
46
48
|
LEGACY_STATE="$STATE_DIR/install-state.json" # pre-multi-bin single state == entwurf-bridge
|
|
47
49
|
|
|
48
|
-
MANAGED_BINS="entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint"
|
|
50
|
+
MANAGED_BINS="entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline"
|
|
49
51
|
|
|
50
52
|
log() { printf '%s\n' "$*"; }
|
|
51
53
|
warn() { printf '%s\n' "$*" >&2; }
|
|
@@ -56,6 +58,7 @@ bin_target() { # $1 = name → prints target path (env-overridable for smoke)
|
|
|
56
58
|
entwurf-bridge) echo "${ENTWURF_BRIDGE_TARGET:-$REPO_DIR/mcp/entwurf-bridge/start.sh}" ;;
|
|
57
59
|
entwurf-agy-statusline) echo "${ENTWURF_AGY_STATUSLINE_TARGET:-$REPO_DIR/scripts/agy-statusline.sh}" ;;
|
|
58
60
|
entwurf-agy-imprint) echo "${ENTWURF_AGY_IMPRINT_TARGET:-$REPO_DIR/scripts/agy-imprint.sh}" ;;
|
|
61
|
+
entwurf-copilot-statusline) echo "${ENTWURF_COPILOT_STATUSLINE_TARGET:-$REPO_DIR/scripts/copilot-statusline.sh}" ;;
|
|
59
62
|
*) return 1 ;;
|
|
60
63
|
esac
|
|
61
64
|
}
|
|
@@ -4,20 +4,32 @@
|
|
|
4
4
|
# proves the Claude SessionStart/CwdChanged hook re-armed the mailbox watch. A
|
|
5
5
|
# later `INFO attach record (event=UserPromptSubmit, ...)` is merely degraded
|
|
6
6
|
# backfill and must NOT clear an arm/upsert failure.
|
|
7
|
-
|
|
7
|
+
#
|
|
8
|
+
# THIS JUDGEMENT IS CLAUDE'S ONLY, and since #82 it has to say so. The Copilot birth
|
|
9
|
+
# unit appends to the SAME log file (one grep for the operator), tagging its lines
|
|
10
|
+
# `LEVEL [copilot]`. Those lines must be invisible here, for a reason stronger than
|
|
11
|
+
# tidiness: a copilot ERROR is often a CORRECT fail-closed refusal (a degraded envelope
|
|
12
|
+
# the unit refused to mint from), and its recovery token can never appear — `armed
|
|
13
|
+
# watch` is the Claude hook rail's token. Copilot's measured extension transport has a
|
|
14
|
+
# different, not-yet-admitted lifecycle and writes no token this doctor owns. Counting them
|
|
15
|
+
# would make the CLAUDE doctor permanently red for a refusal on a different rail, with
|
|
16
|
+
# a prescription pointing at a Claude wake failure that never happened (cross-review,
|
|
17
|
+
# glm, 2026-08-21). Copilot's own recovery rule lives in copilot-bridge-doctor.sh.
|
|
8
18
|
meta_bridge_hook_log_status() {
|
|
9
19
|
local log="$1"
|
|
10
|
-
|
|
20
|
+
local own
|
|
21
|
+
own="$(grep -v ' \[copilot\] ' "$log" 2>/dev/null || true)"
|
|
22
|
+
if ! printf '%s\n' "$own" | grep -q ' ERROR '; then
|
|
11
23
|
echo "no-error"
|
|
12
24
|
return 0
|
|
13
25
|
fi
|
|
14
26
|
|
|
15
27
|
local last_err last_recovery
|
|
16
|
-
last_err="$(grep -n ' ERROR '
|
|
17
|
-
last_recovery="$(grep -n ' INFO armed watch '
|
|
28
|
+
last_err="$(printf '%s\n' "$own" | grep -n ' ERROR ' | tail -1 | cut -d: -f1)"
|
|
29
|
+
last_recovery="$(printf '%s\n' "$own" | grep -n ' INFO armed watch ' | tail -1 | cut -d: -f1)"
|
|
18
30
|
|
|
19
31
|
if [ -z "$last_recovery" ] || [ "$last_err" -gt "$last_recovery" ]; then
|
|
20
|
-
|
|
32
|
+
printf '%s\n' "$own" | grep ' ERROR ' | tail -1
|
|
21
33
|
return 1
|
|
22
34
|
fi
|
|
23
35
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "capability-cache",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "CAPABILITY-REGISTRY-REREAD-SEES-REPLACEMENT",
|
|
7
|
+
"title": "memoizing the capability registry into a process-lifetime singleton makes every already-running dispatcher answer out of the registry it booted with — an install that moves a grade or wakeMode is invisible until the operator restarts, and nothing says so",
|
|
8
|
+
"subject": "pi-extensions/lib/meta-session.ts",
|
|
9
|
+
"find": [
|
|
10
|
+
"export function loadMetaCapabilityRegistry(): MetaCapabilityRegistry {",
|
|
11
|
+
"\treturn parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), \"utf8\"));",
|
|
12
|
+
"}"
|
|
13
|
+
],
|
|
14
|
+
"replace": [
|
|
15
|
+
"let cachedMetaCapabilities: MetaCapabilityRegistry | null = null;",
|
|
16
|
+
"export function loadMetaCapabilityRegistry(): MetaCapabilityRegistry {",
|
|
17
|
+
"\tif (cachedMetaCapabilities === null) {",
|
|
18
|
+
"\t\tcachedMetaCapabilities = parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), \"utf8\"));",
|
|
19
|
+
"\t}",
|
|
20
|
+
"\treturn cachedMetaCapabilities;",
|
|
21
|
+
"}"
|
|
22
|
+
],
|
|
23
|
+
"gate": ["bash", "run.sh", "check-meta-capability-source"],
|
|
24
|
+
"timeoutSeconds": 300,
|
|
25
|
+
"signature": "[QK:CAPABILITY-REGISTRY-REREAD-SEES-REPLACEMENT]",
|
|
26
|
+
"signatureSource": "scripts/check-meta-capability-source.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"claim": "CAPABILITY-REGISTRY-MISSING-STILL-THROWS",
|
|
30
|
+
"title": "answering a missing registry with a built-in default turns a broken install into a citizen that silently claims capabilities nobody shipped",
|
|
31
|
+
"subject": "pi-extensions/lib/meta-session.ts",
|
|
32
|
+
"find": ["\treturn parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), \"utf8\"));"],
|
|
33
|
+
"replace": [
|
|
34
|
+
"\tconst registryPath = metaCapabilitiesFilePath();",
|
|
35
|
+
"\tif (!fs.existsSync(registryPath)) {",
|
|
36
|
+
"\t\treturn { schemaVersion: CAPABILITY_SCHEMA_VERSION, backends: {} } as unknown as MetaCapabilityRegistry;",
|
|
37
|
+
"\t}",
|
|
38
|
+
"\treturn parseMetaCapabilityRegistry(fs.readFileSync(registryPath, \"utf8\"));"
|
|
39
|
+
],
|
|
40
|
+
"gate": ["bash", "run.sh", "check-meta-capability-source"],
|
|
41
|
+
"timeoutSeconds": 300,
|
|
42
|
+
"signature": "[QK:CAPABILITY-REGISTRY-MISSING-STILL-THROWS]",
|
|
43
|
+
"signatureSource": "scripts/check-meta-capability-source.ts"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"claim": "CAPABILITY-REGISTRY-CORRUPT-STILL-THROWS",
|
|
47
|
+
"title": "swallowing a parse failure and returning a fallback lets a half-written or hand-edited registry read as a valid one, so the strict parser stops being load-bearing",
|
|
48
|
+
"subject": "pi-extensions/lib/meta-session.ts",
|
|
49
|
+
"find": ["\treturn parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), \"utf8\"));"],
|
|
50
|
+
"replace": [
|
|
51
|
+
"\tconst rawRegistry = fs.readFileSync(metaCapabilitiesFilePath(), \"utf8\");",
|
|
52
|
+
"\ttry {",
|
|
53
|
+
"\t\treturn parseMetaCapabilityRegistry(rawRegistry);",
|
|
54
|
+
"\t} catch {",
|
|
55
|
+
"\t\treturn { schemaVersion: CAPABILITY_SCHEMA_VERSION, backends: {} } as unknown as MetaCapabilityRegistry;",
|
|
56
|
+
"\t}"
|
|
57
|
+
],
|
|
58
|
+
"gate": ["bash", "run.sh", "check-meta-capability-source"],
|
|
59
|
+
"timeoutSeconds": 300,
|
|
60
|
+
"signature": "[QK:CAPABILITY-REGISTRY-CORRUPT-STILL-THROWS]",
|
|
61
|
+
"signatureSource": "scripts/check-meta-capability-source.ts"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "copilot-birth",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "COPILOT-BIRTH-NO-ARGV-LAUNCH",
|
|
7
|
+
"title": "reusing the Claude no-argv refusal in the Copilot launcher kills every Copilot birth (this is the exact defect that held copilot at 0 of 409 records)",
|
|
8
|
+
"subject": "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
|
|
9
|
+
"find": ["exec \"$NODE_BIN\" \"$HOOK_ENTRY\""],
|
|
10
|
+
"replace": [
|
|
11
|
+
"if [ \"$#\" -eq 0 ]; then",
|
|
12
|
+
"\techo 'entwurf meta-bridge: this hook was launched with NO arguments.' >&2",
|
|
13
|
+
"\texit 1",
|
|
14
|
+
"fi",
|
|
15
|
+
"exec \"$NODE_BIN\" \"$HOOK_ENTRY\""
|
|
16
|
+
],
|
|
17
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
18
|
+
"timeoutSeconds": 300,
|
|
19
|
+
"signature": "[QK:COPILOT-BIRTH-NO-ARGV-LAUNCH]",
|
|
20
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"claim": "COPILOT-BIRTH-MINTS-COPILOT",
|
|
24
|
+
"title": "minting a Copilot session under the claude-code backend gives it a citizenship it cannot own and hides the Copilot lane's own count",
|
|
25
|
+
"subject": "pi-extensions/meta-bridge-hook-copilot.ts",
|
|
26
|
+
"find": ["\t\t\tinput: { backend: \"copilot\", nativeSessionId: envelope.sessionId, cwd: envelope.cwd },"],
|
|
27
|
+
"replace": ["\t\t\tinput: { backend: \"claude-code\", nativeSessionId: envelope.sessionId, cwd: envelope.cwd },"],
|
|
28
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
29
|
+
"timeoutSeconds": 300,
|
|
30
|
+
"signature": "[QK:COPILOT-BIRTH-MINTS-COPILOT]",
|
|
31
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"claim": "COPILOT-BIRTH-ID-DISAGREEMENT",
|
|
35
|
+
"title": "preferring one id when the two envelope keys disagree mints a citizen no live session can be joined back to",
|
|
36
|
+
"subject": "pi-extensions/meta-bridge-hook-copilot.ts",
|
|
37
|
+
"find": [
|
|
38
|
+
"\tif (native && compat && native !== compat) {",
|
|
39
|
+
"\t\treturn { refusal: `sessionId and session_id disagree (${native} vs ${compat})` };",
|
|
40
|
+
"\t}"
|
|
41
|
+
],
|
|
42
|
+
"replace": ["\t// disagreement tolerated: the native key simply wins"],
|
|
43
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
44
|
+
"timeoutSeconds": 300,
|
|
45
|
+
"signature": "[QK:COPILOT-BIRTH-ID-DISAGREEMENT]",
|
|
46
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"claim": "COPILOT-BIRTH-EXEC-IS-STRING",
|
|
50
|
+
"title": "declaring the Copilot hook in Claude's array form is rejected at plugin load, before any prompt — the plugin never runs at all",
|
|
51
|
+
"subject": "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json",
|
|
52
|
+
"find": ["\t\t\"sessionStart\": [{ \"exec\": \"__COPILOT_LAUNCHER__\" }],"],
|
|
53
|
+
"replace": ["\t\t\"sessionStart\": [{ \"exec\": [\"__COPILOT_LAUNCHER__\"] }],"],
|
|
54
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
55
|
+
"timeoutSeconds": 300,
|
|
56
|
+
"signature": "[QK:COPILOT-BIRTH-EXEC-IS-STRING]",
|
|
57
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"claim": "CAPABILITY-DRIFT-SCOPE-FROM-CONST",
|
|
61
|
+
"title": "a literal lane list in the capability drift guard lets any new backend's descriptor ship without ever being compared — the hole #82 found and closed",
|
|
62
|
+
"subject": "scripts/check-entwurf-capabilities.ts",
|
|
63
|
+
"find": ["const driftScope: readonly MetaBackend[] = META_BACKENDS;"],
|
|
64
|
+
"replace": ["const driftScope: readonly MetaBackend[] = [\"claude-code\", \"antigravity\", \"codex\"];"],
|
|
65
|
+
"gate": ["bash", "run.sh", "check-entwurf-capabilities"],
|
|
66
|
+
"timeoutSeconds": 300,
|
|
67
|
+
"signature": "[QK:CAPABILITY-DRIFT-SCOPE-FROM-CONST]",
|
|
68
|
+
"signatureSource": "scripts/check-entwurf-capabilities.ts"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"claim": "COPILOT-INSTALL-QUALIFIED-STALE",
|
|
72
|
+
"title": "a BARE stale-unit name reaches a same-named plugin from somebody else's marketplace and uninstalls it",
|
|
73
|
+
"subject": "scripts/copilot-bridge-install.sh",
|
|
74
|
+
"find": ["STALE_CLAUDE_UNIT=\"entwurf-meta-receive@meta-bridge-local\""],
|
|
75
|
+
"replace": ["STALE_CLAUDE_UNIT=\"entwurf-meta-receive\""],
|
|
76
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
77
|
+
"timeoutSeconds": 300,
|
|
78
|
+
"signature": "[QK:COPILOT-INSTALL-QUALIFIED-STALE]",
|
|
79
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"claim": "COPILOT-INSTALL-UNINSTALL-FAILURE-IS-FATAL",
|
|
83
|
+
"title": "swallowing a failed stale-unit uninstall installs on top of a unit that is still firing and reports success",
|
|
84
|
+
"subject": "scripts/copilot-bridge-install.sh",
|
|
85
|
+
"find": [
|
|
86
|
+
" copilot plugin uninstall \"$STALE_CLAUDE_UNIT\" >/dev/null \\",
|
|
87
|
+
" || die \"the stale Claude unit '$STALE_CLAUDE_UNIT' is installed in Copilot but could not be removed. It fires on every prompt and exits 1 before node starts, so installing on top of it would leave that noise in place. Fix the Copilot CLI error above, or re-run with --keep-stale-claude-unit to proceed deliberately.\""
|
|
88
|
+
],
|
|
89
|
+
"replace": [" copilot plugin uninstall \"$STALE_CLAUDE_UNIT\" >/dev/null 2>&1 || true"],
|
|
90
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
91
|
+
"timeoutSeconds": 300,
|
|
92
|
+
"signature": "[QK:COPILOT-INSTALL-UNINSTALL-FAILURE-IS-FATAL]",
|
|
93
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"claim": "HOOK-LOG-RAIL-SCOPED",
|
|
97
|
+
"title": "an untagged ERROR grep lets a Copilot fail-closed refusal redden the CLAUDE doctor forever — copilot's recovery token cannot exist on that rail",
|
|
98
|
+
"subject": "scripts/meta-bridge-hook-log.sh",
|
|
99
|
+
"find": [" own=\"$(grep -v ' \\[copilot\\] ' \"$log\" 2>/dev/null || true)\""],
|
|
100
|
+
"replace": [" own=\"$(cat \"$log\" 2>/dev/null || true)\""],
|
|
101
|
+
"gate": ["bash", "run.sh", "check-meta-doctor-oracle"],
|
|
102
|
+
"timeoutSeconds": 600,
|
|
103
|
+
"signature": "[QK:HOOK-LOG-RAIL-SCOPED]",
|
|
104
|
+
"signatureSource": "scripts/check-meta-doctor-oracle.sh"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"claim": "COPILOT-BIRTH-DOES-NOT-ARM-RECEIVER",
|
|
108
|
+
"title": "arming a mailbox from the BIRTH hook claims a watch this process does not hold. Copilot now has a doorbell (#82 RAIL 5), which is exactly why the writer matters: the extension owns the watch, so a marker minted here would name the host pid as a watcher and keep a wired-to-nothing citizen deliverable for as long as the TUI stayed open",
|
|
109
|
+
"subject": "pi-extensions/meta-bridge-hook-copilot.ts",
|
|
110
|
+
"find": ["\t\tlogLine(\n\t\t\t\"INFO\","],
|
|
111
|
+
"replace": [
|
|
112
|
+
"\t\tfs.mkdirSync(path.join(path.dirname(defaultMetaSessionsDir()), \"meta-mailbox\", result.record.gardenId), { recursive: true });\n\t\tlogLine(\n\t\t\t\"INFO\","
|
|
113
|
+
],
|
|
114
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
115
|
+
"timeoutSeconds": 300,
|
|
116
|
+
"signature": "[QK:COPILOT-BIRTH-DOES-NOT-ARM-RECEIVER]",
|
|
117
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"claim": "COPILOT-BIRTH-WRITES-SENDER-MARKER",
|
|
121
|
+
"title": "dropping the sender-marker write leaves the reader open and the writer closed — the citizen calls entwurf_v2, the bridge holds its owner pid, finds no marker, and the send is refused as anonymous with nothing in any log naming the cause (the #46 defect, re-planted on the copilot rail)",
|
|
122
|
+
"subject": "pi-extensions/meta-bridge-hook-copilot.ts",
|
|
123
|
+
"find": ["\t\twriteCopilotSenderMarker(result.record.gardenId, envelope);"],
|
|
124
|
+
"replace": ["\t\t// sender marker skipped"],
|
|
125
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
126
|
+
"timeoutSeconds": 300,
|
|
127
|
+
"signature": "[QK:COPILOT-BIRTH-WRITES-SENDER-MARKER]",
|
|
128
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"claim": "COPILOT-SENDER-READER-OPEN",
|
|
132
|
+
"title": "closing the reader half leaves the writer talking to nobody — the hook writes a copilot marker, the bridge never looks in that directory, and the citizen's sends are refused as anonymous (the #46 shape, which is why writer and reader open in one change)",
|
|
133
|
+
"subject": "pi-extensions/lib/meta-sender-identity.ts",
|
|
134
|
+
"find": [
|
|
135
|
+
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\", \"antigravity\", \"copilot\"];"
|
|
136
|
+
],
|
|
137
|
+
"replace": ["export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\", \"antigravity\"];"],
|
|
138
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
139
|
+
"timeoutSeconds": 300,
|
|
140
|
+
"signature": "[QK:COPILOT-SENDER-READER-OPEN]",
|
|
141
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"claim": "COPILOT-INSTALL-LIST-FAILURE-IS-FATAL",
|
|
145
|
+
"title": "reading a failed `copilot plugin list` as an empty host installs on top of a stale unit that is still firing",
|
|
146
|
+
"subject": "scripts/copilot-bridge-install.sh",
|
|
147
|
+
"find": [" if ! INSTALLED_LIST=\"$(copilot plugin list 2>/dev/null)\"; then"],
|
|
148
|
+
"replace": [" INSTALLED_LIST=\"$(copilot plugin list 2>/dev/null || true)\"", " if false; then"],
|
|
149
|
+
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
150
|
+
"timeoutSeconds": 300,
|
|
151
|
+
"signature": "[QK:COPILOT-INSTALL-LIST-FAILURE-IS-FATAL]",
|
|
152
|
+
"signatureSource": "scripts/check-copilot-birth-hook.ts"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|