@junghanacs/entwurf 0.19.0 → 0.20.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 (74) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +23 -5
  3. package/CHANGELOG.md +261 -0
  4. package/DELIVERY.md +59 -6
  5. package/README.md +41 -9
  6. package/VERIFY.md +9 -1
  7. package/docs/acp-backend-rail.md +41 -16
  8. package/docs/external-mcp-host.md +10 -5
  9. package/docs/fresh-cut-policy.md +3 -2
  10. package/docs/setup-clean-host.md +93 -7
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
  15. package/package.json +3 -3
  16. package/pi-extensions/acp-provider.ts +1 -1
  17. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  18. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  19. package/pi-extensions/lib/acp/backend.ts +3 -3
  20. package/pi-extensions/lib/acp/event-mapper.ts +4 -4
  21. package/pi-extensions/lib/acp/models.ts +3 -3
  22. package/pi-extensions/lib/acp/overlay.ts +6 -2
  23. package/pi-extensions/lib/acp/tool-surface.ts +6 -0
  24. package/pi-extensions/lib/meta-session.ts +25 -0
  25. package/run.sh +104 -33
  26. package/scripts/agy-bridge.sh +1 -1
  27. package/scripts/agy-imprint.sh +10 -7
  28. package/scripts/check-acp-carrier-augment.ts +6 -5
  29. package/scripts/check-acp-cortex.ts +41 -0
  30. package/scripts/check-acp-provider-surface.ts +9 -6
  31. package/scripts/check-acp-usage-accounting.ts +9 -9
  32. package/scripts/check-copilot-birth-hook.ts +3 -1
  33. package/scripts/check-copilot-receive-arm.ts +169 -21
  34. package/scripts/check-copilot-statusline.ts +3 -1
  35. package/scripts/check-gate-qualification.ts +11 -10
  36. package/scripts/check-hook-launch-topology.ts +6 -5
  37. package/scripts/check-install-surface.ts +25 -8
  38. package/scripts/check-meta-facts.ts +6 -4
  39. package/scripts/check-meta-hook-session-switch.ts +4 -3
  40. package/scripts/check-meta-identity-consumers.ts +8 -7
  41. package/scripts/check-meta-receiver-marker.ts +2 -1
  42. package/scripts/check-omp-birth-hook.ts +3 -2
  43. package/scripts/check-probe-bridge-command.ts +3 -1
  44. package/scripts/check-setup-qualification.sh +21 -0
  45. package/scripts/copilot-bridge-doctor.sh +15 -5
  46. package/scripts/copilot-bridge-install.sh +6 -2
  47. package/scripts/copilot-launch.sh +16 -2
  48. package/scripts/copilot-receive-bridge.sh +80 -13
  49. package/scripts/lib/reclaim-on-exit.ts +86 -0
  50. package/scripts/meta-bridge-doctor.sh +56 -15
  51. package/scripts/meta-bridge-install.sh +13 -7
  52. package/scripts/meta-bridge-uninstall.sh +6 -4
  53. package/scripts/mutants/acp-cortex.json +26 -2
  54. package/scripts/mutants/copilot-receive.json +26 -0
  55. package/scripts/mutants/omp-birth.json +23 -6
  56. package/scripts/mutants/pi-package-ownership.json +26 -0
  57. package/scripts/mutants/setup-verdict.json +13 -0
  58. package/scripts/omp-bridge-doctor.sh +110 -41
  59. package/scripts/omp-bridge-install.sh +6 -2
  60. package/scripts/omp-receive-install.sh +7 -2
  61. package/scripts/pi_settings_io.py +3 -1
  62. package/scripts/raw-async-delivery/README.md +10 -1
  63. package/scripts/raw-codex-measure/README.md +689 -0
  64. package/scripts/raw-codex-measure/source-audit.md +243 -0
  65. package/scripts/raw-macos-measure/README.md +148 -0
  66. package/scripts/raw-macos-measure/probe.sh +389 -0
  67. package/scripts/register-pi-package.py +38 -30
  68. package/scripts/register-pi-provider.py +3 -2
  69. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  70. package/scripts/smoke-meta-async-drift.sh +7 -5
  71. package/scripts/smoke-meta-install-state.sh +70 -19
  72. package/scripts/smoke-omp-bridge-state.sh +38 -0
  73. package/scripts/smoke-setup-verdict.sh +100 -0
  74. package/scripts/smoke-user-scope-citizen.sh +199 -71
@@ -0,0 +1,389 @@
1
+ #!/bin/sh
2
+ # raw-macos-measure/probe.sh — Darwin host measurement for the macOS parity lane (#78).
3
+ #
4
+ # MEASUREMENT ONLY. This script writes nothing outside its own temp dir, installs
5
+ # nothing, needs no entwurf installation, and touches no operator config. It is
6
+ # meant to be run ONCE on a borrowed Mac and its whole stdout pasted into
7
+ # scripts/raw-macos-measure/README.md.
8
+ #
9
+ # It is /bin/sh (not bash) on purpose: macOS ships bash 3.2 and one of the things
10
+ # under measurement is what a shell can assume here at all.
11
+ #
12
+ # ./probe.sh # human-readable ledger on stdout
13
+ #
14
+ # Every cell prints its own verdict token so a reader never has to interpret raw
15
+ # tool output: PRESENT / ABSENT / OK / DIFFERS / FAIL / UNKNOWN.
16
+
17
+ set -u
18
+
19
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}/entwurf-macos-probe.XXXXXX") || exit 1
20
+ trap 'rm -rf "$TMP"' EXIT INT TERM
21
+
22
+ hdr() {
23
+ printf '\n== %s ==\n' "$1"
24
+ }
25
+
26
+ # `have <cmd>` -> PRESENT/ABSENT plus the resolved path.
27
+ have() {
28
+ if p=$(command -v "$1" 2>/dev/null); then
29
+ printf ' %-18s PRESENT %s\n' "$1" "$p"
30
+ return 0
31
+ fi
32
+ printf ' %-18s ABSENT\n' "$1"
33
+ return 1
34
+ }
35
+
36
+ printf 'entwurf raw-macos-measure probe\n'
37
+ printf 'run at: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') UTC"
38
+
39
+ hdr 'M1. host identity'
40
+ if command -v sw_vers >/dev/null 2>&1; then
41
+ sw_vers | sed 's/^/ /'
42
+ else
43
+ printf ' sw_vers ABSENT — this is not a Darwin host; the ledger cell is void.\n'
44
+ fi
45
+ printf ' uname -srm: %s\n' "$(uname -srm)"
46
+ printf ' hw.model: %s\n' "$(sysctl -n hw.model 2>/dev/null || echo UNKNOWN)"
47
+ printf ' cpu: %s\n' "$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo UNKNOWN)"
48
+
49
+ hdr 'M2. shell — what a script may assume'
50
+ printf ' /bin/sh -> %s\n' "$(/bin/sh -c 'echo ${BASH_VERSION:-not-bash}' 2>/dev/null)"
51
+ printf ' /bin/bash -> %s\n' "$(/bin/bash -c 'echo ${BASH_VERSION:-unknown}' 2>/dev/null || echo ABSENT)"
52
+ printf ' env bash -> %s (%s)\n' \
53
+ "$(env bash -c 'echo ${BASH_VERSION:-unknown}' 2>/dev/null || echo ABSENT)" \
54
+ "$(command -v bash 2>/dev/null || echo no-bash-on-PATH)"
55
+ # bash 4 features the repo's gates use: `declare -A`, `mapfile`.
56
+ if env bash -c 'declare -A _x' >/dev/null 2>&1; then
57
+ printf ' declare -A (bash4) OK\n'
58
+ else
59
+ printf ' declare -A (bash4) FAIL — associative arrays unavailable to `env bash`\n'
60
+ fi
61
+ if env bash -c 'mapfile -t _y < /dev/null' >/dev/null 2>&1; then
62
+ printf ' mapfile (bash4) OK\n'
63
+ else
64
+ printf ' mapfile (bash4) FAIL\n'
65
+ fi
66
+
67
+ hdr 'M3. runtime prerequisites'
68
+ have node && printf ' %-18s %s\n' 'node --version' "$(node --version 2>/dev/null)"
69
+ have npm && printf ' %-18s %s\n' 'npm --version' "$(npm --version 2>/dev/null)"
70
+ have pnpm && printf ' %-18s %s\n' 'pnpm --version' "$(pnpm --version 2>/dev/null)"
71
+ # python3 on macOS may be the CommandLineTools STUB at /usr/bin/python3, which
72
+ # prints nothing useful and pops a GUI installer instead. The shipped operator
73
+ # surface calls python3 in hundreds of places, so a stub is a total install
74
+ # failure, not a degraded cell.
75
+ if have python3; then
76
+ printf ' %-18s %s\n' 'python3 -V' "$(python3 -V 2>&1)"
77
+ if python3 -c 'print("live")' 2>/dev/null | grep -q live; then
78
+ printf ' %-18s REAL (executes code, no install prompt)\n' 'python3 kind'
79
+ else
80
+ printf ' %-18s STUB/BROKEN — the whole install+doctor surface dies here\n' 'python3 kind'
81
+ fi
82
+ fi
83
+ have git
84
+ have tmux && printf ' %-18s %s\n' 'tmux -V' "$(tmux -V 2>/dev/null)"
85
+
86
+ hdr 'M4. GNU-vs-BSD tool matrix (the P1 substitution surface)'
87
+ have sha256sum || true
88
+ have shasum || true
89
+ have openssl || true
90
+ have realpath || true
91
+ have timeout || true
92
+ have gtimeout || true
93
+ have gsed || true
94
+ have gstat || true
95
+ printf '\n -- behavioural probes (presence is not compatibility) --\n'
96
+ printf 'abc' >"$TMP/f"
97
+ # stat: GNU `-c %s` vs BSD `-f %z`
98
+ if stat -c %s "$TMP/f" >/dev/null 2>&1; then
99
+ printf ' stat -c %%s OK (GNU form works)\n'
100
+ else
101
+ printf ' stat -c %%s FAIL (GNU form rejected) — BSD `stat -f %%z` = %s\n' \
102
+ "$(stat -f %z "$TMP/f" 2>/dev/null || echo FAIL)"
103
+ fi
104
+ # readlink -f
105
+ ln -s "$TMP/f" "$TMP/link"
106
+ if readlink -f "$TMP/link" >/dev/null 2>&1; then
107
+ printf ' readlink -f OK\n'
108
+ else
109
+ printf ' readlink -f FAIL (no GNU -f)\n'
110
+ fi
111
+ # stat: both fields the repo reads (size AND mtime), because a working `-f %z`
112
+ # does not promise `-f %m`.
113
+ printf ' stat -f %%z (size) %s\n' "$(stat -f %z "$TMP/f" 2>/dev/null || echo FAIL)"
114
+ printf ' stat -f %%m (mtime) %s\n' "$(stat -f %m "$TMP/f" 2>/dev/null || echo FAIL)"
115
+ printf ' stat -c %%Y (mtime) %s\n' "$(stat -c %Y "$TMP/f" 2>/dev/null || echo FAIL)"
116
+ # sha256: which producer, and does the digest text match across producers?
117
+ S_SUM=$(sha256sum "$TMP/f" 2>/dev/null | awk '{print $1}')
118
+ S_SHA=$(shasum -a 256 "$TMP/f" 2>/dev/null | awk '{print $1}')
119
+ S_SSL=$(openssl dgst -sha256 "$TMP/f" 2>/dev/null | awk '{print $NF}')
120
+ printf ' sha256sum %s\n' "${S_SUM:-ABSENT}"
121
+ printf ' shasum -a 256 %s\n' "${S_SHA:-ABSENT}"
122
+ printf ' openssl dgst %s\n' "${S_SSL:-ABSENT}"
123
+ for v in "$S_SUM" "$S_SHA" "$S_SSL"; do
124
+ [ -n "$v" ] && [ "$v" != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ] &&
125
+ printf ' DIGEST MISMATCH a producer disagrees with the known sha256 of "abc"\n'
126
+ done
127
+ # NUL-delimited pipelines
128
+ if printf 'a\0b\0' | sort -z >/dev/null 2>&1; then
129
+ printf ' sort -z OK\n'
130
+ else
131
+ printf ' sort -z FAIL (no BSD equivalent flag)\n'
132
+ fi
133
+ if printf 'a\0' | xargs -0 true >/dev/null 2>&1; then
134
+ printf ' xargs -0 OK\n'
135
+ else
136
+ printf ' xargs -0 FAIL\n'
137
+ fi
138
+ if printf 'a\0' | xargs -0r true >/dev/null 2>&1; then
139
+ printf ' xargs -0r OK\n'
140
+ else
141
+ printf ' xargs -0r FAIL (GNU-only -r; BSD xargs already skips empty input)\n'
142
+ fi
143
+ # grep -P
144
+ if printf 'a\n' | grep -P 'a' >/dev/null 2>&1; then
145
+ printf ' grep -P OK\n'
146
+ else
147
+ printf ' grep -P FAIL (no PCRE); grep -E = %s\n' \
148
+ "$(printf 'a\n' | grep -E 'a' >/dev/null 2>&1 && echo OK || echo FAIL)"
149
+ fi
150
+ # date -d
151
+ if date -d '@0' >/dev/null 2>&1; then
152
+ printf ' date -d OK\n'
153
+ else
154
+ printf ' date -d FAIL (BSD date uses -r/-j -f)\n'
155
+ fi
156
+ # mktemp with a template (both accept it, but confirm)
157
+ printf ' mktemp -d %s\n' "$(mktemp -d "${TMPDIR:-/tmp}/probe.XXXXXX" >/dev/null 2>&1 && echo OK || echo FAIL)"
158
+
159
+ hdr 'M5. start-key sources — the identity rail (meta-session.ts:1554)'
160
+ printf ' /proc present? %s\n' "$([ -d /proc ] && echo YES || echo NO)"
161
+ SELF=$$
162
+ printf ' ps -o lstart= self: %s\n' "$(ps -o lstart= -p "$SELF" 2>/dev/null | sed 's/^ *//;s/ *$//')"
163
+ printf ' ps -o command= self: %s\n' "$(ps -o command= -p "$SELF" 2>/dev/null | sed 's/^ *//' | cut -c1-100)"
164
+ # RESOLUTION TEST: two children started back to back. If their lstart strings are
165
+ # equal, the `ps:` scheme's pid-reuse defense is 1-second granular.
166
+ sleep 4 &
167
+ C1=$!
168
+ sleep 4 &
169
+ C2=$!
170
+ L1=$(ps -o lstart= -p "$C1" 2>/dev/null | sed 's/^ *//;s/ *$//')
171
+ L2=$(ps -o lstart= -p "$C2" 2>/dev/null | sed 's/^ *//;s/ *$//')
172
+ printf ' child A (%s): %s\n' "$C1" "$L1"
173
+ printf ' child B (%s): %s\n' "$C2" "$L2"
174
+ if [ "$L1" = "$L2" ]; then
175
+ printf ' RESOLUTION 1-SECOND (two back-to-back starts share one key string)\n'
176
+ else
177
+ printf ' RESOLUTION SUB-SECOND (back-to-back starts differ) — re-run to confirm\n'
178
+ fi
179
+ # WIDENING candidate: would including argv in the key have separated them? Both
180
+ # children run the SAME argv on purpose — that is the WORST case for widening, and
181
+ # the worst case is the one a fail-closed identity rail has to survive.
182
+ W1=$(ps -o lstart=,command= -p "$C1" 2>/dev/null | sed 's/^ *//;s/ *$//')
183
+ W2=$(ps -o lstart=,command= -p "$C2" 2>/dev/null | sed 's/^ *//;s/ *$//')
184
+ printf ' widened key A %s\n' "$W1"
185
+ printf ' widened key B %s\n' "$W2"
186
+ if [ "$W1" = "$W2" ]; then
187
+ printf ' WIDENING INSUFFICIENT ALONE (identical argv collides even widened)\n'
188
+ else
189
+ printf ' WIDENING SEPARATES (argv distinguished two same-second starts)\n'
190
+ fi
191
+ kill "$C1" "$C2" 2>/dev/null
192
+ wait "$C1" 2>/dev/null
193
+ wait "$C2" 2>/dev/null
194
+ # MICROSECOND candidate: libproc proc_pidinfo(PROC_PIDTBSDINFO) via python3 ctypes.
195
+ if command -v python3 >/dev/null 2>&1; then
196
+ python3 - "$SELF" <<'PY' 2>&1 | sed 's/^/ /'
197
+ import ctypes, ctypes.util, sys
198
+ pid = int(sys.argv[1])
199
+ lib = ctypes.util.find_library("proc") or "/usr/lib/libproc.dylib"
200
+ try:
201
+ libproc = ctypes.CDLL(lib)
202
+ except OSError as e:
203
+ print(f"libproc UNAVAILABLE ({e})")
204
+ raise SystemExit(0)
205
+
206
+ PROC_PIDTBSDINFO = 3
207
+ class ProcBsdInfo(ctypes.Structure):
208
+ _fields_ = [
209
+ ("pbi_flags", ctypes.c_uint32), ("pbi_status", ctypes.c_uint32),
210
+ ("pbi_xstatus", ctypes.c_uint32), ("pbi_pid", ctypes.c_uint32),
211
+ ("pbi_ppid", ctypes.c_uint32), ("pbi_uid", ctypes.c_uint32),
212
+ ("pbi_gid", ctypes.c_uint32), ("pbi_ruid", ctypes.c_uint32),
213
+ ("pbi_rgid", ctypes.c_uint32), ("pbi_svuid", ctypes.c_uint32),
214
+ ("pbi_svgid", ctypes.c_uint32), ("rfu_1", ctypes.c_uint32),
215
+ ("pbi_comm", ctypes.c_char * 16), ("pbi_name", ctypes.c_char * 32),
216
+ ("pbi_nfiles", ctypes.c_uint32), ("pbi_pgid", ctypes.c_uint32),
217
+ ("pbi_pjobc", ctypes.c_uint32), ("e_tdev", ctypes.c_uint32),
218
+ ("e_tpgid", ctypes.c_uint32), ("pbi_nice", ctypes.c_int32),
219
+ ("pbi_start_tvsec", ctypes.c_uint64), ("pbi_start_tvusec", ctypes.c_uint64),
220
+ ]
221
+
222
+ info = ProcBsdInfo()
223
+ n = libproc.proc_pidinfo(pid, PROC_PIDTBSDINFO, ctypes.c_uint64(0),
224
+ ctypes.byref(info), ctypes.sizeof(info))
225
+ if n != ctypes.sizeof(info):
226
+ print(f"proc_pidinfo FAIL (returned {n}, expected {ctypes.sizeof(info)})")
227
+ else:
228
+ print(f"proc_pidinfo OK pid={info.pbi_pid} "
229
+ f"start={info.pbi_start_tvsec}.{info.pbi_start_tvusec:06d} "
230
+ f"comm={info.pbi_comm.decode(errors='replace')}")
231
+ print("RESOLUTION MICROSECOND (a sub-second start key IS reachable here)")
232
+ PY
233
+ else
234
+ printf ' proc_pidinfo SKIPPED (no python3)\n'
235
+ fi
236
+
237
+ hdr 'M6. process discovery — can a reader see another process at all?'
238
+ # The bridge/receiver doctors narrow candidates through /proc/<pid>/{environ,cmdline}
239
+ # (scripts/omp-bridge-doctor.sh:258-267, scripts/copilot-receive-bridge.sh:264-289).
240
+ # Darwin has neither, so `ps -E` is the only candidate — and WHICH PROCESS IS ASKED
241
+ # decides the answer. Apple-signed, SIP-protected binaries refuse environ reads by
242
+ # construction, so probing `/bin/sleep` would report a false REJECTED.
243
+ #
244
+ # SCOPE, STATED EXACTLY: the node group below is a PROXY for entwurf's own MCP child,
245
+ # which is a node process this probe can start. It is NOT a measurement of the Copilot
246
+ # CLI or of omp: those are different binaries with their own signing and hardened-
247
+ # runtime state, and a proxy result does not transfer to them. The optional group 3
248
+ # probes a REAL copilot/omp process only if the operator already has one running; when
249
+ # it does not, that is reported as NOT MEASURED, never as coverage.
250
+ probe_environ() {
251
+ # $1 = label, $2 = pid, $3 = marker expected in the environment (or "")
252
+ _lbl=$1
253
+ _pid=$2
254
+ _marker=$3
255
+ if ps -E -p "$_pid" >/dev/null 2>&1; then
256
+ _out=$(ps -Eww -p "$_pid" 2>/dev/null | tail -1)
257
+ if [ -n "$_marker" ]; then
258
+ if printf '%s' "$_out" | grep -q "$_marker"; then
259
+ printf ' %-22s VISIBLE — marker found in `ps -Eww` output\n' "$_lbl"
260
+ else
261
+ printf ' %-22s ACCEPTED-BUT-EMPTY — ps -E returned, marker ABSENT\n' "$_lbl"
262
+ fi
263
+ else
264
+ printf ' %-22s ACCEPTED (width %s chars)\n' "$_lbl" \
265
+ "$(printf '%s' "$_out" | wc -c | tr -d ' ')"
266
+ fi
267
+ else
268
+ printf ' %-22s REJECTED by ps -E\n' "$_lbl"
269
+ fi
270
+ }
271
+ # Group 1 — UNRESTRICTED target: a node process we start ourselves, carrying a marker.
272
+ # This is the group that decides whether the doctors' discovery path survives.
273
+ if command -v node >/dev/null 2>&1; then
274
+ ENTWURF_PROBE_MARKER=hello-entwurf \
275
+ node -e 'setTimeout(function(){}, 5000)' >/dev/null 2>&1 &
276
+ N1=$!
277
+ sleep 1
278
+ printf ' node target pid %s\n' "$N1"
279
+ printf ' node argv %s\n' \
280
+ "$(ps -o command= -p "$N1" 2>/dev/null | sed 's/^ *//' | cut -c1-120)"
281
+ probe_environ 'node environ' "$N1" 'ENTWURF_PROBE_MARKER=hello-entwurf'
282
+ printf ' node ppid %s (expect this shell, %s)\n' \
283
+ "$(ps -o ppid= -p "$N1" 2>/dev/null | tr -d ' ')" "$SELF"
284
+ kill "$N1" 2>/dev/null
285
+ wait "$N1" 2>/dev/null
286
+ else
287
+ printf ' node target SKIPPED (no node on PATH) — THIS CELL IS THE IMPORTANT ONE\n'
288
+ fi
289
+ # Group 2 — RESTRICTED control: an Apple-signed system binary. A REJECTED here beside
290
+ # a VISIBLE above is the proof that the refusal is SIP, not a missing capability.
291
+ sleep 5 &
292
+ D1=$!
293
+ printf ' sleep argv %s\n' \
294
+ "$(ps -o command= -p "$D1" 2>/dev/null | sed 's/^ *//' | cut -c1-100)"
295
+ probe_environ '/bin/sleep environ' "$D1" ''
296
+ kill "$D1" 2>/dev/null
297
+ wait "$D1" 2>/dev/null
298
+ # Group 3 — THE REAL TARGETS, when the operator already has one running. Group 1 is a
299
+ # proxy; these are the binaries the doctors actually interrogate. A different signing
300
+ # or hardened-runtime state here than on our own node child is exactly the result that
301
+ # would invalidate the proxy, so an absent process is NOT MEASURED, never coverage.
302
+ for real in copilot omp claude; do
303
+ _rp=$(pgrep -x "$real" 2>/dev/null | head -1)
304
+ if [ -n "${_rp:-}" ]; then
305
+ printf ' %-22s live pid %s\n' "$real (real target)" "$_rp"
306
+ probe_environ "$real environ" "$_rp" ''
307
+ else
308
+ printf ' %-22s NOT MEASURED — no live `%s` process on this host\n' \
309
+ "$real (real target)" "$real"
310
+ fi
311
+ done
312
+ # Full-table scan: the fallback if environ is unreadable is argv-only matching over
313
+ # every process. Measure that it works AND what it costs.
314
+ SCAN_START=$(date +%s)
315
+ SCAN_N=$(ps -A -o pid=,ppid=,command= 2>/dev/null | wc -l | tr -d ' ')
316
+ SCAN_END=$(date +%s)
317
+ printf ' ps -A scan %s rows in ~%ss\n' "$SCAN_N" "$((SCAN_END - SCAN_START))"
318
+ printf ' pgrep present? %s\n' "$(command -v pgrep >/dev/null 2>&1 && echo YES || echo NO)"
319
+ printf ' lsof present? %s\n' "$(command -v lsof >/dev/null 2>&1 && echo YES || echo NO)"
320
+
321
+ hdr 'M7. pid space — how reachable is a same-second pid reuse?'
322
+ printf ' kern.maxproc: %s\n' "$(sysctl -n kern.maxproc 2>/dev/null || echo UNKNOWN)"
323
+ printf ' kern.maxprocperuid: %s\n' "$(sysctl -n kern.maxprocperuid 2>/dev/null || echo UNKNOWN)"
324
+ printf ' current pid: %s\n' "$SELF"
325
+ printf ' (PID_MAX on Darwin is a compile-time 99999; a wrap needs that many spawns)\n'
326
+
327
+ hdr 'M8. filesystem semantics that installers depend on'
328
+ printf ' case-sensitive? '
329
+ touch "$TMP/CaseFile"
330
+ if [ -e "$TMP/casefile" ]; then printf 'NO (case-INSENSITIVE volume)\n'; else printf 'YES\n'; fi
331
+ printf ' HOME: %s\n' "${HOME:-UNSET}"
332
+ printf ' XDG_DATA_HOME: %s\n' "${XDG_DATA_HOME:-unset (entwurf defaults apply)}"
333
+ printf ' symlink support: %s\n' "$([ -L "$TMP/link" ] && echo OK || echo FAIL)"
334
+ printf ' ~/.local/bin on PATH? %s\n' \
335
+ "$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -qx "$HOME/.local/bin" && echo YES || echo NO)"
336
+
337
+ hdr 'M9. fs.watch rename semantics — the mailbox doorbell'
338
+ # The receiver arms a watch and a sender delivers by writing then renaming into
339
+ # place. Darwin's fs.watch is FSEvents-backed, not inotify: confirm a rename-over
340
+ # actually raises an event for the WATCHED path.
341
+ if command -v node >/dev/null 2>&1; then
342
+ mkdir -p "$TMP/watch"
343
+ printf 'v1\n' >"$TMP/watch/target"
344
+ node -e '
345
+ const fs = require("fs");
346
+ const dir = process.argv[1];
347
+ let fired = [];
348
+ let watchErr = null;
349
+ let w = null;
350
+ try {
351
+ w = fs.watch(dir + "/target", (ev) => fired.push(ev));
352
+ w.on("error", (e) => { watchErr = e; });
353
+ } catch (e) {
354
+ watchErr = e;
355
+ }
356
+ setTimeout(() => {
357
+ try {
358
+ fs.writeFileSync(dir + "/staged", "v2\n");
359
+ fs.renameSync(dir + "/staged", dir + "/target");
360
+ } catch (e) {
361
+ watchErr = watchErr || e;
362
+ }
363
+ }, 200);
364
+ setTimeout(() => {
365
+ try { if (w) w.close(); } catch (e) { /* closing a dead watcher is not the measurement */ }
366
+ // EVERY path prints exactly one verdict token. An error is an honest UNKNOWN, not a
367
+ // missing line: a borrowed host must never hand back a cell that simply is not there.
368
+ if (watchErr) {
369
+ console.log(" fs.watch rename UNKNOWN — watcher errored: " + (watchErr.code || watchErr.message));
370
+ } else if (fired.length) {
371
+ console.log(" fs.watch rename FIRED [" + fired.join(",") + "]");
372
+ } else {
373
+ console.log(" fs.watch rename SILENT — doorbell would not ring");
374
+ }
375
+ try {
376
+ const dw = fs.watch(dir, () => {});
377
+ dw.close();
378
+ console.log(" fs.watch on dir SUPPORTED");
379
+ } catch (e) {
380
+ console.log(" fs.watch on dir UNSUPPORTED — " + (e.code || e.message));
381
+ }
382
+ }, 1200);
383
+ ' "$TMP/watch" 2>&1 | sed 's/^/ /;s/^ \( *fs\.watch\)/\1/'
384
+ else
385
+ printf ' fs.watch SKIPPED (no node on PATH)\n'
386
+ fi
387
+
388
+ printf '\n== probe complete ==\n'
389
+ printf 'Paste this whole output into scripts/raw-macos-measure/README.md under the matching cell.\n'
@@ -22,29 +22,36 @@ settings-RELATIVE entry that resolves to repo_dir (see below). A look-alike repo
22
22
  removed. Every non-entwurf package and every other settings key is preserved.
23
23
 
24
24
  CANONICAL IS NOT ONLY THE ABSOLUTE PATH. A packages[] entry is resolved by pi
25
- against the SETTINGS FILE'S OWN DIRECTORY, so this repo's committed
26
- `.pi/settings.json` names itself portably as `".."` the exact form
27
- check-install-surface S7c pins. Comparing entries against the resolved absolute
28
- path ALONE did not recognize that as entwurf, so `setup` appended the absolute
29
- path beside it and rewrote the tracked, biome-governed file in a foreign style:
30
- a dev clone went RED at `pnpm check` step 1, diagnosed as a "formatting" error
31
- (#53 B). Two rules follow, and they are the same rule read forwards and backwards:
25
+ against the SETTINGS FILE'S OWN DIRECTORY, so a settings-relative entry such as
26
+ `".."` can name the very repo being registered. Comparing entries against the
27
+ resolved absolute path ALONE did not recognize that as entwurf, so `setup` appended
28
+ the absolute path beside it and rewrote a tracked, biome-governed file in a foreign
29
+ style: a dev clone went RED at `pnpm check` step 1, diagnosed as a "formatting"
30
+ error (#53 B). Two rules follow, and they are the same rule read forwards and
31
+ backwards:
32
32
  - register: an entry that RESOLVES to repo_dir is already canonical → no-op, and
33
33
  when a rewrite is genuinely needed a settings-relative self-reference is kept
34
34
  as the survivor, so the portable form is never silently absolutized;
35
35
  - remove: register only ever WRITES the absolute form, so a settings-relative
36
- self-reference cannot be install's own output — it was authored by the repo
37
- (this one commits `".."`) or by the operator. Uninstall is install's inverse,
38
- not a settings editor: it leaves that ONE class in place and SAYS SO on stdout,
39
- rather than deleting source bytes install never wrote. The cost is stated
40
- rather than hidden: on a settings file whose only entwurf entry is relative,
41
- `--remove` is a no-op and the package stays registered until a human edits it.
42
- would_remove() asks the same split, so `--dry-run` can never disagree with what
43
- `--remove` does.
36
+ self-reference cannot be install's own output — it was authored by hand.
37
+ Uninstall is install's inverse, not a settings editor: it leaves that ONE class
38
+ in place and SAYS SO on stdout, rather than deleting source bytes install never
39
+ wrote. The cost is stated rather than hidden: on a settings file whose only
40
+ entwurf entry is relative, `--remove` is a no-op and the package stays
41
+ registered until a human edits it. would_remove() asks the same split, so
42
+ `--dry-run` can never disagree with what `--remove` does.
43
+ This repo's own `.pi/settings.json` USED to be the load-bearing instance of that
44
+ shape. It no longer carries a packages[] entry at all: entwurf's checkout
45
+ self-registers through USER scope only, because a project entry naming "whichever
46
+ checkout I am in" plus a user entry naming the installed one are two package
47
+ identities to pi, and a second checkout then loads a rival copy of pi-extensions
48
+ and wins `--entwurf-control` (#110). The predicate stays because the shape is still
49
+ writable by hand, and because resolving-then-comparing is also what keeps an
50
+ unrelated relative package like `../../repos/gh/andenken` from being read as ours.
44
51
  A rewrite also preserves the file's existing indentation instead of forcing 2
45
52
  spaces. That is narrower than it sounds — it keeps the indent UNIT, not a
46
- formatter's line-collapsing decisions — so the byte-identity guarantee this repo's
47
- own settings depend on comes from the no-op path, never from the writer's style.
53
+ formatter's line-collapsing decisions — so a byte-identity guarantee always comes
54
+ from the no-op path, never from the writer's style.
48
55
 
49
56
  This wiring (user scope) dropped when `pi install` was removed from setup
50
57
  (2026-07-03: `--entwurf-control` unknown in a foreign cwd). Extracting it here
@@ -129,11 +136,11 @@ def source_of(item: object) -> object:
129
136
  def is_settings_relative_self(source: str, settings_dir: str, repo_dir: str) -> bool:
130
137
  """True iff this entry is a SETTINGS-RELATIVE path naming repo_dir itself.
131
138
 
132
- This is the repo's own committed portable form (`".."` in
133
- <repo>/.pi/settings.json) functionally identical to the absolute entry,
134
- because pi resolves a relative package source against the settings file's own
135
- directory. It is also the one shape register never writes, which is what lets
136
- remove treat it as source rather than as install state.
139
+ The portable form (`".."` in <repo>/.pi/settings.json) — functionally identical
140
+ to the absolute entry, because pi resolves a relative package source against the
141
+ settings file's own directory. It is also the one shape register never writes,
142
+ which is what lets remove treat it as source rather than as install state. This
143
+ repo stopped shipping it at #110; an operator may still write it by hand.
137
144
 
138
145
  Anything carrying a scheme (npm:/git:/https:) is a spec, not a path; anything
139
146
  absolute (`/`, `~`) is not settings-relative. Everything else is resolved and
@@ -159,8 +166,8 @@ def is_entwurf_source(source: str, repo_dir: str, settings_dir: str | None = Non
159
166
  - the exact resolved repo dir;
160
167
  - an npm install path ending in node_modules/@junghanacs/entwurf;
161
168
  - an explicit npm package source for @junghanacs/entwurf;
162
- - a settings-relative path that RESOLVES to the repo dir (the committed
163
- portable `".."`), when the caller supplies settings_dir;
169
+ - a settings-relative path that RESOLVES to the repo dir (the portable
170
+ `".."` form), when the caller supplies settings_dir;
164
171
  - a local filesystem path whose final directory is literally "entwurf"
165
172
  (dev clone / stale move). Remote URL/git-like strings are NOT treated as
166
173
  local paths merely because their last segment is "entwurf".
@@ -242,7 +249,7 @@ def register(settings_path: Path, repo_dir_arg: str) -> str:
242
249
  # the canonical absolute string or as a settings-relative path resolving there.
243
250
  # Both are the same registration to pi, so both are a no-op: order-insensitive, no
244
251
  # rewrite, mtime stable. Recognizing only the absolute form is what made `setup`
245
- # duplicate this repo's own committed `".."` and restyle the tracked file (#53 B).
252
+ # duplicate a settings-relative `".."` and restyle the tracked file (#53 B).
246
253
  # Both arms require the STRING form, so an object-form entry still collapses to a
247
254
  # canonical string exactly as before.
248
255
  if len(entwurf_entries) == 1 and isinstance(entwurf_entries[0], str) and (
@@ -252,8 +259,9 @@ def register(settings_path: Path, repo_dir_arg: str) -> str:
252
259
  return "noop"
253
260
 
254
261
  # A rewrite keeps the PORTABLE form when the file already had one: absolutizing a
255
- # committed `".."` would repair the duplicate and dirty the tracked bytes in the
256
- # same breath. Otherwise the canonical absolute path is what install writes.
262
+ # hand-written `".."` would repair the duplicate and dirty bytes install never
263
+ # wrote in the same breath. Otherwise the canonical absolute path is what install
264
+ # writes.
257
265
  survivor = next(
258
266
  (
259
267
  source_of(e) for e in entwurf_entries
@@ -277,7 +285,7 @@ def _removable(packages: list, repo_dir: str, settings_dir: str) -> tuple[list,
277
285
 
278
286
  PRESERVED = settings-relative self-references, the one shape register CANNOT
279
287
  have produced (it always writes the resolved absolute path). Such an entry is
280
- the repo's committed portable registration or the operator's own hand edit.
288
+ the operator's own hand edit.
281
289
  Deleting it would make uninstall a source editor, which is the defect #53 B is
282
290
  about, pointed the other way. main() prints what was kept so the incompleteness
283
291
  is loud rather than silent.
@@ -665,7 +673,7 @@ def remove_user(settings_path: Path, repo_dir_arg: str, state_path: Path, orphan
665
673
  else:
666
674
  print(f"remove: no entwurf packages[] entry naming this root ({settings_path})")
667
675
  if preserved_rel:
668
- print(f"remove: kept {len(preserved_rel)} settings-relative entwurf entr{'y' if len(preserved_rel) == 1 else 'ies'} — committed/operator source, not install state")
676
+ print(f"remove: kept {len(preserved_rel)} settings-relative entwurf entr{'y' if len(preserved_rel) == 1 else 'ies'} — operator source, not install state")
669
677
  if foreign_like:
670
678
  print(f"remove: kept {len(foreign_like)} entwurf-shaped entr{'y' if len(foreign_like) == 1 else 'ies'} with no recorded owner "
671
679
  "(not provably this root's) — use './run.sh doctor-pi-package' / takeover-user-scope to resolve ownership")
@@ -829,7 +837,7 @@ def main(argv: list[str]) -> int:
829
837
  if kept:
830
838
  print(
831
839
  f"remove: kept {kept} settings-relative entwurf entr{'y' if kept == 1 else 'ies'} "
832
- f"({settings_path}) — install never writes that form, so it is committed/operator "
840
+ f"({settings_path}) — install never writes that form, so it is operator "
833
841
  "source, not install state; edit the file by hand to drop it"
834
842
  )
835
843
  return 0
@@ -25,8 +25,9 @@ Scope asymmetry (봉인계약 4·6, REASONED — not the unfounded asymmetry dev
25
25
  of our key is tidied; siblings kept.
26
26
  project <repo>/.pi/settings.json — checkout-LOCAL, disposable, re-creatable, and `run.sh
27
27
  remove` already covers it → NO state. install normalizes the command; remove strips
28
- our-managed shapes (the bare bin AND the legacy repo path). project-scope state is a
29
- NAMED FOLLOW-UP (NEXT), deliberately out of this lane.
28
+ our-managed shapes (the bare bin AND the legacy repo path). project scope takes NO
29
+ state, deliberately: it is checkout-local, disposable, and `run.sh remove` is its
30
+ whole inverse.
30
31
 
31
32
  USER-SCOPE OWNER BINDING (#86 C2): the user install-state additionally records
32
33
  `installerRoot` (the repo/package root that installed the key), so an old root's
@@ -3,7 +3,7 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-raw-turn-live
4
4
  //
5
5
  // What this proves (and ONLY this): the pinned Claude ACP adapter
6
- // (@agentclientprotocol/claude-agent-acp@0.75.1) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp@0.76.0) spawns, speaks the ACP wire
7
7
  // protocol over stdio NDJSON, and returns one real model turn. It is the
8
8
  // bytes-flow proof that the S2a dep surface is not just installable but
9
9
  // actually drivable — before any provider/overlay/streamSimple code (S2b+).
@@ -62,11 +62,13 @@ RAW_DIR="$HERE/raw-async-delivery"
62
62
  # 1.0 -> 1.1) is the real "re-verify the markers + Gotchas + raw/LIVE probes"
63
63
  # trigger and DOES scream.
64
64
  PIN_CLAUDE_MINOR="2.1"
65
- # codex 0.144 (2026-07-14): OBSERVED bump, not a re-verification. Codex is not a shipped
66
- # native-citizen lane in 0.12.x; the probe evidence in DELIVERY.md §Codex was measured at
67
- # 0.136.0 and has NOT been re-run on 0.144 that explicit verdict lives there. Re-run the
68
- # raw probes before building any codex adapter on this line.
69
- PIN_CODEX_MINOR="0.144"
65
+ # codex 0.153 (2026-09-08): RE-VERIFIED, not merely observed. The archived 0.136.0 raw probe
66
+ # (codex-local-appserver.sh + raw-codex-ws-turn-start.py) was re-run AS SHIPPED on codex-cli
67
+ # 0.153.4 and went green idle plain `codex` auto-attached to a bare `codex app-server
68
+ # --listen` woke with zero typing and the model replied in the same visible session. Receipts
69
+ # and the six step-1 vendor measurements: scripts/raw-codex-measure/README.md (see raw-codex-
70
+ # measure); coordinates restated in DELIVERY.md §Codex. Codex is still NOT a citizen backend.
71
+ PIN_CODEX_MINOR="0.153"
70
72
  # agy 1.1 (2026-07-14): re-verified live — entwurf_self called without a permission prompt,
71
73
  # bidirectional native-push reply on the same gid, LIVE=1 smoke-agy-native-push-live 13/13
72
74
  # at agy 1.1.0 (evidence recorded in DELIVERY.md §Antigravity).