@junghanacs/entwurf 0.19.0 → 0.20.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.
Files changed (59) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +23 -5
  3. package/CHANGELOG.md +236 -0
  4. package/DELIVERY.md +59 -6
  5. package/README.md +40 -9
  6. package/VERIFY.md +9 -1
  7. package/docs/acp-backend-rail.md +9 -1
  8. package/docs/external-mcp-host.md +6 -3
  9. package/docs/setup-clean-host.md +93 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
  12. package/package.json +2 -2
  13. package/pi-extensions/acp-provider.ts +1 -1
  14. package/pi-extensions/lib/acp/models.ts +3 -3
  15. package/pi-extensions/lib/acp/overlay.ts +6 -2
  16. package/pi-extensions/lib/meta-session.ts +25 -0
  17. package/run.sh +71 -26
  18. package/scripts/agy-bridge.sh +1 -1
  19. package/scripts/agy-imprint.sh +10 -7
  20. package/scripts/check-acp-carrier-augment.ts +6 -5
  21. package/scripts/check-acp-cortex.ts +41 -0
  22. package/scripts/check-acp-provider-surface.ts +9 -6
  23. package/scripts/check-copilot-birth-hook.ts +3 -1
  24. package/scripts/check-copilot-receive-arm.ts +169 -21
  25. package/scripts/check-copilot-statusline.ts +3 -1
  26. package/scripts/check-gate-qualification.ts +10 -9
  27. package/scripts/check-hook-launch-topology.ts +6 -5
  28. package/scripts/check-install-surface.ts +2 -1
  29. package/scripts/check-meta-facts.ts +6 -4
  30. package/scripts/check-meta-hook-session-switch.ts +4 -3
  31. package/scripts/check-meta-identity-consumers.ts +8 -7
  32. package/scripts/check-meta-receiver-marker.ts +2 -1
  33. package/scripts/check-omp-birth-hook.ts +3 -2
  34. package/scripts/check-probe-bridge-command.ts +3 -1
  35. package/scripts/check-setup-qualification.sh +21 -0
  36. package/scripts/copilot-bridge-doctor.sh +15 -5
  37. package/scripts/copilot-bridge-install.sh +6 -2
  38. package/scripts/copilot-launch.sh +16 -2
  39. package/scripts/copilot-receive-bridge.sh +80 -13
  40. package/scripts/lib/reclaim-on-exit.ts +86 -0
  41. package/scripts/meta-bridge-doctor.sh +56 -15
  42. package/scripts/meta-bridge-install.sh +13 -7
  43. package/scripts/meta-bridge-uninstall.sh +6 -4
  44. package/scripts/mutants/acp-cortex.json +26 -2
  45. package/scripts/mutants/copilot-receive.json +26 -0
  46. package/scripts/mutants/omp-birth.json +23 -6
  47. package/scripts/mutants/setup-verdict.json +13 -0
  48. package/scripts/omp-bridge-doctor.sh +110 -41
  49. package/scripts/omp-bridge-install.sh +6 -2
  50. package/scripts/omp-receive-install.sh +7 -2
  51. package/scripts/raw-async-delivery/README.md +10 -1
  52. package/scripts/raw-codex-measure/README.md +689 -0
  53. package/scripts/raw-codex-measure/source-audit.md +243 -0
  54. package/scripts/raw-macos-measure/README.md +148 -0
  55. package/scripts/raw-macos-measure/probe.sh +389 -0
  56. package/scripts/smoke-meta-async-drift.sh +7 -5
  57. package/scripts/smoke-meta-install-state.sh +70 -19
  58. package/scripts/smoke-omp-bridge-state.sh +38 -0
  59. package/scripts/smoke-setup-verdict.sh +100 -0
@@ -51,6 +51,7 @@ import {
51
51
  processStartKey,
52
52
  readMetaReceiverMarker,
53
53
  } from "../pi-extensions/lib/meta-session.ts";
54
+ import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
54
55
 
55
56
  const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
56
57
  const UNIT = "entwurf-meta-omp";
@@ -62,7 +63,8 @@ function ok(label: string, cond: boolean): void {
62
63
  passed++;
63
64
  }
64
65
 
65
- const root = mkdtempSync(path.join(tmpdir(), "entwurf-omp-birth."));
66
+ const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-omp-birth.")));
67
+
66
68
  const asm = path.join(root, "asm");
67
69
 
68
70
  // ── 1. the real assembler ────────────────────────────────────────────────────
@@ -1045,5 +1047,4 @@ for (const cell of ROOT_CELLS) {
1045
1047
  );
1046
1048
  }
1047
1049
 
1048
- rmSync(root, { recursive: true, force: true });
1049
1050
  console.log(`[check-omp-birth-hook] ${passed} assertions ok`);
@@ -13,6 +13,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
13
13
  import { tmpdir } from "node:os";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
16
17
  import { EXPECTED_TOOLS, probeBridgeCommand } from "./probe-bridge-command.ts";
17
18
 
18
19
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -32,7 +33,8 @@ function ok(label: string, cond: boolean, detail?: string): void {
32
33
  passed++;
33
34
  }
34
35
 
35
- const dir = mkdtempSync(join(tmpdir(), "entwurf-probe-gate-"));
36
+ const dir = reclaimOnExit(mkdtempSync(join(tmpdir(), "entwurf-probe-gate-")));
37
+
36
38
  const stub = (name: string, body: string): string => {
37
39
  const p = join(dir, name);
38
40
  writeFileSync(p, body);
@@ -160,5 +160,26 @@ want "F: an EXPLICIT operator tools.xdev:true is refused by name, never overwrit
160
160
  want "F control: the disagreement is a component FAIL that leaves the other omp units composed" \
161
161
  "printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-receive: PASS' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
162
162
 
163
+ # ── Cell G: a COMPLETED harness install on a platform whose RAIL has no receipt ──
164
+ # 0.20.0 opened the harness installers to Darwin, which retired the only thing that
165
+ # used to keep a detected Mac host non-green — the installer's own refusal. Install
166
+ # portability and rail certification are separate evidence axes (Hard Rule 17) and
167
+ # `setup` measures only the first, so the completed install must land as the NAMED
168
+ # non-green, never as a PASS pointing at a doctor that refuses on that platform.
169
+ # `uname` is faked on PATH, the same seam smoke-meta-install-state.sh uses for the
170
+ # installer's platform gate; Cell E above is the certified-platform control (same
171
+ # stub omp, same sandbox, unfaked platform → PASS rows).
172
+ FAKE_UNAME="$SB/fake-uname"; mkdir -p "$FAKE_UNAME"
173
+ printf '#!%s\nprintf "%%s\\n" Darwin\n' "$(command -v bash)" > "$FAKE_UNAME/uname"
174
+ chmod +x "$FAKE_UNAME/uname"
175
+ run_setup "$SB/home-g" "$SB/proj-g" "$FAKE_UNAME:$STUB_OMP:$SB/bin:$PATH" "$ABSENT" "" "$STUB_OMP/omp"
176
+ # One assertion by design: the row, its REASON, and the ARTIFACT proving the install
177
+ # actually completed. Drop the artifact half and the cell would also pass if the fake
178
+ # platform had merely broken the installer — the opposite of what is being claimed.
179
+ want "G: a completed install whose rail has no receipt is the named non-green, never a cosmetic PASS [QK:SETUP-DARWIN-RAIL-COSMETIC-PASS]" \
180
+ "printf '%s' \"\$OUT\" | grep -q 'omp-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'NOT CERTIFIED — pending physical host on Darwin' && [ -d '$SB/home-g/.omp/agent/extensions/entwurf-meta-omp' ]"
181
+ want "G control: the reason reads as an evidence boundary, not an install refusal, and names the axis owner" \
182
+ "printf '%s' \"\$OUT\" | grep -q 'the wiring WAS written and nothing failed to install' && printf '%s' \"\$OUT\" | grep -qF \"owned by './run.sh doctor-omp-bridge'\" && ! printf '%s' \"\$OUT\" | grep -qi 'unsupported platform'"
183
+
163
184
  echo ""
164
185
  echo "check-setup-qualification: $PASS checks passed (mutation-attribution oracle only — behavior evidence lives in smoke-setup-verdict and check-pack-install)"
@@ -183,6 +183,8 @@ if [ -f "$HOOK_LOG" ]; then
183
183
  #
184
184
  # grep -c prints 0 AND exits 1 with no match, so a `|| echo 0` fallback would append a
185
185
  # SECOND line and every numeric test below would die on "0\n0". Keep it to one line.
186
+ # That rule still governs the plain-literal marker counters further down; the mint
187
+ # selector no longer needs it, for the reason in the next paragraph.
186
188
  #
187
189
  # MINT ERRORS ONLY. Since #82 RAIL 5b this unit also writes a SENDER marker, and a
188
190
  # failed marker write is an ERROR that lands AFTER the successful mint line — so the
@@ -190,10 +192,18 @@ if [ -f "$HOOK_LOG" ]; then
190
192
  # sentence that is simply false (the record IS there). The two failures are separated
191
193
  # here rather than downgraded in the payload, because a marker write that keeps
192
194
  # 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)"
195
+ #
196
+ # PORTABLE SELECTOR, SAME CONTRACT. That separation is a negative lookahead, and BSD
197
+ # grep (macOS) has no `-P` at all so the selector is awk, where two conditions ARE
198
+ # `(?!…)`. It also removes the `grep -c` exit-1 hazard the paragraph above works
199
+ # around: awk prints 0 and exits 0 on a zero-match file, so there is no second line
200
+ # to launder. The success selector moves from GNU BRE `\(a\|b\)` to ERE `(a|b)` for
201
+ # the same portability reason — BRE alternation is a GNU extension, so on BSD grep
202
+ # that pattern matches nothing and a RECOVERED host reads as unrecovered (false RED).
203
+ MINT_ERRORS='/ ERROR \[copilot\] / && !/ ERROR \[copilot\] sender-marker-/'
204
+ LAST_ERROR_LINE="$(awk "$MINT_ERRORS{print NR}" "$HOOK_LOG" 2>/dev/null | tail -1)"
205
+ LAST_OK_LINE="$(grep -nE ' INFO \[copilot\] (create|attach) ' "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
206
+ TOTAL_ERRORS="$(awk "$MINT_ERRORS{n++} END{print n+0}" "$HOOK_LOG" 2>/dev/null)"
197
207
  TOTAL_ERRORS="${TOTAL_ERRORS:-0}"
198
208
  if [ -z "$LAST_ERROR_LINE" ]; then
199
209
  ok "no copilot ERROR lines in $HOOK_LOG"
@@ -201,7 +211,7 @@ if [ -f "$HOOK_LOG" ]; then
201
211
  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
212
  else
203
213
  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/^/ /'
214
+ awk "$MINT_ERRORS" "$HOOK_LOG" | tail -3 | sed 's/^/ /'
205
215
  fi
206
216
 
207
217
  # WHO-SENT, judged on its own axis. Both outcomes leave a citizen that EXISTS and can
@@ -22,7 +22,8 @@
22
22
  # first-party extension, installed by `run.sh install-copilot-receive`, which also owns
23
23
  # the launch-flag check. Four surfaces, four installers, four failure modes.
24
24
  #
25
- # Platform: Linux only, same fence as the Claude installer.
25
+ # Platform: Linux and Darwin, same fence as the Claude installer. Its own platform
26
+ # dependency is python3 + node (checked below); no /proc, no GNU-only tool.
26
27
  set -euo pipefail
27
28
 
28
29
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -66,7 +67,10 @@ for arg in "$@"; do
66
67
  esac
67
68
  done
68
69
 
69
- [ "$(uname -s)" = "Linux" ] || die "Linux only; $(uname -s) is not a certified axis for this install."
70
+ case "$(uname -s)" in
71
+ Linux | Darwin) ;;
72
+ *) die "unsupported platform '$(uname -s)'. This installer requires Linux or Darwin." ;;
73
+ esac
70
74
  if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
71
75
  command -v copilot >/dev/null 2>&1 || die "the 'copilot' CLI is not on PATH."
72
76
  fi
@@ -117,9 +117,23 @@ copilot_bin="$(type -P "$VENDOR_CMD" 2>/dev/null || true)"
117
117
 
118
118
  # Self-exec fence, the second half of the recursion guard: resolve symlinks and refuse
119
119
  # anything that is one of our own entrypoints even if the sentinel was stripped.
120
- resolved_bin="$(readlink -f "$copilot_bin" 2>/dev/null || printf '%s' "$copilot_bin")"
120
+ # POSIX walk, same shape as mcp/entwurf-bridge/start.sh BSD readlink has no -f.
121
+ resolve_path() {
122
+ local SOURCE="$1" DIR TARGET
123
+ while [ -L "$SOURCE" ]; do
124
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
125
+ TARGET="$(readlink "$SOURCE")"
126
+ case "$TARGET" in
127
+ /*) SOURCE="$TARGET" ;;
128
+ *) SOURCE="$DIR/$TARGET" ;;
129
+ esac
130
+ done
131
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
132
+ printf '%s/%s\n' "$DIR" "$(basename "$SOURCE")"
133
+ }
134
+ resolved_bin="$(resolve_path "$copilot_bin")"
121
135
  for own in "$HERE/copilot-launch.sh" "$REPO_DIR/run.sh"; do
122
- own_resolved="$(readlink -f "$own" 2>/dev/null || printf '%s' "$own")"
136
+ own_resolved="$(resolve_path "$own")"
123
137
  [ "$resolved_bin" = "$own_resolved" ] \
124
138
  && fail "'$VENDOR_CMD' on PATH resolves to entwurf's own '$own_resolved' — that is a launch loop, not the vendor CLI."
125
139
  done
@@ -233,7 +233,9 @@ do_doctor() {
233
233
  # THE INVISIBLE FAILURE. A Copilot launched without the flag never scans for
234
234
  # extensions and prints nothing at all, so a perfectly installed unit stays inert
235
235
  # with no symptom anywhere. Reading the live processes' own environment is the only
236
- # place that silence becomes visible. Linux /proc only; elsewhere it is a note.
236
+ # place that silence becomes visible. That read is `/proc`-bound today; where the
237
+ # interface does not exist the axis is reported UNVERIFIABLE and non-green, never
238
+ # as a benign note (see the branch below).
237
239
  #
238
240
  # WHY NOT `pgrep -x copilot` (what this used to do). It matched nothing, ever, and
239
241
  # reported that as the benign "no live copilot" note — so the detector built to break
@@ -255,7 +257,27 @@ do_doctor() {
255
257
  # from OUR unit and carry `COPILOT_EXTENSION_PARENT_PID`; counting one as a session
256
258
  # would report a receiver as its own missing session.
257
259
  if [ ! -d /proc ]; then
258
- note "no /proc on this platform cannot read the live CLI environments"
260
+ # D1 NON-GREEN, NOT A NOTE. This used to `note`, and a note lets this doctor end
261
+ # in PASS on a platform where the flag axis is blind: the exact false-success the
262
+ # paragraph at the unreadable-environ branch below refuses on Linux ("A note would
263
+ # let the doctor end in PASS while a session that can never arm is running"). The
264
+ # epistemic state here is the SAME one — the flag verdict's predicate INPUT is
265
+ # missing — so it gets the same verdict, and the reason is stated rather than
266
+ # rounded off.
267
+ #
268
+ # AND ABSENCE CANNOT BE CLAIMED EITHER. Without /proc there is no candidate
269
+ # enumeration at all, so this branch cannot say "no live Copilot CLI" the way the
270
+ # zero-count note below does; it can only say the axis was not measurable. That
271
+ # is a strictly weaker sentence than the note it replaces, which is the point.
272
+ #
273
+ # WHY NO `ps` SUBSTITUTE IS ATTEMPTED HERE. `ps -A -o pid=,args=` would recover the
274
+ # argv half, but macOS truncates argv for processes the caller does not own, and the
275
+ # environment half (`ps -E`/`ps eww`) succeeds there only for unrestricted targets —
276
+ # both are unmeasured (scripts/raw-macos-measure/probe.sh cell M6 is the receipt that
277
+ # would decide it). A guessed reader that silently identifies nothing would restore
278
+ # exactly the fail-open this branch just closed, so the read is not guessed. When M6
279
+ # lands, the portable read belongs right here, in place of this refusal.
280
+ bad "UNVERIFIABLE on this platform: there is no per-process environment interface, so the $FLAG_ENV state of a live Copilot CLI cannot be read — and without one this doctor cannot enumerate processes either, so it cannot claim that no CLI is running. The receiver IS installed, so a session launched without $FLAG_ENV=$FLAG_VALUE (never scans extensions, prints nothing, can never arm) is NOT ruled out. Launch through 'entwurf copilot', which sets the flag, and treat this axis as unmeasured until a native Darwin receipt exists."
259
281
  else
260
282
  local pid_seam="" scan_json
261
283
  # WHICH PROCESSES ARE CANDIDATES. Normally every pid under /proc.
@@ -274,7 +296,7 @@ do_doctor() {
274
296
  # stepped on once. The status is captured instead, and a broken scan becomes a verdict.
275
297
  local scan_rc=0
276
298
  scan_json="$(ENTWURF_PID_SEAM="${pid_seam}" ENTWURF_PID_SEAM_SET="${ENTWURF_COPILOT_RECEIVE_PIDS+1}" FLAG_ENV="$FLAG_ENV" FLAG_VALUE="$FLAG_VALUE" python3 - <<'PY'
277
- import os, pathlib
299
+ import errno, os, pathlib
278
300
 
279
301
  flag_env = os.environ["FLAG_ENV"]
280
302
  flag_value = os.environ["FLAG_VALUE"]
@@ -284,17 +306,43 @@ if os.environ.get("ENTWURF_PID_SEAM_SET"):
284
306
  else:
285
307
  candidates = [p.name for p in pathlib.Path("/proc").iterdir() if p.name.isdigit()]
286
308
 
309
+ # THREE ANSWERS, NOT TWO. "The process is gone" and "the read was refused" are different
310
+ # facts about a live host, and collapsing them is how this scanner reported a process it
311
+ # could not read as a process that was not there. A returned list is the bytes; `None` is
312
+ # a PROVEN absence; `UNREADABLE` is "the input could not be obtained", which is unknown.
313
+ UNREADABLE = object()
314
+
287
315
  def read_nul(pid, what):
288
316
  try:
289
317
  return pathlib.Path(f"/proc/{pid}/{what}").read_bytes().split(b"\0")
290
- except OSError:
291
- return None
318
+ except OSError as exc:
319
+ # ONLY A PROVEN ABSENCE IS BENIGN. ENOENT/ESRCH is the process itself leaving
320
+ # between enumeration and this read — nothing is left to judge. EVERYTHING else
321
+ # (EACCES/EPERM under a `hidepid` /proc or a session owned by another user, and
322
+ # any other I/O failure) leaves the process THERE and the input MISSING. Unknown
323
+ # is the DEFAULT side on purpose: a new errno class must arrive as non-green
324
+ # rather than inherit the benign one.
325
+ if exc.errno in (errno.ENOENT, errno.ESRCH):
326
+ return None
327
+ return UNREADABLE
292
328
 
293
- armed_ok, armed_missing, unreadable, contaminated = 0, 0, 0, 0
294
- missing_pids, unreadable_pids, contaminated_pids = [], [], []
329
+ armed_ok, armed_missing, unreadable, contaminated, argv_unknown = 0, 0, 0, 0, 0
330
+ missing_pids, unreadable_pids, contaminated_pids, argv_unknown_pids = [], [], [], []
295
331
 
296
332
  for pid in candidates:
297
333
  argv = read_nul(pid, "cmdline")
334
+ if argv is UNREADABLE:
335
+ # WE CANNOT EVEN SAY WHAT THIS PROCESS IS. Identity is the FIRST predicate here, so
336
+ # a refused argv read is not "not a Copilot CLI" — it is "unknown", and a Copilot
337
+ # CLI is NOT ruled out. It carries its own counter because the unreadable-ENVIRON
338
+ # counter below means something strictly stronger: there, identity already
339
+ # succeeded and the process IS a Copilot CLI, so that verdict can name a session.
340
+ # This branch can only say the doctor was blinded — which is exactly why the `bad`
341
+ # path further down cannot cover it (that one is reachable only AFTER a successful
342
+ # argv match).
343
+ argv_unknown += 1
344
+ argv_unknown_pids.append(pid)
345
+ continue
298
346
  # No cmdline at all is a kernel thread or a process that exited mid-scan; neither is
299
347
  # a Copilot session and neither is evidence of anything.
300
348
  if not argv:
@@ -322,9 +370,13 @@ for pid in candidates:
322
370
  continue
323
371
 
324
372
  env = read_nul(pid, "environ")
325
- if env is None:
326
- # FAIL-CLOSED: we identified a native CLI but cannot read its environment, so its
327
- # flag state is unknown. That is reported, never rounded to armed.
373
+ if env is None or env is UNREADABLE:
374
+ # FAIL-CLOSED, AND DELIBERATELY NOT SPLIT. We identified a native CLI and then did
375
+ # not learn its environment whether it left mid-scan or refused the read, its
376
+ # flag state is unknown and it WAS a live Copilot CLI when identity matched. Both
377
+ # halves stay in this one reported bucket. Only the argv branch above needs the
378
+ # distinction, because there absence and refusal answer different questions
379
+ # ("is anything running at all?" vs "what is this?").
328
380
  unreadable += 1
329
381
  unreadable_pids.append(pid)
330
382
  continue
@@ -351,24 +403,30 @@ for pid in candidates:
351
403
  armed_missing += 1
352
404
  missing_pids.append(pid)
353
405
 
354
- print(f"{armed_ok} {armed_missing} {unreadable} {contaminated}")
406
+ print(f"{armed_ok} {armed_missing} {unreadable} {contaminated} {argv_unknown}")
355
407
  print(" ".join(missing_pids))
356
408
  print(" ".join(unreadable_pids))
357
409
  print(" ".join(contaminated_pids))
410
+ print(" ".join(argv_unknown_pids))
358
411
  PY
359
412
  )" || scan_rc=$?
360
- local counts armed_ok armed_missing unreadable contaminated missing_pids unreadable_pids contaminated_pids
413
+ local counts armed_ok armed_missing unreadable contaminated argv_unknown
414
+ local missing_pids unreadable_pids contaminated_pids argv_unknown_pids
361
415
  counts="$(printf '%s\n' "$scan_json" | sed -n '1p')"
362
416
  missing_pids="$(printf '%s\n' "$scan_json" | sed -n '2p')"
363
417
  unreadable_pids="$(printf '%s\n' "$scan_json" | sed -n '3p')"
364
418
  contaminated_pids="$(printf '%s\n' "$scan_json" | sed -n '4p')"
419
+ argv_unknown_pids="$(printf '%s\n' "$scan_json" | sed -n '5p')"
365
420
  armed_ok="$(printf '%s' "$counts" | awk '{print $1}')"
366
421
  armed_missing="$(printf '%s' "$counts" | awk '{print $2}')"
367
422
  unreadable="$(printf '%s' "$counts" | awk '{print $3}')"
368
423
  contaminated="$(printf '%s' "$counts" | awk '{print $4}')"
424
+ argv_unknown="$(printf '%s' "$counts" | awk '{print $5}')"
369
425
  if [ "$scan_rc" -ne 0 ] || [ -z "$armed_ok" ]; then
370
426
  bad "the /proc scan for live Copilot CLIs FAILED (exit $scan_rc) — the flag axis is UNKNOWN, so an inert session cannot be ruled out. This is a broken doctor, not a clean host."
371
- elif [ "$armed_ok" -eq 0 ] && [ "$armed_missing" -eq 0 ] && [ "$unreadable" -eq 0 ]; then
427
+ elif [ "$armed_ok" -eq 0 ] && [ "$armed_missing" -eq 0 ] && [ "$unreadable" -eq 0 ] && [ "${argv_unknown:-0}" -eq 0 ]; then
428
+ # The one branch that may claim ABSENCE, and it may only claim it because every
429
+ # candidate answered: each was enumerated and its argv was read (or proved gone).
372
430
  note "no live GitHub Copilot CLI process — start one with 'entwurf copilot' (or $FLAG_ENV=$FLAG_VALUE copilot) to arm a receiver"
373
431
  else
374
432
  [ "$armed_ok" -gt 0 ] && ok "$armed_ok live Copilot CLI process(es) carry $FLAG_ENV=$FLAG_VALUE"
@@ -379,6 +437,15 @@ PY
379
437
  # exact false-success the section was written to break.
380
438
  bad "$unreadable live Copilot CLI process(es) have an unreadable environment (pids: $unreadable_pids) — their $FLAG_ENV state is UNKNOWN and is NOT assumed armed. Re-run this doctor as the user that owns those sessions."
381
439
  fi
440
+ if [ "${argv_unknown:-0}" -gt 0 ]; then
441
+ # RED, and NOT the same finding as the line above. There, identity had already
442
+ # succeeded, so the doctor could name a Copilot session and say what was unknown
443
+ # about it. Here identity ITSELF failed to be decided: a live process would not
444
+ # let its argv be read, so whether it is a Copilot CLI is unknown and one is not
445
+ # ruled out. Rounding that to "no live CLI" is the false success this branch
446
+ # exists to break.
447
+ bad "UNVERIFIABLE — the argv of $argv_unknown live process(es) could not be read (pids: $argv_unknown_pids), so whether any of them is a GitHub Copilot CLI is UNKNOWN. This doctor therefore does NOT claim that no CLI is running, and an inert session cannot be ruled out. Re-run it as the user that owns those processes, or on a host whose /proc is not restricted."
448
+ fi
382
449
  if [ "$armed_missing" -gt 0 ]; then
383
450
  bad "$armed_missing live Copilot CLI process(es) lack $FLAG_ENV=$FLAG_VALUE while the receiver is installed (pids: $missing_pids) — relaunch them with 'entwurf copilot', or uninstall the receiver so nothing promises a doorbell"
384
451
  fi
@@ -0,0 +1,86 @@
1
+ // Reclaim a gate's OWN process resources when that gate exits — however it exits.
2
+ //
3
+ // WHY THIS EXISTS (test-infra, not a product capability):
4
+ // A gate ends red by THROWING (`ok()` is `assert.ok`), and ^C or a runner's SIGTERM
5
+ // ends it with no statement running at all. Teardown written as the last line of the
6
+ // file is therefore skipped on every path except the green one, and the mutant runner
7
+ // makes that the COMMON path: qualification runs each committed mutant expecting the
8
+ // gate to go red. Measured on `oracle` 2026-09-08, before this leaf existed: ~9,200
9
+ // stale temp roots and 3.8G under `/tmp`, plus 360 reparented fixture Node processes
10
+ // from `check-copilot-receive-arm` alone. `pnpm check` was still minting a fresh one
11
+ // per run at that point.
12
+ //
13
+ // WHAT IT IS NOT. It is not error handling and it does not soften a failure: the exit
14
+ // code is untouched (a red gate stays red, a signal still ends as 128+n), nothing is
15
+ // logged, and no failure is converted into a warning (Hard Rule 15). It reclaims
16
+ // process resources only — never a record, transcript or operator artifact (Hard Rule
17
+ // 8). A SIGKILL of the gate still runs nothing here, by construction; that residue is
18
+ // the release protocol's `P9. Reclaim the host` step, and a fixture child that must
19
+ // outlive nothing needs its own parent-death watchdog.
20
+
21
+ import type { ChildProcess } from "node:child_process";
22
+ import { rmSync } from "node:fs";
23
+
24
+ const roots: string[] = [];
25
+ const children: ChildProcess[] = [];
26
+ let wired = false;
27
+ let done = false;
28
+
29
+ /** Sync-only, idempotent: an `exit` handler may not await anything. */
30
+ function reclaim(): void {
31
+ if (done) return;
32
+ done = true;
33
+ for (const c of children) {
34
+ try {
35
+ c.kill("SIGKILL");
36
+ } catch {
37
+ // kill() throws ESRCH on an already-reaped pid — gone either way
38
+ }
39
+ }
40
+ for (const dir of roots) {
41
+ try {
42
+ rmSync(dir, { recursive: true, force: true });
43
+ } catch {
44
+ // Best-effort teardown must never become the reason a GREEN gate exits
45
+ // non-zero: a throw inside an `exit` handler would flip the verdict of a
46
+ // run that had already passed every assertion it makes.
47
+ }
48
+ }
49
+ }
50
+
51
+ function wire(): void {
52
+ if (wired) return;
53
+ wired = true;
54
+ process.on("exit", reclaim);
55
+ // A signal is not an exit: without these, `exit` never fires. Re-raising as
56
+ // 128+n keeps the shell's own convention rather than inventing a code.
57
+ for (const [signal, n] of [
58
+ ["SIGINT", 2],
59
+ ["SIGTERM", 15],
60
+ ["SIGHUP", 1],
61
+ ] as const) {
62
+ process.on(signal, () => {
63
+ reclaim();
64
+ process.exit(128 + n);
65
+ });
66
+ }
67
+ }
68
+
69
+ /** Register a temp root to remove on exit. Returns it, so it wraps `mkdtempSync`. */
70
+ export function reclaimOnExit(dir: string): string {
71
+ wire();
72
+ roots.push(dir);
73
+ return dir;
74
+ }
75
+
76
+ /** Register a spawned child to SIGKILL on exit. Returns it, so it wraps `spawn`. */
77
+ export function killOnExit<T extends ChildProcess>(child: T): T {
78
+ wire();
79
+ children.push(child);
80
+ return child;
81
+ }
82
+
83
+ /** Run the registered teardown now (the green tail); later exits become no-ops. */
84
+ export function reclaimNow(): void {
85
+ reclaim();
86
+ }
@@ -66,13 +66,18 @@ echo "meta-bridge doctor"
66
66
  echo "config=$CLAUDE_CFG agent-dir=$AGENT"
67
67
 
68
68
  echo "[platform]"
69
- # The #51 repair contract is Linux-only because certification requires discovery of
70
- # each live bridge process's environment through /proc. Darwin is not an install lane
71
- # for this cut and must stay nonzero here too never downgrade unsupported to a
72
- # static/synthetic PASS.
69
+ # WHAT THIS FENCE IS ABOUT, NARROWLY. It is not "GNU tools" and it is not the
70
+ # start-key: the python join below now mints the SAME `ps:` string the TS core mints
71
+ # (audit Q2), so start-key and ppid are portable in this doctor too and that half of
72
+ # the old reason is retired. What is left is bridge DISCOVERY — finding the live
73
+ # entwurf MCP children by their per-process ENVIRONMENT — which has no measured
74
+ # portable equivalent, so the live-owner tier is unreachable rather than merely
75
+ # unobserved (the rc=3 branch below is the same verdict, stated where it fires).
76
+ # Darwin therefore stays nonzero here: a doctor that cannot measure its central axis
77
+ # must never downgrade "unsupported" to a static/synthetic PASS.
73
78
  case "$(uname -s)" in
74
79
  Linux) ok "Linux supported — the certified live axis (live owner join is instrumentable)" ;;
75
- Darwin) bad "macOS is NOT YET VERIFIED/CERTIFIED for this repair cut: strict live-owner certification currently requires /proc. The installer refuses Darwin; this doctor remains nonzero for diagnosis/legacy cleanup. Future validation may reopen the lane." ;;
80
+ Darwin) bad "macOS is NOT CERTIFIED for this cut: the live-owner join needs per-process ENVIRONMENT discovery of the live bridge children, and no portable Darwin equivalent has been MEASURED yet (start-key/ppid are portable here now — that half of the old reason is retired). This doctor stays nonzero on Darwin for diagnosis and legacy cleanup until a physical Darwin receipt exists." ;;
76
81
  *) bad "$(uname -s) unsupported (Claude meta-bridge repair cut is Linux-only)" ;;
77
82
  esac
78
83
 
@@ -982,7 +987,7 @@ fi
982
987
  echo "[live Claude MCP owner join]"
983
988
  if command -v python3 >/dev/null; then
984
989
  if JOIN_OUT="$(python3 - "$AGENT" "$META_SESSIONS" <<'PY' 2>&1
985
- import json, os, sys
990
+ import json, re, subprocess, sys
986
991
  from pathlib import Path
987
992
 
988
993
  agent = Path(sys.argv[1]).expanduser().resolve()
@@ -990,23 +995,52 @@ store = Path(sys.argv[2]).expanduser().resolve()
990
995
  proc = Path("/proc")
991
996
  if not proc.is_dir():
992
997
  # rc=3 is a PLATFORM verdict, distinct from rc=2 ("no session open right now").
993
- # The runtime this doctor verifies has a `ps` fallback for start-key/ppid, but
994
- # bridge DISCOVERY needs per-process environ, which has no portable equivalent
995
- # here so the live tier is unreachable, not merely unobserved.
996
- print("live owner join is NOT INSTRUMENTABLE on this platform: /proc is unavailable, so entwurf MCP children cannot be discovered")
998
+ # NARROWLY: start-key and ppid are NOT the reason — `parent()`/`start_key()` below
999
+ # carry the same two-tier `/proc`-then-`ps` fallback the runtime writer has, so they
1000
+ # answer on any platform with a `ps`. Bridge DISCOVERY is the reason: it needs each
1001
+ # candidate's per-process ENVIRONMENT, which has no measured portable equivalent, so
1002
+ # the live tier is unreachable here rather than merely unobserved.
1003
+ print("live owner join is NOT INSTRUMENTABLE on this platform: what holds the lane closed is per-process environment DISCOVERY alone — start-key and ppid are portable there (`ps` fallback, same string the TS core mints), but no measured Darwin equivalent exists for reading each live bridge child's environment")
997
1004
  raise SystemExit(3)
998
1005
 
999
1006
  def stat_fields(pid):
1000
1007
  text = Path(f"/proc/{pid}/stat").read_text()
1001
1008
  return text[text.rfind(")") + 2:].split()
1002
1009
 
1010
+ def ps_field(pid, fmt):
1011
+ # The SAME argv vector the writer uses (meta-session.ts parentPid/processStartKey),
1012
+ # so the two writers cannot disagree by construction.
1013
+ try:
1014
+ out = subprocess.run(["ps", "-o", fmt, "-p", str(pid)], capture_output=True, text=True).stdout
1015
+ except Exception:
1016
+ return ""
1017
+ return out.strip()
1018
+
1003
1019
  def parent(pid):
1020
+ if not isinstance(pid, int) or pid <= 0: return None
1004
1021
  try: return int(stat_fields(pid)[1])
1005
- except Exception: return None
1022
+ except Exception: pass
1023
+ # `ps -o ppid=` — literally the fallback parentPid() already ships.
1024
+ ppid = ps_field(pid, "ppid=")
1025
+ return int(ppid) if ppid.isdigit() and int(ppid) > 0 else None
1006
1026
 
1007
1027
  def start_key(pid):
1008
- try: return "linux:" + stat_fields(pid)[19]
1009
- except Exception: return ""
1028
+ # TWO WRITERS, ONE STRING. The runtime mints this key (meta-session.ts
1029
+ # processStartKey) and this doctor RE-mints it to compare against the marker; if the
1030
+ # two ever mint different strings for the same live pid the join fails for a reason
1031
+ # that has nothing to do with the install. So this is not "a portable start key" —
1032
+ # it is the same two tiers in the same order with the same prefixes: `/proc/<pid>/stat`
1033
+ # field 22 as `linux:<ticks>`, else `ps -o lstart= -p <pid>` trimmed as `ps:<lstart>`.
1034
+ # python `.strip()` and JS `.trim()` both strip ASCII whitespace from this single-line
1035
+ # output, and the argv vector is identical, so the bytes are identical (audit Q2).
1036
+ # "" stays the UNKNOWN answer and never matches a marker — fail-closed, unchanged.
1037
+ if not isinstance(pid, int) or pid <= 0: return ""
1038
+ try:
1039
+ starttime = stat_fields(pid)[19]
1040
+ if re.fullmatch(r"\d+", starttime): return "linux:" + starttime
1041
+ except Exception: pass
1042
+ lstart = ps_field(pid, "lstart=")
1043
+ return "ps:" + lstart if lstart else ""
1010
1044
 
1011
1045
  def process_agent(env):
1012
1046
  raw = env.get("PI_CODING_AGENT_DIR")
@@ -1088,7 +1122,7 @@ PY
1088
1122
  # send an operator to completely different places.
1089
1123
  case "$JOIN_RC" in
1090
1124
  2) bad "NOT CERTIFIED — $JOIN_OUT. Nothing here says the install is broken: the evidence simply does not exist yet. Open a Claude Code session (or restart the affected one) and run this doctor again." ;;
1091
- 3) bad "NOT CERTIFIED on this platform — $JOIN_OUT. This repair cut currently certifies the Claude meta-bridge on Linux only; static/synthetic evidence cannot certify $(uname -s), and future native validation may reopen that lane." ;;
1125
+ 3) bad "NOT CERTIFIED on this platform — $JOIN_OUT. This repair cut certifies the Claude meta-bridge on Linux only; static/synthetic evidence cannot certify $(uname -s), and only a physical native run can reopen that lane." ;;
1092
1126
  *) bad "$JOIN_OUT. Re-run install-meta-bridge, restart the affected Claude session(s), then run doctor again." ;;
1093
1127
  esac
1094
1128
  fi
@@ -1121,7 +1155,14 @@ livewrite_schema() { # $1=meta-session.<ext> → v3|v2|v1|absent
1121
1155
  elif grep -q "serializeMetaIdentity" "$1"; then echo "v2"
1122
1156
  else echo "v1"; fi
1123
1157
  }
1124
- hash12() { [ -f "$1" ] && sha256sum "$1" | cut -c1-12 || echo "------------"; }
1158
+ # 12-hex digest, portable. NOT `sha256sum` (coreutils, absent on macOS) and NOT
1159
+ # `shasum -a 256` — that would be a second digest convention, while python3 is already
1160
+ # a hard prerequisite of this whole doctor (line 31 reads install-state through it). So
1161
+ # this reuses the python3 hashlib form omp-receive-doctor.sh:99 already ships. An absent
1162
+ # file still yields `------------`, and a hashing failure now falls into that same
1163
+ # placeholder instead of an empty string — either way the parity rows below read it as
1164
+ # drift and go red, which is the fail-closed side.
1165
+ hash12() { [ -f "$1" ] && python3 -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest()[:12])' "$1" 2>/dev/null || echo "------------"; }
1125
1166
  registry_for_ms() { # $1=bundle meta-session.ts → sibling plugin-root registry path
1126
1167
  [ -f "$1" ] || { echo ""; return; }
1127
1168
  dirname "$(dirname "$1")"
@@ -25,9 +25,13 @@
25
25
  # Idempotent: re-running removes the prior marketplace/plugin first, so a
26
26
  # `nix rebuild` that moved node just re-bakes and re-installs cleanly.
27
27
  #
28
- # Platform: Linux only for the #51 repair cut. macOS cannot reach the strict
29
- # live-owner certification tier, so accepting an install there would advertise a
30
- # surface this release can never certify. Windows and every other platform fail fast.
28
+ # Platform: Linux and Darwin. This installer's own platform dependency is python3 +
29
+ # node + the claude CLI (all checked below); it touches no /proc, no systemd, no
30
+ # GNU-only tool. Windows and every other platform fail fast. Live-owner rail
31
+ # certification is a SEPARATE evidence axis owned by doctor-meta-bridge (Hard Rule 17:
32
+ # installation portability and rail/runtime support are separate evidence axes) — a
33
+ # green install here does not certify the Darwin rail; run ./run.sh doctor-meta-bridge
34
+ # for that verdict.
31
35
  set -euo pipefail
32
36
 
33
37
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -64,11 +68,13 @@ esac
64
68
 
65
69
  die() { echo "meta-bridge-install: $*" >&2; exit 1; }
66
70
 
67
- # --- platform gate (Linux-only repair contract) -----------------------------
71
+ # --- platform gate (installer's own dependency, not rail certification) -----
72
+ # Darwin is open on the same precedent as meta-bridge-uninstall.sh:20-23: this
73
+ # installer touches no /proc and no GNU-only tool, so its own mechanism is portable
74
+ # even while the live-owner rail (doctor-meta-bridge) stays a separate, unproven axis.
68
75
  case "$(uname -s)" in
69
- Linux) ;;
70
- Darwin) die "macOS is not yet verified/certified for this repair cut. Strict live-owner certification currently requires /proc, so Darwin install is refused rather than left NOT CERTIFIED; future validation may reopen this lane." ;;
71
- *) die "unsupported platform '$(uname -s)'. The Claude meta-bridge repair cut supports Linux only (no unverified fallback)." ;;
76
+ Linux | Darwin) ;;
77
+ *) die "unsupported platform '$(uname -s)'. This installer requires Linux or Darwin." ;;
72
78
  esac
73
79
 
74
80
  # --- toolchain gate, part 1: this machine's own runtime ---------------------
@@ -14,12 +14,14 @@ PLUGIN="entwurf-meta-receive"
14
14
 
15
15
  die() { echo "meta-bridge-uninstall: $*" >&2; exit 1; }
16
16
 
17
- # Darwin stays here intentionally even though NEW installs are Linux-only in the #51
18
- # repair cut. Removing the inverse would strand an older macOS install precisely when
19
- # support narrows; uninstall is a legacy-cleanup capability, not install certification.
17
+ # Darwin is accepted on both halves as of 0.20.0. It was accepted HERE first, while new
18
+ # installs were still refused, because removing the inverse would strand an older macOS
19
+ # install precisely when support narrows. That asymmetry is now gone the installer
20
+ # refuses on its real dependency instead of the platform name — so the two halves agree
21
+ # and this fence only turns away platforms neither half has ever run on.
20
22
  case "$(uname -s)" in
21
23
  Linux | Darwin) ;;
22
- *) die "unsupported platform '$(uname -s)'. This repair cut certifies new Claude meta-bridge installs on Linux only; uninstall additionally permits Darwin so legacy state can be removed honestly." ;;
24
+ *) die "unsupported platform '$(uname -s)'. Both halves of the Claude meta-bridge repair cut run on Linux and Darwin only. Rail certification is a separate axis owned by doctor-meta-bridge." ;;
23
25
  esac
24
26
 
25
27
  command -v python3 >/dev/null || die "'python3' not on PATH. It is required for stateful uninstall."