@junghanacs/entwurf 0.13.0 → 0.13.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 (72) hide show
  1. package/AGENTS.md +5 -3
  2. package/BASELINE.md +57 -203
  3. package/CHANGELOG.md +19 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +117 -261
  6. package/README.md +68 -408
  7. package/VERIFY.md +58 -87
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +231 -0
  10. package/docs/external-mcp-host.md +132 -0
  11. package/docs/fresh-cut-policy.md +99 -0
  12. package/docs/setup-clean-host.md +123 -328
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +3 -3
  20. package/mcp/entwurf-bridge/src/index.ts +21 -48
  21. package/package.json +12 -9
  22. package/pi-extensions/entwurf-control.ts +20 -49
  23. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  24. package/pi-extensions/lib/acp/augment.ts +42 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  26. package/pi-extensions/lib/acp/backend.ts +305 -36
  27. package/pi-extensions/lib/acp/config.ts +1 -1
  28. package/pi-extensions/lib/acp/engraving.ts +43 -1
  29. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  30. package/pi-extensions/lib/acp/models.ts +4 -4
  31. package/pi-extensions/lib/acp/overlay.ts +3 -3
  32. package/prompts/engraving.md +13 -5
  33. package/run.sh +244 -108
  34. package/scripts/check-acp-carrier-augment.ts +246 -10
  35. package/scripts/check-acp-cortex.ts +5 -5
  36. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  37. package/scripts/check-acp-sdk-surface.ts +60 -21
  38. package/scripts/check-acp-stop-reason.ts +342 -0
  39. package/scripts/check-entwurf-v2-surface.ts +41 -0
  40. package/scripts/check-gate-qualification.ts +6 -3
  41. package/scripts/check-probe-ordering.ts +39 -7
  42. package/scripts/check-release-gate-outcomes.ts +332 -0
  43. package/scripts/lib/live-skip.ts +33 -0
  44. package/scripts/lib/probe-acp-turn.ts +33 -12
  45. package/scripts/lib/step-outcome.sh +88 -0
  46. package/scripts/mutants/acp-augment.json +106 -0
  47. package/scripts/mutants/acp-cortex.json +2 -2
  48. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  49. package/scripts/mutants/acp-stop-reason.json +80 -0
  50. package/scripts/mutants/probe-ordering.json +27 -5
  51. package/scripts/mutants/release-gate.json +105 -0
  52. package/scripts/mutants/v2-surface.json +26 -15
  53. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  54. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  55. package/scripts/smoke-acp-cortex-live.ts +8 -14
  56. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  57. package/scripts/smoke-acp-mcp-live.ts +2 -2
  58. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  59. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  60. package/scripts/smoke-acp-overlay-live.ts +2 -2
  61. package/scripts/smoke-acp-provider-live.ts +2 -2
  62. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  63. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  64. package/scripts/smoke-acp-skill-live.ts +2 -2
  65. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  66. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  67. package/scripts/smoke-agy-native-push-live.ts +4 -3
  68. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  69. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  70. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  71. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  72. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -0,0 +1,185 @@
1
+ // 0.13.1 — LIVE acceptance for the prompt-lifecycle contract. OUT of `pnpm check`.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-acp-long-turn-live
4
+ //
5
+ // THE CLAIM THIS PAYS FOR. `check-acp-prompt-lifecycle` proves against fakes
6
+ // that a running prompt has no wall clock, and `check-probe-ordering` proves the
7
+ // source carries none. Neither can prove the whole stack — pi's runner, its
8
+ // retry policy, the real adapter, and a real model — lets a turn run past the
9
+ // number we deleted. This smoke does exactly that and nothing else: it drives
10
+ // ONE real turn whose tool work is deliberately longer than the retired 600s
11
+ // cutoff, and requires it to finish as ONE turn.
12
+ //
13
+ // Two facts are asserted together, because the old defect was their PRODUCT:
14
+ //
15
+ // survives the reply carries the nonce and the turn took LONGER than the
16
+ // retired cutoff — a fixed wall clock would have killed it.
17
+ // not replayed
18
+ // the persisted transcript contains exactly ONE bootstrap notice
19
+ // ("[acp: preparing claude session]"). The old failure mode
20
+ // replayed the same full prompt from a COLD ACP session up to
21
+ // `retry.maxRetries` times, so every replay re-announced that
22
+ // bootstrap. One occurrence is the direct evidence that the answer
23
+ // came from the original prompt.
24
+ //
25
+ // The work is split into several tool calls under the backend's own per-command
26
+ // tool cap rather than one huge wait, which is also the realistic shape of the
27
+ // turns that were being killed (continuous tool activity, no output for minutes).
28
+ //
29
+ // WHY THE WORK IS NOT `sleep` (measured 2026-07-30, first LIVE attempt). Asking
30
+ // for `sleep 240` produced a 22.9s turn with no nonce: the Claude backend's own
31
+ // Bash tool refuses a FOREGROUND `sleep` ("Foreground `sleep` is blocked; use
32
+ // Monitor with an until-loop"), so the model detached it, reported "Command
33
+ // running in background", said it would wait, and ended the turn. The tool work
34
+ // never happened, so nothing about the wall clock was measured. The blocking
35
+ // command below waits the same wall time without invoking `sleep`, which is the
36
+ // only reason the shape changed — the claim is unchanged.
37
+
38
+ import { strict as assert } from "node:assert";
39
+ import { spawnSync } from "node:child_process";
40
+ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
41
+ import { tmpdir } from "node:os";
42
+ import { dirname, join, resolve } from "node:path";
43
+ import { fileURLToPath } from "node:url";
44
+ import { skipLive } from "./lib/live-skip.ts";
45
+
46
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
47
+ const MODEL = process.env.ENTWURF_ACP_LONG_TURN_MODEL?.trim() || "claude-sonnet-5";
48
+ const PROVIDER = "entwurf";
49
+
50
+ /** The cutoff this cut removed. The turn must outlive it. */
51
+ const RETIRED_CUTOFF_MS = 600_000;
52
+ /** Seconds per tool call — each stays under the backend's own per-command cap. */
53
+ const SLEEP_SECONDS = Number(process.env.ENTWURF_ACP_LONG_TURN_SLEEP_SECONDS) || 240;
54
+ /** How many sequential waits to ask for (3 × 240s = 720s > the retired cutoff). */
55
+ const SLEEP_ROUNDS = Number(process.env.ENTWURF_ACP_LONG_TURN_ROUNDS) || 3;
56
+ /** Harness bound for the WHOLE smoke — an observation horizon, not a turn contract. */
57
+ const SMOKE_HORIZON_MS = Number(process.env.ENTWURF_ACP_LONG_TURN_HORIZON_MS) || 2_400_000;
58
+
59
+ function fail(msg: string): never {
60
+ console.error(`[smoke-acp-long-turn-live] FAIL: ${msg}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ if (process.env.LIVE !== "1") {
65
+ skipLive("smoke-acp-long-turn-live", "set LIVE=1 to run the real long ACP turn.");
66
+ }
67
+
68
+ const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
69
+ const expected = `LONGTURN_OK_${nonce}`;
70
+ const sessionId = `long-turn-${nonce}`;
71
+
72
+ const scratch = mkdtempSync(join(tmpdir(), "entwurf-long-turn-"));
73
+ const sessionDir = mkdtempSync(join(tmpdir(), "entwurf-long-turn-sessions-"));
74
+ const startedAt = Date.now();
75
+ try {
76
+ // Blocks for SLEEP_SECONDS in the FOREGROUND without invoking `sleep` (see the
77
+ // header): select() with only a timeout is a plain wait, and the echo gives
78
+ // each round a visible completion marker in the transcript.
79
+ const waitCommand = `python3 -c "import select; select.select([], [], [], ${SLEEP_SECONDS})" && echo ROUND_COMPLETE`;
80
+
81
+ const prompt =
82
+ `Run exactly ${SLEEP_ROUNDS} shell commands with your shell tool, one at a time, waiting for each to print ` +
83
+ `ROUND_COMPLETE before starting the next. Each command is exactly: ${waitCommand}\n\n` +
84
+ `Each one blocks for about ${SLEEP_SECONDS} seconds, so give the shell tool a per-command limit of at least ` +
85
+ `${(SLEEP_SECONDS + 60) * 1000} ms. Run every command in the FOREGROUND and wait for its output — do not ` +
86
+ "detach it, do not run it in the background, do not poll for it, do not shorten it, and do not run them in " +
87
+ `parallel. When all ${SLEEP_ROUNDS} have printed ROUND_COMPLETE, reply with exactly ${expected} and nothing else.`;
88
+
89
+ const args = [
90
+ "--no-extensions",
91
+ "-e",
92
+ REPO_ROOT,
93
+ "--mode",
94
+ "text",
95
+ "--session-id",
96
+ sessionId,
97
+ "--session-dir",
98
+ sessionDir,
99
+ "-p",
100
+ "--approve",
101
+ "--provider",
102
+ PROVIDER,
103
+ "--model",
104
+ MODEL,
105
+ prompt,
106
+ ];
107
+
108
+ console.error(`[smoke-acp-long-turn-live] repo: ${REPO_ROOT}`);
109
+ console.error(`[smoke-acp-long-turn-live] cwd: ${scratch}`);
110
+ console.error(`[smoke-acp-long-turn-live] model: ${PROVIDER}/${MODEL}`);
111
+ console.error(`[smoke-acp-long-turn-live] work: ${SLEEP_ROUNDS} × ${SLEEP_SECONDS}s foreground wait`);
112
+ console.error(`[smoke-acp-long-turn-live] expects: ${expected} after > ${RETIRED_CUTOFF_MS}ms`);
113
+
114
+ const res = spawnSync("pi", args, { cwd: scratch, encoding: "utf8", timeout: SMOKE_HORIZON_MS, env: process.env });
115
+ const elapsed = Date.now() - startedAt;
116
+
117
+ const stdout = res.stdout ?? "";
118
+ const stderr = res.stderr ?? "";
119
+ const combined = `${stdout}\n${stderr}`;
120
+ const tail = combined.split("\n").slice(-30).join("\n");
121
+
122
+ if (res.error) {
123
+ console.error(`[smoke-acp-long-turn-live] output tail:\n${tail}`);
124
+ fail(`pi spawn failed after ${elapsed}ms: ${res.error.message}`);
125
+ }
126
+ if (res.status !== 0) {
127
+ console.error(`[smoke-acp-long-turn-live] output tail:\n${tail}`);
128
+ fail(`pi exited ${res.status} (signal=${res.signal ?? "none"}) after ${elapsed}ms`);
129
+ }
130
+
131
+ assert.ok(
132
+ stdout.includes(expected),
133
+ `the long turn did not deliver its nonce ${expected} after ${elapsed}ms (stdout tail: ${JSON.stringify(stdout.slice(-400))})`,
134
+ );
135
+ assert.ok(
136
+ elapsed > RETIRED_CUTOFF_MS,
137
+ `the turn finished in ${elapsed}ms — under the retired ${RETIRED_CUTOFF_MS}ms cutoff, so it proves nothing. ` +
138
+ "Raise ENTWURF_ACP_LONG_TURN_ROUNDS / _SLEEP_SECONDS until the tool work outlasts the cutoff.",
139
+ );
140
+
141
+ // No cold replay: exactly one bootstrap in the persisted transcript.
142
+ const jsonl = readdirSync(sessionDir, { recursive: true })
143
+ .filter((f): f is string => typeof f === "string" && f.endsWith(".jsonl"))
144
+ .map((f) => readFileSync(join(sessionDir, f), "utf8"))
145
+ .join("\n");
146
+ assert.ok(jsonl.length > 0, `no session JSONL under ${sessionDir} — cannot judge replay`);
147
+ const bootstraps = jsonl.split("[acp: preparing claude session]").length - 1;
148
+ assert.equal(
149
+ bootstraps,
150
+ 1,
151
+ `the transcript shows ${bootstraps} cold ACP bootstraps — a long turn must be answered by its ORIGINAL prompt, ` +
152
+ "not replayed from a fresh session (that replay is what turned one long turn into four)",
153
+ );
154
+ // The DEFECT's own signatures, not the word "timeout" anywhere: the model
155
+ // narrates its shell tool's per-command limit in this transcript, so a bare
156
+ // /timeout/i would fail an otherwise perfect run on the assistant's own prose.
157
+ // These three strings are what the old pairing actually emitted — the retired
158
+ // cutoff's message, and pi's retry-exhaustion wording.
159
+ const defectMarkers = [
160
+ /prompt timed out after/i,
161
+ /Aborted after \d+ retry attempts?/i,
162
+ /\bretry attempt \d+/i,
163
+ ].filter((re) => re.test(combined));
164
+ assert.equal(
165
+ defectMarkers.length,
166
+ 0,
167
+ `the run reported the retired cutoff or a pi retry — neither may happen to a healthy long turn ` +
168
+ `(matched ${defectMarkers.join(", ")}; tail: ${JSON.stringify(tail)})`,
169
+ );
170
+
171
+ console.log("[smoke-acp-long-turn-live] PASS — a real ACP turn outlived the retired wall clock and answered once");
172
+ console.log(` model: ${PROVIDER}/${MODEL}`);
173
+ console.log(` elapsed: ${elapsed}ms (> retired cutoff ${RETIRED_CUTOFF_MS}ms)`);
174
+ console.log(` nonce: ${expected} present in the assistant reply`);
175
+ console.log(" replay: exactly 1 cold ACP bootstrap in the persisted transcript");
176
+ console.log(" retry: no retry / timeout reported anywhere in the run");
177
+ } finally {
178
+ for (const d of [scratch, sessionDir]) {
179
+ try {
180
+ rmSync(d, { recursive: true, force: true });
181
+ } catch {
182
+ // best-effort cleanup
183
+ }
184
+ }
185
+ }
@@ -26,6 +26,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
26
26
  import { tmpdir } from "node:os";
27
27
  import { dirname, join, resolve } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
+ import { skipLive } from "./lib/live-skip.ts";
29
30
 
30
31
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
32
  const PROBE_SERVER = join(REPO_ROOT, "scripts", "fixtures", "probe-mcp-server.ts");
@@ -39,8 +40,7 @@ function fail(msg: string): never {
39
40
  }
40
41
 
41
42
  if (process.env.LIVE !== "1") {
42
- console.error("[smoke-acp-mcp-live] skipped — set LIVE=1 to run the real pi provider turn.");
43
- process.exit(0);
43
+ skipLive("smoke-acp-mcp-live", "set LIVE=1 to run the real pi provider turn.");
44
44
  }
45
45
 
46
46
  const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
@@ -68,6 +68,7 @@ import {
68
68
  DEFAULT_CLAUDE_TOOLS,
69
69
  } from "../pi-extensions/lib/acp/tool-surface.ts";
70
70
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
71
+ import { skipLive } from "./lib/live-skip.ts";
71
72
 
72
73
  const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_MEMORY_MODEL ?? "claude-sonnet-5";
73
74
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK === "1";
@@ -109,8 +110,7 @@ async function listMemoryFiles(dir: string): Promise<string[]> {
109
110
  }
110
111
 
111
112
  if (process.env.LIVE !== "1") {
112
- console.error("[smoke-acp-memory-containment-live] skipped — set LIVE=1 to run the real memory-containment turn.");
113
- process.exit(0);
113
+ skipLive("smoke-acp-memory-containment-live", "set LIVE=1 to run the real memory-containment turn.");
114
114
  }
115
115
 
116
116
  function resolveLaunch(): { command: string; args: string[]; source: string; acceptance: boolean } {
@@ -50,6 +50,7 @@ import { type AcpClientHandlers, connectAcpClient } from "../pi-extensions/lib/a
50
50
  import type { AcpBackendAdapter } from "../pi-extensions/lib/acp/backend-adapter.ts";
51
51
  import type { ResolvedAcpConfig } from "../pi-extensions/lib/acp/config.ts";
52
52
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
53
+ import { skipLive } from "./lib/live-skip.ts";
53
54
  import { driveProbeTurn, type ProbeMcpEnricher, ProbePhaseError } from "./lib/probe-acp-turn.ts";
54
55
  import {
55
56
  AMBIENT_OVERRIDE_ENV,
@@ -103,8 +104,7 @@ function fail(msg: string): never {
103
104
  }
104
105
 
105
106
  if (process.env.LIVE !== "1") {
106
- console.error("[smoke-acp-ordering-probe-live] skipped — set LIVE=1 to run the real paired probe.");
107
- process.exit(0);
107
+ skipLive("smoke-acp-ordering-probe-live", "set LIVE=1 to run the real paired probe.");
108
108
  }
109
109
 
110
110
  for (const [label, d] of [
@@ -55,6 +55,7 @@ import {
55
55
  DEFAULT_CLAUDE_TOOLS,
56
56
  } from "../pi-extensions/lib/acp/tool-surface.ts";
57
57
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
58
+ import { skipLive } from "./lib/live-skip.ts";
58
59
 
59
60
  const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_OVERLAY_MODEL ?? "claude-sonnet-5";
60
61
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK === "1";
@@ -76,8 +77,7 @@ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
76
77
  }
77
78
 
78
79
  if (process.env.LIVE !== "1") {
79
- console.error("[smoke-acp-overlay-live] skipped — set LIVE=1 to run the real overlay ACP turn.");
80
- process.exit(0);
80
+ skipLive("smoke-acp-overlay-live", "set LIVE=1 to run the real overlay ACP turn.");
81
81
  }
82
82
 
83
83
  function resolveLaunch(): { command: string; args: string[]; source: string; acceptance: boolean } {
@@ -26,6 +26,7 @@ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
26
26
  import { tmpdir } from "node:os";
27
27
  import { dirname, join, resolve } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
+ import { skipLive } from "./lib/live-skip.ts";
29
30
 
30
31
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
32
  const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
@@ -38,8 +39,7 @@ function fail(msg: string): never {
38
39
  }
39
40
 
40
41
  if (process.env.LIVE !== "1") {
41
- console.error("[smoke-acp-provider-live] skipped — set LIVE=1 to run the real pi provider turn.");
42
- process.exit(0);
42
+ skipLive("smoke-acp-provider-live", "set LIVE=1 to run the real pi provider turn.");
43
43
  }
44
44
 
45
45
  // A nonce makes the expected reply unique so a stray "OK" elsewhere in the
@@ -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.62.0) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp@0.64.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+).
@@ -34,6 +34,7 @@ import { Readable, Writable } from "node:stream";
34
34
  import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
35
35
  import { connectAcpClient } from "../pi-extensions/lib/acp/acp-client.ts";
36
36
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
37
+ import { skipLive } from "./lib/live-skip.ts";
37
38
 
38
39
  const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_RAW_TURN_MODEL ?? "claude-sonnet-5";
39
40
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK === "1";
@@ -59,8 +60,7 @@ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
59
60
  }
60
61
 
61
62
  if (process.env.LIVE !== "1") {
62
- console.error("[smoke-acp-raw-turn-live] skipped — set LIVE=1 to run the real ACP turn.");
63
- process.exit(0);
63
+ skipLive("smoke-acp-raw-turn-live", "set LIVE=1 to run the real ACP turn.");
64
64
  }
65
65
 
66
66
  // ---------------------------------------------------------------------------
@@ -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
@@ -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) ──
@@ -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