@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
|
@@ -28,11 +28,40 @@ Scope asymmetry (봉인계약 4·6, REASONED — not the unfounded asymmetry dev
|
|
|
28
28
|
our-managed shapes (the bare bin AND the legacy repo path). project-scope state is a
|
|
29
29
|
NAMED FOLLOW-UP (NEXT), deliberately out of this lane.
|
|
30
30
|
|
|
31
|
+
USER-SCOPE OWNER BINDING (#86 C2): the user install-state additionally records
|
|
32
|
+
`installerRoot` (the repo/package root that installed the key), so an old root's
|
|
33
|
+
inverse can no longer delete the CURRENT stable provider key. Schema stays
|
|
34
|
+
version 1 — installerRoot is an optional field, judged by ONE typed classifier
|
|
35
|
+
(pi_settings_io.classify_installer_root, shared with the doctor-pi-package
|
|
36
|
+
coupling): absent/null is LEGACY, a non-empty string is the owner, and every
|
|
37
|
+
OTHER type (empty string, number, bool, object, array) is CORRUPT — install and
|
|
38
|
+
remove both refuse it (exit 4) before any write, because a state MORE
|
|
39
|
+
unattributed than legacy must never be treated MORE leniently than legacy:
|
|
40
|
+
- install, legacy state → safe ADOPTION: proceed and rewrite the state
|
|
41
|
+
with installerRoot = this root (named on stdout);
|
|
42
|
+
- install, installerRoot ≠ root → REFUSE (exit 6), zero settings write, unless
|
|
43
|
+
the operator-explicit --takeover replaces it
|
|
44
|
+
(old root reported);
|
|
45
|
+
- remove, installerRoot == root → today's honest inverse;
|
|
46
|
+
- remove, installerRoot ≠ root, owner LIVE → REFUSE (exit 6);
|
|
47
|
+
- remove, installerRoot ≠ root, owner MISSING → REFUSE unless --orphan-cleanup,
|
|
48
|
+
which only run.sh's aligned remove-user-scope path passes (package entry +
|
|
49
|
+
package state + provider installerRoot all naming that same missing root);
|
|
50
|
+
- remove, legacy state (no installerRoot) → REFUSE (fail-closed): an
|
|
51
|
+
unattributed key accepts no inverse; a same-root install/setup ADOPTS the
|
|
52
|
+
state first (named), then remove works.
|
|
53
|
+
- takeover over a USER-OVERRIDE key → split verdict: the override is preserved
|
|
54
|
+
and stays unowned, the stale ownership state is cleared, and the report says
|
|
55
|
+
"package owner moved; provider override preserved" — never a false both-owned.
|
|
56
|
+
`--preflight` runs the SAME ownership decision READ-ONLY (identical exit codes,
|
|
57
|
+
zero writes) so run.sh can complete both the package and provider preflights
|
|
58
|
+
before either writer runs (atomic user-scope operations).
|
|
59
|
+
|
|
31
60
|
Subcommands:
|
|
32
|
-
install <settings_path> <repo_dir> --scope <user|project> [--state <state_path>]
|
|
33
|
-
remove <settings_path> <repo_dir> --scope <user|project> [--state <state_path>]
|
|
61
|
+
install <settings_path> <repo_dir> --scope <user|project> [--state <state_path>] [--takeover] [--preflight]
|
|
62
|
+
remove <settings_path> <repo_dir> --scope <user|project> [--state <state_path>] [--orphan-cleanup] [--preflight]
|
|
34
63
|
|
|
35
|
-
Exit codes: 0 ok · 2 no-state · 3 refuse-symlink · 4 invalid-json · 5 usage.
|
|
64
|
+
Exit codes: 0 ok · 2 no-state · 3 refuse-symlink · 4 invalid-json/corrupt-state · 5 usage · 6 ownership-refusal.
|
|
36
65
|
"""
|
|
37
66
|
|
|
38
67
|
import json
|
|
@@ -47,7 +76,7 @@ import sys
|
|
|
47
76
|
# holds this directory when the script is run by path (how run.sh and the gates invoke
|
|
48
77
|
# it); the explicit insert keeps the import true under any other invocation form.
|
|
49
78
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
50
|
-
from pi_settings_io import detect_indent, dumps, unchanged # noqa: E402
|
|
79
|
+
from pi_settings_io import classify_installer_root, detect_indent, dumps, unchanged # noqa: E402
|
|
51
80
|
|
|
52
81
|
SERVER_KEY = "entwurf-bridge"
|
|
53
82
|
BARE_COMMAND = "entwurf-bridge"
|
|
@@ -172,12 +201,49 @@ def _provider_servers(data: dict, create: bool):
|
|
|
172
201
|
return provider, servers
|
|
173
202
|
|
|
174
203
|
|
|
175
|
-
def
|
|
204
|
+
def _load_state_file(state_path: str) -> dict | None:
|
|
205
|
+
if not state_path or not os.path.exists(state_path):
|
|
206
|
+
return None
|
|
207
|
+
state = _load(state_path)
|
|
208
|
+
return state
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str, takeover: bool = False,
|
|
212
|
+
preflight: bool = False) -> None:
|
|
176
213
|
if os.path.islink(settings_path):
|
|
177
214
|
target = os.readlink(settings_path)
|
|
178
215
|
_die(3, f"register-pi-provider: refusing to adopt {settings_path} — it is a symlink to {target} "
|
|
179
216
|
f"(someone else's SSOT). Manage it there, or replace it with a regular file, then retry.")
|
|
180
217
|
|
|
218
|
+
# Owner binding BEFORE any settings mutation (#86 C2): a user-scope state whose
|
|
219
|
+
# installerRoot names another root refuses the whole install — zero settings
|
|
220
|
+
# bytes — unless the operator-explicit takeover replaces it.
|
|
221
|
+
prior_state = _load_state_file(state_path) if scope == "user" else None
|
|
222
|
+
prior_kind, prior_root = ("legacy", None)
|
|
223
|
+
if scope == "user" and isinstance(prior_state, dict):
|
|
224
|
+
# State↔target binding (#86 C2 final amendment): the state names WHICH
|
|
225
|
+
# settings file it manages; an operation targeting a different file is an
|
|
226
|
+
# ownership mismatch — fail closed before either writer runs.
|
|
227
|
+
prior_managed = prior_state.get("managedSettingsPath")
|
|
228
|
+
if not isinstance(prior_managed, str):
|
|
229
|
+
_die(4, f"register-pi-provider: install-state {state_path} has no managedSettingsPath string")
|
|
230
|
+
if os.path.abspath(prior_managed) != os.path.abspath(settings_path):
|
|
231
|
+
_die(6, f"register-pi-provider: install-state {state_path} manages {prior_managed}, but this "
|
|
232
|
+
f"operation targets {settings_path} — ownership record and target disagree; zero settings bytes written.")
|
|
233
|
+
# One typed installerRoot verdict (#86 C2 corrective amendment, B blocker 3):
|
|
234
|
+
# a wrong-TYPE value used to fall between the isinstance(str) arm and the
|
|
235
|
+
# is-None legacy arm and proceed unattributed — now it is CORRUPT, refused
|
|
236
|
+
# before any settings or state write.
|
|
237
|
+
prior_kind, prior_root = classify_installer_root(prior_state)
|
|
238
|
+
if prior_kind == "corrupt":
|
|
239
|
+
_die(4, f"register-pi-provider: install-state {state_path} has a CORRUPT installerRoot — expected "
|
|
240
|
+
"a non-empty string (owner) or null/absent (legacy). Refusing to install over an "
|
|
241
|
+
"unattributed state; repair or remove the state file first — zero settings bytes written.")
|
|
242
|
+
if scope == "user" and prior_kind == "owner" and prior_root != repo_dir and not takeover:
|
|
243
|
+
_die(6, f"register-pi-provider: the user-scope {SERVER_KEY} key is owned by another root: {prior_root}. "
|
|
244
|
+
"Normal install never replaces another owner — zero settings bytes written. "
|
|
245
|
+
"Use './run.sh takeover-user-scope' to move it explicitly.")
|
|
246
|
+
|
|
181
247
|
raw = _read(settings_path)
|
|
182
248
|
# Parsed TWICE on purpose: everything below mutates in place, so `before` has to be
|
|
183
249
|
# an independent document or the comparison would be against itself.
|
|
@@ -189,12 +255,28 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str)
|
|
|
189
255
|
existing_cmd = existing.get("command") if isinstance(existing, dict) else existing
|
|
190
256
|
ownership = _classify(existing_cmd, repo_dir)
|
|
191
257
|
|
|
258
|
+
if preflight:
|
|
259
|
+
# Read-only half of the atomic user-scope operation: the ownership decision
|
|
260
|
+
# above already refused a foreign installerRoot; everything past this point
|
|
261
|
+
# would write, so report the classification and stop.
|
|
262
|
+
sys.stdout.write(f"preflight: install ok (ownership={ownership}{', takeover' if takeover else ''})\n")
|
|
263
|
+
return
|
|
264
|
+
|
|
192
265
|
if ownership == "user-override":
|
|
193
266
|
# DO NOT overwrite our key, DO NOT own it (no state). doctor reports it as unowned. Still
|
|
194
267
|
# persist IF the legacy prune above (or a materialized parent) actually changed something.
|
|
195
268
|
sys.stdout.write(
|
|
196
269
|
f"install: preserved entwurfProvider.mcpServers.{SERVER_KEY} (user override, NOT owned: {existing_cmd!r})\n"
|
|
197
270
|
)
|
|
271
|
+
if scope == "user" and takeover and state_path and os.path.exists(state_path):
|
|
272
|
+
# Split verdict (#86 C2): the package half moved, but the provider key is
|
|
273
|
+
# the OPERATOR'S override — preserve it, and clear the stale ownership
|
|
274
|
+
# state so no root claims a key nobody manages. Never a false "both owned".
|
|
275
|
+
os.remove(state_path)
|
|
276
|
+
sys.stdout.write(
|
|
277
|
+
"takeover: provider override preserved, unowned — stale provider ownership state cleared "
|
|
278
|
+
"(package owner moved; the provider key remains the operator's)\n"
|
|
279
|
+
)
|
|
198
280
|
if not _persist(settings_path, before, data, raw):
|
|
199
281
|
sys.stdout.write(f"install: no change — {settings_path} left untouched (bytes and mtime stable)\n")
|
|
200
282
|
return
|
|
@@ -219,6 +301,16 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str)
|
|
|
219
301
|
|
|
220
302
|
if scope == "user":
|
|
221
303
|
if state_path:
|
|
304
|
+
if prior_state is not None and prior_kind == "legacy":
|
|
305
|
+
# LEGACY v1 state (pre-#86 C2, no installerRoot): safe adoption — the
|
|
306
|
+
# rewrite below binds it to this root, and the adoption is named.
|
|
307
|
+
sys.stdout.write(
|
|
308
|
+
f"install: adopted legacy provider install-state (no installerRoot recorded) — now bound to {repo_dir}\n"
|
|
309
|
+
)
|
|
310
|
+
if takeover and prior_kind == "owner" and prior_root != repo_dir:
|
|
311
|
+
sys.stdout.write(
|
|
312
|
+
f"takeover: user-scope {SERVER_KEY} provider ownership moved {prior_root} -> {repo_dir}\n"
|
|
313
|
+
)
|
|
222
314
|
state = {
|
|
223
315
|
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
224
316
|
"managedSettingsPath": os.path.abspath(settings_path),
|
|
@@ -226,6 +318,7 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str)
|
|
|
226
318
|
"key": f"entwurfProvider.mcpServers.{SERVER_KEY}",
|
|
227
319
|
"command": BARE_COMMAND,
|
|
228
320
|
"ownership": ownership, # absent | managed-current | managed-legacy
|
|
321
|
+
"installerRoot": repo_dir, # #86 C2: the root whose inverse may remove this key
|
|
229
322
|
"preimage": existing, # raw prior value (audit only; NOT restored)
|
|
230
323
|
"installedAt": _now(),
|
|
231
324
|
}
|
|
@@ -237,22 +330,66 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str)
|
|
|
237
330
|
)
|
|
238
331
|
|
|
239
332
|
|
|
240
|
-
def cmd_remove(settings_path: str, repo_dir: str, scope: str, state_path: str
|
|
333
|
+
def cmd_remove(settings_path: str, repo_dir: str, scope: str, state_path: str, orphan: bool = False,
|
|
334
|
+
preflight: bool = False) -> None:
|
|
241
335
|
if scope == "user":
|
|
242
336
|
if not state_path or not os.path.exists(state_path):
|
|
243
337
|
sys.stdout.write("remove: no install-state — nothing to undo (never owned, or already removed).\n")
|
|
244
338
|
return
|
|
245
339
|
state = _load(state_path)
|
|
340
|
+
# Same-owner-only inverse (#86 C2): a state bound to another root must not
|
|
341
|
+
# let that OTHER root's stale inverse delete the CURRENT stable key. The
|
|
342
|
+
# typed classifier (B blocker 3) closes the wrong-TYPE gap: corrupt refuses
|
|
343
|
+
# BEFORE the foreign/legacy arms, so no value can fall between them.
|
|
344
|
+
installer_kind, installer_root = classify_installer_root(state)
|
|
345
|
+
if installer_kind == "corrupt":
|
|
346
|
+
_die(4, f"register-pi-provider: install-state {state_path} has a CORRUPT installerRoot — expected "
|
|
347
|
+
"a non-empty string (owner) or null/absent (legacy). Refusing to remove on an unattributed "
|
|
348
|
+
"state; repair or remove the state file first — zero settings bytes written.")
|
|
349
|
+
if installer_kind == "owner" and installer_root != repo_dir:
|
|
350
|
+
if os.path.isdir(installer_root):
|
|
351
|
+
_die(6, f"register-pi-provider: the user-scope {SERVER_KEY} key is owned by another LIVE root: "
|
|
352
|
+
f"{installer_root}. This root's inverse must not remove it — zero settings bytes written.")
|
|
353
|
+
if not orphan:
|
|
354
|
+
_die(6, f"register-pi-provider: the recorded owner root is MISSING: {installer_root}. "
|
|
355
|
+
"Refusing outside run.sh's aligned orphan path (remove-user-scope checks package entry, "
|
|
356
|
+
"package state and provider installerRoot together).")
|
|
357
|
+
if not preflight:
|
|
358
|
+
sys.stdout.write(
|
|
359
|
+
f"remove: orphan cleanup — proceeding for the MISSING owner {installer_root}\n"
|
|
360
|
+
)
|
|
361
|
+
elif installer_kind == "legacy":
|
|
362
|
+
# LEGACY v1 state (no installerRoot): fail-closed (#86 C2 amendment). An
|
|
363
|
+
# unattributed state must not let ANY root's inverse — least of all an old
|
|
364
|
+
# checkout's — delete the current stable key. Adoption is install-only:
|
|
365
|
+
# run install/setup from the owning root first, then remove.
|
|
366
|
+
_die(6, f"register-pi-provider: install-state {state_path} is LEGACY (no installerRoot recorded) — "
|
|
367
|
+
"refusing to remove an unattributed key. Run './run.sh setup' or 'entwurf install' from the "
|
|
368
|
+
"owning root first (named adoption binds the state), then remove.")
|
|
369
|
+
# PREFLIGHT COMPLETENESS (#86 C2 final amendment): everything the writer
|
|
370
|
+
# would check is checked HERE, before any green — managedSettingsPath shape
|
|
371
|
+
# and target equality, the symlink refusal, and settings/provider
|
|
372
|
+
# parseability — so a run.sh caller that saw this preflight green cannot
|
|
373
|
+
# have its provider writer break after the package writer already wrote.
|
|
246
374
|
managed = state.get("managedSettingsPath")
|
|
247
375
|
if not isinstance(managed, str):
|
|
248
376
|
_die(4, f"register-pi-provider: install-state {state_path} has no managedSettingsPath")
|
|
377
|
+
if os.path.abspath(managed) != os.path.abspath(settings_path):
|
|
378
|
+
_die(6, f"register-pi-provider: install-state {state_path} manages {managed}, but this operation "
|
|
379
|
+
f"targets {settings_path} — ownership record and target disagree; zero settings bytes written.")
|
|
249
380
|
if os.path.islink(managed):
|
|
250
381
|
_die(3, f"register-pi-provider: refusing to uninstall — {managed} became a symlink since install.")
|
|
382
|
+
raw = ""
|
|
383
|
+
before = data = provider = servers = None
|
|
251
384
|
if os.path.exists(managed):
|
|
252
385
|
raw = _read(managed)
|
|
253
|
-
before = _parse_settings(managed, raw)
|
|
386
|
+
before = _parse_settings(managed, raw) # dies 4 on corrupt — preflight and writer alike
|
|
254
387
|
data = _parse_settings(managed, raw)
|
|
255
388
|
provider, servers = _provider_servers(data, create=False)
|
|
389
|
+
if preflight:
|
|
390
|
+
sys.stdout.write("preflight: remove ok (owner verified; managed target bound and parseable)\n")
|
|
391
|
+
return
|
|
392
|
+
if os.path.exists(managed):
|
|
256
393
|
# honest inverse: absent/managed-* → remove OUR key (a legacy repo path is NOT
|
|
257
394
|
# restored — it was our old managed value, not a user value).
|
|
258
395
|
if isinstance(servers, dict):
|
|
@@ -304,8 +441,9 @@ def cmd_remove(settings_path: str, repo_dir: str, scope: str, state_path: str) -
|
|
|
304
441
|
|
|
305
442
|
|
|
306
443
|
def _parse(argv: list):
|
|
307
|
-
# positional: settings_path repo_dir ; flags: --scope <s> [--state <p>]
|
|
444
|
+
# positional: settings_path repo_dir ; flags: --scope <s> [--state <p>] [--takeover] [--orphan-cleanup] [--preflight]
|
|
308
445
|
pos, scope, state_path = [], None, ""
|
|
446
|
+
takeover, orphan, preflight = False, False, False
|
|
309
447
|
i = 0
|
|
310
448
|
while i < len(argv):
|
|
311
449
|
a = argv[i]
|
|
@@ -315,17 +453,23 @@ def _parse(argv: list):
|
|
|
315
453
|
elif a == "--state":
|
|
316
454
|
i += 1
|
|
317
455
|
state_path = argv[i] if i < len(argv) else ""
|
|
456
|
+
elif a == "--takeover":
|
|
457
|
+
takeover = True
|
|
458
|
+
elif a == "--orphan-cleanup":
|
|
459
|
+
orphan = True
|
|
460
|
+
elif a == "--preflight":
|
|
461
|
+
preflight = True
|
|
318
462
|
else:
|
|
319
463
|
pos.append(a)
|
|
320
464
|
i += 1
|
|
321
|
-
return pos, scope, state_path
|
|
465
|
+
return pos, scope, state_path, takeover, orphan, preflight
|
|
322
466
|
|
|
323
467
|
|
|
324
468
|
def main(argv: list) -> None:
|
|
325
469
|
if len(argv) < 2:
|
|
326
470
|
_die(5, "usage: register-pi-provider.py <install|remove> <settings_path> <repo_dir> --scope <user|project> [--state <path>]")
|
|
327
471
|
sub = argv[1]
|
|
328
|
-
pos, scope, state_path = _parse(argv[2:])
|
|
472
|
+
pos, scope, state_path, takeover, orphan, preflight = _parse(argv[2:])
|
|
329
473
|
if sub not in ("install", "remove"):
|
|
330
474
|
_die(5, f"register-pi-provider.py: unknown subcommand {sub!r}")
|
|
331
475
|
if len(pos) != 2:
|
|
@@ -335,10 +479,16 @@ def main(argv: list) -> None:
|
|
|
335
479
|
settings_path, repo_dir = pos[0], str(os.path.abspath(pos[1]))
|
|
336
480
|
if scope == "user" and not state_path:
|
|
337
481
|
_die(5, "register-pi-provider.py: --state is required for --scope user")
|
|
482
|
+
if (takeover or orphan or preflight) and scope != "user":
|
|
483
|
+
_die(5, "register-pi-provider.py: --takeover/--orphan-cleanup/--preflight require --scope user")
|
|
484
|
+
if takeover and sub != "install":
|
|
485
|
+
_die(5, "register-pi-provider.py: --takeover is an install action")
|
|
486
|
+
if orphan and sub != "remove":
|
|
487
|
+
_die(5, "register-pi-provider.py: --orphan-cleanup is a remove action")
|
|
338
488
|
if sub == "install":
|
|
339
|
-
cmd_install(settings_path, repo_dir, scope, state_path)
|
|
489
|
+
cmd_install(settings_path, repo_dir, scope, state_path, takeover, preflight)
|
|
340
490
|
else:
|
|
341
|
-
cmd_remove(settings_path, repo_dir, scope, state_path)
|
|
491
|
+
cmd_remove(settings_path, repo_dir, scope, state_path, orphan, preflight)
|
|
342
492
|
|
|
343
493
|
|
|
344
494
|
if __name__ == "__main__":
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
# - CREATE-NEW → uninstall removes the file it created.
|
|
21
21
|
# - SETUP INTEGRATION (막힘 ①): the `wire_agy_bridge` wrapper folded into `./run.sh setup` —
|
|
22
22
|
# agy absent → honest skip + NO state; agy present + regular → idempotent install + state;
|
|
23
|
-
# agy present + symlink/corrupt →
|
|
24
|
-
#
|
|
23
|
+
# agy present + symlink/corrupt → reason-specific WARN + leaf EXIT NONZERO (#86 C6/C7:
|
|
24
|
+
# named component FAIL, aggregate keeps later components alive; no clobber, no state).
|
|
25
|
+
# Driven via the hidden `wire-agy-bridge` subcommand with AGY_BIN pinned.
|
|
25
26
|
# - DEV BIN (막힘 ②): the managed `entwurf-bridge` symlink dev-bin.sh exposes so the agy
|
|
26
27
|
# config's BARE command resolves in a dev checkout — ownership-checked link (REFUSE a
|
|
27
28
|
# foreign bin, never a blind ln -sf), state + honest inverse (remove only OUR link), and
|
|
28
|
-
# the
|
|
29
|
+
# the truthful setup wrapper (foreign → WARN + rc=3, every unit still attempted — #86 C10).
|
|
30
|
+
# Isolated bin dir + fake target.
|
|
29
31
|
# - ⓪ discipline day-one: the checkout stays byte-identical (nothing written under $REPO).
|
|
30
32
|
# Offline + deterministic (deps: bash + python3).
|
|
31
33
|
set -euo pipefail
|
|
@@ -377,13 +379,16 @@ want "cache-prune(symlink): symlinked legacy cache left intact (not clobbered)"
|
|
|
377
379
|
"[ -L '$CACHE/pi-tools-bridge' ] && [ -d '$SB/foreign-cache' ]"
|
|
378
380
|
bash "$BRIDGE" uninstall >/dev/null; rm -f "$CACHE/pi-tools-bridge" "$GLOBAL" "$STATE"
|
|
379
381
|
|
|
380
|
-
# ── I: setup integration — wire_agy_bridge (막힘 ①: detection-gated,
|
|
382
|
+
# ── I: setup integration — wire_agy_bridge (막힘 ①: detection-gated, truthful) ─────
|
|
381
383
|
# The setup wrapper folded into `./run.sh setup`. Driven here via the hidden `wire-agy-bridge`
|
|
382
384
|
# subcommand with AGY_BIN pinned (a fake agy / a nonexistent path) so detection is hermetic
|
|
383
|
-
# regardless of the CI/dev host's real agy. Locks: agy absent → honest skip + NO
|
|
384
|
-
# present + regular → idempotent install + state; agy present +
|
|
385
|
-
#
|
|
386
|
-
#
|
|
385
|
+
# regardless of the CI/dev host's real agy. Locks (#86 C6/C7): agy absent → honest skip + NO
|
|
386
|
+
# state + exit 0; agy present + regular → idempotent install + state + exit 0; agy present +
|
|
387
|
+
# symlink/corrupt → reason-specific WARN, no clobber, no state, and EXIT NONZERO — the leaf
|
|
388
|
+
# reports a detected-incomplete integration truthfully, and the aggregate setup records the
|
|
389
|
+
# named component FAIL while keeping the remaining components alive. The old NON-FATAL exit-0
|
|
390
|
+
# posture was the false-green shape this contract retires. Clean slate here (H left no
|
|
391
|
+
# config/state).
|
|
387
392
|
rm -f "$GLOBAL" "$LEGACY" "$STATE"
|
|
388
393
|
printf '#!/usr/bin/env bash\necho fake-agy\n' > "$SB/bin/agy"; chmod +x "$SB/bin/agy"
|
|
389
394
|
|
|
@@ -408,11 +413,12 @@ want "wire(agy+regular, re-run): config still valid + entwurf-bridge present" \
|
|
|
408
413
|
want "wire(agy+regular, re-run): unrelated server still preserved" "grep -q '\"other\"' '$GLOBAL'"
|
|
409
414
|
bash "$BRIDGE" uninstall >/dev/null; rm -f "$GLOBAL"
|
|
410
415
|
|
|
411
|
-
# I-3: agy PRESENT + SYMLINK config →
|
|
416
|
+
# I-3: agy PRESENT + SYMLINK config → reason-specific WARN + EXIT NONZERO (component FAIL),
|
|
417
|
+
# no clobber, no state
|
|
412
418
|
printf '{"mcpServers":{}}\n' > "$SB/real_wire_cfg.json"
|
|
413
419
|
ln -s "$SB/real_wire_cfg.json" "$GLOBAL"
|
|
414
420
|
set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-bridge 2>&1)"; RC=$?; set -e
|
|
415
|
-
want "wire(agy+symlink): exits
|
|
421
|
+
want "wire(agy+symlink): exits nonzero (detected agy + refused config = component FAIL, not cosmetic green) [QK:SETUP-AGY-COSMETIC-RETURN]" "[ '$RC' -ne 0 ]"
|
|
416
422
|
want "wire(agy+symlink): reason-specific WARN names the symlink/SSOT" \
|
|
417
423
|
"printf '%s' \"\$OUT\" | grep -qi 'symlink'"
|
|
418
424
|
want "wire(agy+symlink): linked SSOT NOT clobbered" \
|
|
@@ -420,31 +426,69 @@ want "wire(agy+symlink): linked SSOT NOT clobbered" \
|
|
|
420
426
|
want "wire(agy+symlink): NO state written" "[ ! -f '$STATE' ]"
|
|
421
427
|
rm -f "$GLOBAL"
|
|
422
428
|
|
|
423
|
-
# I-4: agy PRESENT + CORRUPT config (invalid JSON) →
|
|
429
|
+
# I-4: agy PRESENT + CORRUPT config (invalid JSON) → reason-specific WARN + EXIT NONZERO,
|
|
430
|
+
# corrupt-specific
|
|
424
431
|
printf 'this is not json{{{' > "$GLOBAL"
|
|
425
432
|
set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-bridge 2>&1)"; RC=$?; set -e
|
|
426
|
-
want "wire(agy+corrupt): exits
|
|
433
|
+
want "wire(agy+corrupt): exits nonzero (detected agy + corrupt config = component FAIL)" "[ '$RC' -ne 0 ]"
|
|
427
434
|
want "wire(agy+corrupt): reason-specific WARN flags invalid JSON (not a silent skip)" \
|
|
428
435
|
"printf '%s' \"\$OUT\" | grep -qi 'invalid JSON'"
|
|
429
436
|
want "wire(agy+corrupt): NO state written" "[ ! -f '$STATE' ]"
|
|
430
437
|
rm -f "$GLOBAL" "$SB/bin/agy"
|
|
431
438
|
|
|
432
439
|
# ── J: dev bin exposure — dev-bin.sh (막힘 ②: managed stable-bin symlinks) ─────────
|
|
433
|
-
# dev-bin.sh
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
#
|
|
440
|
+
# dev-bin.sh manages multiple package commands, each with its OWN <name>.install-state.json.
|
|
441
|
+
# J-0 drives the NO-ARG setup composition and proves the source checkout supplies the operator
|
|
442
|
+
# command Copilot fresh resolves (`entwurf` → this checkout's run.sh-equivalent target), including
|
|
443
|
+
# a real argv-preserving invocation through PATH. J-1 onward keeps the older entwurf-bridge
|
|
444
|
+
# ownership regression byte-for-byte: REFUSE foreign, state + honest inverse, remove only OUR
|
|
445
|
+
# link, the truthful setup wrapper (rc=3 propagated), and legacy-state migration. Isolated:
|
|
446
|
+
# sandbox bin/targets/XDG.
|
|
439
447
|
DEVBIN="$REPO_DIR/scripts/dev-bin.sh"
|
|
440
448
|
DBIN_DIR="$SB/devbin"
|
|
441
449
|
DLINK="$DBIN_DIR/entwurf-bridge"
|
|
442
450
|
DSTATE="$XDG_DATA_HOME/entwurf/dev-bin/entwurf-bridge.install-state.json" # bin-scoped state
|
|
451
|
+
OLINK="$DBIN_DIR/entwurf"
|
|
452
|
+
OSTATE="$XDG_DATA_HOME/entwurf/dev-bin/entwurf.install-state.json"
|
|
443
453
|
printf '#!/usr/bin/env bash\necho fake-bridge\n' > "$SB/fake-start.sh"; chmod +x "$SB/fake-start.sh"
|
|
444
454
|
printf '#!/usr/bin/env bash\necho fake-status\n' > "$SB/fake-status.sh"; chmod +x "$SB/fake-status.sh"
|
|
455
|
+
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$@" > "${ENTWURF_OPERATOR_ARGV_LOG:?}"\n' > "$SB/fake-entwurf.sh"; chmod +x "$SB/fake-entwurf.sh"
|
|
456
|
+
printf '#!/usr/bin/env bash\nexit 0\n' > "$SB/fake-aux.sh"; chmod +x "$SB/fake-aux.sh"
|
|
445
457
|
export ENTWURF_DEV_BIN_DIR="$DBIN_DIR"
|
|
458
|
+
export ENTWURF_OPERATOR_TARGET="$SB/fake-entwurf.sh"
|
|
446
459
|
export ENTWURF_BRIDGE_TARGET="$SB/fake-start.sh"
|
|
447
|
-
export ENTWURF_AGY_STATUSLINE_TARGET="$SB/fake-status.sh"
|
|
460
|
+
export ENTWURF_AGY_STATUSLINE_TARGET="$SB/fake-status.sh"
|
|
461
|
+
export ENTWURF_AGY_IMPRINT_TARGET="$SB/fake-aux.sh"
|
|
462
|
+
export ENTWURF_COPILOT_STATUSLINE_TARGET="$SB/fake-aux.sh"
|
|
463
|
+
|
|
464
|
+
# J-0: the setup wrapper calls no-arg expose. This is the cross-contract cell the #82 regression
|
|
465
|
+
# lacked: an npm consumer gets `entwurf` from package bin linking, and source setup must expose the
|
|
466
|
+
# same operator command without a second global package install.
|
|
467
|
+
set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$REPO_DIR/run.sh" expose-dev-bin 2>&1)"; RC=$?; set -e
|
|
468
|
+
want "source setup exposes the managed Copilot runtime on PATH [QK:SOURCE-OPERATOR-BIN]" \
|
|
469
|
+
"[ '$RC' -eq 0 ] && [ -L '$OLINK' ] && [ \"\$(readlink '$OLINK')\" = '$SB/fake-entwurf.sh' ] && [ -f '$OSTATE' ]"
|
|
470
|
+
want "source operator bin resolves through the sandbox PATH" \
|
|
471
|
+
"PATH='$DBIN_DIR':\"\$PATH\" command -v entwurf | grep -Fx '$OLINK' >/dev/null"
|
|
472
|
+
OPERATOR_ARGV="$SB/operator.argv"
|
|
473
|
+
ENTWURF_OPERATOR_ARGV_LOG="$OPERATOR_ARGV" "$OLINK" copilot --model auto
|
|
474
|
+
want "source operator bin reaches its target with argv byte-for-byte" \
|
|
475
|
+
"python3 -c 'import sys; sys.exit(0 if open(sys.argv[1]).read().splitlines() == [\"copilot\",\"--model\",\"auto\"] else 1)' '$OPERATOR_ARGV'"
|
|
476
|
+
bash "$DEVBIN" remove >/dev/null 2>&1
|
|
477
|
+
want "source operator inverse removes its link and state" "[ ! -e '$OLINK' ] && [ ! -f '$OSTATE' ]"
|
|
478
|
+
bash "$DEVBIN" remove entwurf >/dev/null 2>&1
|
|
479
|
+
ok "source operator inverse is idempotent"
|
|
480
|
+
printf 'FOREIGN OPERATOR\n' > "$OLINK"
|
|
481
|
+
if bash "$DEVBIN" expose entwurf >/dev/null 2>&1; then die "dev-bin: entwurf expose should REFUSE a foreign operator bin"; fi
|
|
482
|
+
want "source operator expose leaves a foreign bin untouched" "[ \"\$(cat '$OLINK')\" = 'FOREIGN OPERATOR' ]"
|
|
483
|
+
set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$REPO_DIR/run.sh" expose-dev-bin 2>&1)"; RC=$?; set -e
|
|
484
|
+
want "source setup fails loud when it cannot own its operator command [QK:SOURCE-OPERATOR-FAILS-LOUD]" \
|
|
485
|
+
"[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'source command is not the PATH winner owned by this checkout'"
|
|
486
|
+
rm -f "$OLINK"
|
|
487
|
+
bash "$DEVBIN" expose entwurf >/dev/null 2>&1
|
|
488
|
+
set +e; OUT="$(PATH="$PATH" bash "$REPO_DIR/run.sh" expose-dev-bin 2>&1)"; RC=$?; set -e
|
|
489
|
+
want "source setup fails for the operator-certification reason when its managed bin directory is outside PATH" \
|
|
490
|
+
"[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'source command is not the PATH winner owned by this checkout'"
|
|
491
|
+
bash "$DEVBIN" remove >/dev/null 2>&1
|
|
448
492
|
|
|
449
493
|
# J-1: expose the entwurf-bridge bin BY NAME → creates the managed symlink + state (created-new)
|
|
450
494
|
bash "$DEVBIN" expose entwurf-bridge >/dev/null 2>&1
|
|
@@ -479,11 +523,20 @@ if bash "$DEVBIN" expose entwurf-bridge >/dev/null 2>&1; then die "dev-bin: expo
|
|
|
479
523
|
ok "dev-bin expose: refused a foreign bin (nonzero exit)"
|
|
480
524
|
want "dev-bin expose: foreign bin NOT clobbered" "[ \"\$(cat '$DLINK')\" = 'FOREIGN NPM BIN' ]"
|
|
481
525
|
want "dev-bin expose: no state written on foreign refuse" "[ ! -f '$DSTATE' ]"
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
526
|
+
# The setup wrapper exposes/certifies the CORE operator, and dev-bin attempts EVERY unit
|
|
527
|
+
# independently (#86 C10): a foreign helper no longer aborts the loop or hides behind a WARN —
|
|
528
|
+
# the wrapper propagates rc=3, the summary reports attempted/ok/refused truthfully, later units
|
|
529
|
+
# are still attempted, and the aggregate setup records a named bins FAIL. A helper conflict must
|
|
530
|
+
# still not erase the operator command the source lane needs.
|
|
531
|
+
set +e; OUT="$(PATH="$DBIN_DIR:$PATH" bash "$REPO_DIR/run.sh" expose-dev-bin 2>&1)"; RC=$?; set -e
|
|
532
|
+
want "dev-bin wrapper(helper foreign): rc=3 with the source operator still certified" \
|
|
533
|
+
"[ '$RC' -eq 3 ] && [ \"\$(readlink '$OLINK')\" = '$SB/fake-entwurf.sh' ]"
|
|
534
|
+
want "dev-bin wrapper(helper foreign): WARNs about a foreign bin (not ours)" "printf '%s' \"\$OUT\" | grep -qi 'not ours'"
|
|
535
|
+
want "dev-bin wrapper(helper foreign): later units were still attempted (loop did not stop at the refusal) [QK:DEV-BIN-ATTEMPTS-ALL]" \
|
|
536
|
+
"[ -L '$DBIN_DIR/entwurf-agy-statusline' ] && [ -L '$DBIN_DIR/entwurf-copilot-statusline' ]"
|
|
537
|
+
want "dev-bin wrapper(helper foreign): truthful attempted/refused summary names the refused unit" \
|
|
538
|
+
"printf '%s' \"\$OUT\" | grep -q 'refused=1: entwurf-bridge'"
|
|
539
|
+
bash "$DEVBIN" remove >/dev/null 2>&1 # clear the attempted helper links/states for the cells below
|
|
487
540
|
rm -f "$DLINK"
|
|
488
541
|
|
|
489
542
|
# J-4: remove is honest-inverse — removes ONLY our link, refuses a link that became foreign
|
|
@@ -523,7 +576,9 @@ printf '{"linkPath":"%s/something-else","target":"x"}\n' "$DBIN_DIR" > "$LEGACY"
|
|
|
523
576
|
if bash "$DEVBIN" expose entwurf-bridge >/dev/null 2>&1; then die "dev-bin migrate: foreign legacy should REFUSE"; fi
|
|
524
577
|
ok "dev-bin migrate: foreign legacy refused (linkPath basename != entwurf-bridge)"
|
|
525
578
|
rm -f "$LEGACY" "$DLINK" "$DSTATE"
|
|
526
|
-
|
|
579
|
+
bash "$DEVBIN" remove >/dev/null 2>&1
|
|
580
|
+
unset ENTWURF_DEV_BIN_DIR ENTWURF_OPERATOR_TARGET ENTWURF_BRIDGE_TARGET ENTWURF_AGY_STATUSLINE_TARGET
|
|
581
|
+
unset ENTWURF_AGY_IMPRINT_TARGET ENTWURF_COPILOT_STATUSLINE_TARGET
|
|
527
582
|
|
|
528
583
|
# ── K: permission grant — the OTHER half of a usable bridge ───────────────────
|
|
529
584
|
# Registering the server only makes the tools REACHABLE. agy defaults every `mcp` action to Ask, so
|
|
@@ -872,15 +927,16 @@ want "permission: a symlinked settings.json is left untouched (someone else's SS
|
|
|
872
927
|
"! has_rule '$SB/foreign-settings.json'"
|
|
873
928
|
want "permission: no permission state is written for a refused symlink" "[ ! -e '$PSTATE' ]"
|
|
874
929
|
|
|
875
|
-
# …and the SAME failure, seen from setup:
|
|
930
|
+
# …and the SAME failure, seen from setup: reason-specific, never silent, and truthful (#86
|
|
931
|
+
# C6/C7): a detected agy whose grant cannot be written is a component FAIL, so the wrapper
|
|
932
|
+
# exits NONZERO and the aggregate setup records it while later components stay attempted.
|
|
876
933
|
# Detection must be hermetic here exactly as it is in section I: re-mint the fake agy (I tore it
|
|
877
934
|
# down at its end) and pin AGY_BIN at it. Unpinned, the wrapper falls back to the host's PATH —
|
|
878
|
-
#
|
|
879
|
-
#
|
|
880
|
-
# NOT-GRANTED one can tell a degrade from a skip, which is the whole point of this pair.
|
|
935
|
+
# on a CI runner with no agy it would take the skip branch (exit 0, no WARN) and only the
|
|
936
|
+
# NOT-GRANTED assertion could tell a degrade from a skip.
|
|
881
937
|
printf '#!/usr/bin/env bash\necho fake-agy\n' > "$SB/bin/agy"; chmod +x "$SB/bin/agy"
|
|
882
938
|
set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-bridge 2>&1)"; RC=$?; set -e
|
|
883
|
-
want "permission(setup): the wrapper
|
|
939
|
+
want "permission(setup): the wrapper exits nonzero on the failed grant (detected FAIL, not cosmetic green)" "[ '$RC' -ne 0 ]"
|
|
884
940
|
want "permission(setup): the wrapper says the bridge is REGISTERED but NOT GRANTED (no silent pass)" \
|
|
885
941
|
"printf '%s' \"\$OUT\" | grep -q 'NOT GRANTED'"
|
|
886
942
|
rm -f "$SETTINGS" "$PSTATE" "$STATE" "$GLOBAL" "$SB/bin/agy"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
# - CREATE-NEW → uninstall removes the file it created.
|
|
17
17
|
# - SETUP INTEGRATION: the wire-agy-statusline wrapper — agy absent → honest skip + NO state;
|
|
18
18
|
# agy present + regular → idempotent install + state; agy present + symlink/corrupt →
|
|
19
|
-
#
|
|
19
|
+
# reason-specific WARN + leaf EXIT NONZERO (#86 C6/C7: the aggregate records a named
|
|
20
|
+
# component FAIL and keeps later components alive), no clobber, no state.
|
|
20
21
|
# - checkout stays byte-identical (nothing written under $REPO).
|
|
21
22
|
# Offline + deterministic (deps: bash + python3).
|
|
22
23
|
set -euo pipefail
|
|
@@ -201,7 +202,7 @@ want "create-new: state removed" "[ ! -f '$STATE' ]"
|
|
|
201
202
|
bash "$BRIDGE" uninstall >/dev/null 2>&1
|
|
202
203
|
ok "idempotent: uninstall with no state exits 0 (nothing to undo)"
|
|
203
204
|
|
|
204
|
-
# ── I: setup integration — wire-agy-statusline (detection-gated,
|
|
205
|
+
# ── I: setup integration — wire-agy-statusline (detection-gated, truthful) ──
|
|
205
206
|
rm -f "$SET" "$STATE"
|
|
206
207
|
printf '#!/usr/bin/env bash\necho fake-agy\n' > "$SB/bin/agy"; chmod +x "$SB/bin/agy"
|
|
207
208
|
|
|
@@ -222,21 +223,23 @@ set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline
|
|
|
222
223
|
want "wire(agy+regular, re-run): idempotent exit 0" "[ '$RC' -eq 0 ]"
|
|
223
224
|
bash "$BRIDGE" uninstall >/dev/null; rm -f "$SET"
|
|
224
225
|
|
|
225
|
-
# I-3: agy PRESENT + SYMLINK settings →
|
|
226
|
+
# I-3: agy PRESENT + SYMLINK settings → reason-specific WARN + EXIT NONZERO (detected component
|
|
227
|
+
# FAIL — the aggregate keeps later components alive), no clobber, no state
|
|
226
228
|
printf '{"model":"x"}\n' > "$SB/real_wire_set.json"
|
|
227
229
|
ln -s "$SB/real_wire_set.json" "$SET"
|
|
228
230
|
set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
|
|
229
|
-
want "wire(agy+symlink): exits
|
|
231
|
+
want "wire(agy+symlink): exits nonzero (detected agy + refused settings = component FAIL, not cosmetic green)" "[ '$RC' -ne 0 ]"
|
|
230
232
|
want "wire(agy+symlink): reason-specific WARN names the symlink" "printf '%s' \"\$OUT\" | grep -qi 'symlink'"
|
|
231
233
|
want "wire(agy+symlink): linked SSOT NOT clobbered" \
|
|
232
234
|
"python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if \"statusLine\" not in d else 1)' '$SB/real_wire_set.json'"
|
|
233
235
|
want "wire(agy+symlink): NO state written" "[ ! -f '$STATE' ]"
|
|
234
236
|
rm -f "$SET"
|
|
235
237
|
|
|
236
|
-
# I-4: agy PRESENT + CORRUPT settings (invalid JSON) →
|
|
238
|
+
# I-4: agy PRESENT + CORRUPT settings (invalid JSON) → reason-specific WARN + EXIT NONZERO,
|
|
239
|
+
# corrupt-specific
|
|
237
240
|
printf 'not json{{{' > "$SET"
|
|
238
241
|
set +e; OUT="$(AGY_BIN="$SB/bin/agy" bash "$REPO_DIR/run.sh" wire-agy-statusline 2>&1)"; RC=$?; set -e
|
|
239
|
-
want "wire(agy+corrupt): exits
|
|
242
|
+
want "wire(agy+corrupt): exits nonzero (detected agy + corrupt settings = component FAIL)" "[ '$RC' -ne 0 ]"
|
|
240
243
|
want "wire(agy+corrupt): reason-specific WARN flags invalid JSON" "printf '%s' \"\$OUT\" | grep -qi 'invalid JSON'"
|
|
241
244
|
want "wire(agy+corrupt): NO state written" "[ ! -f '$STATE' ]"
|
|
242
245
|
rm -f "$SET" "$SB/bin/agy"
|