@junghanacs/entwurf 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +149 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +120 -20
  5. package/VERIFY.md +10 -7
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +118 -18
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
  17. package/mcp/entwurf-bridge/src/index.ts +10 -6
  18. package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
  19. package/package.json +22 -11
  20. package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
  21. package/pi/entwurf-capabilities.json +1 -0
  22. package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
  23. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
  24. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
  25. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
  26. package/pi-extensions/entwurf-control.ts +17 -8
  27. package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
  28. package/pi-extensions/lib/meta-sender-identity.ts +10 -2
  29. package/pi-extensions/lib/meta-session.ts +102 -29
  30. package/pi-extensions/lib/mux-fresh-call.ts +82 -11
  31. package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
  32. package/run.sh +1079 -186
  33. package/scripts/agy-bridge.sh +4 -2
  34. package/scripts/check-copilot-birth-hook.ts +1174 -0
  35. package/scripts/check-copilot-launch.ts +395 -0
  36. package/scripts/check-copilot-receive-arm.ts +734 -0
  37. package/scripts/check-copilot-statusline.ts +122 -0
  38. package/scripts/check-entwurf-capabilities.ts +25 -6
  39. package/scripts/check-fresh-cut-gate.sh +104 -27
  40. package/scripts/check-gate-qualification.ts +10 -1
  41. package/scripts/check-install-container.sh +2 -2
  42. package/scripts/check-install-surface.ts +1 -1
  43. package/scripts/check-meta-capability-source.ts +89 -5
  44. package/scripts/check-meta-doctor-oracle.sh +26 -0
  45. package/scripts/check-meta-manifest-schema.py +38 -0
  46. package/scripts/check-meta-receiver-marker.ts +25 -2
  47. package/scripts/check-meta-session.ts +32 -0
  48. package/scripts/check-setup-qualification.sh +126 -0
  49. package/scripts/copilot-bridge-doctor.sh +242 -0
  50. package/scripts/copilot-bridge-install.sh +384 -0
  51. package/scripts/copilot-bridge-oracle.sh +223 -0
  52. package/scripts/copilot-bridge-uninstall.sh +137 -0
  53. package/scripts/copilot-launch.sh +236 -0
  54. package/scripts/copilot-mcp-bridge.sh +177 -0
  55. package/scripts/copilot-mcp-config.py +221 -0
  56. package/scripts/copilot-receive-bridge.sh +483 -0
  57. package/scripts/copilot-statusline-bridge.sh +149 -0
  58. package/scripts/copilot-statusline-config.py +201 -0
  59. package/scripts/copilot-statusline.sh +68 -0
  60. package/scripts/dev-bin.sh +53 -9
  61. package/scripts/fake-copilot-vendor.sh +67 -0
  62. package/scripts/meta-bridge-hook-log.sh +17 -5
  63. package/scripts/mutants/capability-cache.json +64 -0
  64. package/scripts/mutants/copilot-birth.json +244 -0
  65. package/scripts/mutants/copilot-launch.json +187 -0
  66. package/scripts/mutants/copilot-receive.json +263 -0
  67. package/scripts/mutants/fresh-cut.json +17 -0
  68. package/scripts/mutants/mux-fresh-call.json +216 -2
  69. package/scripts/mutants/pack-install.json +17 -0
  70. package/scripts/mutants/pi-package-ownership.json +92 -0
  71. package/scripts/mutants/setup-verdict.json +148 -0
  72. package/scripts/mutants/source-install.json +32 -0
  73. package/scripts/pi_settings_io.py +22 -0
  74. package/scripts/raw-async-delivery/README.md +170 -125
  75. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  76. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  77. package/scripts/register-pi-package.py +525 -7
  78. package/scripts/register-pi-provider.py +162 -12
  79. package/scripts/smoke-agy-install-state.sh +86 -30
  80. package/scripts/smoke-agy-statusline-state.sh +9 -6
  81. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  82. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  83. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  84. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  85. package/scripts/smoke-setup-verdict.sh +291 -0
  86. package/scripts/smoke-user-scope-citizen.sh +388 -1
  87. package/scripts/tsconfig.json +1 -0
@@ -168,6 +168,38 @@ check("upsertMetaSession: creates the store dir if absent; leaves no .tmp residu
168
168
  }
169
169
  });
170
170
 
171
+ check("upsertMetaSession: unknown-backend neighbour refuses as a stale reader, not a fresh-cut", () => {
172
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-stale-reader-"));
173
+ try {
174
+ const gid = "20260101T000000-0a1b2c";
175
+ fs.writeFileSync(
176
+ path.join(dir, `${gid}.meta.json`),
177
+ `${JSON.stringify({
178
+ schemaVersion: 3,
179
+ gardenId: gid,
180
+ backend: "gemini",
181
+ nativeSessionId: "n-unknown-backend",
182
+ cwd: "/synthetic",
183
+ model: null,
184
+ transcriptPath: null,
185
+ createdAt: T0.toISOString(),
186
+ recordUpdatedAt: T0.toISOString(),
187
+ })}\n`,
188
+ );
189
+ assert.throws(
190
+ () => upsertMetaSession({ dir, input: claudeInput(), now: T1 }),
191
+ (err: unknown) =>
192
+ err instanceof MetaRecordError &&
193
+ err.message.includes('"backend" must be one of') &&
194
+ err.message.includes("the reader is stale") &&
195
+ err.message.includes("do not archive those records"),
196
+ "unknown-backend store must name a stale reader, not only a fresh-cut",
197
+ );
198
+ } finally {
199
+ fs.rmSync(dir, { recursive: true, force: true });
200
+ }
201
+ });
202
+
171
203
  // ---------------------------------------------------------------- exclusive CREATE publish (#66 fail-closed)
172
204
  check("publishExclusiveIdentity: absent final publishes, round-trips, no tmp residue", () => {
173
205
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # check-setup-qualification.sh — snapshot-safe mutation-attribution oracle for the
3
+ # aggregate-setup verdict claims (#86 C1 five + C3b three). Invoked DIRECTLY by the mutant
4
+ # manifests (`bash scripts/check-setup-qualification.sh`); deliberately NOT a
5
+ # run.sh subcommand and NOT in any check tier. This file proves ONLY that each
6
+ # mutant dies at its own claim token inside the tracked-files qualification
7
+ # snapshot (no node_modules, no network, no pack). It is NOT behavior, package,
8
+ # or preflight evidence: the rich `smoke-setup-verdict` (check:package) and the
9
+ # actual packed consumer row in `check-pack-install` keep those authorities.
10
+ #
11
+ # The fake installed tree copies run.sh+package.json under a temp node_modules
12
+ # and plants ONE explicit PRECONDITION STUB at the installed store-doctor path
13
+ # (exit 0 + named stub verdict) because preflight_v3_store runs before
14
+ # SETUP_RESULTS exists. The stub is not a compiled-product twin. No bridge
15
+ # launcher is planted, so the core component FAILs BY DESIGN and every cell
16
+ # expects setup rc=1 with a computed NON-GREEN summary.
17
+ set -euo pipefail
18
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ REPO_DIR="$(cd "$HERE/.." && pwd)"
20
+ export PYTHONDONTWRITEBYTECODE=1
21
+
22
+ SB="$(mktemp -d -t entwurf-setup-qualification.XXXXXX)"
23
+ trap 'rm -rf "$SB"' EXIT
24
+ PASS=0
25
+ ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$*"; }
26
+ die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
27
+ want() { if eval "$2"; then ok "$1"; else die "$1"; fi; }
28
+
29
+ PKG="$SB/node_modules/@junghanacs/entwurf"
30
+ mkdir -p "$PKG/mcp/entwurf-bridge/dist/scripts"
31
+ cp "$REPO_DIR/run.sh" "$PKG/run.sh"; chmod +x "$PKG/run.sh"
32
+ cp "$REPO_DIR/package.json" "$PKG/package.json"
33
+ printf '%s\n' 'console.log("stub-store-doctor: precondition stub for the setup-qualification oracle — empty sandbox store, verdict clean by construction");' \
34
+ > "$PKG/mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
35
+
36
+ # Copilot composition surface (#86 C3b, Cell D): the three writer units and the
37
+ # birth installer's pre-vendor path are real tracked files; only the compiled
38
+ # dist closure is stubbed (the receive installer copies and digests those bytes,
39
+ # it never executes them — same not-a-product-twin discipline as the store stub).
40
+ mkdir -p "$PKG/scripts" "$PKG/mcp/entwurf-bridge/dist/pi-extensions/lib" "$PKG/pi-extensions/lib"
41
+ for _f in copilot-bridge-install.sh copilot-bridge-oracle.sh copilot-mcp-bridge.sh copilot-mcp-config.py \
42
+ copilot-receive-bridge.sh copilot-statusline-bridge.sh copilot-statusline-config.py; do
43
+ cp "$REPO_DIR/scripts/$_f" "$PKG/scripts/$_f"
44
+ done
45
+ cp -r "$REPO_DIR/pi" "$PKG/pi"
46
+ cp "$REPO_DIR/pi-extensions/lib/session-id.js" "$PKG/pi-extensions/lib/session-id.js"
47
+ printf '%s\n' '// dist stub: copied+digested by the receive installer, never executed here' \
48
+ > "$PKG/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
49
+ cp "$PKG/pi-extensions/lib/session-id.js" "$PKG/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js"
50
+
51
+ # Fake pnpm: resolvable (so an unconditional require_cmd would pass), but any
52
+ # INVOCATION writes a marker and exits uniquely — the bootstrap tripwire.
53
+ MARKER="$SB/pnpm-invoked.marker"
54
+ mkdir -p "$SB/bin"
55
+ printf '#!/usr/bin/env bash\necho invoked > "%s"\nexit 97\n' "$MARKER" > "$SB/bin/pnpm"
56
+ chmod +x "$SB/bin/pnpm"
57
+
58
+ ABSENT="$SB/definitely-absent"
59
+ run_setup() { # $1=HOME-root $2=project $3=PATH $4=PI_BIN $5=COPILOT_BIN(optional, default absent) → OUT/RC
60
+ mkdir -p "$1/.pi/agent" "$2"
61
+ set +e
62
+ # ONE physical line by contract: check-install-surface S5c is a line-scoped static tripwire,
63
+ # so the sandbox env assignments must ride the same line as the run.sh drive they guard.
64
+ OUT="$(HOME="$1" XDG_DATA_HOME="$1/.local/share" XDG_STATE_HOME="$1/.local/state" XDG_CACHE_HOME="$1/.cache" XDG_CONFIG_HOME="$1/.config" PI_CODING_AGENT_DIR="$1/.pi/agent" PATH="$3" PI_BIN="$4" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="${5:-$ABSENT}" bash "$PKG/run.sh" setup "$2" 2>&1)"
65
+ RC=$?
66
+ set -e
67
+ }
68
+
69
+ # ── Cell A: installed / all-absent (fake pnpm resolvable, never invoked) ──
70
+ HOME_A="$SB/home-a"; mkdir -p "$HOME_A/.pi/agent"
71
+ printf '{"anthropic":{"type":"oauth","access":"oracle-token"}}\n' > "$HOME_A/.pi/agent/auth.json"
72
+ AUTH_SHA="$(sha256sum "$HOME_A/.pi/agent/auth.json" | cut -d' ' -f1)"
73
+ run_setup "$HOME_A" "$SB/proj-a" "$SB/bin:$PATH" "$ABSENT"
74
+ want "A: the source bootstrap never invoked pnpm in installed mode (marker absent) [QK:SETUP-INSTALLED-NO-BOOTSTRAP]" \
75
+ "[ ! -e '$MARKER' ]"
76
+ want "A: the deliberate core FAIL owns setup exit 1 (computed, never cosmetic green) [QK:SETUP-FALSE-GREEN]" \
77
+ "[ '$RC' -eq 1 ]"
78
+ want "A: no auth.json.bak and credential bytes identical [QK:SETUP-CREDENTIAL-FREE]" \
79
+ "[ ! -e '$HOME_A/.pi/agent/auth.json.bak' ] && [ \"\$(sha256sum '$HOME_A/.pi/agent/auth.json' | cut -d' ' -f1)\" = '$AUTH_SHA' ]"
80
+ want "A: an absent copilot is one zero-state SKIP — no unit composed, no .copilot written [QK:SETUP-COPILOT-ABSENT-SKIP]" \
81
+ "printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && [ ! -e '$HOME_A/.copilot' ]"
82
+ want "A control: mode named first, pi/claude/agy SKIP, bins PASS, core FAIL, NON-GREEN summary" \
83
+ "printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: installed package' && printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'bins: PASS' && printf '%s' \"\$OUT\" | grep -q 'core: FAIL' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
84
+
85
+ # ── Cell B: detected pi below floor — the verdict LABEL is the oracle ──
86
+ printf '#!/usr/bin/env bash\necho 0.1.0\n' > "$SB/pi-stale"; chmod +x "$SB/pi-stale"
87
+ run_setup "$SB/home-b" "$SB/proj-b" "$SB/bin:$PATH" "$SB/pi-stale"
88
+ want "B: a below-floor pi is a detected FAIL naming the supported range, never SKIP [QK:SETUP-PI-FLOOR-SKIP]" \
89
+ "printf '%s' \"\$OUT\" | grep -q 'pi: FAIL' && printf '%s' \"\$OUT\" | grep -q 'outside the supported range' && ! printf '%s' \"\$OUT\" | grep -q 'pi: SKIP'"
90
+
91
+ # ── Cell C: genuinely pnpm-scrubbed PATH — installed setup needs no pnpm ──
92
+ SHIMS="$SB/scrub-shims"; mkdir -p "$SHIMS"
93
+ for _t in bash node python3 head tr cut sed grep dirname readlink cat mkdir cp chmod rm env sha256sum; do
94
+ _s="$(command -v "$_t" 2>/dev/null || true)"; [ -n "$_s" ] && ln -s "$_s" "$SHIMS/$_t"
95
+ done
96
+ SCRUBBED="$SHIMS"
97
+ IFS=':' read -r -a _dirs <<<"$PATH"
98
+ for _d in "${_dirs[@]}"; do
99
+ [ -n "$_d" ] || continue
100
+ [ -x "$_d/pnpm" ] && continue
101
+ SCRUBBED="$SCRUBBED:$_d"
102
+ done
103
+ want "C scrub proof: pnpm absent, needed tools present on the scrubbed PATH" \
104
+ "! PATH=\"$SCRUBBED\" command -v pnpm >/dev/null 2>&1 && PATH=\"$SCRUBBED\" command -v node >/dev/null && PATH=\"$SCRUBBED\" command -v python3 >/dev/null"
105
+ run_setup "$SB/home-c" "$SB/proj-c" "$SCRUBBED" "$ABSENT"
106
+ want "C: installed setup needs no pnpm to decide/compose — summary reached, no Missing-command refusal [QK:SETUP-INSTALLED-NO-PNPM]" \
107
+ "printf '%s' \"\$OUT\" | grep -q 'setup summary (computed from component outcomes)' && ! printf '%s' \"\$OUT\" | grep -q 'Missing command: pnpm'"
108
+
109
+ # ── Cell D: copilot PRESENT, vendor lists failing — independence + no cosmetic PASS ──
110
+ # A two-line always-failing vendor is enough here: the birth installer dies at its
111
+ # read-only `plugin list` preflight (UNKNOWN, never absence), while the three writer
112
+ # units never touch the vendor and land in the sandbox. Behavior evidence for the
113
+ # full success composition lives in smoke-setup-verdict S-6 / check-pack-install.
114
+ BROKEN="$SB/broken-vendor"; mkdir -p "$BROKEN"
115
+ printf '#!/usr/bin/env bash\necho "not authenticated" >&2\nexit 1\n' > "$BROKEN/copilot"
116
+ chmod +x "$BROKEN/copilot"
117
+ run_setup "$SB/home-d" "$SB/proj-d" "$BROKEN:$SB/bin:$PATH" "$ABSENT" "$BROKEN/copilot"
118
+ want "D: a failed birth leaves the other three units attempted with their own rows [QK:SETUP-COPILOT-INDEPENDENT]" \
119
+ "printf '%s' \"\$OUT\" | grep -q 'copilot-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'copilot-receive: PASS' && printf '%s' \"\$OUT\" | grep -q 'copilot-statusline: PASS'"
120
+ want "D: the failing-vendor birth is a named FAIL, never a cosmetic PASS [QK:SETUP-COPILOT-COSMETIC-PASS]" \
121
+ "printf '%s' \"\$OUT\" | grep -q 'copilot-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'copilot-birth: PASS'"
122
+ want "D control: detected copilot never reads SKIP, and the summary names copilot-birth NON-GREEN" \
123
+ "! printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN' && printf '%s' \"\$OUT\" | grep -q 'copilot-birth'"
124
+
125
+ echo ""
126
+ echo "check-setup-qualification: $PASS checks passed (mutation-attribution oracle only — behavior evidence lives in smoke-setup-verdict and check-pack-install)"
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env bash
2
+ # copilot-bridge-doctor.sh — the FAIL-LOUD surface for the Copilot birth plugin (#82).
3
+ #
4
+ # WHY IT IS NOT `doctor-meta-bridge --copilot`. The Claude doctor's red conditions
5
+ # certify Claude's receiver marker and FileChanged/asyncRewake hook rail. Copilot has a
6
+ # receiver too since #82 RAIL 5, but it is a different mechanism owned by a different
7
+ # process (a forked extension) and certified by its own doctor,
8
+ # `doctor-copilot-receive`. This BIRTH doctor certifies birth and who-sent. A shared
9
+ # doctor would make one rail pass on evidence owned by another, which is a branch, not a
10
+ # shared surface.
11
+ #
12
+ # (Until #82 RAIL 5b it had none of the four, and this comment said so. The sender
13
+ # marker joined because who-sent needs a shared parent, not a doorbell — the two facts
14
+ # had been merged under one absence.)
15
+ #
16
+ # WHAT A PASS FROM THIS DOCTOR MEANS, AND WHAT IT DOES NOT. Copilot exposes no plugin
17
+ # load or hook-execution receipt, so this doctor can prove the artifact is correct and
18
+ # REGISTERED, never that Copilot loaded it. A PASS with zero records is therefore
19
+ # consistent with two different worlds — a session not yet spoken to, and a unit Copilot
20
+ # silently never invokes. Only a real first prompt separates them, and that receipt (a
21
+ # record in the store) is what closes admission — not this doctor (cross-review, terra).
22
+ #
23
+ # THE ONE HONEST DIFFERENCE FROM THE CLAUDE DOCTOR. There, a plugin installed with
24
+ # zero meta-records is a SILENT MISS and exits non-zero, because a Claude session
25
+ # mints at session open — so zero records means something ate the hook. A Copilot
26
+ # session is born on its FIRST PROMPT (measured: opening the TUI fires no hook at
27
+ # all), so "installed, zero records" is the ordinary state of a Copilot that has been
28
+ # opened and not yet spoken to. Calling that red would train the operator to ignore
29
+ # the doctor. It is reported as NOT-YET, by name, and the red condition is a hook that
30
+ # RAN and FAILED — an ERROR line this unit wrote to the shared hook log.
31
+ set -uo pipefail
32
+
33
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ REPO="$(cd "$HERE/.." && pwd)"
35
+ MKT_NAME="meta-bridge-copilot-local"
36
+ PLUGIN="entwurf-meta-receive-copilot"
37
+ # QUALIFIED ids throughout: `copilot plugin list` prints `plugin@marketplace` (measured
38
+ # 2026-08-21), and a bare name would both accept a same-named plugin from somebody
39
+ # else's marketplace as ours and flag theirs as our stale unit (cross-review, terra).
40
+ QUALIFIED="$PLUGIN@$MKT_NAME"
41
+ STALE_CLAUDE_UNIT="entwurf-meta-receive@meta-bridge-local"
42
+ ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-copilot/.assembled"
43
+ STATE_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/copilot-bridge/install-state.json"
44
+ AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
45
+ SESSIONS_DIR="$AGENT_DIR/meta-sessions"
46
+ HOOK_LOG="$AGENT_DIR/meta-bridge-hook.log"
47
+
48
+ # RUNTIME truth and OWNERSHIP truth are separate axes (#86 C3a, same discipline as
49
+ # doctor-pi-package): a visibly working registration does not prove entwurf owns it,
50
+ # and a broken ownership record does not erase working runtime configuration. Either
51
+ # required axis red makes the final verdict red.
52
+ fail=0
53
+ own_fail=0
54
+ ok() { echo " ok $*"; }
55
+ bad() { echo " FAIL $*"; fail=1; }
56
+ badown(){ echo " FAIL $*"; own_fail=1; }
57
+ note() { echo " note $*"; }
58
+
59
+ # Shared with the installer and the inverse: one structural oracle, one state
60
+ # validator, one exact list-row grammar — three surfaces, zero drifting copies.
61
+ # shellcheck source=copilot-bridge-oracle.sh
62
+ . "$HERE/copilot-bridge-oracle.sh"
63
+
64
+ echo "[copilot-bridge-doctor] toolchain"
65
+ command -v copilot >/dev/null 2>&1 && ok "copilot CLI on PATH" || bad "copilot CLI missing from PATH"
66
+ command -v node >/dev/null 2>&1 && ok "node on PATH" || bad "node missing from PATH"
67
+
68
+ echo "[copilot-bridge-doctor] assembled artifact"
69
+ # The SAME structural oracle the installer's adoption preflight and the inverse use
70
+ # (C3a amendment, B defect 1): launcher presence/bake, live baked node and hook
71
+ # entry, capability registry at the plugin root, and the native baked hooks.json form
72
+ # are all ONE parser in copilot-bridge-oracle.sh — the doctor keeps no second copy
73
+ # that could drift from what install/adopt actually accepts. The oracle prints its
74
+ # first failing reason, which is surfaced verbatim.
75
+ if ORACLE_REASON="$(copilot_assembly_valid "$ASM" "$PLUGIN" 2>&1)"; then
76
+ ok "assembly at $ASM passes the shared structural oracle (baked launcher, live node + hook entry, capability registry, native hooks.json)"
77
+ else
78
+ bad "assembly fails the shared structural oracle: ${ORACLE_REASON:-no reason printed} — run ./run.sh install-copilot-bridge"
79
+ fi
80
+
81
+ echo "[copilot-bridge-doctor] copilot wiring"
82
+ # A failing list is UNKNOWN, never an absence (#86 C3a): reporting "not installed" off
83
+ # a broken/unauthenticated CLI would send the operator to reinstall over a host whose
84
+ # real contents nobody read.
85
+ QUALIFIED_REGISTERED=0
86
+ LISTED_VERSION=""
87
+ if ! PLUGIN_LIST="$(copilot plugin list 2>/dev/null)"; then
88
+ bad "'copilot plugin list' failed — this host's installed plugins are UNKNOWN (not empty); fix the Copilot CLI error and re-run"
89
+ PLUGIN_LIST=""
90
+ else
91
+ # EXACT row read with its version captured: a substring read would accept a longer id
92
+ # that merely contains ours, and a malformed/ambiguous listing is something nobody
93
+ # may act on. WHAT A MATCH DOES NOT PROVE: Copilot exposes no load/execution receipt,
94
+ # so a listing says the plugin is REGISTERED, never that Copilot loaded this unit or
95
+ # will invoke its hook. Only a real first prompt settles that — see birth evidence.
96
+ if LISTED_ROW="$(copilot_exact_row_version "$PLUGIN_LIST" "$QUALIFIED" 2>&1)"; then
97
+ if [ "$LISTED_ROW" = "absent" ]; then
98
+ bad "$QUALIFIED is NOT installed in Copilot — run ./run.sh install-copilot-bridge"
99
+ else
100
+ QUALIFIED_REGISTERED=1
101
+ LISTED_VERSION="${LISTED_ROW#one }"
102
+ ok "$QUALIFIED (v$LISTED_VERSION) is registered in Copilot (registration, not proof it is loaded)"
103
+ fi
104
+ else
105
+ bad "the plugin listing for $QUALIFIED is malformed or ambiguous: $LISTED_ROW"
106
+ fi
107
+ case "$PLUGIN_LIST" in
108
+ *"$STALE_CLAUDE_UNIT"*)
109
+ bad "the Claude unit '$STALE_CLAUDE_UNIT' is still installed in Copilot — it fires on every prompt and exits 1 before node starts (Copilot's schema has no args key). Re-run the installer without --keep-stale-claude-unit." ;;
110
+ *) ok "the Claude unit is not installed in Copilot" ;;
111
+ esac
112
+ fi
113
+
114
+ echo "[copilot-bridge-doctor] ownership (package-owned state; separate axis from runtime)"
115
+ if ! MKT_LIST="$(copilot plugin marketplace list 2>/dev/null)"; then
116
+ badown "'copilot plugin marketplace list' failed — this host's marketplaces are UNKNOWN (not empty); fix the Copilot CLI error and re-run"
117
+ MKT_LIST=""
118
+ else
119
+ # The SAME marketplace-row grammar install/inverse refuse on (C3a amendment, B
120
+ # defects 1+2): a malformed, non-Local, or DUPLICATE same-named listing is a red
121
+ # ownership fact here, never "the first row".
122
+ if MKT_ROW="$(copilot_marketplace_local_path "$MKT_LIST" "$MKT_NAME" 2>&1)"; then
123
+ if [ "$MKT_ROW" = "absent" ]; then
124
+ note "marketplace $MKT_NAME is not registered (consistent with an uninstalled or partially installed host)"
125
+ else
126
+ MKT_PATH="${MKT_ROW#one }"
127
+ if [ "$MKT_PATH" = "$ASM" ]; then
128
+ ok "marketplace $MKT_NAME is registered at this package's assembly path"
129
+ else
130
+ badown "marketplace '$MKT_NAME' is registered at '$MKT_PATH', not at $ASM — ownership drift; that registration is not provably ours"
131
+ fi
132
+ fi
133
+ else
134
+ badown "the marketplace listing for '$MKT_NAME' is malformed, non-Local, or duplicated: $MKT_ROW"
135
+ fi
136
+ fi
137
+ if [ -L "$STATE_FILE" ]; then
138
+ badown "ownership state $STATE_FILE is a symlink — not a trustworthy record"
139
+ elif [ -f "$STATE_FILE" ]; then
140
+ # The SAME fail-closed validator the installer and the inverse use (exact keyset,
141
+ # exact types, constants + effective-ASM binding, no flag coercion).
142
+ if STATE_VERDICT="$(copilot_state_read "$STATE_FILE" "$QUALIFIED" "$MKT_NAME" "$ASM" 2>&1)"; then
143
+ ok "ownership state present and bound to this installation (ownedMarketplace/ownedAssembly/pluginVersion: $STATE_VERDICT)"
144
+ # version drift (final amendment): the vendor lists an exact row at a version the
145
+ # ownership record did not install — the inverse would refuse, so the doctor names
146
+ # it now instead of letting the operator discover it there.
147
+ STATE_PLUGIN_VERSION="$(printf '%s' "$STATE_VERDICT" | cut -d' ' -f3)"
148
+ if [ -n "$LISTED_VERSION" ] && [ "$LISTED_VERSION" != "$STATE_PLUGIN_VERSION" ]; then
149
+ badown "version drift — $QUALIFIED is listed at v$LISTED_VERSION but the ownership state recorded v$STATE_PLUGIN_VERSION; repair: './run.sh install-copilot-bridge' re-binds ownership"
150
+ fi
151
+ else
152
+ badown "ownership state is corrupt or names a different installation: $STATE_VERDICT — inspect $STATE_FILE"
153
+ fi
154
+ elif [ "$QUALIFIED_REGISTERED" -eq 1 ]; then
155
+ badown "LEGACY no-state installation: $QUALIFIED is registered but no ownership state exists — repair: './run.sh install-copilot-bridge' (same-host adoption binds the state)"
156
+ else
157
+ note "no ownership state (nothing this package records as installed here)"
158
+ fi
159
+
160
+ echo "[copilot-bridge-doctor] birth evidence"
161
+ COPILOT_RECORDS=0
162
+ if [ -d "$SESSIONS_DIR" ]; then
163
+ COPILOT_RECORDS="$(grep -l '"backend": "copilot"' "$SESSIONS_DIR"/*.meta.json 2>/dev/null | wc -l | tr -d ' ')"
164
+ fi
165
+ if [ "$COPILOT_RECORDS" -gt 0 ]; then
166
+ ok "$COPILOT_RECORDS copilot meta-record(s) in $SESSIONS_DIR — the citizen exists"
167
+ else
168
+ # NOT a failure. See the header: a Copilot session mints on its first prompt, so an
169
+ # opened-but-unspoken session legitimately has no record yet.
170
+ note "no copilot meta-record yet. A Copilot session is born on its FIRST PROMPT, not when"
171
+ note "the window opens — open Copilot, send one prompt, then re-run this doctor."
172
+ note "NOT-YET is only meaningful BEFORE that first prompt. If a Copilot session has"
173
+ note "already been prompted on this host and this still says zero, the unit is not"
174
+ note "being invoked and that IS the failure — Copilot gives no load receipt to tell"
175
+ note "the two apart from here."
176
+ fi
177
+
178
+ if [ -f "$HOOK_LOG" ]; then
179
+ # RECOVERY RULE. The hook log is append-only, so a repaired install would otherwise
180
+ # stay red forever on errors it has already outgrown (cross-review, terra). What is
181
+ # red is an ERROR with NO successful mint after it: a failure the unit never recovered
182
+ # from. An ERROR followed by a create/attach is history, and is reported as such.
183
+ #
184
+ # grep -c prints 0 AND exits 1 with no match, so a `|| echo 0` fallback would append a
185
+ # SECOND line and every numeric test below would die on "0\n0". Keep it to one line.
186
+ #
187
+ # MINT ERRORS ONLY. Since #82 RAIL 5b this unit also writes a SENDER marker, and a
188
+ # failed marker write is an ERROR that lands AFTER the successful mint line — so the
189
+ # order rule above would read it as "no successful mint after the error" and print a
190
+ # sentence that is simply false (the record IS there). The two failures are separated
191
+ # here rather than downgraded in the payload, because a marker write that keeps
192
+ # breaking must stay loud somewhere; it just is not a birth failure.
193
+ MINT_ERRORS=' ERROR \[copilot\] (?!sender-marker-)'
194
+ LAST_ERROR_LINE="$(grep -nP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
195
+ LAST_OK_LINE="$(grep -n ' INFO \[copilot\] \(create\|attach\) ' "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
196
+ TOTAL_ERRORS="$(grep -cP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | head -1)"
197
+ TOTAL_ERRORS="${TOTAL_ERRORS:-0}"
198
+ if [ -z "$LAST_ERROR_LINE" ]; then
199
+ ok "no copilot ERROR lines in $HOOK_LOG"
200
+ elif [ -n "$LAST_OK_LINE" ] && [ "$LAST_OK_LINE" -gt "$LAST_ERROR_LINE" ]; then
201
+ note "$TOTAL_ERRORS historical copilot ERROR line(s) in $HOOK_LOG, all followed by a successful mint (line $LAST_OK_LINE > $LAST_ERROR_LINE) — recovered, not red"
202
+ else
203
+ bad "the newest copilot line in $HOOK_LOG is an unrecovered ERROR — the hook RAN and did not mint:"
204
+ grep -P "$MINT_ERRORS" "$HOOK_LOG" | tail -3 | sed 's/^/ /'
205
+ fi
206
+
207
+ # WHO-SENT, judged on its own axis. Both outcomes leave a citizen that EXISTS and can
208
+ # be addressed by others; what is missing is only its ability to send under its own
209
+ # garden id. So a REFUSAL is a note (fail-closed by design — a session opened before
210
+ # the current install reaches the payload without launch provenance and correctly
211
+ # claims no owner), while a failed WRITE is red (we tried and the store would not take
212
+ # it, and nothing downstream will say why).
213
+ MARKER_FAILED="$(grep -c ' ERROR \[copilot\] sender-marker-failed ' "$HOOK_LOG" 2>/dev/null | head -1)"
214
+ MARKER_REFUSED="$(grep -c ' WARN \[copilot\] sender-marker-refused ' "$HOOK_LOG" 2>/dev/null | head -1)"
215
+ MARKER_OK="$(grep -c ' INFO \[copilot\] sender marker ' "$HOOK_LOG" 2>/dev/null | head -1)"
216
+ if [ "${MARKER_FAILED:-0}" -gt 0 ]; then
217
+ bad "${MARKER_FAILED} sender-marker WRITE failure(s) in $HOOK_LOG — those citizens exist but cannot send under their own garden id:"
218
+ grep ' ERROR \[copilot\] sender-marker-failed ' "$HOOK_LOG" | tail -3 | sed 's/^/ /'
219
+ elif [ "${MARKER_REFUSED:-0}" -gt 0 ]; then
220
+ note "${MARKER_REFUSED} sender-marker refusal(s) and ${MARKER_OK:-0} armed — a refusal is fail-closed, not a fault. A session that predates this install reaches the hook without launch provenance; RESTART it to arm who-sent"
221
+ elif [ "${MARKER_OK:-0}" -gt 0 ]; then
222
+ ok "${MARKER_OK} sender marker(s) armed — these citizens send under their own garden id"
223
+ else
224
+ note "no sender-marker lines yet in $HOOK_LOG (nothing has fired since who-sent landed)"
225
+ fi
226
+ else
227
+ note "no hook log yet at $HOOK_LOG (nothing has fired on this host)"
228
+ fi
229
+
230
+ echo
231
+ # The two axes are reported separately and either red is a red verdict (#86 C3a):
232
+ # runtime coverage does not prove ownership, and broken ownership does not erase
233
+ # visibly working runtime configuration.
234
+ if [ "$fail" -ne 0 ] || [ "$own_fail" -ne 0 ]; then
235
+ [ "$fail" -ne 0 ] && echo "[copilot-bridge-doctor] runtime axis: FAIL" || echo "[copilot-bridge-doctor] runtime axis: PASS"
236
+ [ "$own_fail" -ne 0 ] && echo "[copilot-bridge-doctor] ownership axis: FAIL" || echo "[copilot-bridge-doctor] ownership axis: PASS"
237
+ echo "[copilot-bridge-doctor] FAIL"
238
+ exit 1
239
+ fi
240
+ echo "[copilot-bridge-doctor] runtime axis: PASS"
241
+ echo "[copilot-bridge-doctor] ownership axis: PASS"
242
+ echo "[copilot-bridge-doctor] PASS"