@junghanacs/entwurf 0.16.0 → 0.17.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 (48) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +337 -0
  3. package/README.md +8 -11
  4. package/VERIFY.md +8 -1
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +25 -14
  7. package/docs/setup-clean-host.md +24 -10
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +34 -10
  10. package/package.json +10 -10
  11. package/pi-extensions/lib/acp/acp-client.ts +57 -4
  12. package/pi-extensions/lib/acp/backend-adapter.ts +78 -9
  13. package/pi-extensions/lib/acp/backend.ts +578 -18
  14. package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
  15. package/pi-extensions/lib/acp/event-mapper.ts +43 -6
  16. package/run.sh +129 -32
  17. package/scripts/check-acp-launch-namespace.ts +127 -0
  18. package/scripts/check-acp-prompt-lifecycle.ts +145 -2
  19. package/scripts/check-acp-stop-reason.ts +8 -2
  20. package/scripts/check-acp-usage-accounting.ts +1074 -0
  21. package/scripts/check-copilot-birth-hook.ts +28 -1
  22. package/scripts/check-gate-qualification.ts +4 -2
  23. package/scripts/check-omp-fresh-preflight.ts +27 -0
  24. package/scripts/check-setup-qualification.sh +40 -2
  25. package/scripts/copilot-bridge-oracle.sh +14 -6
  26. package/scripts/fake-copilot-vendor.sh +4 -2
  27. package/scripts/lib/pi-record-discovery.ts +47 -0
  28. package/scripts/mutants/acp-launch-namespace.json +34 -0
  29. package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
  30. package/scripts/mutants/acp-stream-hooks.json +4 -2
  31. package/scripts/mutants/acp-usage-accounting.json +181 -0
  32. package/scripts/mutants/copilot-birth.json +3 -5
  33. package/scripts/mutants/pack-install.json +2 -2
  34. package/scripts/mutants/setup-verdict.json +35 -0
  35. package/scripts/omp-config-xdev.py +310 -0
  36. package/scripts/omp-config-xdev.sh +76 -0
  37. package/scripts/omp-tool-surface.py +61 -10
  38. package/scripts/raw-acp-child-exit-measure/README.md +285 -0
  39. package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
  40. package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
  41. package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
  42. package/scripts/smoke-acp-cortex-live.ts +2 -2
  43. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  44. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  45. package/scripts/smoke-acp-v2-send-live.ts +2 -2
  46. package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
  47. package/scripts/smoke-mux-lifecycle-live.ts +46 -2
  48. package/scripts/smoke-setup-verdict.sh +48 -3
@@ -213,6 +213,46 @@ function pidIsAlive(pid: number): boolean {
213
213
  }
214
214
  }
215
215
 
216
+ /**
217
+ * What the launched window actually shows, read at the moment a nonce callback never arrived.
218
+ *
219
+ * The launch receipt says so itself — "if it never comes, the window is visible and can be read
220
+ * directly" — but on a headless release-gate run nobody is there to look, and the window is torn
221
+ * down in teardown seconds later. So the harness looks for the operator: is the pane process even
222
+ * alive, and what is painted in it. That separates the three states a bare timeout collapses into
223
+ * one — the runtime never started (or died), it started and is sitting on an error or a prompt, or
224
+ * it is running fine and the model simply did not call the callback tool. A pi-native callback
225
+ * timeout with no window evidence is what blocked the second 0.17.0 `--cut` (and, in the same
226
+ * shape, a 0.16.1 run), leaving nothing to tell those three apart afterwards.
227
+ *
228
+ * Diagnostics must never become the failure: every step is best-effort, and a tmux that cannot
229
+ * answer is reported as its own line rather than thrown.
230
+ */
231
+ function paneForensics(label: string, socket: string, env: NodeJS.ProcessEnv, coords: Coordinates): string {
232
+ const lines: string[] = [`--- ${label} window forensics (the callback never came) ---`];
233
+ try {
234
+ lines.push(`pane ${coords.paneId} pid ${coords.panePid}: ${pidIsAlive(Number(coords.panePid)) ? "ALIVE" : "GONE"}`);
235
+ } catch (err) {
236
+ lines.push(`pane pid liveness unreadable: ${err instanceof Error ? err.message : String(err)}`);
237
+ }
238
+ const panes = tmux(
239
+ socket,
240
+ ["list-panes", "-a", "-F", "#{pane_id} #{pane_pid} #{pane_dead} #{pane_current_command}"],
241
+ env,
242
+ );
243
+ lines.push(
244
+ panes.status === 0 ? `list-panes:\n${panes.stdout || "(none)"}` : `list-panes failed (status ${panes.status})`,
245
+ );
246
+ const captured = tmux(socket, ["capture-pane", "-p", "-t", coords.paneId], env);
247
+ if (captured.status === 0) {
248
+ const tail = captured.stdout.split("\n").slice(-40).join("\n");
249
+ lines.push(`capture-pane (last 40 lines):\n${tail || "(the pane painted nothing)"}`);
250
+ } else {
251
+ lines.push(`capture-pane failed (status ${captured.status}) — the window is already gone`);
252
+ }
253
+ return lines.join("\n");
254
+ }
255
+
216
256
  async function waitForPidsGone(pids: ReadonlySet<number>, timeoutMs = 10_000): Promise<boolean> {
217
257
  const deadline = Date.now() + timeoutMs;
218
258
  while (Date.now() < deadline) {
@@ -704,7 +744,9 @@ async function main(): Promise<void> {
704
744
  ok(
705
745
  `${cell}: the nonce came back and its SENDER ENVELOPE carries a garden id — correlation without asking the sibling`,
706
746
  gid !== null && GARDEN_ID.test(gid),
707
- `--- launch receipt ---\n${launch.text}`,
747
+ gid === null
748
+ ? `--- launch receipt ---\n${launch.text}\n${paneForensics(cell, srv.socket, srv.env, coords)}`
749
+ : `--- launch receipt ---\n${launch.text}`,
708
750
  );
709
751
  const citizen = gid as string;
710
752
  siblingGids.add(citizen);
@@ -936,7 +978,9 @@ async function main(): Promise<void> {
936
978
  ok(
937
979
  "claude-code: the nonce came back and its SENDER ENVELOPE carries a garden id",
938
980
  ccGid !== null && GARDEN_ID.test(ccGid),
939
- `--- launch receipt ---\n${ccLaunch.text}`,
981
+ ccGid === null
982
+ ? `--- launch receipt ---\n${ccLaunch.text}\n${paneForensics("claude-code", cc.socket, cc.env, ccCoords)}`
983
+ : `--- launch receipt ---\n${ccLaunch.text}`,
940
984
  );
941
985
  const ccCitizen = ccGid as string;
942
986
  siblingGids.add(ccCitizen);
@@ -7,13 +7,15 @@
7
7
  # gate. This fixture drives the REAL `run.sh setup` end to end in a sandbox and
8
8
  # pins exactly the #86 C1 acceptance cells:
9
9
  #
10
- # S-1 all-harness-absent → core PASS, pi/claude/agy SKIP, computed green,
10
+ # S-1 all-harness-absent → core PASS, pi/claude/agy/copilot/omp SKIP, computed green,
11
11
  # zero harness writes, credential store untouched
12
12
  # S-2 pi below floor → detected FAIL (never SKIP), nonzero, no Pi wiring
13
13
  # S-3 pi at floor → presence completes project+user wiring, green,
14
14
  # credential store still byte-identical
15
15
  # S-4 agy detected+corrupt→ named component FAIL + NON-GREEN + nonzero exit,
16
16
  # core and later components still attempted
17
+ # S-8 omp present → the four omp units compose (birth/MCP/tools.xdev
18
+ # setting/receiver), states exist, second run idempotent
17
19
  # S-5 installed mode → the installed-vs-source branch is a NAMED verdict
18
20
  # printed before anything else, and never reaches
19
21
  # the source pnpm bootstrap
@@ -25,7 +27,8 @@
25
27
  # below-floor and false-green shapes as living end-to-end evidence.
26
28
  #
27
29
  # Deterministic: no model, no network, no cost. Presence probes are pinned via
28
- # PI_BIN / CLAUDE_BIN / AGY_BIN (the same hermetic seam smoke-agy-install-state
30
+ # PI_BIN / CLAUDE_BIN / AGY_BIN / COPILOT_BIN / OMP_BIN (the same hermetic seam
31
+ # smoke-agy-install-state
29
32
  # uses), and every write root is sandboxed: HOME, XDG roots, the pi agent dir,
30
33
  # and the dev-bin dir. The source bootstrap (`pnpm install --frozen-lockfile`)
31
34
  # runs against the ALREADY-INSTALLED checkout, which is a verified no-op under a
@@ -64,7 +67,7 @@ mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness"
64
67
  # Presence pins: default every harness to a definitely-absent path; each cell
65
68
  # re-pins what it needs. Production leaves these unset.
66
69
  ABSENT="$SB/harness/definitely-absent"
67
- export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT"
70
+ export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT" OMP_BIN="$ABSENT"
68
71
 
69
72
  PASS=0
70
73
  ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$*"; }
@@ -98,6 +101,7 @@ want "S-1: pi absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | gr
98
101
  want "S-1: claude absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'claude: SKIP'"
99
102
  want "S-1: agy absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'agy: SKIP'"
100
103
  want "S-1: copilot absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP'"
104
+ want "S-1: omp absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'omp: SKIP'"
101
105
  want "S-1: core bridge boundary validated (PASS)" "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
102
106
  want "S-1: final verdict is computed, not unconditional" \
103
107
  "printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
@@ -107,6 +111,7 @@ want "S-1: zero Pi wiring written for an absent pi" "[ ! -e '$PROJ1/.pi' ]"
107
111
  want "S-1: no user-scope pi settings were created" "[ ! -e '$PI_CODING_AGENT_DIR/settings.json' ]"
108
112
  want "S-1: no agy config was created" "[ ! -e '$HOME/.gemini' ]"
109
113
  want "S-1: no Copilot config/units were created" "[ ! -e '$HOME/.copilot' ]"
114
+ want "S-1: no OMP config/units were created" "[ ! -e '$HOME/.omp' ]"
110
115
  want_auth_untouched "S-1"
111
116
 
112
117
  # ── S-2: pi resolvable but BELOW floor → detected FAIL, never SKIP, no writes ──
@@ -242,6 +247,46 @@ want_auth_untouched "S-7"
242
247
  rm -rf "$HOME/.copilot" "$XDG_DATA_HOME/entwurf/copilot-mcp" "$XDG_DATA_HOME/entwurf/copilot-receive" \
243
248
  "$XDG_DATA_HOME/entwurf/copilot-statusline"
244
249
 
250
+ # ── S-8: omp present → the FOUR omp units compose, including the operator setting ──
251
+ # OMP shipped in v0.16.0 with installers, doctors and inverses for every unit, but
252
+ # `setup` did not compose it — the fifth backend was a hand-run verb list and the
253
+ # `tools.xdev` setting was a documentation step, so a one-command host came up with
254
+ # an omp citizen whose MCP tools the model could not call. This cell is the standing
255
+ # consumer of that composition. The omp unit scripts touch NO vendor process: they
256
+ # only probe `omp` on PATH and write into the agent dir, so a stub binary is a
257
+ # faithful presence pin and every write lands in the sandbox HOME.
258
+ echo "[smoke-setup-verdict] S-8 omp present — four-unit composition"
259
+ PROJ8="$SB/proj8"; mkdir -p "$PROJ8"
260
+ FAKE_OMP="$SB/harness/omp-fake"; mkdir -p "$FAKE_OMP"
261
+ printf '#!/usr/bin/env bash\necho "omp/18.0.0"\n' > "$FAKE_OMP/omp"
262
+ chmod +x "$FAKE_OMP/omp"
263
+ seed_auth
264
+ # This sandbox exports PI_CODING_AGENT_DIR, which omp reads too — the oracle refuses
265
+ # that ambiguity by design (ledger M6), so the cell names the agent dir explicitly, the
266
+ # same seam a real operator on a pi-rail host would use.
267
+ OMP_AGENT="$HOME/.omp/agent"
268
+ set +e; OUT="$(OMP_BIN="$FAKE_OMP/omp" ENTWURF_OMP_AGENT_DIR="$OMP_AGENT" PATH="$FAKE_OMP:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ8" 2>&1)"; RC=$?; set -e
269
+ want "S-8: omp-present setup exits 0 (four units + core all green)" "[ '$RC' -eq 0 ]"
270
+ want "S-8: birth unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS'"
271
+ want "S-8: MCP unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'omp-mcp: PASS'"
272
+ want "S-8: the tools.xdev operator setting composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'omp-config: PASS'"
273
+ want "S-8: receiver unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'omp-receive: PASS'"
274
+ want "S-8: computed summary is green" \
275
+ "printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
276
+ want "S-8: all four package-owned install-states exist (each unit keeps its inverse authority)" \
277
+ "[ -f '$XDG_DATA_HOME/entwurf/omp-bridge/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/omp-mcp/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/omp-config/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/omp-receive/install-state.json' ]"
278
+ want "S-8: both extension units landed in the sandbox omp agent dir" \
279
+ "[ -d '$HOME/.omp/agent/extensions/entwurf-meta-omp' ] && [ -d '$HOME/.omp/agent/extensions/entwurf-receive-omp' ]"
280
+ want "S-8: the MCP hand wrote the pinned key and the setting reached the config the vendor reads" \
281
+ "grep -q 'entwurf-bridge' '$HOME/.omp/agent/mcp.json' && [ \"\$(python3 '$REPO_DIR/scripts/omp-tool-surface.py' '$HOME/.omp/agent' | awk '/^verdict /{print \$2}')\" = 'xdev-off' ]"
282
+ want "S-8: setup is idempotent — a second run over the composed host is still green" \
283
+ "OMP_BIN='$FAKE_OMP/omp' ENTWURF_OMP_AGENT_DIR='$HOME/.omp/agent' PATH='$FAKE_OMP:$PATH' bash '$REPO_DIR/run.sh' setup '$PROJ8' 2>&1 | grep -q 'result: green (computed from the component outcomes above)'"
284
+ want_auth_untouched "S-8"
285
+ # Reset the composed OMP state so the next cell decides on a clean host.
286
+ rm -rf "$HOME/.omp" "$XDG_DATA_HOME/entwurf/omp-bridge" "$XDG_DATA_HOME/entwurf/omp-mcp" \
287
+ "$XDG_DATA_HOME/entwurf/omp-config" "$XDG_DATA_HOME/entwurf/omp-receive" \
288
+ "$XDG_DATA_HOME/entwurf/meta-bridge-omp" "$XDG_DATA_HOME/entwurf/omp-receive"
289
+
245
290
  # ── S-5: installed mode is a NAMED first verdict, never the source bootstrap ──
246
291
  # A bare copy of run.sh under a fake node_modules root pins the mode seam
247
292
  # cheaply: the copy is NOT a runnable installed package (no dist), so this cell