@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -25,6 +25,7 @@ import { tmpdir } from "node:os";
25
25
  import { dirname, resolve } from "node:path";
26
26
  import { fileURLToPath, pathToFileURL } from "node:url";
27
27
  import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
28
+ import { skipLive } from "./lib/live-skip.ts";
28
29
 
29
30
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
30
31
  const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
@@ -46,8 +47,7 @@ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
46
47
  }
47
48
 
48
49
  if (process.env.LIVE !== "1") {
49
- console.error("[smoke-acp-session-reuse-live] skipped — set LIVE=1 to run the real 2-turn reuse acceptance.");
50
- process.exit(0);
50
+ skipLive("smoke-acp-session-reuse-live", "set LIVE=1 to run the real 2-turn reuse acceptance.");
51
51
  }
52
52
 
53
53
  type Stream = AsyncIterable<AssistantMessageEvent> & {
@@ -28,6 +28,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
29
  import { dirname, join, resolve } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
+ import { skipLive } from "./lib/live-skip.ts";
31
32
 
32
33
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
33
34
  const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
@@ -40,8 +41,7 @@ function fail(msg: string): never {
40
41
  }
41
42
 
42
43
  if (process.env.LIVE !== "1") {
43
- console.error("[smoke-acp-skill-live] skipped — set LIVE=1 to run the real pi provider turn.");
44
- process.exit(0);
44
+ skipLive("smoke-acp-skill-live", "set LIVE=1 to run the real pi provider turn.");
45
45
  }
46
46
 
47
47
  const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
@@ -31,6 +31,7 @@ import { fileURLToPath } from "node:url";
31
31
  import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
32
32
  import { scanSocketProbes } from "../pi-extensions/lib/socket-discovery.ts";
33
33
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
34
+ import { skipLive } from "./lib/live-skip.ts";
34
35
  import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
35
36
 
36
37
  const ACP_PROVIDER = "entwurf";
@@ -76,10 +77,10 @@ async function waitForGone(sockPath: string, timeoutMs: number): Promise<boolean
76
77
 
77
78
  async function main(): Promise<void> {
78
79
  if (process.env.LIVE !== "1") {
79
- console.log(
80
- "[smoke-acp-socket-citizen-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).",
80
+ skipLive(
81
+ "smoke-acp-socket-citizen-live",
82
+ "set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).",
81
83
  );
82
- return;
83
84
  }
84
85
 
85
86
  const tmp = os.tmpdir();
@@ -81,6 +81,7 @@ import * as readline from "node:readline";
81
81
  import { fileURLToPath } from "node:url";
82
82
  import { upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
83
83
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
84
+ import { skipLive } from "./lib/live-skip.ts";
84
85
  import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
85
86
 
86
87
  const ACP_PROVIDER = "entwurf";
@@ -196,10 +197,10 @@ function driveTurn(child: ChildProcess, prompt: string): Promise<TurnCapture> {
196
197
 
197
198
  async function main(): Promise<void> {
198
199
  if (process.env.LIVE !== "1") {
199
- console.log(
200
- "[smoke-acp-v2-send-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control resident + drives one model turn).",
200
+ skipLive(
201
+ "smoke-acp-v2-send-live",
202
+ "set LIVE=1 to run (spawns a real pi --entwurf-control resident + drives one model turn).",
201
203
  );
202
- return;
203
204
  }
204
205
 
205
206
  // One temp root holding the whole garden this smoke can see: store, mailbox and
@@ -4,11 +4,11 @@
4
4
  * adapter (pgrep/ss/`agentapi get-conversation-metadata`+`send-message`), the register core,
5
5
  * and `runEntwurfV2` with production deps — against a REAL, already-running agy conversation,
6
6
  * and asserts the rail end to end: probe finds the volatile route, register create/re-attach is
7
- * idempotent, a fire-and-forget dispatches over native-push (delivered), an owned-outcome is
8
- * rejected (no resume authority), and a bogus conversation is rejected (probe-indeterminate).
7
+ * idempotent, a fire-and-forget dispatches over native-push (delivered), and a bogus conversation
8
+ * is rejected (probe-indeterminate).
9
9
  *
10
10
  * This automates what the GPT manual L1~L5 pass proved by hand (register create/attach, v2
11
- * native-push delivered ×2, owned reject, mailbox/marker/pi-domain non-intrusion) — same
11
+ * native-push delivered ×2, mailbox/marker/pi-domain non-intrusion) — same
12
12
  * production MCP surface, now a repeatable gate.
13
13
  *
14
14
  * STORE ISOLATION (페블-approved): only the agy CONVERSATION round-trip is real (that is the
@@ -54,6 +54,7 @@ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
54
54
  import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
55
55
  import { antigravityAdapter } from "../pi-extensions/lib/native-push/adapter.ts";
56
56
  import { registerNativeConversation } from "../pi-extensions/lib/native-push/register.ts";
57
+ import { skipLive } from "./lib/live-skip.ts";
57
58
 
58
59
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
59
60
 
@@ -79,10 +80,10 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
79
80
 
80
81
  async function main(): Promise<void> {
81
82
  if (process.env.LIVE !== "1") {
82
- console.log(
83
- "[smoke-agy-native-push-live] skipped — set LIVE=1 (+ AGY_CONVERSATION_ID) to run (sends into a real agy conversation).",
83
+ skipLive(
84
+ "smoke-agy-native-push-live",
85
+ "set LIVE=1 (+ AGY_CONVERSATION_ID) to run (sends into a real agy conversation).",
84
86
  );
85
- return;
86
87
  }
87
88
 
88
89
  // ── doctor-static preflight (runs FIRST; a dangling agy config FAILs here — the ③ gate) ──
@@ -188,20 +189,9 @@ async function main(): Promise<void> {
188
189
  const post = await antigravityAdapter.probe(conversationId);
189
190
  ok("post-send re-probe still alive (delivery target reachable, D7 partial)", post.status === "alive");
190
191
 
191
- // ── 5. owned-outcome rejected: a native-push backend has no resume authority ──
192
- const owned: EntwurfV2RunResult = await runEntwurfV2(
193
- { target: reg1.gardenId, intent: "owned-outcome", message: "should be rejected" },
194
- prodDeps,
195
- );
196
- ok(
197
- "owned-outcome rejected as native-push-no-resume-authority",
198
- owned.kind === "rejected" && owned.receipt.reason === "native-push-no-resume-authority",
199
- );
200
- ok(
201
- "owned reject stamped observedLiveness (post-probe, non-null)",
202
- owned.kind === "rejected" && owned.receipt.observedLiveness !== null,
203
- );
204
-
192
+ // ── 5. (retired) the owned-outcome × native-push refusal cell. The intent was removed
193
+ // from the contract in the visible-first cut, so there is no longer a call to make here:
194
+ // a caller cannot declare it, and the reason it produced left the taxonomy with it. ──
205
195
  // ── 6. bogus conversation → rejected native-push-probe-indeterminate ──
206
196
  // agy is alive but no LS port serves this fabricated conv id → INDETERMINATE (never coerced
207
197
  // to dead). A meta-record is upserted DIRECTLY (register would refuse a non-live probe), so
@@ -18,11 +18,21 @@ set -euo pipefail
18
18
  REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
19
 
20
20
  if [ "${LIVE:-0}" != "1" ]; then
21
- echo "[smoke-claude-native-resume-live] skipped — set LIVE=1 to run (spawns two real Claude Code native turns)."
22
- exit 0
21
+ . "$REPO/scripts/lib/step-outcome.sh"
22
+ echo "[entwurf:skip] smoke-claude-native-resume-live — set LIVE=1 to run (spawns two real Claude Code native turns)."
23
+ exit "$ENTWURF_STEP_SKIP_EXIT"
23
24
  fi
24
25
 
25
- command -v claude >/dev/null || { echo "FAIL: claude not on PATH" >&2; exit 1; }
26
+ # A missing `claude` is a PREREQUISITE, not a defect: this smoke drives the real
27
+ # native binary, so a host without it has nothing to prove wrong. Under the P1
28
+ # outcome protocol that is a SKIP the aggregate reports and `--cut` refuses —
29
+ # reporting it as FAIL would make a host's own toolchain look like a release
30
+ # defect, and the old exit 1 did exactly that once this smoke joined the gate.
31
+ command -v claude >/dev/null || {
32
+ . "$REPO/scripts/lib/step-outcome.sh"
33
+ echo "[entwurf:skip] smoke-claude-native-resume-live — claude not on PATH (install Claude Code and complete its own login flow)."
34
+ exit "$ENTWURF_STEP_SKIP_EXIT"
35
+ }
26
36
  command -v node >/dev/null || { echo "FAIL: node not on PATH" >&2; exit 1; }
27
37
  command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH" >&2; exit 1; }
28
38
 
@@ -0,0 +1,352 @@
1
+ // P3 — the cross-harness delivery CHAIN, on real authenticated rails.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-entwurf-chain-live
4
+ //
5
+ // WHY THIS EXISTS. Every v2 delivery proof we had covered ONE hop between TWO
6
+ // citizens on ONE rail: matrix-live dispatches programmatically, v2-send-live has
7
+ // an ACP model send into a mailbox, bundled-mcp-live only reads identity. The
8
+ // claim the project actually makes — that independent harnesses address each
9
+ // other by garden id — is a CHAIN claim, and nothing exercised a chain. A gate
10
+ // that never links three harnesses cannot catch the failure where hop 2 loses the
11
+ // sender identity hop 1 carried, or where a rail change breaks only the seam
12
+ // between two backends.
13
+ //
14
+ // THE CHAIN (each arrow is a real model turn on a real authenticated backend):
15
+ //
16
+ // A native Claude Code --entwurf_v2--> B pi / openai-codex (GPT)
17
+ // B --entwurf_v2--> C pi / entwurf claude-sonnet-5 (ACP)
18
+ // C --entwurf_v2--> D mailbox-backed self-fetch citizen
19
+ //
20
+ // Three harnesses, three rails: A is a native self-fetch citizen born by its own
21
+ // SessionStart hook, B and C are live control-socket citizens, and D is the
22
+ // mailbox terminus. The terminus is what makes the READ-RECEIPT axis observable
23
+ // at all — a control-socket delivery has no receipt to stamp, so a chain that
24
+ // ended at C could not produce one. Draining D through the production inbox
25
+ // reader stamps `lastReadAt`, and that is the receipt.
26
+ //
27
+ // WHAT IS REAL AND WHAT IS SMOKE-OWNED (state this honestly or the evidence is
28
+ // worth nothing):
29
+ // - REAL: A's meta-record and its first arming are minted by the operator's own
30
+ // Claude Code SessionStart hook; all three deliveries are production
31
+ // `entwurf_v2` dispatches chosen by the decider; all three model turns are
32
+ // real authenticated calls; every sender envelope is the real one.
33
+ // - SMOKE-OWNED: the isolated world (store / mailbox / receivers under one temp
34
+ // root) so the operator's live garden is never paged, and D's arming marker.
35
+ // D is a seeded citizen exactly as smoke-acp-v2-send-live seeds its peer.
36
+ // - NOT CLAIMED: this proves the chain LINKS, not that any model chooses the
37
+ // entwurf surface on its own. Each hop is TOLD which tool to call, which is
38
+ // why this is a MUST-tier delivery gate and not a BEHAVIOR-lane signal (the
39
+ // lane note in run.sh draws exactly that line).
40
+ //
41
+ // The nonce is the thread: it is minted here, embedded in A's instruction, and
42
+ // must arrive at D having been re-sent by B and again by C. A nonce at D proves
43
+ // the payload traversed every hop — no hop can fabricate it.
44
+ //
45
+ // Sender identity is proven the same way v2-send-live proves it: no hop is ever
46
+ // told its OWN garden id, so a gid can only appear downstream because the real
47
+ // envelope carried it.
48
+
49
+ import { strict as assert } from "node:assert";
50
+ import { type ChildProcess, spawn, spawnSync } from "node:child_process";
51
+ import crypto from "node:crypto";
52
+ import { existsSync } from "node:fs";
53
+ import fsp from "node:fs/promises";
54
+ import os from "node:os";
55
+ import path from "node:path";
56
+ import { fileURLToPath } from "node:url";
57
+ import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
58
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
59
+ import { skipLive } from "./lib/live-skip.ts";
60
+
61
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
62
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
63
+ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
64
+ const SOCKET_SUFFIX = ".sock";
65
+
66
+ const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.4";
67
+ const ACP_TARGET = process.env.ENTWURF_CHAIN_ACP_TARGET?.trim() || "entwurf/claude-sonnet-5";
68
+ const BOOT_TIMEOUT_MS = 45_000;
69
+ const CLAUDE_TURN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_CLAUDE_TIMEOUT_MS) || 240_000;
70
+ const CHAIN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_TIMEOUT_MS) || 420_000;
71
+ const POLL_MS = 250;
72
+
73
+ let passed = 0;
74
+ function ok(label: string, cond: boolean): void {
75
+ assert.ok(cond, `smoke-entwurf-chain-live: ${label}`);
76
+ console.log(` ok ${label}`);
77
+ passed++;
78
+ }
79
+
80
+ function splitTarget(t: string): [provider: string, model: string] {
81
+ const i = t.indexOf("/");
82
+ assert.ok(i > 0, `target must be <provider>/<model>, got ${t}`);
83
+ return [t.slice(0, i), t.slice(i + 1)];
84
+ }
85
+
86
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
87
+
88
+ async function readRecords(dir: string): Promise<Array<{ gardenId: string; backend: string }>> {
89
+ const out: Array<{ gardenId: string; backend: string }> = [];
90
+ for (const f of await fsp.readdir(dir).catch(() => [] as string[])) {
91
+ if (!f.endsWith(".meta.json")) continue;
92
+ try {
93
+ const r = JSON.parse(await fsp.readFile(path.join(dir, f), "utf8")) as Record<string, unknown>;
94
+ if (typeof r.gardenId === "string" && typeof r.backend === "string") {
95
+ out.push({ gardenId: r.gardenId, backend: r.backend });
96
+ }
97
+ } catch {
98
+ /* a half-written record on the next poll is not an error */
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** Wait for ONE new record of `backend` that is not already in `known`. */
105
+ async function waitForNewRecord(dir: string, backend: string, known: Set<string>, ms: number): Promise<string | null> {
106
+ const until = Date.now() + ms;
107
+ while (Date.now() < until) {
108
+ for (const r of await readRecords(dir)) {
109
+ if (r.backend === backend && !known.has(r.gardenId)) return r.gardenId;
110
+ }
111
+ await sleep(POLL_MS);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ async function waitForSocket(p: string, ms: number): Promise<boolean> {
117
+ const until = Date.now() + ms;
118
+ while (Date.now() < until) {
119
+ if (existsSync(p)) return true;
120
+ await sleep(POLL_MS);
121
+ }
122
+ return false;
123
+ }
124
+
125
+ async function main(): Promise<void> {
126
+ if (process.env.LIVE !== "1") {
127
+ skipLive(
128
+ "smoke-entwurf-chain-live",
129
+ "set LIVE=1 to run (drives three real authenticated model turns across Claude Code, pi GPT and pi ACP).",
130
+ );
131
+ }
132
+ if (spawnSync("bash", ["-lc", "command -v claude"], { encoding: "utf8" }).status !== 0) {
133
+ skipLive("smoke-entwurf-chain-live", "claude not on PATH — hop 1 is a real native Claude Code turn.");
134
+ }
135
+ const [gptProvider, gptModel] = splitTarget(GPT_TARGET);
136
+ const [acpProvider, acpModel] = splitTarget(ACP_TARGET);
137
+ // pi holds each backend's credentials in its own auth store; a missing one is a
138
+ // PREREQUISITE (the operator has not logged that backend in), never a defect.
139
+ const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
140
+ let auth: Record<string, unknown> = {};
141
+ try {
142
+ auth = JSON.parse(await fsp.readFile(authPath, "utf8")) as Record<string, unknown>;
143
+ } catch {
144
+ skipLive("smoke-entwurf-chain-live", `no pi auth store at ${authPath} — log in the chain's backends first.`);
145
+ }
146
+ if (!(gptProvider in auth)) {
147
+ skipLive(
148
+ "smoke-entwurf-chain-live",
149
+ `pi has no '${gptProvider}' credentials (hop 2 target ${GPT_TARGET}); log that backend in, or point ` +
150
+ "ENTWURF_CHAIN_GPT_TARGET at one you have.",
151
+ );
152
+ }
153
+
154
+ const world = await fsp.mkdtemp(path.join(os.tmpdir(), "entwurf-chain-"));
155
+ const sessionsDir = path.join(world, "sessions");
156
+ const mailboxDir = path.join(world, "mailbox");
157
+ const receiversDir = path.join(world, "receivers");
158
+ for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
159
+ const worldEnv = {
160
+ ENTWURF_META_SESSIONS_DIR: sessionsDir,
161
+ ENTWURF_META_MAILBOX_DIR: mailboxDir,
162
+ ENTWURF_META_RECEIVERS_DIR: receiversDir,
163
+ };
164
+
165
+ const nonce = `ENTWURF-CHAIN-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
166
+ console.error(`[smoke-entwurf-chain-live] repo: ${REPO_ROOT}`);
167
+ console.error(`[smoke-entwurf-chain-live] world: ${world}`);
168
+ console.error(`[smoke-entwurf-chain-live] hop2: ${GPT_TARGET}`);
169
+ console.error(`[smoke-entwurf-chain-live] hop3: ${ACP_TARGET}`);
170
+ console.error(`[smoke-entwurf-chain-live] nonce: ${nonce}`);
171
+
172
+ // ── D: the mailbox terminus (seeded + armed by this process, like v2-send) ──
173
+ const terminus = upsertMetaSession({
174
+ input: { backend: "claude-code", nativeSessionId: `chain-terminus-${process.pid}`, cwd: world },
175
+ dir: sessionsDir,
176
+ });
177
+ const gidD = terminus.record.gardenId;
178
+ writeMetaReceiverMarker({
179
+ gardenId: gidD,
180
+ backend: "claude-code",
181
+ nativeSessionId: terminus.record.nativeSessionId,
182
+ ownerPid: process.pid,
183
+ armProvenance: "session-start",
184
+ receiversDir,
185
+ });
186
+ console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
187
+
188
+ const children: ChildProcess[] = [];
189
+ let streamB = "";
190
+ let streamC = "";
191
+ try {
192
+ // ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
193
+ const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
194
+ const c = spawn(
195
+ "pi",
196
+ [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", acpProvider, "--model", acpModel, "--mode", "rpc"],
197
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
198
+ );
199
+ children.push(c);
200
+ c.stdout?.on("data", (b: Buffer) => {
201
+ streamC = (streamC + b.toString()).slice(-200_000);
202
+ });
203
+ c.stderr?.on("data", (b: Buffer) => {
204
+ streamC = (streamC + b.toString()).slice(-200_000);
205
+ });
206
+ const gidC = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
207
+ ok(`C birthed its own pi record (${ACP_TARGET})`, gidC !== null);
208
+ known.add(gidC as string);
209
+ ok(
210
+ "C stood up a control socket",
211
+ await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidC}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
212
+ );
213
+
214
+ // ── B: the pi GPT citizen ────────────────────────────────────────────────
215
+ const b = spawn(
216
+ "pi",
217
+ [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", gptProvider, "--model", gptModel, "--mode", "rpc"],
218
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
219
+ );
220
+ children.push(b);
221
+ b.stdout?.on("data", (x: Buffer) => {
222
+ streamB = (streamB + x.toString()).slice(-200_000);
223
+ });
224
+ b.stderr?.on("data", (x: Buffer) => {
225
+ streamB = (streamB + x.toString()).slice(-200_000);
226
+ });
227
+ const gidB = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
228
+ ok(`B birthed its own pi record (${GPT_TARGET})`, gidB !== null);
229
+ known.add(gidB as string);
230
+ ok(
231
+ "B stood up a control socket",
232
+ await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidB}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
233
+ );
234
+ ok("B, C and D are three distinct citizens", new Set([gidB, gidC, gidD]).size === 3);
235
+
236
+ // ── A: one real native Claude Code turn that opens the chain ─────────────
237
+ // The instruction each hop receives is the message the previous hop sends.
238
+ // No hop is told its own garden id — that is what makes the downstream
239
+ // sender assertions meaningful.
240
+ const hop3 = [
241
+ `${nonce} hop3. You are the third hop of a delivery chain.`,
242
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidD}, intent=fire-and-forget,`,
243
+ `and message=${nonce} terminus. Then reply with only the tool's outcome line.`,
244
+ ].join(" ");
245
+ const hop2 = [
246
+ `${nonce} hop2. You are the second hop of a delivery chain.`,
247
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
248
+ "and message set to exactly the text between INNER-BEGIN and INNER-END.",
249
+ "Then reply with only the tool's outcome line.",
250
+ "INNER-BEGIN",
251
+ hop3,
252
+ "INNER-END",
253
+ ].join("\n");
254
+ const hop1Prompt = [
255
+ "You are the first hop of a delivery chain. Do exactly this and nothing else.",
256
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidB}, intent=fire-and-forget,`,
257
+ "and message set to exactly the text between BEGIN and END.",
258
+ "Then reply with only the tool's outcome line.",
259
+ "BEGIN",
260
+ hop2,
261
+ "END",
262
+ ].join("\n");
263
+
264
+ const claudeRun = spawnSync("claude", ["-p", "--output-format=json", hop1Prompt], {
265
+ cwd: world,
266
+ encoding: "utf8",
267
+ timeout: CLAUDE_TURN_TIMEOUT_MS,
268
+ env: { ...process.env, ...worldEnv },
269
+ });
270
+ const claudeOut = `${claudeRun.stdout ?? ""}\n${claudeRun.stderr ?? ""}`;
271
+ ok("A's native Claude Code turn ran", claudeRun.status === 0);
272
+
273
+ const gidA = await waitForNewRecord(sessionsDir, "claude-code", new Set([gidD]), BOOT_TIMEOUT_MS);
274
+ ok("A was born a garden citizen by its OWN SessionStart hook (not seeded here)", gidA !== null);
275
+ ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
276
+ console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
277
+
278
+ // ── the chain runs on its own from here; wait for the terminus ───────────
279
+ const boxDir = path.join(mailboxDir, gidD);
280
+ const until = Date.now() + CHAIN_TIMEOUT_MS;
281
+ let msgs: string[] = [];
282
+ while (Date.now() < until) {
283
+ msgs = (await fsp.readdir(boxDir).catch(() => [] as string[])).filter(
284
+ (f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
285
+ );
286
+ if (msgs.length > 0) break;
287
+ await sleep(POLL_MS);
288
+ }
289
+ if (msgs.length === 0) {
290
+ console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
291
+ console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
292
+ console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
293
+ }
294
+ ok(`the chain reached the mailbox terminus (${msgs.length} .msg)`, msgs.length === 1);
295
+ ok("the terminus doorbell was poked", existsSync(path.join(boxDir, "inbox.signal")));
296
+
297
+ const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
298
+
299
+ // ── hop-by-hop evidence ─────────────────────────────────────────────────
300
+ // hop 1: B saw the payload AND the real sender identity of a native
301
+ // Claude Code citizen. A never learned its own gid, so its presence in B's
302
+ // stream can only come from the envelope entwurf_v2 rendered.
303
+ ok("hop 1 — B received the chain payload", streamB.includes(nonce));
304
+ ok("hop 1 — B saw A's real garden id as the sender", streamB.includes(gidA as string));
305
+ ok("hop 1 — the sender was rendered replyable to B", /replyable/i.test(streamB));
306
+
307
+ // hop 2: C saw the payload and B's identity, so the chain did not collapse
308
+ // into A talking to C directly.
309
+ ok("hop 2 — C received the chain payload", streamC.includes(nonce));
310
+ ok("hop 2 — C saw B's real garden id as the sender", streamC.includes(gidB as string));
311
+
312
+ // hop 3: the terminus body carries C's identity, its model, and replyable.
313
+ ok("hop 3 — the terminus payload is the original nonce", body.includes(nonce));
314
+ ok("hop 3 — the terminus names C as the sender", body.includes(gidC as string));
315
+ ok(`hop 3 — the terminus names C's backend model (${acpModel})`, body.includes(acpModel));
316
+ ok("hop 3 — the terminus sender is replyable", /replyable/i.test(body));
317
+ ok(
318
+ "the terminus was not reached by a shortcut — A and B are not its sender",
319
+ !body.includes(gidA as string) && !body.includes(gidB as string),
320
+ );
321
+
322
+ // ── the read receipt: the one axis only the mailbox rail can produce ─────
323
+ // The PRODUCTION reader, not a re-implementation: reading is what stamps the
324
+ // receipt, so a hand-rolled drain would prove nothing about the real path.
325
+ const statePath = path.join(mailboxDir, gidD, "state.json");
326
+ const beforeRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
327
+ const drained = readMetaInbox({ gardenId: gidD, sessionsDir, mailboxDir });
328
+ ok(
329
+ "draining the terminus returned the chain payload",
330
+ drained.messages.some((m) => m.body.includes(nonce)),
331
+ );
332
+ ok(
333
+ "the drain stamped a read receipt (lastReadAt)",
334
+ typeof drained.readAt === "string" && drained.readAt.length > 0,
335
+ );
336
+ const afterRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
337
+ ok(
338
+ "the receipt is durable on disk, not just in the return value",
339
+ /lastReadAt/.test(afterRaw) && afterRaw !== beforeRaw,
340
+ );
341
+
342
+ console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
343
+ } finally {
344
+ for (const ch of children) await terminateChild(ch).catch(() => undefined);
345
+ }
346
+
347
+ console.log(
348
+ `[smoke-entwurf-chain-live] ok — ${passed} assertions; three harnesses linked on real authenticated rails`,
349
+ );
350
+ }
351
+
352
+ await main();
@@ -64,6 +64,7 @@ import {
64
64
  writeMetaReceiverMarker,
65
65
  } from "../pi-extensions/lib/meta-session.ts";
66
66
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
+ import { skipLive } from "./lib/live-skip.ts";
67
68
  import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
68
69
 
69
70
  // pi's control socket lives at the canonical dir keyed by the RECORD's garden id (#50 C4: the
@@ -130,8 +131,7 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
130
131
 
131
132
  async function main(): Promise<void> {
132
133
  if (process.env.LIVE !== "1") {
133
- console.log("[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi + opens a real socket).");
134
- return;
134
+ skipLive("smoke-entwurf-v2-matrix-live", "set LIVE=1 to run (spawns a real pi + opens a real socket).");
135
135
  }
136
136
 
137
137
  const { provider, model } = resolveTarget();
@@ -260,7 +260,7 @@ async function main(): Promise<void> {
260
260
  const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
261
261
  ok("C1b real record-less `pi --entwurf-control` stood up a control socket", up);
262
262
 
263
- for (const intent of ["fire-and-forget", "owned-outcome"] as const) {
263
+ for (const intent of ["fire-and-forget"] as const) {
264
264
  const result: EntwurfV2RunResult = await runEntwurfV2(
265
265
  { target: c1bGid, intent, message: `matrix-live C1b record-less probe (${intent})` },
266
266
  prodDeps(smokeSender(c1bGid, tmp)),
@@ -17,7 +17,7 @@
17
17
  # alarm), and must log sender-marker evidence so send-side replyable identity
18
18
  # is not silently lost.
19
19
  #
20
- # Deterministic + offline (no `claude -p`, no network). Safe for pnpm check /
20
+ # Deterministic + offline (no `claude -p`, no network). Safe for pnpm run check:full /
21
21
  # pre-commit. Isolates its store via PI_CODING_AGENT_DIR in a temp dir.
22
22
  #
23
23
  # RUNTIME DEPS: bash + node (the hook runs under `node --experimental-strip-types`)
@@ -87,6 +87,10 @@ case "$1${2:+ $2}" in
87
87
  "mcp get")
88
88
  printf '%s\n' "Scope: User config" "Status: ✔ Connected"
89
89
  [ "${FAKE_MCP_RC:-0}" != 0 ] && exit "$FAKE_MCP_RC"
90
+ # Give grep -q a scheduling window to consume the match and close the pipe
91
+ # before the long tail starts. Without this, a fast host can buffer the whole
92
+ # tail before grep exits, making the old broken probe spuriously green.
93
+ sleep 0.05
90
94
  # Keep writing well past the pipe buffer: a reader that leaves early SIGPIPEs us.
91
95
  cat "$FAKE_MCP_TAIL"
92
96
  exit $? ;;