@junghanacs/entwurf 0.15.0 → 0.16.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 +1 -0
- package/CHANGELOG.md +299 -0
- package/DELIVERY.md +4 -3
- package/README.md +99 -67
- package/VERIFY.md +13 -7
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +221 -16
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +14 -4
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +1041 -174
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +700 -23
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-fresh-cut-gate.sh +12 -4
- package/scripts/check-gate-qualification.ts +8 -2
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +104 -71
- package/scripts/copilot-bridge-install.sh +180 -20
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/dev-bin.sh +50 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +96 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -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-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +2 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# copilot-bridge-oracle.sh — the ONE structural oracle for an assembled Copilot birth
|
|
3
|
+
# unit (#86 C3a). Sourced by the installer (adoption preflight) and the doctor
|
|
4
|
+
# (ownership axis) so "valid assembly" cannot mean two different things: a unit the
|
|
5
|
+
# doctor would flag must never be silently adopted as ours.
|
|
6
|
+
#
|
|
7
|
+
# copilot_assembly_valid <asm-root> <plugin-name>
|
|
8
|
+
# rc 0 = the assembly at <asm-root> is a complete, baked unit
|
|
9
|
+
# rc 1 = it is not; the first failing reason is printed to stderr
|
|
10
|
+
#
|
|
11
|
+
# copilot_state_read <state_file> <qualified> <mkt_name> <asm>
|
|
12
|
+
# Fail-closed EXACT-schema reader for the ownership state — one validator for the
|
|
13
|
+
# installer, the inverse and the doctor so "valid state" cannot drift into three
|
|
14
|
+
# copies. rc 0 prints `<ownedMarketplace> <ownedAssembly> <pluginVersion>`; any
|
|
15
|
+
# missing/unknown key, wrong type, empty string, relative assemblyPath, constant
|
|
16
|
+
# drift or effective-ASM mismatch is rc 1 with the reason on stderr. Flags are never
|
|
17
|
+
# coerced: a missing or non-boolean owned* refuses instead of defaulting to false.
|
|
18
|
+
#
|
|
19
|
+
# copilot_marketplace_local_path <mkt-list-text> <mkt-name>
|
|
20
|
+
# The one exact marketplace-row grammar (see its own header below): absent /
|
|
21
|
+
# exactly-one Local path / malformed-non-Local-or-duplicate refusal.
|
|
22
|
+
#
|
|
23
|
+
# Deliberately BOUNDED: structure, one state schema, one plugin list-row grammar, one
|
|
24
|
+
# marketplace list-row grammar. Registration, records, and hook-log truth stay with
|
|
25
|
+
# the doctor's own axes.
|
|
26
|
+
copilot_state_read() {
|
|
27
|
+
local state_file="$1" qualified="$2" mkt="$3" asm="$4"
|
|
28
|
+
STATE_FILE_ENV="$state_file" QUALIFIED_ENV="$qualified" MKT_ENV="$mkt" ASM_ENV="$asm" python3 - <<'PY'
|
|
29
|
+
import json, os, sys
|
|
30
|
+
try:
|
|
31
|
+
state = json.load(open(os.environ["STATE_FILE_ENV"]))
|
|
32
|
+
except (OSError, json.JSONDecodeError) as err:
|
|
33
|
+
print(f"state parse failed: {err}", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
EXACT_KEYS = {
|
|
36
|
+
"schemaVersion", "qualifiedId", "marketplaceName", "assemblyPath",
|
|
37
|
+
"pluginVersion", "ownedMarketplace", "ownedAssembly", "installedAt",
|
|
38
|
+
}
|
|
39
|
+
if not isinstance(state, dict) or set(state) != EXACT_KEYS:
|
|
40
|
+
print(f"state keyset drifted: {sorted(state) if isinstance(state, dict) else type(state).__name__} != {sorted(EXACT_KEYS)}", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
if state["schemaVersion"] != 1:
|
|
43
|
+
print(f"state schemaVersion is {state['schemaVersion']!r}, want the literal 1", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
for key in ("qualifiedId", "marketplaceName", "assemblyPath", "pluginVersion", "installedAt"):
|
|
46
|
+
if not isinstance(state[key], str) or not state[key]:
|
|
47
|
+
print(f"state {key} must be a nonempty string, got {state[key]!r}", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
if any(ord(c) < 0x21 or ord(c) == 0x7f for c in state["pluginVersion"]):
|
|
50
|
+
# C3a amendment (B defect 4): these facts travel space-separated and consumers
|
|
51
|
+
# cut field 3, so a version carrying whitespace/control would silently truncate
|
|
52
|
+
# into a FABRICATED value (and a fabricated drift reason downstream). Refuse the
|
|
53
|
+
# state instead of shipping an ambiguous transport.
|
|
54
|
+
print(f"state pluginVersion contains whitespace/control characters: {state['pluginVersion']!r} — "
|
|
55
|
+
"the space-separated fact transport would truncate it into a fabricated version", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
for key in ("ownedMarketplace", "ownedAssembly"):
|
|
58
|
+
if not isinstance(state[key], bool):
|
|
59
|
+
print(f"state {key} must be a boolean, got {state[key]!r} — flags are never coerced", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
if not os.path.isabs(state["assemblyPath"]):
|
|
62
|
+
print(f"state assemblyPath must be absolute, got {state['assemblyPath']!r}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
for key, want in (("qualifiedId", os.environ["QUALIFIED_ENV"]), ("marketplaceName", os.environ["MKT_ENV"])):
|
|
65
|
+
if state[key] != want:
|
|
66
|
+
print(f"state {key} is {state[key]!r}, want {want!r}", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
if state["assemblyPath"] != os.path.abspath(os.environ["ASM_ENV"]):
|
|
69
|
+
print(f"state assemblyPath is {state['assemblyPath']!r}, but this host's effective assembly is {os.path.abspath(os.environ['ASM_ENV'])!r}", file=sys.stderr)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
print(f"{'true' if state['ownedMarketplace'] else 'false'} {'true' if state['ownedAssembly'] else 'false'} {state['pluginVersion']}")
|
|
72
|
+
PY
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# copilot_exact_row_version <list-text> <qualified>
|
|
76
|
+
# The finer read of the SAME row grammar (final amendment): reports whether the exact
|
|
77
|
+
# QUALIFIED is absent, present exactly once with a parsed nonempty version, or in a
|
|
78
|
+
# shape nobody may act on. rc 0 prints `absent` or `one <version>`; rc 1 with the
|
|
79
|
+
# reason on stderr for a MALFORMED exact row (claims our qualified id but does not
|
|
80
|
+
# parse as `<qualified> (v<nonempty>)`) or MULTIPLE exact rows. Longer tokens that
|
|
81
|
+
# merely contain the qualified id remain foreign and are ignored, same as above.
|
|
82
|
+
copilot_exact_row_version() {
|
|
83
|
+
local list_text="$1" qualified="$2"
|
|
84
|
+
LIST_TEXT_ENV="$list_text" QUALIFIED_ENV="$qualified" python3 - <<'PY'
|
|
85
|
+
import os, sys
|
|
86
|
+
qualified = os.environ["QUALIFIED_ENV"]
|
|
87
|
+
versions = []
|
|
88
|
+
for raw in os.environ["LIST_TEXT_ENV"].splitlines():
|
|
89
|
+
row = raw.strip()
|
|
90
|
+
for glyph in ("•", "◆", "-"):
|
|
91
|
+
if row.startswith(glyph):
|
|
92
|
+
row = row[len(glyph):].strip()
|
|
93
|
+
# C3a amendment (B defect 3): the claim test is the exact QUALIFIED as the row's
|
|
94
|
+
# first whitespace-delimited token. Every claiming row must then parse as the one
|
|
95
|
+
# measured form `<qualified> (v<nonempty>)` IN FULL — a truncated/garbled tail
|
|
96
|
+
# (e.g. `(v0.1` with no closing paren) used to fall through the old
|
|
97
|
+
# startswith/endswith pair and round to ABSENT, which the inverse reads as
|
|
98
|
+
# retry-safe absence. Malformed is malformed; nothing rounds to absent.
|
|
99
|
+
parts = row.split(None, 1)
|
|
100
|
+
if not parts or parts[0] != qualified:
|
|
101
|
+
continue # foreign row, including longer ids that merely contain ours
|
|
102
|
+
rest = parts[1] if len(parts) == 2 else ""
|
|
103
|
+
if not (rest.startswith("(v") and rest.endswith(")")):
|
|
104
|
+
print(f"malformed exact row for {qualified}: {row!r} does not parse as '<qualified> (v<version>)'", file=sys.stderr)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
version = rest[2:-1]
|
|
107
|
+
if not version:
|
|
108
|
+
print(f"malformed exact row for {qualified}: empty version in {row!r}", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
if any(ord(c) < 0x21 or ord(c) == 0x7f for c in version):
|
|
111
|
+
# Same transport rule as the state validator: the parsed version rides
|
|
112
|
+
# space-separated verdicts (`one <version>`), so an embedded whitespace or
|
|
113
|
+
# control character is ambiguity, not a version.
|
|
114
|
+
print(f"malformed exact row for {qualified}: version {version!r} carries whitespace/control characters", file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
versions.append(version)
|
|
117
|
+
if len(versions) > 1:
|
|
118
|
+
print(f"multiple exact rows for {qualified}: versions {versions} — nobody may act on an ambiguous listing", file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
print(f"one {versions[0]}" if versions else "absent")
|
|
121
|
+
PY
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# copilot_marketplace_local_path <mkt-list-text> <mkt-name>
|
|
125
|
+
# The ONE exact marketplace-row grammar (C3a amendment, B defects 1+2) — installer,
|
|
126
|
+
# inverse and doctor all read the `plugin marketplace list` output through THIS
|
|
127
|
+
# parser instead of three copy-pasted `grep|head -1` pipelines that silently took
|
|
128
|
+
# the first of several same-named rows. rc 0 prints `absent` or `one <local abs
|
|
129
|
+
# path>`; rc 1 with the reason on stderr when a row claims the exact name but does
|
|
130
|
+
# not parse as the measured `<name> (Local: <path>)` form (a non-Local source is
|
|
131
|
+
# not provably ours), or when MULTIPLE rows claim the name (duplicates are
|
|
132
|
+
# ambiguity nobody may act on — never "the first one"). Rows whose first token
|
|
133
|
+
# merely contains the name remain foreign and are ignored. Path-vs-assembly drift
|
|
134
|
+
# stays with the callers: this parser reports the exact registered path, and each
|
|
135
|
+
# surface names its own refusal when that path is not its assembly.
|
|
136
|
+
copilot_marketplace_local_path() {
|
|
137
|
+
local list_text="$1" mkt_name="$2"
|
|
138
|
+
LIST_TEXT_ENV="$list_text" MKT_NAME_ENV="$mkt_name" python3 - <<'PY'
|
|
139
|
+
import os, sys
|
|
140
|
+
name = os.environ["MKT_NAME_ENV"]
|
|
141
|
+
paths = []
|
|
142
|
+
for raw in os.environ["LIST_TEXT_ENV"].splitlines():
|
|
143
|
+
row = raw.strip()
|
|
144
|
+
for glyph in ("•", "◆", "-"):
|
|
145
|
+
if row.startswith(glyph):
|
|
146
|
+
row = row[len(glyph):].strip()
|
|
147
|
+
parts = row.split(None, 1)
|
|
148
|
+
if not parts or parts[0] != name:
|
|
149
|
+
continue # foreign row, including longer names that merely contain ours
|
|
150
|
+
rest = parts[1] if len(parts) == 2 else ""
|
|
151
|
+
if not (rest.startswith("(Local: ") and rest.endswith(")")):
|
|
152
|
+
print(f"malformed marketplace row for {name}: {row!r} does not parse as '<name> (Local: <path>)' — "
|
|
153
|
+
"a non-Local or garbled registration is not provably ours", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
path = rest[len("(Local: "):-1]
|
|
156
|
+
if not path:
|
|
157
|
+
print(f"malformed marketplace row for {name}: empty Local path in {row!r}", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
paths.append(path)
|
|
160
|
+
if len(paths) > 1:
|
|
161
|
+
print(f"multiple marketplace rows named {name}: paths {paths} — duplicates are ambiguity nobody may act on", file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
print(f"one {paths[0]}" if paths else "absent")
|
|
164
|
+
PY
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
copilot_assembly_valid() {
|
|
168
|
+
local asm="$1" plugin="$2"
|
|
169
|
+
local unit="$asm/$plugin"
|
|
170
|
+
local launcher="$unit/scripts/copilot-hook-launch.sh"
|
|
171
|
+
local hooks="$unit/hooks/hooks.json"
|
|
172
|
+
[ -d "$unit" ] || { echo "structural oracle: unit dir missing: $unit" >&2; return 1; }
|
|
173
|
+
[ -x "$launcher" ] || { echo "structural oracle: launcher missing or not executable: $launcher" >&2; return 1; }
|
|
174
|
+
if grep -q "__NODE_BIN__\|__HOOK_ENTRY__" "$launcher"; then
|
|
175
|
+
echo "structural oracle: launcher still carries an installer placeholder (unbaked): $launcher" >&2
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
local baked_node
|
|
179
|
+
baked_node="$(sed -n 's/^NODE_BIN="\(.*\)"$/\1/p' "$launcher" | head -1)"
|
|
180
|
+
[ -n "$baked_node" ] && [ -x "$baked_node" ] || {
|
|
181
|
+
echo "structural oracle: baked node missing or not executable: ${baked_node:-(unparsed)}" >&2
|
|
182
|
+
return 1
|
|
183
|
+
}
|
|
184
|
+
local baked_entry
|
|
185
|
+
baked_entry="$(sed -n 's|^HOOK_ENTRY="\$PLUGIN_ROOT/\(.*\)"$|\1|p' "$launcher" | head -1)"
|
|
186
|
+
[ -n "$baked_entry" ] && [ -f "$unit/$baked_entry" ] || {
|
|
187
|
+
echo "structural oracle: hook entry missing beside the launcher: ${baked_entry:-(unparsed)}" >&2
|
|
188
|
+
return 1
|
|
189
|
+
}
|
|
190
|
+
[ -f "$unit/entwurf-capabilities.json" ] || {
|
|
191
|
+
echo "structural oracle: capability registry missing at the plugin root" >&2
|
|
192
|
+
return 1
|
|
193
|
+
}
|
|
194
|
+
[ -f "$hooks" ] || { echo "structural oracle: hooks.json missing: $hooks" >&2; return 1; }
|
|
195
|
+
if ! HOOKS_PATH="$hooks" ORACLE_LAUNCHER="$launcher" python3 - <<'PY'
|
|
196
|
+
import json, os, sys
|
|
197
|
+
from pathlib import Path
|
|
198
|
+
try:
|
|
199
|
+
hooks = json.loads(Path(os.environ["HOOKS_PATH"]).read_text(encoding="utf-8"))
|
|
200
|
+
except (OSError, json.JSONDecodeError) as err:
|
|
201
|
+
print(f"structural oracle: hooks.json unreadable: {err}", file=sys.stderr)
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
launcher = os.environ["ORACLE_LAUNCHER"]
|
|
204
|
+
if hooks.get("version") != 1:
|
|
205
|
+
print("structural oracle: hooks.json version is not the literal 1", file=sys.stderr)
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
events = hooks.get("hooks") or {}
|
|
208
|
+
if set(events) != {"sessionStart", "userPromptSubmitted"}:
|
|
209
|
+
print(f"structural oracle: hook events drifted: {sorted(events)}", file=sys.stderr)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
for name, entries in events.items():
|
|
212
|
+
for i, entry in enumerate(entries if isinstance(entries, list) else []):
|
|
213
|
+
exec_value = entry.get("exec")
|
|
214
|
+
if not isinstance(exec_value, str) or exec_value != launcher or "args" in entry:
|
|
215
|
+
print(f"structural oracle: hooks.{name}[{i}] is not the baked exec-string form", file=sys.stderr)
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
sys.exit(0)
|
|
218
|
+
PY
|
|
219
|
+
then
|
|
220
|
+
return 1
|
|
221
|
+
fi
|
|
222
|
+
return 0
|
|
223
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# copilot-bridge-uninstall.sh — the package-owned INVERSE of copilot-bridge-install.sh
|
|
3
|
+
# (#86 C3a). Removes exactly what the ownership state records this package installed —
|
|
4
|
+
# the qualified plugin, the local marketplace registration, the assembly — and nothing
|
|
5
|
+
# else. The stale Claude unit is INSTALL-ONLY territory; this inverse never touches it.
|
|
6
|
+
#
|
|
7
|
+
# ORDER AND AUTHORITY (amendment: the ownership preflight is COMPLETE before any vendor
|
|
8
|
+
# mutation). The state is required and validated fail-closed against this package's
|
|
9
|
+
# constants and the effective assembly path; both vendor lists are read next and a
|
|
10
|
+
# failing list is UNKNOWN (an unknown host is not an empty one — refuse); then EVERY
|
|
11
|
+
# ownership and safety fact — marketplace name/path/recorded ownership, assembly path
|
|
12
|
+
# safety, exact plugin row presence — is decided read-only. Only after all of it passes
|
|
13
|
+
# may the first vendor write run. So a refusal, any refusal, is zero vendor/filesystem
|
|
14
|
+
# writes with the state retained. Vendor steps run before filesystem steps, the state
|
|
15
|
+
# is deleted LAST, and any failure stops with the state retained so a rerun can finish
|
|
16
|
+
# the job. `--force` is forbidden: `marketplace remove --force` uninstalls that
|
|
17
|
+
# marketplace's plugins as a side effect (measured 2026-08-27).
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
MKT_NAME="meta-bridge-copilot-local"
|
|
22
|
+
PLUGIN="entwurf-meta-receive-copilot"
|
|
23
|
+
QUALIFIED="$PLUGIN@$MKT_NAME"
|
|
24
|
+
ASM="${ENTWURF_COPILOT_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-copilot/.assembled}"
|
|
25
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/copilot-bridge"
|
|
26
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
27
|
+
|
|
28
|
+
die() { echo "[copilot-bridge-uninstall] $*" >&2; exit 1; }
|
|
29
|
+
|
|
30
|
+
[ $# -eq 0 ] || die "unknown argument: $1 (this inverse takes no arguments; --force does not exist here)"
|
|
31
|
+
command -v copilot >/dev/null 2>&1 || die "the 'copilot' CLI is not on PATH."
|
|
32
|
+
command -v python3 >/dev/null 2>&1 || die "python3 is required and is not on PATH."
|
|
33
|
+
|
|
34
|
+
# shellcheck source=copilot-bridge-oracle.sh
|
|
35
|
+
. "$HERE/copilot-bridge-oracle.sh"
|
|
36
|
+
|
|
37
|
+
# --- 1. the ownership state is the sole removal authority --------------------
|
|
38
|
+
[ -L "$STATE_FILE" ] && die "ownership state $STATE_FILE is a symlink — refusing to trust it."
|
|
39
|
+
[ -f "$STATE_FILE" ] || die "no ownership state at $STATE_FILE — nothing this package provably owns to remove. A legacy no-state installation is adopted by './run.sh install-copilot-bridge' first, then removed."
|
|
40
|
+
STATE_FACTS="$(copilot_state_read "$STATE_FILE" "$QUALIFIED" "$MKT_NAME" "$ASM")" \
|
|
41
|
+
|| die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero vendor/filesystem writes."
|
|
42
|
+
OWNED_MKT="$(printf '%s' "$STATE_FACTS" | cut -d' ' -f1)"
|
|
43
|
+
OWNED_ASM="$(printf '%s' "$STATE_FACTS" | cut -d' ' -f2)"
|
|
44
|
+
STATE_PLUGIN_VERSION="$(printf '%s' "$STATE_FACTS" | cut -d' ' -f3)"
|
|
45
|
+
|
|
46
|
+
# --- 2. read-only vendor facts (a failing list is UNKNOWN, never absence) ----
|
|
47
|
+
if ! INSTALLED_LIST="$(copilot plugin list 2>/dev/null)"; then
|
|
48
|
+
die "'copilot plugin list' failed, so this host's installed plugins are UNKNOWN — refusing with zero writes. Fix the Copilot CLI error and re-run."
|
|
49
|
+
fi
|
|
50
|
+
if ! MKT_LIST="$(copilot plugin marketplace list 2>/dev/null)"; then
|
|
51
|
+
die "'copilot plugin marketplace list' failed, so this host's marketplaces are UNKNOWN — refusing with zero writes. Fix the Copilot CLI error and re-run."
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# --- 3. COMPLETE ownership/safety preflight, read-only, before ANY mutation --
|
|
55
|
+
# 3a. the marketplace half: exact name must sit at the recorded path AND be recorded
|
|
56
|
+
# as ours. Refusing HERE — before the plugin uninstall — is the amendment's point:
|
|
57
|
+
# the same qualified id could have been installed from a same-named marketplace at
|
|
58
|
+
# another path, and an inverse that had already uninstalled the plugin would have
|
|
59
|
+
# removed someone else's unit before discovering that.
|
|
60
|
+
MKT_PRESENT=0
|
|
61
|
+
# ONE marketplace-row grammar (C3a amendment, B defects 1+2): the shared oracle
|
|
62
|
+
# refuses malformed/non-Local rows and DUPLICATE same-named rows instead of silently
|
|
63
|
+
# acting on the first — an inverse steered at the wrong same-named marketplace would
|
|
64
|
+
# remove a registration it cannot attribute.
|
|
65
|
+
MKT_ROW="$(copilot_marketplace_local_path "$MKT_LIST" "$MKT_NAME")" \
|
|
66
|
+
|| die "the 'copilot plugin marketplace list' rows for '$MKT_NAME' are malformed, non-Local, or duplicated (see above) — refusing with zero vendor/filesystem writes."
|
|
67
|
+
if [ "$MKT_ROW" != "absent" ]; then
|
|
68
|
+
MKT_PRESENT=1
|
|
69
|
+
MKT_PATH="${MKT_ROW#one }"
|
|
70
|
+
if [ "$MKT_PATH" != "$ASM" ]; then
|
|
71
|
+
die "a marketplace named '$MKT_NAME' is registered at '$MKT_PATH', not at the recorded assembly ($ASM). That registration is not provably ours — refusing with zero vendor/filesystem writes; inspect 'copilot plugin marketplace list' manually."
|
|
72
|
+
fi
|
|
73
|
+
if [ "$OWNED_MKT" != "true" ]; then
|
|
74
|
+
die "marketplace '$MKT_NAME' is registered at our recorded path, but the ownership state does not record marketplace ownership. Completing this inverse would leave a registered marketplace pointing at removed backing — refusing with zero vendor/filesystem writes (plugin, marketplace, assembly and state all preserved). Resolve the marketplace registration manually, or re-run './run.sh install-copilot-bridge' to re-bind ownership."
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
# 3b. assembly delete safety, decided BEFORE any vendor write: the recorded path must
|
|
78
|
+
# be a sane rm target or nothing at all may start.
|
|
79
|
+
if [ "$OWNED_ASM" = "true" ]; then
|
|
80
|
+
case "$ASM" in
|
|
81
|
+
/) die "recorded assembly path is '/' — refusing everything." ;;
|
|
82
|
+
/*) : ;;
|
|
83
|
+
*) die "recorded assembly path is not absolute: $ASM — refusing everything." ;;
|
|
84
|
+
esac
|
|
85
|
+
[ -L "$ASM" ] && die "recorded assembly path $ASM is a symlink — refusing everything; the inverse removes only the real recorded directory."
|
|
86
|
+
if [ -e "$ASM" ] && [ ! -d "$ASM" ]; then
|
|
87
|
+
die "recorded assembly path $ASM exists but is not a directory — refusing everything."
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
# 3c. the plugin half (final amendment): the exact QUALIFIED must be ABSENT (a previous
|
|
91
|
+
# run got that far — retry-safe) or present EXACTLY ONCE at the state's recorded
|
|
92
|
+
# version. A different version is `version-drift` — something other than the
|
|
93
|
+
# recorded install put it there, and this inverse must not remove what it cannot
|
|
94
|
+
# prove it installed. Malformed/multiple exact rows refuse too. Longer tokens that
|
|
95
|
+
# merely contain the qualified id remain foreign, same as everywhere.
|
|
96
|
+
PLUGIN_PRESENT=0
|
|
97
|
+
LISTED_ROW="$(copilot_exact_row_version "$INSTALLED_LIST" "$QUALIFIED")" \
|
|
98
|
+
|| die "the 'copilot plugin list' rows for $QUALIFIED are malformed or ambiguous (see above) — refusing with zero vendor/filesystem writes."
|
|
99
|
+
case "$LISTED_ROW" in
|
|
100
|
+
absent) : ;;
|
|
101
|
+
"one $STATE_PLUGIN_VERSION") PLUGIN_PRESENT=1 ;;
|
|
102
|
+
one\ *)
|
|
103
|
+
die "version-drift: $QUALIFIED is listed at ${LISTED_ROW#one }, but the ownership state recorded v$STATE_PLUGIN_VERSION. This inverse removes only what it provably installed — refusing with zero vendor/filesystem writes (plugin, marketplace, assembly and state all preserved). Re-run './run.sh install-copilot-bridge' to re-bind ownership, then remove." ;;
|
|
104
|
+
*) die "unexpected exact-row verdict '$LISTED_ROW' — refusing with zero writes." ;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
# --- 4. vendor mutation (licensed by the complete preflight above) -----------
|
|
108
|
+
if [ "$PLUGIN_PRESENT" -eq 1 ]; then
|
|
109
|
+
copilot plugin uninstall "$QUALIFIED" >/dev/null \
|
|
110
|
+
|| die "'copilot plugin uninstall $QUALIFIED' failed — stopping with the state retained; re-run to finish."
|
|
111
|
+
echo "[copilot-bridge-uninstall] uninstalled $QUALIFIED"
|
|
112
|
+
else
|
|
113
|
+
echo "[copilot-bridge-uninstall] $QUALIFIED (v$STATE_PLUGIN_VERSION) is already absent from 'copilot plugin list' (a previous run got this far) — continuing"
|
|
114
|
+
fi
|
|
115
|
+
if [ "$MKT_PRESENT" -eq 1 ]; then
|
|
116
|
+
copilot plugin marketplace remove "$MKT_NAME" >/dev/null \
|
|
117
|
+
|| die "'copilot plugin marketplace remove $MKT_NAME' failed (never retried with --force) — stopping with the state retained; re-run to finish."
|
|
118
|
+
echo "[copilot-bridge-uninstall] removed marketplace $MKT_NAME"
|
|
119
|
+
else
|
|
120
|
+
echo "[copilot-bridge-uninstall] marketplace $MKT_NAME is already absent (a previous run got this far) — continuing"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- 5. the assembly: only the recorded directory, only when owned -----------
|
|
124
|
+
if [ "$OWNED_ASM" = "true" ]; then
|
|
125
|
+
if [ -d "$ASM" ]; then
|
|
126
|
+
rm -rf "$ASM"
|
|
127
|
+
echo "[copilot-bridge-uninstall] removed assembly $ASM"
|
|
128
|
+
else
|
|
129
|
+
echo "[copilot-bridge-uninstall] assembly already absent at $ASM (a previous run got this far) — continuing"
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
echo "[copilot-bridge-uninstall] state does not record assembly ownership — preserving $ASM"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# --- 6. the state, LAST ------------------------------------------------------
|
|
136
|
+
rm -f "$STATE_FILE"
|
|
137
|
+
echo "[copilot-bridge-uninstall] DONE — ownership state cleared ($STATE_FILE). The stale Claude unit, if any, was deliberately not touched (install-only territory)."
|
package/scripts/dev-bin.sh
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
# each recorded in its OWN checkout-OUTSIDE install-state (<name>.install-state.json) for an
|
|
8
8
|
# honest inverse.
|
|
9
9
|
#
|
|
10
|
-
# Managed bins (
|
|
11
|
-
#
|
|
10
|
+
# Managed bins (all are BARE names — no repo/checkout path is EVER written into a harness
|
|
11
|
+
# config; this only makes each package command RESOLVE in a dev checkout):
|
|
12
|
+
# entwurf -> run.sh (operator command / managed Copilot runtime)
|
|
12
13
|
# entwurf-bridge -> mcp/entwurf-bridge/start.sh (agy mcp_config command)
|
|
13
14
|
# entwurf-agy-statusline -> scripts/agy-statusline.sh (agy settings.statusLine command)
|
|
14
15
|
# entwurf-agy-imprint -> scripts/agy-imprint.sh (agy PreInvocation birth hook)
|
|
@@ -27,12 +28,16 @@
|
|
|
27
28
|
# name on a later expose is re-reported (drift visibility).
|
|
28
29
|
#
|
|
29
30
|
# expose [name] create/refresh managed link(s) (idempotent; REFUSE foreign → exit 3).
|
|
30
|
-
# no name = all managed bins
|
|
31
|
+
# no name = all managed bins, each attempted INDEPENDENTLY (#86 C1): one
|
|
32
|
+
# foreign refusal no longer aborts the loop, the trailing summary reports
|
|
33
|
+
# attempted/ok/refused truthfully, and any refusal still exits 3.
|
|
31
34
|
# remove [name] honest inverse from state (remove only OUR link; REFUSE if it became foreign).
|
|
32
35
|
# no name = all managed bins.
|
|
36
|
+
# verify <name> require OUR symlink → target and that exact link as the PATH winner.
|
|
33
37
|
#
|
|
34
38
|
# Overridable for the isolated smoke:
|
|
35
|
-
# ENTWURF_DEV_BIN_DIR
|
|
39
|
+
# ENTWURF_DEV_BIN_DIR link location (default: ~/.local/bin — where agy itself lives)
|
|
40
|
+
# ENTWURF_OPERATOR_TARGET entwurf target (default: $REPO/run.sh)
|
|
36
41
|
# ENTWURF_BRIDGE_TARGET entwurf-bridge target (default: $REPO/mcp/entwurf-bridge/start.sh)
|
|
37
42
|
# ENTWURF_AGY_STATUSLINE_TARGET entwurf-agy-statusline target (default: $REPO/scripts/agy-statusline.sh)
|
|
38
43
|
# ENTWURF_AGY_IMPRINT_TARGET entwurf-agy-imprint target (default: $REPO/scripts/agy-imprint.sh)
|
|
@@ -47,7 +52,7 @@ BIN_DIR="${ENTWURF_DEV_BIN_DIR:-$HOME/.local/bin}"
|
|
|
47
52
|
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/dev-bin"
|
|
48
53
|
LEGACY_STATE="$STATE_DIR/install-state.json" # pre-multi-bin single state == entwurf-bridge
|
|
49
54
|
|
|
50
|
-
MANAGED_BINS="entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline"
|
|
55
|
+
MANAGED_BINS="entwurf entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline"
|
|
51
56
|
|
|
52
57
|
log() { printf '%s\n' "$*"; }
|
|
53
58
|
warn() { printf '%s\n' "$*" >&2; }
|
|
@@ -55,6 +60,7 @@ fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
|
|
55
60
|
|
|
56
61
|
bin_target() { # $1 = name → prints target path (env-overridable for smoke)
|
|
57
62
|
case "$1" in
|
|
63
|
+
entwurf) echo "${ENTWURF_OPERATOR_TARGET:-$REPO_DIR/run.sh}" ;;
|
|
58
64
|
entwurf-bridge) echo "${ENTWURF_BRIDGE_TARGET:-$REPO_DIR/mcp/entwurf-bridge/start.sh}" ;;
|
|
59
65
|
entwurf-agy-statusline) echo "${ENTWURF_AGY_STATUSLINE_TARGET:-$REPO_DIR/scripts/agy-statusline.sh}" ;;
|
|
60
66
|
entwurf-agy-imprint) echo "${ENTWURF_AGY_IMPRINT_TARGET:-$REPO_DIR/scripts/agy-imprint.sh}" ;;
|
|
@@ -139,7 +145,9 @@ PY
|
|
|
139
145
|
fi
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
expose_one() { # $1 = name
|
|
148
|
+
expose_one() { # $1 = name; a foreign link RETURNS 3 (no exit) so the no-arg
|
|
149
|
+
# composition can attempt every unit independently (#86 C1) —
|
|
150
|
+
# a broken checkout (missing/unknown target) still fails loud.
|
|
143
151
|
local name="$1" target link sf detect
|
|
144
152
|
target="$(bin_target "$name")" || fail "unknown managed bin: $name"
|
|
145
153
|
link="$BIN_DIR/$name"; sf="$(state_file_for "$name")"
|
|
@@ -149,7 +157,7 @@ expose_one() { # $1 = name
|
|
|
149
157
|
[ -x "$target" ] || fail "target is not an executable file: $target (is this a dev checkout?)"
|
|
150
158
|
if ! link_is_ours "$link" "$target" "$sf"; then
|
|
151
159
|
warn "[dev-bin] REFUSE ($name): $link exists and is NOT ours (a foreign file/symlink — e.g. a real npm consumer bin). Not clobbering someone else's SSOT."
|
|
152
|
-
|
|
160
|
+
return 3
|
|
153
161
|
fi
|
|
154
162
|
detect="created-new"; [ -L "$link" ] && detect="refresh-ours"
|
|
155
163
|
mkdir -p "$BIN_DIR"
|
|
@@ -161,6 +169,21 @@ expose_one() { # $1 = name
|
|
|
161
169
|
fi
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
verify_one() { # $1 = name
|
|
173
|
+
local name="$1" target link resolved
|
|
174
|
+
target="$(bin_target "$name")" || fail "unknown managed bin: $name"
|
|
175
|
+
link="$BIN_DIR/$name"
|
|
176
|
+
resolved="$(command -v "$name" 2>/dev/null || true)"
|
|
177
|
+
if [ ! -L "$link" ] || [ "$(readlink "$link" 2>/dev/null || true)" != "$target" ] || [ "$resolved" != "$link" ]; then
|
|
178
|
+
warn "[dev-bin] FAIL ($name): source command is not the PATH winner owned by this checkout."
|
|
179
|
+
warn " expected: $link -> $target"
|
|
180
|
+
warn " resolved: ${resolved:-<none>}"
|
|
181
|
+
warn " fix: remove a foreign/shadowing bin or put $BIN_DIR on PATH, then re-run setup."
|
|
182
|
+
return 1
|
|
183
|
+
fi
|
|
184
|
+
log "[dev-bin verify] $name → $target (PATH winner: $link)"
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
remove_one() { # $1 = name
|
|
165
188
|
local name="$1" target link sf recorded
|
|
166
189
|
target="$(bin_target "$name")" || fail "unknown managed bin: $name"
|
|
@@ -185,7 +208,24 @@ remove_one() { # $1 = name
|
|
|
185
208
|
|
|
186
209
|
do_expose() { # $1 = optional bin name
|
|
187
210
|
migrate_legacy
|
|
188
|
-
if [ -n "${1:-}" ]; then expose_one "$1";
|
|
211
|
+
if [ -n "${1:-}" ]; then expose_one "$1"; return $?; fi
|
|
212
|
+
# No-arg = the setup composition. Attempt EVERY unit independently (#86 C1):
|
|
213
|
+
# the old loop stopped at the first foreign refusal while the caller's warning
|
|
214
|
+
# claimed only that one bin was left as-is — units after it were never
|
|
215
|
+
# attempted. Now each refusal is recorded, later units still run, and the
|
|
216
|
+
# summary line reports attempted/ok/refused so no caller can claim less than
|
|
217
|
+
# what happened. Any refusal keeps the loop's exit 3 contract.
|
|
218
|
+
local attempted=0 exposed=0 refused="" rc b
|
|
219
|
+
for b in $MANAGED_BINS; do
|
|
220
|
+
attempted=$((attempted + 1))
|
|
221
|
+
rc=0; expose_one "$b" || rc=$?
|
|
222
|
+
if [ "$rc" -eq 0 ]; then exposed=$((exposed + 1)); else refused="$refused $b"; fi
|
|
223
|
+
done
|
|
224
|
+
if [ -n "$refused" ]; then
|
|
225
|
+
warn "[dev-bin expose] summary: attempted=$attempted ok=$exposed refused=$((attempted - exposed)):$refused (every unit was attempted independently)"
|
|
226
|
+
return 3
|
|
227
|
+
fi
|
|
228
|
+
log "[dev-bin expose] summary: attempted=$attempted ok=$exposed refused=0"
|
|
189
229
|
}
|
|
190
230
|
do_remove() { # $1 = optional bin name
|
|
191
231
|
if [ -n "${1:-}" ]; then remove_one "$1"; else for b in $MANAGED_BINS; do remove_one "$b"; done; fi
|
|
@@ -194,5 +234,6 @@ do_remove() { # $1 = optional bin name
|
|
|
194
234
|
case "${1:-}" in
|
|
195
235
|
expose) shift; do_expose "${1:-}" ;;
|
|
196
236
|
remove) shift; do_remove "${1:-}" ;;
|
|
197
|
-
|
|
237
|
+
verify) shift; [ -n "${1:-}" ] || fail "verify requires one managed bin name"; verify_one "$1" ;;
|
|
238
|
+
*) echo "usage: dev-bin.sh <expose|remove> [bin-name] | verify <bin-name>" >&2; exit 2 ;;
|
|
198
239
|
esac
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fake-copilot-vendor.sh — ONE factory for the stateful fake Copilot CLI the
|
|
3
|
+
# aggregate-setup gates drive (#86 C3b). Two consumers share it so their fakes
|
|
4
|
+
# cannot drift: smoke-setup-verdict's copilot-present cell and check-pack-install's
|
|
5
|
+
# installed copilot-present consumer row. The answer shapes are the MEASURED CLI's
|
|
6
|
+
# (copilot 1.0.80, 2026-08-27) — the same shapes check-copilot-birth-hook.ts bakes
|
|
7
|
+
# into its own TS fake: `plugin list` prints qualified ids with a `(vX)` suffix,
|
|
8
|
+
# `plugin marketplace list` prints `<name> (Local: <abs path>)`, and `--force`
|
|
9
|
+
# anywhere is refused loudly (the real `marketplace remove --force` uninstalls that
|
|
10
|
+
# marketplace's plugins as a side effect; no entwurf surface may reach for it).
|
|
11
|
+
#
|
|
12
|
+
# This factory models the SUCCESS host only. A failing-list vendor is two lines and
|
|
13
|
+
# stays inline in the cell that needs it (check-setup-qualification Cell D).
|
|
14
|
+
#
|
|
15
|
+
# usage: bash scripts/fake-copilot-vendor.sh <dir> <plugin_json>
|
|
16
|
+
#
|
|
17
|
+
# EXECUTE this file; do not source it. `_check_pack_install_impl` holds a
|
|
18
|
+
# `trap … RETURN` around its temp roots, and bash fires a RETURN trap when a
|
|
19
|
+
# `.`/`source` finishes too — sourcing this factory there deleted the whole
|
|
20
|
+
# npm sandbox mid-gate (measured 2026-08-27). A child process cannot.
|
|
21
|
+
#
|
|
22
|
+
# Writes <dir>/copilot (executable) plus its state files into <dir>:
|
|
23
|
+
# installed.txt qualified ids `plugin install` appended
|
|
24
|
+
# marketplaces.txt `<name>\t<path>` rows `plugin marketplace add` appended
|
|
25
|
+
# calls.log every argv line the fake answered
|
|
26
|
+
# The advertised version is read from <plugin_json> (the same file the real
|
|
27
|
+
# installer reads), so the post-install exact-row check joins on one SSOT.
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
make_fake_copilot() { # $1 = dir, $2 = plugin.json path
|
|
31
|
+
local dir="$1" plugin_json="$2" ver
|
|
32
|
+
ver="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["version"])' "$plugin_json")" || return 1
|
|
33
|
+
mkdir -p "$dir"
|
|
34
|
+
: > "$dir/installed.txt"
|
|
35
|
+
: > "$dir/marketplaces.txt"
|
|
36
|
+
: > "$dir/calls.log"
|
|
37
|
+
cat > "$dir/copilot" <<FAKE
|
|
38
|
+
#!/usr/bin/env bash
|
|
39
|
+
STATE="$dir/installed.txt"
|
|
40
|
+
MKTS="$dir/marketplaces.txt"
|
|
41
|
+
LOG="$dir/calls.log"
|
|
42
|
+
VER="$ver"
|
|
43
|
+
echo "\$*" >> "\$LOG"
|
|
44
|
+
for a in "\$@"; do [ "\$a" = "--force" ] && { echo "fake copilot: --force is forbidden here" >&2; exit 99; }; done
|
|
45
|
+
case "\$1 \$2 \$3" in
|
|
46
|
+
"plugin marketplace list")
|
|
47
|
+
echo "Included with GitHub Copilot:"
|
|
48
|
+
while IFS=\$'\t' read -r n p; do [ -n "\$n" ] && echo " • \$n (Local: \$p)"; done < "\$MKTS"
|
|
49
|
+
exit 0 ;;
|
|
50
|
+
"plugin marketplace add") printf "%s\t%s\n" "meta-bridge-copilot-local" "\$4" >> "\$MKTS"; exit 0 ;;
|
|
51
|
+
"plugin marketplace remove") awk -F"\t" -v n="\$4" '\$1 != n' "\$MKTS" > "\$MKTS.tmp"; mv "\$MKTS.tmp" "\$MKTS"; exit 0 ;;
|
|
52
|
+
esac
|
|
53
|
+
case "\$1 \$2" in
|
|
54
|
+
"plugin list") echo "Installed plugins:"; sed "s/^/ • /;s/\$/ (v\$VER)/" "\$STATE"; exit 0 ;;
|
|
55
|
+
"plugin uninstall") grep -Fvx "\$3" "\$STATE" > "\$STATE.tmp"; mv "\$STATE.tmp" "\$STATE"; exit 0 ;;
|
|
56
|
+
"plugin install") echo "\$3" >> "\$STATE"; exit 0 ;;
|
|
57
|
+
esac
|
|
58
|
+
exit 0
|
|
59
|
+
FAKE
|
|
60
|
+
chmod +x "$dir/copilot"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if [ "$#" -ne 2 ]; then
|
|
64
|
+
echo "usage: fake-copilot-vendor.sh <dir> <plugin_json>" >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
make_fake_copilot "$1" "$2"
|
|
@@ -90,7 +90,7 @@ const H_FS = /from\s+["']node:fs["']|require\(["']node:fs["']\)/;
|
|
|
90
90
|
// "process.env.LIVE"; it spawns no live turn of its own. So the predicate runs on the
|
|
91
91
|
// CODE-ONLY projection (comments and inert literals blanked, shell expansions kept)
|
|
92
92
|
// and matches a variable READ or an assignment, never prose.
|
|
93
|
-
const H_LIVE = /process\.env\.LIVE\b|\$\{LIVE[:}
|
|
93
|
+
const H_LIVE = /process\.env\.LIVE\b|\$\{LIVE[:}-]|\$LIVE\b|(?:^|\n)\s*(?:export\s+)?LIVE=1\b/;
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Blank what cannot gate execution, so H_LIVE reads code and not prose.
|
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
# would make the CLAUDE doctor permanently red for a refusal on a different rail, with
|
|
16
16
|
# a prescription pointing at a Claude wake failure that never happened (cross-review,
|
|
17
17
|
# glm, 2026-08-21). Copilot's own recovery rule lives in copilot-bridge-doctor.sh.
|
|
18
|
+
#
|
|
19
|
+
# The OMP birth unit (#87) joins the same file tagged `LEVEL [omp]`, and the rule extends
|
|
20
|
+
# for the same reason plus a sharper one: omp's own scope fence writes a line for EVERY
|
|
21
|
+
# task subagent it refuses (`INFO [omp] scope-refused`), which is the designed answer, and
|
|
22
|
+
# its failures are mint/marker faults on a rail whose recovery token — `create`/`attach` —
|
|
23
|
+
# is not `armed watch` either. omp's recovery rule lives in omp-bridge-doctor.sh. Excluding
|
|
24
|
+
# a backend here is never a way to quiet a red: it is how each doctor keeps judging only
|
|
25
|
+
# the evidence it owns.
|
|
18
26
|
meta_bridge_hook_log_status() {
|
|
19
27
|
local log="$1"
|
|
20
28
|
local own
|
|
21
|
-
own="$(grep -v ' \[copilot\] ' "$log" 2>/dev/null || true)"
|
|
29
|
+
own="$(grep -v -e ' \[copilot\] ' -e ' \[omp\] ' "$log" 2>/dev/null || true)"
|
|
22
30
|
if ! printf '%s\n' "$own" | grep -q ' ERROR '; then
|
|
23
31
|
echo "no-error"
|
|
24
32
|
return 0
|