@junghanacs/entwurf 0.15.1 → 0.16.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 (91) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +354 -0
  3. package/DELIVERY.md +3 -2
  4. package/README.md +68 -88
  5. package/VERIFY.md +4 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/external-mcp-host.md +147 -9
  9. package/docs/setup-clean-host.md +125 -6
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +19 -10
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  20. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  21. package/mcp/entwurf-bridge/src/index.ts +65 -19
  22. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  23. package/mcp/tsconfig.json +10 -0
  24. package/package.json +19 -9
  25. package/pi/entwurf-capabilities.json +1 -0
  26. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  27. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  28. package/pi-extensions/entwurf-control.ts +12 -9
  29. package/pi-extensions/lib/acp/backend-adapter.ts +19 -9
  30. package/pi-extensions/lib/acp/backend.ts +125 -7
  31. package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
  32. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  33. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  34. package/pi-extensions/lib/meta-session.ts +219 -5
  35. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  36. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  37. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  38. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  39. package/run.sh +399 -31
  40. package/scripts/check-acp-launch-namespace.ts +127 -0
  41. package/scripts/check-acp-prompt-lifecycle.ts +145 -2
  42. package/scripts/check-copilot-birth-hook.ts +28 -1
  43. package/scripts/check-entwurf-self-address.ts +31 -0
  44. package/scripts/check-gate-qualification.ts +7 -3
  45. package/scripts/check-harness-admission-parity.ts +143 -0
  46. package/scripts/check-meta-doctor-oracle.sh +20 -0
  47. package/scripts/check-omp-birth-hook.ts +1049 -0
  48. package/scripts/check-omp-fresh-preflight.ts +208 -0
  49. package/scripts/check-omp-receive-arm.ts +516 -0
  50. package/scripts/check-setup-qualification.sh +40 -2
  51. package/scripts/copilot-bridge-oracle.sh +14 -6
  52. package/scripts/fake-copilot-vendor.sh +4 -2
  53. package/scripts/inventory-verification-surface.ts +1 -1
  54. package/scripts/meta-bridge-hook-log.sh +9 -1
  55. package/scripts/mutants/acp-launch-namespace.json +34 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
  57. package/scripts/mutants/copilot-birth.json +10 -10
  58. package/scripts/mutants/mux-fresh-call.json +22 -22
  59. package/scripts/mutants/omp-birth.json +173 -0
  60. package/scripts/mutants/omp-fresh.json +300 -0
  61. package/scripts/mutants/omp-receive.json +135 -0
  62. package/scripts/mutants/pack-install.json +2 -2
  63. package/scripts/mutants/self-address.json +34 -0
  64. package/scripts/mutants/setup-verdict.json +35 -0
  65. package/scripts/omp-bridge-doctor.sh +315 -0
  66. package/scripts/omp-bridge-install.sh +221 -0
  67. package/scripts/omp-bridge-oracle.sh +154 -0
  68. package/scripts/omp-bridge-uninstall.sh +57 -0
  69. package/scripts/omp-config-xdev.py +310 -0
  70. package/scripts/omp-config-xdev.sh +76 -0
  71. package/scripts/omp-mcp-bridge.sh +320 -0
  72. package/scripts/omp-mcp-config.py +392 -0
  73. package/scripts/omp-receive-doctor.sh +246 -0
  74. package/scripts/omp-receive-facts.ts +106 -0
  75. package/scripts/omp-receive-install.sh +228 -0
  76. package/scripts/omp-receive-uninstall.sh +60 -0
  77. package/scripts/omp-tool-surface.py +400 -0
  78. package/scripts/raw-acp-child-exit-measure/README.md +285 -0
  79. package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
  80. package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
  81. package/scripts/raw-omp-measure/README.md +420 -0
  82. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  83. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  84. package/scripts/raw-omp-measure/source-audit.md +414 -0
  85. package/scripts/smoke-omp-bridge-state.sh +221 -0
  86. package/scripts/smoke-omp-fresh-live.ts +497 -0
  87. package/scripts/smoke-omp-mcp-state.sh +327 -0
  88. package/scripts/smoke-omp-receive-live.ts +340 -0
  89. package/scripts/smoke-omp-receive-state.sh +196 -0
  90. package/scripts/smoke-setup-verdict.sh +48 -3
  91. package/scripts/tsconfig.json +2 -0
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env bash
2
+ # omp-bridge-doctor.sh — the fail-loud surface for the OMP BIRTH extension (#87).
3
+ #
4
+ # TWO AXES, REPORTED SEPARATELY, EITHER RED IS RED (AGENTS.md Hard Rule 13):
5
+ # runtime — is there a unit omp will actually import, does it carry the CURRENT
6
+ # writer, and what did it do the last time it ran?
7
+ # ownership — does entwurf own that unit, by a state whose shape and bindings hold?
8
+ # Runtime coverage does not prove ownership, and broken ownership does not erase a
9
+ # visibly working configuration.
10
+ #
11
+ # THE PLACEMENT LAYER, AND WHY THIS DOCTOR CAN READ IT AT ALL. omp offers two vendor
12
+ # surfaces for declaring an extension (ledger M2): a native root scan of
13
+ # `<agent-dir>/extensions` (`discovery/builtin.ts:483` → `discovery/helpers.ts:625-712`)
14
+ # and an `extensions:` array in a settings file. This unit uses the FIRST, so the whole
15
+ # installed artifact is one directory entwurf created and can remove exactly — there is
16
+ # no operator-owned SSOT to preimage, merge into, or accidentally clobber, and this
17
+ # doctor's runtime axis is a structural read rather than a diff against somebody else's
18
+ # config. (`~/.omp/agent/config.yml` carries the operator's model roles, status line and
19
+ # plan settings; nothing here writes to it. The MCP hand is the opposite case and does
20
+ # write a vendor config file, with the preimage/inverse discipline that comes with it —
21
+ # see doctor-omp-mcp.)
22
+ #
23
+ # WHAT IT DELIBERATELY DOES NOT CLAIM. A green runtime axis is not a live citizen: only
24
+ # a real omp TUI session minting a record is that (`adding-a-harness.md` step 3(b)). An
25
+ # installed unit with zero omp records is reported as NOT-YET, never as red.
26
+ set -uo pipefail
27
+
28
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ REPO="$(cd "$HERE/.." && pwd)"
30
+ UNIT="entwurf-meta-omp"
31
+ SRC="$REPO/pi/meta-bridge-omp"
32
+ ASM="${ENTWURF_OMP_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled}"
33
+ STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge"
34
+ STATE_FILE="$STATE_DIR/install-state.json"
35
+
36
+ fail=0
37
+ own_fail=0
38
+ ok() { echo " ok $*"; }
39
+ note() { echo " note $*"; }
40
+ bad() { echo " FAIL $*"; fail=1; }
41
+ own_bad() { echo " FAIL $*"; own_fail=1; }
42
+
43
+ # shellcheck source=scripts/omp-bridge-oracle.sh
44
+ . "$HERE/omp-bridge-oracle.sh"
45
+
46
+ echo "[omp birth extension — runtime axis]"
47
+
48
+ if ! command -v omp >/dev/null 2>&1; then
49
+ note "the 'omp' CLI is not on PATH — this host has no omp to be a citizen of (entwurf never installs one)"
50
+ fi
51
+
52
+ AGENT_DIR="$(omp_agent_dir)"
53
+ if [ -z "$AGENT_DIR" ]; then
54
+ bad "the omp agent directory this host reads is AMBIGUOUS (see the reason above) — no verdict can be given about a directory we refuse to guess"
55
+ AGENT_DIR=""
56
+ UNIT_DIR=""
57
+ else
58
+ UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
59
+ ok "effective omp agent dir: $AGENT_DIR (unit path: $UNIT_DIR)"
60
+ fi
61
+
62
+ if [ -n "$UNIT_DIR" ]; then
63
+ if [ -L "$UNIT_DIR" ]; then
64
+ bad "$UNIT_DIR is a SYMLINK — this installer never creates one, so what omp imports there is not ours"
65
+ elif [ ! -d "$UNIT_DIR" ]; then
66
+ if [ -f "$STATE_FILE" ]; then
67
+ bad "ownership state exists but the unit is GONE: $UNIT_DIR — no omp session can be born. Re-run ./run.sh install-omp-bridge"
68
+ else
69
+ note "no unit installed at $UNIT_DIR and no ownership state — this host has not run install-omp-bridge (zero state, not a fault)"
70
+ fi
71
+ elif omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" 2>/tmp/omp-doctor-oracle.$$; then
72
+ ENTRY_NAME="index.ts"; [ -f "$UNIT_DIR/index.ts" ] || ENTRY_NAME="index.js"
73
+ ok "installed unit is structurally complete (entry $ENTRY_NAME + lib + capability registry + type:module)"
74
+ ok "omp discovers it by its own native rule: <agent-dir>/extensions/$UNIT/$ENTRY_NAME (discovery/helpers.ts:700-710)"
75
+ # WRITER PARITY. The unit carries a COPY of the shared V3 writer and of the capability
76
+ # registry, exactly like the Claude and Copilot units. A stale copy is the failure mode
77
+ # `adding-a-harness.md` step 2(c) names: the deployed reader refuses records the new
78
+ # backend list admits, including its own. Same judgement, same prescription.
79
+ LIB_EXT="ts"; [ "$ENTRY_NAME" = "index.js" ] && LIB_EXT="js"
80
+ case "$REPO" in
81
+ */node_modules/@junghanacs/entwurf) SRC_LIB="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"; SRC_ENTRY="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js" ;;
82
+ *) SRC_LIB="$REPO/pi-extensions/lib/meta-session.ts"; SRC_ENTRY="$REPO/pi-extensions/meta-bridge-omp.ts" ;;
83
+ esac
84
+ for pair in "lib/meta-session.$LIB_EXT|$SRC_LIB|writer" "$ENTRY_NAME|$SRC_ENTRY|birth entry" "entwurf-capabilities.json|$REPO/pi/entwurf-capabilities.json|capability registry"; do
85
+ rel="${pair%%|*}"; rest="${pair#*|}"; src="${rest%%|*}"; label="${rest##*|}"
86
+ if [ ! -f "$src" ]; then
87
+ note "$label source is absent in this checkout ($src) — parity not measurable here"
88
+ continue
89
+ fi
90
+ INST_SHA="$(sha256sum "$UNIT_DIR/$rel" 2>/dev/null | cut -c1-12)"
91
+ SRC_SHA="$(sha256sum "$src" 2>/dev/null | cut -c1-12)"
92
+ if [ -n "$INST_SHA" ] && [ "$INST_SHA" = "$SRC_SHA" ]; then
93
+ ok "installed $label matches source ($INST_SHA)"
94
+ else
95
+ bad "installed $label is STALE: installed=${INST_SHA:-missing} vs source=$SRC_SHA ($UNIT_DIR/$rel). The live omp extension is running old bytes — run ./run.sh install-omp-bridge, then restart any open omp session"
96
+ fi
97
+ done
98
+ else
99
+ bad "installed unit at $UNIT_DIR fails the structural oracle: $(cat /tmp/omp-doctor-oracle.$$ 2>/dev/null)"
100
+ fi
101
+ rm -f /tmp/omp-doctor-oracle.$$
102
+ fi
103
+
104
+ # ── WHERE THE GARDEN ROOTS ARE, under the SAME policy production uses (#87 B1) ──
105
+ # NOT `PI_CODING_AGENT_DIR ?? ~/.pi/agent`. For backend omp that variable is the VENDOR's
106
+ # agent dir — `setProfile` exports it for every named profile (`oh-my-pi` v18.0.0
107
+ # `utils/src/dirs.ts:452-473`) — so reading it here would point this doctor at a different
108
+ # store than the one the extension and the bridge child actually write to. The OMP policy
109
+ # is: the four `ENTWURF_META_*` overrides, else `<HOME>/.pi/agent/<surface>`, never PI.
110
+ #
111
+ # We do not re-implement that policy in bash. We reach it the same way the OMP bridge child
112
+ # does — drop the foreign variable, then let entwurf's own default resolver answer — and we
113
+ # ask the OWNER for the answer: `meta-facts` (#65) is the read-only projection of the
114
+ # CERTIFIED store, so it reports both the store directory it resolved and the records that
115
+ # passed certification. One call, two facts, no second parser.
116
+ #
117
+ # PI presence itself is NOT a fault and is never reported as one: under `omp --profile work`
118
+ # the vendor sets it deliberately. It is simply not a garden-root input.
119
+ # PREFLIGHT THE SAME GRAMMAR THE POLICY ENFORCES, BEFORE ASKING ANYTHING (#87 A2). The
120
+ # overrides are absolute or `~`/`~/…` only. A relative value would resolve against each
121
+ # process's own working directory, and this doctor does NOT share one with the extension —
122
+ # `run_ts` cd's to the repository. Asking `meta-facts` first would therefore answer from an
123
+ # unrelated empty directory and print NOT-YET (or PASS) about a store that is not the one
124
+ # the extension writes to. A refused environment is RUNTIME RED here, never laundered.
125
+ ROOT_POLICY_REFUSAL=""
126
+ for var in ENTWURF_META_SESSIONS_DIR ENTWURF_META_MAILBOX_DIR ENTWURF_META_SENDERS_DIR ENTWURF_META_RECEIVERS_DIR; do
127
+ val="${!var:-}"
128
+ [ -n "$val" ] || continue
129
+ case "$val" in
130
+ /*|"~"|"~/"*) ;;
131
+ *) ROOT_POLICY_REFUSAL="${ROOT_POLICY_REFUSAL:+$ROOT_POLICY_REFUSAL, }$var=$val" ;;
132
+ esac
133
+ done
134
+
135
+ FACTS_RC=0
136
+ FACTS_JSON=""
137
+ STORE=""
138
+ OMP_RECORDS=0
139
+ STORE_DEFECTS=0
140
+ if [ -n "$ROOT_POLICY_REFUSAL" ]; then
141
+ bad "the omp meta-root policy REFUSES this environment: $ROOT_POLICY_REFUSAL — a garden root must be absolute or ~-rooted. The extension receives the same refusal and mints nothing; no store can be named here, so this is RED rather than a NOT-YET verdict about some other directory"
142
+ else
143
+ FACTS_JSON="$(env -u PI_CODING_AGENT_DIR "$REPO/run.sh" meta-facts 2>&1)" || FACTS_RC=$?
144
+ fi
145
+ if [ -z "$ROOT_POLICY_REFUSAL" ] && [ "$FACTS_RC" -eq 0 ]; then
146
+ FACTS_LINE="$(printf '%s' "$FACTS_JSON" | python3 -c '
147
+ import json, sys
148
+ facts = json.load(sys.stdin)
149
+ citizens = facts.get("citizens") or []
150
+ omp = [c for c in citizens if c.get("backend") == "omp"]
151
+ print(facts.get("storeDir", ""), len(omp), len(facts.get("defects") or []))
152
+ ' 2>/dev/null)" || FACTS_LINE=""
153
+ if [ -n "$FACTS_LINE" ]; then
154
+ STORE="${FACTS_LINE%% *}"
155
+ REST="${FACTS_LINE#* }"
156
+ OMP_RECORDS="${REST%% *}"
157
+ STORE_DEFECTS="${REST##* }"
158
+ fi
159
+ fi
160
+
161
+ if [ -n "$ROOT_POLICY_REFUSAL" ]; then
162
+ AGENT=""
163
+ HOOK_LOG=""
164
+ elif [ -z "$STORE" ]; then
165
+ bad "the certified record store could not be read (meta-facts rc=$FACTS_RC) — no honest statement about omp citizens can be made on this host:"
166
+ printf '%s\n' "$FACTS_JSON" | tail -3 | sed 's/^/ /'
167
+ AGENT=""
168
+ HOOK_LOG=""
169
+ else
170
+ AGENT="$(dirname "$STORE")"
171
+ HOOK_LOG="$AGENT/meta-bridge-hook.log"
172
+ ok "omp garden roots resolve to $AGENT (ENTWURF_META_* overrides, else \$HOME/.pi/agent — PI_CODING_AGENT_DIR is vendor-owned here and ignored)"
173
+ fi
174
+
175
+ # ── what the unit DID, read off the shared hook log ──────────────────────────
176
+ # Mint errors and marker errors are judged on SEPARATE axes, and that separation is not
177
+ # stylistic: a failed marker write lands AFTER the successful mint line, so a doctor that
178
+ # folded them together would read "the hook ran and did not mint" about a session whose
179
+ # record is right there (`adding-a-harness.md` step 6, measured on Copilot).
180
+ echo
181
+ echo "[what the extension did — ${HOOK_LOG:-<unresolved>}]"
182
+ if [ -n "$HOOK_LOG" ] && [ -f "$HOOK_LOG" ]; then
183
+ MINT_ERRORS=' ERROR \[omp\] (?!sender-marker-)'
184
+ LAST_ERROR_LINE="$(grep -nP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
185
+ LAST_OK_LINE="$(grep -n ' INFO \[omp\] \(create\|attach\) ' "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
186
+ TOTAL_ERRORS="$(grep -cP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | head -1)"; TOTAL_ERRORS="${TOTAL_ERRORS:-0}"
187
+ if [ -z "$LAST_ERROR_LINE" ]; then
188
+ ok "no omp mint ERROR lines in $HOOK_LOG"
189
+ elif [ -n "$LAST_OK_LINE" ] && [ "$LAST_OK_LINE" -gt "$LAST_ERROR_LINE" ]; then
190
+ note "$TOTAL_ERRORS historical omp mint ERROR line(s), all followed by a successful mint (line $LAST_OK_LINE > $LAST_ERROR_LINE) — recovered, not red"
191
+ else
192
+ bad "the newest omp mint line in $HOOK_LOG is an unrecovered ERROR — the extension RAN and did not mint:"
193
+ grep -P "$MINT_ERRORS" "$HOOK_LOG" | tail -3 | sed 's/^/ /'
194
+ fi
195
+
196
+ MARKER_FAILED="$(grep -c ' ERROR \[omp\] sender-marker-failed ' "$HOOK_LOG" 2>/dev/null | head -1)"
197
+ MARKER_REFUSED="$(grep -c ' WARN \[omp\] sender-marker-refused ' "$HOOK_LOG" 2>/dev/null | head -1)"
198
+ MARKER_OK="$(grep -c ' INFO \[omp\] sender marker ' "$HOOK_LOG" 2>/dev/null | head -1)"
199
+ if [ "${MARKER_FAILED:-0}" -gt 0 ]; then
200
+ bad "${MARKER_FAILED} sender-marker WRITE failure(s) — those citizens exist but cannot send under their own garden id:"
201
+ grep ' ERROR \[omp\] sender-marker-failed ' "$HOOK_LOG" | tail -3 | sed 's/^/ /'
202
+ elif [ "${MARKER_REFUSED:-0}" -gt 0 ]; then
203
+ note "${MARKER_REFUSED} sender-marker refusal(s) and ${MARKER_OK:-0} armed — a refusal is fail-closed, not a fault"
204
+ elif [ "${MARKER_OK:-0}" -gt 0 ]; then
205
+ ok "${MARKER_OK} sender marker(s) armed — those citizens send under their own garden id"
206
+ else
207
+ note "no omp sender-marker lines yet (nothing has fired on this host)"
208
+ fi
209
+
210
+ # The SCOPE FENCE leaves its own receipt, and it is evidence rather than noise: one
211
+ # line per session this unit refused to mint, naming the mode. A host with subagent
212
+ # traffic and zero refusals would mean the fence never ran.
213
+ SCOPE_REFUSED="$(grep -c ' INFO \[omp\] scope-refused ' "$HOOK_LOG" 2>/dev/null | head -1)"
214
+ STATUS_ISSUES="$(grep -c ' WARN \[omp\] status-' "$HOOK_LOG" 2>/dev/null | head -1)"
215
+ note "${SCOPE_REFUSED:-0} non-tui session(s) refused by the §3.5 scope fence — the designed answer for task subagents, rpc/rpc-ui and acp"
216
+ if [ "${STATUS_ISSUES:-0}" -gt 0 ]; then
217
+ note "${STATUS_ISSUES} visible-identity warning(s) — the garden id did not render on the status line for those sessions (statusLine.showHookStatus is default true; check the operator's config.yml)"
218
+ fi
219
+ elif [ -n "$HOOK_LOG" ]; then
220
+ note "no hook log yet at $HOOK_LOG (nothing has fired on this host)"
221
+ fi
222
+
223
+ # ── the records themselves, read through the CERTIFIED surface ───────────────
224
+ # NEVER a text grep (#87 B3). "This host has an omp garden citizen" is a claim on the
225
+ # record-authority axis, and the production writer earns it by certifying the WHOLE active
226
+ # store before writing — regular non-symlink files, live V3 schema, filename↔body
227
+ # agreement, unique nativeSessionId (`meta-session.ts` certifyActiveStore). A grep for the
228
+ # text `"backend": "omp"` matched a file containing nothing else and printed PASS. A doctor
229
+ # that claims admission must not weaken the contract the writer holds, so the count above
230
+ # comes from `meta-facts`' certified citizen list.
231
+ if [ -n "$STORE" ]; then
232
+ if [ "${STORE_DEFECTS:-0}" -gt 0 ]; then
233
+ note "$STORE_DEFECTS uncertifiable entr(ies) in $STORE are excluded from this count — run ./run.sh doctor-meta-bridge, which owns that axis"
234
+ fi
235
+ if [ "${OMP_RECORDS:-0}" -gt 0 ]; then
236
+ ok "$OMP_RECORDS CERTIFIED omp meta-record(s) in $STORE (garden citizen proven on this host)"
237
+ else
238
+ note "NOT-YET: zero certified omp meta-records. An omp TUI session is born when it OPENS (session_start fires after first paint, before the first prompt) — open one and re-run this doctor"
239
+ fi
240
+ fi
241
+
242
+ # ── §6 identity-carrier contamination, DETECTED (never silently preferred) ───
243
+ # `adding-a-harness.md` step 6 and `docs/external-mcp-host.md`: a complete
244
+ # PI_SESSION_ID + PI_AGENT_ID pair WINS over a native sender marker in the bridge's
245
+ # authoritative-self resolution, so an omp session started from a pi citizen's bash — and
246
+ # every internal agent borrowing its MCP manager — would speak under the parent pi garden
247
+ # id. Bundle C's managed fresh launch has no per-window unset form in tmux, so it writes
248
+ # both names EMPTY instead. That is an honest scrub: authoritative readers trim and require
249
+ # truthy values, so an empty or whitespace-only value is nonauthoritative just like absence.
250
+ # This doctor detects only an actual nonblank carrier and reports it on its own axis rather
251
+ # than absorbing it into any other verdict.
252
+ echo
253
+ echo "[inherited pi identity carriers on live omp processes]"
254
+ if [ -d /proc ]; then
255
+ CONTAMINATED=""
256
+ # Candidate set. Production scans every `omp` pid. `ENTWURF_OMP_CARRIER_PIDS` (set, even
257
+ # to empty) narrows it so a hermetic smoke can hand this branch REAL processes it launched
258
+ # and read their real `/proc/<pid>/environ`. It narrows candidates ONLY — the nonblank
259
+ # predicate below is the same production code either way.
260
+ if [ -n "${ENTWURF_OMP_CARRIER_PIDS+x}" ]; then
261
+ CARRIER_CANDIDATES="$ENTWURF_OMP_CARRIER_PIDS"
262
+ else
263
+ CARRIER_CANDIDATES="$(pgrep -x omp 2>/dev/null || true)"
264
+ fi
265
+ for pid in $CARRIER_CANDIDATES; do
266
+ case "$pid" in *[!0-9]*) continue ;; esac
267
+ ENVIRON="/proc/$pid/environ"
268
+ [ -r "$ENVIRON" ] || continue
269
+ if tr '\0' '\n' < "$ENVIRON" 2>/dev/null | grep -qE '^(PI_SESSION_ID|PI_AGENT_ID)=.*[^[:space:]]'; then
270
+ CONTAMINATED="$CONTAMINATED $pid"
271
+ fi
272
+ done
273
+ if [ -n "$CONTAMINATED" ]; then
274
+ bad "live omp process(es)$CONTAMINATED carry a non-empty PI_SESSION_ID/PI_AGENT_ID inherited from a pi citizen's shell. Their MCP children would speak under the PARENT pi garden id, not their own — close them and relaunch omp from a shell without those variables"
275
+ else
276
+ ok "no live omp process carries a non-empty PI_SESSION_ID/PI_AGENT_ID (empty carrier values are the managed tmux scrub and are nonauthoritative; omp mints neither itself — the danger is pure inheritance passthrough, ledger M6)"
277
+ fi
278
+ else
279
+ note "/proc is unavailable, so live omp environments could not be read on this platform"
280
+ fi
281
+
282
+ # ── ownership axis ───────────────────────────────────────────────────────────
283
+ echo
284
+ echo "[ownership axis]"
285
+ if [ -L "$STATE_FILE" ]; then
286
+ own_bad "ownership state $STATE_FILE is a SYMLINK — refusing to trust it"
287
+ elif [ -f "$STATE_FILE" ]; then
288
+ if STATE_FACTS="$(omp_state_read "$STATE_FILE" "${UNIT_DIR:-$STATE_FILE}" "$ASM" 2>&1)"; then
289
+ ok "ownership state is well-formed and bound to this installation (version ${STATE_FACTS%% *}, entry ${STATE_FACTS##* })"
290
+ if [ -d "$ASM/$UNIT" ]; then
291
+ ok "the recorded assembly is present: $ASM/$UNIT"
292
+ else
293
+ own_bad "ownership state names an assembly that is gone: $ASM/$UNIT — the inverse can no longer prove what it placed. Re-run ./run.sh install-omp-bridge"
294
+ fi
295
+ else
296
+ own_bad "ownership state is corrupt or names a different installation: $STATE_FACTS"
297
+ fi
298
+ else
299
+ if [ -n "$UNIT_DIR" ] && [ -d "$UNIT_DIR" ]; then
300
+ own_bad "a unit is installed at $UNIT_DIR but entwurf holds NO ownership state for it — neither the installer nor the inverse will touch it (#87 B2: a shape is not a proof of ownership). Inspect it; if it is a stale copy of ours, remove it by hand and re-run ./run.sh install-omp-bridge"
301
+ else
302
+ note "no ownership state and no installed unit — zero state, which is a SKIP rather than a fault"
303
+ fi
304
+ fi
305
+
306
+ echo
307
+ if [ "$fail" -ne 0 ] || [ "$own_fail" -ne 0 ]; then
308
+ [ "$fail" -ne 0 ] && echo "[omp-bridge-doctor] runtime axis: FAIL" || echo "[omp-bridge-doctor] runtime axis: PASS"
309
+ [ "$own_fail" -ne 0 ] && echo "[omp-bridge-doctor] ownership axis: FAIL" || echo "[omp-bridge-doctor] ownership axis: PASS"
310
+ echo "[omp-bridge-doctor] FAIL"
311
+ exit 1
312
+ fi
313
+ echo "[omp-bridge-doctor] runtime axis: PASS"
314
+ echo "[omp-bridge-doctor] ownership axis: PASS"
315
+ echo "[omp-bridge-doctor] PASS"
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env bash
2
+ # omp-bridge-install.sh — operator-grade global installer for the OMP BIRTH extension
3
+ # (#87). The omp sibling of copilot-bridge-install.sh, deliberately NOT a mode of it.
4
+ #
5
+ # WHAT THIS INSTALLS. ONE extension directory whose managed job is to mint the visible
6
+ # omp TUI host as a garden citizen, name it as this host's sender, and show its garden id
7
+ # on omp's own status line. The MCP hand is a separate surface (`install-omp-mcp`), and
8
+ # receive does not exist yet — three facts, three installers, three failure modes.
9
+ #
10
+ # WHY THERE IS NO LAUNCHER AND NO MANIFEST BAKE. `[source]` an omp "hook" is an
11
+ # in-process EXTENSION: `--hook` aliases `--extension`, a module default-exports
12
+ # `(pi) => void`, and there is no spawned hook command and no stdin envelope (oh-my-pi
13
+ # v18.0.0, `docs/hooks.md`; `extensibility/extensions/types.ts:1592`). The Claude and
14
+ # Copilot installers bake a node path and an entry path into a launcher because their
15
+ # payload is a separate process; here the vendor imports the module itself, so the whole
16
+ # install is: assemble a self-contained unit, then place it where omp discovers it.
17
+ #
18
+ # WHERE IT PLACES IT, AND WHY THERE. `<agent-dir>/extensions/<unit>/index.{ts,js}` is one
19
+ # of omp's three native discovery rules (`discovery/builtin.ts:483` →
20
+ # `discovery/helpers.ts:625-712`), and the only one that needs nothing rewritten per
21
+ # install shape. The agent dir is resolved by the shared oracle, which REFUSES rather
22
+ # than guesses when an inherited `PI_*` knob makes it ambiguous (ledger M6).
23
+ #
24
+ # Platform: Linux only, same fence as the Claude and Copilot installers.
25
+ set -euo pipefail
26
+
27
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ REPO="$(cd "$HERE/.." && pwd)"
29
+ UNIT="entwurf-meta-omp"
30
+ SRC="$REPO/pi/meta-bridge-omp"
31
+ # `ENTWURF_OMP_ASM` exists for the gate: check-omp-birth-hook drives this script for real,
32
+ # into a temp dir, with --assemble-only. A gate that re-implemented the assembly would be
33
+ # testing its own copy of the logic instead of the shipped one.
34
+ ASM="${ENTWURF_OMP_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled}"
35
+ STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge"
36
+ STATE_FILE="$STATE_DIR/install-state.json"
37
+ ASSEMBLE_ONLY=0
38
+
39
+ die() { echo "[omp-bridge-install] $*" >&2; exit 1; }
40
+
41
+ for arg in "$@"; do
42
+ case "$arg" in
43
+ --assemble-only) ASSEMBLE_ONLY=1 ;;
44
+ *) die "unknown argument: $arg (--assemble-only)" ;;
45
+ esac
46
+ done
47
+
48
+ [ "$(uname -s)" = "Linux" ] || die "Linux only; $(uname -s) is not a certified axis for this install."
49
+ command -v python3 >/dev/null 2>&1 || die "python3 is required and is not on PATH."
50
+
51
+ # shellcheck source=scripts/omp-bridge-oracle.sh
52
+ . "$HERE/omp-bridge-oracle.sh"
53
+
54
+ if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
55
+ # A GATE-ONLY path that writes no ownership state, so it must never touch the LIVE
56
+ # assembly: without a state, a rebuilt live assembly would drift from what the state
57
+ # records. Same refusal the Copilot installer carries, for the same reason.
58
+ DEFAULT_ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled"
59
+ [ -n "${ENTWURF_OMP_ASM:-}" ] \
60
+ || die "--assemble-only requires an explicit ENTWURF_OMP_ASM (a temp dir): this gate-only path writes no ownership state and must not rebuild the live assembly."
61
+ if [ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$ENTWURF_OMP_ASM")" = \
62
+ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$DEFAULT_ASM")" ]; then
63
+ die "--assemble-only refuses the DEFAULT live assembly path ($DEFAULT_ASM): pass a temp ENTWURF_OMP_ASM instead."
64
+ fi
65
+ fi
66
+
67
+ AGENT_DIR=""
68
+ UNIT_DIR=""
69
+ if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
70
+ command -v omp >/dev/null 2>&1 || die "the 'omp' CLI is not on PATH. entwurf never installs a harness — install omp first, then re-run."
71
+ AGENT_DIR="$(omp_agent_dir)" || die "refusing: the omp agent directory this host reads is ambiguous (see above)."
72
+ UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
73
+ fi
74
+
75
+ UNIT_VERSION="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$SRC/$UNIT/package.json")" \
76
+ || die "could not read the shipped unit version from $SRC/$UNIT/package.json"
77
+ case "$UNIT_VERSION" in
78
+ ""|*[![:graph:]]*) die "shipped unit version ${UNIT_VERSION:-<empty>} is empty or carries whitespace/control characters — it travels through space-separated fact transports." ;;
79
+ esac
80
+
81
+ # Same install-shape split as the Claude and Copilot installers: an installed package
82
+ # lives below node_modules where Node refuses strip-types on `.ts`, so it ships the
83
+ # tsc-emitted closure; a dev clone ships the `.ts` source. omp itself reads both (its
84
+ # discovery prefers index.ts and falls back to index.js, `discovery/helpers.ts:700-710`).
85
+ case "$REPO" in
86
+ */node_modules/@junghanacs/entwurf)
87
+ ENTRY_NAME="index.js"
88
+ ENTRY_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js"
89
+ LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
90
+ LIB_EXT="js" ;;
91
+ *)
92
+ ENTRY_NAME="index.ts"
93
+ ENTRY_SRC="$REPO/pi-extensions/meta-bridge-omp.ts"
94
+ LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
95
+ LIB_EXT="ts" ;;
96
+ esac
97
+
98
+ [ -f "$ENTRY_SRC" ] || die "birth entry artifact missing: $ENTRY_SRC (run 'pnpm run build-bridge' in a dev clone, or reinstall the package)."
99
+ [ -f "$LIB_SRC" ] || die "entry lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
100
+
101
+ write_state() { # $1 = ownedUnitDir (true|false)
102
+ mkdir -p "$STATE_DIR"
103
+ STATE_FILE_ENV="$STATE_FILE" UNIT_DIR_ENV="$UNIT_DIR" ASM_ENV="$ASM" \
104
+ UNIT_VERSION_ENV="$UNIT_VERSION" ENTRY_NAME_ENV="$ENTRY_NAME" OWNED_ENV="$1" python3 - <<'PY' \
105
+ || die "could not write the ownership state at $STATE_FILE"
106
+ import datetime, json, os, tempfile
107
+ state_file = os.environ["STATE_FILE_ENV"]
108
+ state = {
109
+ "schemaVersion": 1,
110
+ "unitDir": os.path.abspath(os.environ["UNIT_DIR_ENV"]),
111
+ "assemblyPath": os.path.abspath(os.environ["ASM_ENV"]),
112
+ "unitVersion": os.environ["UNIT_VERSION_ENV"],
113
+ "entryName": os.environ["ENTRY_NAME_ENV"],
114
+ "ownedUnitDir": os.environ["OWNED_ENV"] == "true",
115
+ "installedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
116
+ }
117
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(state_file), prefix=".install-state.")
118
+ with os.fdopen(fd, "w") as fh:
119
+ json.dump(state, fh, indent=2)
120
+ fh.write("\n")
121
+ os.replace(tmp, state_file)
122
+ PY
123
+ }
124
+
125
+ # ── 0. ownership preflight (READ-ONLY) ───────────────────────────────────────
126
+ # Every destructive step below is licensed HERE, and the only licence is ownership STATE.
127
+ # Anything that already exists at our path and cannot be proven ours refuses with zero
128
+ # writes — a structural shape is not a proof of ownership (#87 B2). A SYMLINK at the unit
129
+ # path is always a refusal too: writing through it would mutate somebody else's tree.
130
+ if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
131
+ if [ -L "$STATE_FILE" ]; then
132
+ die "ownership state $STATE_FILE is a symlink — refusing to trust or overwrite it."
133
+ fi
134
+ if [ -L "$UNIT_DIR" ]; then
135
+ die "$UNIT_DIR is a SYMLINK. entwurf never writes through a link into a tree it does not own — remove it by hand if it is stale, then re-run."
136
+ fi
137
+ if [ -f "$STATE_FILE" ]; then
138
+ omp_state_read "$STATE_FILE" "$UNIT_DIR" "$ASM" >/dev/null \
139
+ || die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero writes."
140
+ echo "[omp-bridge-install] ownership state matches this installation (reinstall/repair)"
141
+ elif [ -e "$UNIT_DIR" ]; then
142
+ # NO SHAPE-BASED ADOPTION (#87 B2). A structural oracle answers "is this a complete
143
+ # unit", never "is this OURS": it compares no bytes, rejects no extra files and
144
+ # establishes no provenance. Adopting on that answer meant moving a stranger's
145
+ # directory aside, publishing over it and DELETING the preimage — and the inverse
146
+ # then `rm -rf`s the path on the same unproven claim. A foreign or hand-made
147
+ # extension with these filenames, or our own unit plus operator files beside it, was
148
+ # destroyed unrecoverably. Ownership state is the only proof of ownership, so a
149
+ # no-state path refuses with zero writes.
150
+ die "$UNIT_DIR already exists and entwurf holds NO ownership state for it. A directory that merely LOOKS like our unit is not proof that it is ours — adopting it would overwrite it and delete the preimage with no inverse. Refusing with zero writes: inspect it, and if it is stale remove it by hand, then re-run."
151
+ fi
152
+ fi
153
+
154
+ # ── 1. assemble a self-contained unit ────────────────────────────────────────
155
+ # ATOMIC: assemble beside the live artifact and swap only once it is complete, so a
156
+ # failure never leaves a half-written extension for omp to import on the next launch.
157
+ STAGE="$ASM.staging.$$"
158
+ rm -rf "$STAGE"
159
+ trap 'rm -rf "$STAGE"' EXIT
160
+ mkdir -p "$STAGE/$UNIT/lib"
161
+ cp "$SRC/$UNIT/package.json" "$STAGE/$UNIT/package.json"
162
+ cp "$ENTRY_SRC" "$STAGE/$UNIT/$ENTRY_NAME"
163
+ cp "$LIB_SRC" "$STAGE/$UNIT/lib/meta-session.$LIB_EXT"
164
+ cp "$REPO/pi-extensions/lib/session-id.js" "$STAGE/$UNIT/lib/session-id.js"
165
+ # The capability registry must travel at the unit ROOT: metaCapabilitiesFilePath()
166
+ # resolves it via `../` from lib/ in the bundle layout. Without it every mint throws.
167
+ cp "$REPO/pi/entwurf-capabilities.json" "$STAGE/$UNIT/entwurf-capabilities.json"
168
+
169
+ omp_assembly_valid "$STAGE" "$UNIT" || die "the freshly staged assembly fails the structural oracle (see above) — refusing to publish it."
170
+
171
+ PREV=""
172
+ if [ -d "$ASM" ]; then
173
+ PREV="$ASM.previous.$$"
174
+ mv "$ASM" "$PREV" || die "could not move the previous assembly aside ($ASM)."
175
+ fi
176
+ mkdir -p "$(dirname "$ASM")"
177
+ if ! mv "$STAGE" "$ASM"; then
178
+ [ -n "$PREV" ] && mv "$PREV" "$ASM"
179
+ die "could not publish the staged assembly into $ASM (previous assembly restored)."
180
+ fi
181
+ trap - EXIT
182
+ [ -n "$PREV" ] && rm -rf "$PREV"
183
+
184
+ echo "[omp-bridge-install] assembled $ASM (birth extension only; the MCP hand is install-omp-mcp)"
185
+
186
+ if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
187
+ echo "[omp-bridge-install] --assemble-only: stopping before the omp agent directory is touched"
188
+ exit 0
189
+ fi
190
+
191
+ # ── 2. place it where omp discovers it ───────────────────────────────────────
192
+ # Ownership state BEFORE the vendor-directory mutation: once the complete assembly is
193
+ # published, the state exists so a partial failure below still leaves an inverse
194
+ # authority behind. `ownedUnitDir` flips false→true around the placement.
195
+ write_state false
196
+ echo "[omp-bridge-install] ownership state written: $STATE_FILE"
197
+
198
+ mkdir -p "$(dirname "$UNIT_DIR")"
199
+ PLACE_STAGE="$UNIT_DIR.staging.$$"
200
+ rm -rf "$PLACE_STAGE"
201
+ cp -r "$ASM/$UNIT" "$PLACE_STAGE" || die "could not stage the unit into $(dirname "$UNIT_DIR")."
202
+ PREV_UNIT=""
203
+ if [ -d "$UNIT_DIR" ]; then
204
+ PREV_UNIT="$UNIT_DIR.previous.$$"
205
+ mv "$UNIT_DIR" "$PREV_UNIT" || { rm -rf "$PLACE_STAGE"; die "could not move the previous unit aside ($UNIT_DIR)."; }
206
+ fi
207
+ if ! mv "$PLACE_STAGE" "$UNIT_DIR"; then
208
+ [ -n "$PREV_UNIT" ] && mv "$PREV_UNIT" "$UNIT_DIR"
209
+ rm -rf "$PLACE_STAGE"
210
+ die "could not publish the unit into $UNIT_DIR (previous unit restored)."
211
+ fi
212
+ [ -n "$PREV_UNIT" ] && rm -rf "$PREV_UNIT"
213
+ write_state true
214
+
215
+ omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" || die "the placed unit fails the structural oracle (see above)."
216
+
217
+ echo "[omp-bridge-install] installed $UNIT_DIR (entry $ENTRY_NAME, version $UNIT_VERSION)"
218
+ echo "[omp-bridge-install] DONE. An omp TUI session becomes a citizen when it OPENS —"
219
+ echo "the birth edge is the extension's session_start, which fires after first paint and"
220
+ echo "before the first prompt; a task subagent of that session is refused by design."
221
+ echo "Verify with: ./run.sh doctor-omp-bridge · inverse: ./run.sh uninstall-omp-bridge"