@junghanacs/entwurf 0.12.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 (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,314 @@
1
+ // S2b — Claude config overlay + tool-narrowed raw ACP turn. LIVE-gated, OUT of
2
+ // `pnpm check`.
3
+ //
4
+ // LIVE=1 ./run.sh smoke-acp-overlay-live
5
+ //
6
+ // What this proves (and ONLY this), one layer above the S2a raw-turn baseline:
7
+ // - the pinned Claude ACP adapter is spawned with CLAUDE_CONFIG_DIR pointed at
8
+ // a pi-authored overlay (verified in the child's /proc/<pid>/environ);
9
+ // - that overlay supplies `settings.json` with `hooks:{}` — the honest
10
+ // "mailbox absence by design" claim (no meta-bridge hook on this child's
11
+ // settings surface). NOTE: we do NOT diff the live meta-store for absence —
12
+ // other Claude sessions mint records concurrently, so that assertion is
13
+ // flaky (GPT S2b Q1). The honest claim is overlay-supplies-hooks:{}.
14
+ // - the session is opened with a tool-narrowed `_meta.claudeCode.options`
15
+ // (tools + disallowedTools) and STILL drives one real model turn;
16
+ // - `_meta.systemPrompt` is kept ABSENT so the billing carrier never grows
17
+ // (NEXT §S2-scout 핀1) — asserted on the payload we send.
18
+ //
19
+ // Deliberately NOT here (later cuts / forbidden ahead of the provider path):
20
+ // - no pi provider path, no streamSimple replacement (backend-stub stays
21
+ // fail-loud — that is S2c);
22
+ // - no event mapping, no session reuse / signature, no identity carrier /
23
+ // engraving / first-user augment (S2d);
24
+ // - no behavioral tool-denial probe ("try WebSearch") — flaky and not the
25
+ // required evidence; the deterministic gate (check-acp-tool-surface) owns
26
+ // the truthfulness check.
27
+ //
28
+ // Boundary notes (mirror S2a, GPT-reviewed):
29
+ // - launch source MUST be the resolved package bin; a silent PATH fallback
30
+ // FAILS acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1 (then
31
+ // the run is stamped debug/non-acceptance).
32
+ // - scratch cwd is a fresh mkdtemp (repo isolation); the overlay is a
33
+ // separate fresh mkdtemp with realDir = the operator's ~/.claude so live
34
+ // credentials pass through — entwurf neither copies nor proxies auth.
35
+
36
+ import { strict as assert } from "node:assert";
37
+ import { type ChildProcessByStdio, spawn } from "node:child_process";
38
+ import { readFileSync } from "node:fs";
39
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
40
+ import { createRequire } from "node:module";
41
+ import { tmpdir } from "node:os";
42
+ import { dirname, join } from "node:path";
43
+ import { Readable, Writable } from "node:stream";
44
+ import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
45
+ import { connectAcpClient } from "../pi-extensions/lib/acp/acp-client.ts";
46
+ import {
47
+ CLAUDE_REAL_CONFIG_DIR,
48
+ claudeLaunchEnvDefaults,
49
+ ensureClaudeConfigOverlay,
50
+ } from "../pi-extensions/lib/acp/overlay.ts";
51
+ import {
52
+ buildClaudeSessionMeta,
53
+ DEFAULT_CLAUDE_DISALLOWED_TOOLS,
54
+ DEFAULT_CLAUDE_PERMISSION_ALLOW,
55
+ DEFAULT_CLAUDE_TOOLS,
56
+ } from "../pi-extensions/lib/acp/tool-surface.ts";
57
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
58
+
59
+ const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_OVERLAY_MODEL ?? "claude-sonnet-4-6";
60
+ const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK === "1";
61
+ const RAW_TAIL_CAP = 64 * 1024;
62
+
63
+ function fail(msg: string): never {
64
+ console.error(`[smoke-acp-overlay-live] FAIL: ${msg}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
69
+
70
+ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
71
+ return Promise.race([
72
+ p,
73
+ sleep(ms).then((): never => {
74
+ throw new Error(`${label} timed out after ${ms}ms`);
75
+ }),
76
+ ]);
77
+ }
78
+
79
+ if (process.env.LIVE !== "1") {
80
+ console.error("[smoke-acp-overlay-live] skipped — set LIVE=1 to run the real overlay ACP turn.");
81
+ process.exit(0);
82
+ }
83
+
84
+ function resolveLaunch(): { command: string; args: string[]; source: string; acceptance: boolean } {
85
+ const require = createRequire(import.meta.url);
86
+ try {
87
+ const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
88
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { bin?: string | Record<string, string> };
89
+ const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
90
+ if (binPath) {
91
+ return {
92
+ command: process.execPath,
93
+ args: [join(dirname(pkgJsonPath), binPath)],
94
+ source: "package:@agentclientprotocol/claude-agent-acp",
95
+ acceptance: true,
96
+ };
97
+ }
98
+ fail("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
99
+ } catch (err) {
100
+ if (!ALLOW_PATH_FALLBACK) {
101
+ fail(
102
+ `could not resolve @agentclientprotocol/claude-agent-acp package bin (${(err as Error).message}). ` +
103
+ "This is an acceptance failure — the dep pin / install is broken. Set " +
104
+ "ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1 only for debug.",
105
+ );
106
+ }
107
+ }
108
+ return {
109
+ command: "claude-agent-acp",
110
+ args: [],
111
+ source: "PATH:claude-agent-acp (debug/non-acceptance)",
112
+ acceptance: false,
113
+ };
114
+ }
115
+
116
+ // Read the child's startup environment from /proc (Linux). Returns the value of
117
+ // `key` or undefined. The environ is fixed at exec, so this is strong evidence
118
+ // the var was planted into the spawn — not just our in-process copy.
119
+ async function readChildEnv(pid: number, key: string): Promise<string | undefined> {
120
+ const buf = await readFile(`/proc/${pid}/environ`);
121
+ for (const entry of buf.toString("utf8").split("\0")) {
122
+ const eq = entry.indexOf("=");
123
+ if (eq > 0 && entry.slice(0, eq) === key) return entry.slice(eq + 1);
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ async function main(): Promise<void> {
129
+ const launch = resolveLaunch();
130
+ const scratch = await mkdtemp(join(tmpdir(), "entwurf-s2b-cwd-"));
131
+ const overlayDir = await mkdtemp(join(tmpdir(), "entwurf-s2b-overlay-"));
132
+
133
+ // Materialize the overlay against the operator's REAL ~/.claude so live
134
+ // credentials pass through; settings.json (hooks:{}) is ours.
135
+ ensureClaudeConfigOverlay(CLAUDE_REAL_CONFIG_DIR, overlayDir);
136
+ const overlaySettingsPath = join(overlayDir, "settings.json");
137
+ const overlaySettings = JSON.parse(readFileSync(overlaySettingsPath, "utf8"));
138
+ assert.ok(
139
+ overlaySettings.hooks &&
140
+ typeof overlaySettings.hooks === "object" &&
141
+ Object.keys(overlaySettings.hooks).length === 0,
142
+ "overlay settings.json must supply hooks:{} (mailbox absence by design)",
143
+ );
144
+
145
+ const envOverride = claudeLaunchEnvDefaults(overlayDir);
146
+ assert.equal(envOverride.CLAUDE_CONFIG_DIR, overlayDir, "launch env builder must target the overlay dir");
147
+
148
+ console.error(`[smoke-acp-overlay-live] launch source: ${launch.source}`);
149
+ console.error(`[smoke-acp-overlay-live] scratch cwd: ${scratch}`);
150
+ console.error(`[smoke-acp-overlay-live] overlay dir: ${overlayDir}`);
151
+ console.error(`[smoke-acp-overlay-live] model request: ${REQUESTED_MODEL_ID}`);
152
+
153
+ const child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(launch.command, launch.args, {
154
+ cwd: scratch,
155
+ env: { ...process.env, ...envOverride },
156
+ stdio: ["pipe", "pipe", "pipe"],
157
+ }) as ChildProcessByStdio<Writable, Readable, Readable>;
158
+
159
+ const stderrTail: string[] = [];
160
+ child.stderr.on("data", (c) => {
161
+ stderrTail.push(c.toString());
162
+ if (stderrTail.length > 200) stderrTail.shift();
163
+ });
164
+
165
+ let rawBytes = "";
166
+ child.stdout.on("data", (c) => {
167
+ rawBytes += c.toString();
168
+ if (rawBytes.length > RAW_TAIL_CAP * 2) rawBytes = rawBytes.slice(-RAW_TAIL_CAP);
169
+ });
170
+
171
+ const stdoutWeb = Readable.toWeb(child.stdout) as unknown as ReadableStream<Uint8Array>;
172
+ const stdinWeb = Writable.toWeb(child.stdin) as unknown as WritableStream<Uint8Array>;
173
+ const stream = ndJsonStream(stdinWeb, stdoutWeb);
174
+
175
+ let collectedText = "";
176
+ let unexpectedPermission = 0;
177
+ let unexpectedFileOp = 0;
178
+
179
+ const connection = connectAcpClient(stream as any, {
180
+ sessionUpdate: async (notification: any) => {
181
+ const u = notification?.update;
182
+ if (u?.sessionUpdate === "agent_message_chunk") {
183
+ const t = u?.content?.text;
184
+ if (typeof t === "string") collectedText += t;
185
+ }
186
+ },
187
+ requestPermission: async (): Promise<any> => {
188
+ unexpectedPermission++;
189
+ return { outcome: { outcome: "cancelled" } };
190
+ },
191
+ readTextFile: async (): Promise<any> => {
192
+ unexpectedFileOp++;
193
+ throw new Error("unexpected readTextFile in overlay OK turn");
194
+ },
195
+ writeTextFile: async (): Promise<any> => {
196
+ unexpectedFileOp++;
197
+ throw new Error("unexpected writeTextFile in overlay OK turn");
198
+ },
199
+ });
200
+
201
+ let failure: Error | null = null;
202
+ try {
203
+ if (!launch.acceptance) {
204
+ throw new Error("launch was a PATH fallback (debug) — not an acceptance PASS");
205
+ }
206
+
207
+ // The child's startup env must carry our overlay redirect.
208
+ const childPid = child.pid;
209
+ assert.ok(childPid, "spawned child has no pid");
210
+ const childConfigDir = await readChildEnv(childPid, "CLAUDE_CONFIG_DIR");
211
+ assert.equal(
212
+ childConfigDir,
213
+ overlayDir,
214
+ `child CLAUDE_CONFIG_DIR must be the overlay (got ${JSON.stringify(childConfigDir)})`,
215
+ );
216
+ console.error(`[smoke-acp-overlay-live] child CLAUDE_CONFIG_DIR=${childConfigDir} ✓`);
217
+
218
+ // 1) initialize
219
+ const init = await withTimeout(
220
+ "initialize",
221
+ connection.initialize({
222
+ protocolVersion: PROTOCOL_VERSION,
223
+ clientCapabilities: {},
224
+ clientInfo: { name: "entwurf-smoke", version: "s2b-overlay" },
225
+ } as any),
226
+ 30_000,
227
+ );
228
+ assert.ok(init, "initialize returned no result");
229
+ console.error(`[smoke-acp-overlay-live] initialize ok (protocolVersion=${(init as any)?.protocolVersion})`);
230
+
231
+ // 2) newSession — overlay-aware: scratch cwd, no MCP, tool-narrowed _meta,
232
+ // NO systemPrompt (carrier stays absent).
233
+ const sessionMeta = buildClaudeSessionMeta({
234
+ modelId: REQUESTED_MODEL_ID,
235
+ tools: DEFAULT_CLAUDE_TOOLS,
236
+ permissionAllow: DEFAULT_CLAUDE_PERMISSION_ALLOW,
237
+ disallowedTools: DEFAULT_CLAUDE_DISALLOWED_TOOLS,
238
+ settingSources: [],
239
+ strictMcpConfig: false,
240
+ skillPlugins: [],
241
+ });
242
+ assert.ok(!("systemPrompt" in sessionMeta), "S2b payload must NOT carry _meta.systemPrompt");
243
+ const created = (await withTimeout(
244
+ "newSession",
245
+ connection.newSession({ cwd: scratch, mcpServers: [], _meta: sessionMeta } as any),
246
+ 30_000,
247
+ )) as any;
248
+ const sessionId = created?.sessionId;
249
+ assert.ok(sessionId, "newSession returned no sessionId");
250
+ console.error(
251
+ `[smoke-acp-overlay-live] newSession ok (sessionId=${String(sessionId).slice(0, 12)}…, tool-narrowed _meta)`,
252
+ );
253
+
254
+ // 3) force the requested model — honesty, same as S2a.
255
+ const setConfig = (connection as any).setSessionConfigOption;
256
+ if (typeof setConfig !== "function") {
257
+ throw new Error(`setSessionConfigOption unsupported — cannot enforce ${REQUESTED_MODEL_ID}`);
258
+ }
259
+ await withTimeout(
260
+ "setSessionConfigOption",
261
+ setConfig.call(connection, { sessionId, configId: "model", value: REQUESTED_MODEL_ID }),
262
+ 30_000,
263
+ );
264
+ console.error(`[smoke-acp-overlay-live] model set -> ${REQUESTED_MODEL_ID}`);
265
+
266
+ // 4) one tiny turn.
267
+ const promptResult = (await withTimeout(
268
+ "prompt",
269
+ connection.prompt({
270
+ sessionId,
271
+ prompt: [{ type: "text", text: "Reply with exactly OK and nothing else." }],
272
+ } as any),
273
+ 120_000,
274
+ )) as any;
275
+ console.error(`[smoke-acp-overlay-live] prompt returned (stopReason=${promptResult?.stopReason})`);
276
+
277
+ // ---- assertions ----
278
+ assert.ok(promptResult, "prompt returned no result");
279
+ assert.equal(unexpectedPermission, 0, `unexpected permission request(s): ${unexpectedPermission}`);
280
+ assert.equal(unexpectedFileOp, 0, `unexpected file op(s): ${unexpectedFileOp}`);
281
+ assert.ok(rawBytes.trim().length > 0, "no raw NDJSON bytes captured from backend stdout");
282
+ const normalized = collectedText.replace(/\s+/g, " ").trim().toUpperCase();
283
+ assert.ok(
284
+ normalized.includes("OK"),
285
+ `agent text did not contain "OK" (got: ${JSON.stringify(collectedText.slice(0, 200))})`,
286
+ );
287
+
288
+ console.log("[smoke-acp-overlay-live] PASS — overlay + tool-narrowed raw ACP turn drove a live model reply");
289
+ console.log(` launch: ${launch.source}`);
290
+ console.log(` overlay: ${overlayDir} (hooks:{})`);
291
+ console.log(` CLAUDE_CONFIG_DIR (child): ${overlayDir}`);
292
+ console.log(` model: ${REQUESTED_MODEL_ID}`);
293
+ console.log(` reply: ${JSON.stringify(collectedText.trim().slice(0, 120))}`);
294
+ console.log(` rawBytes: ${rawBytes.length} captured (NDJSON)`);
295
+ } catch (err) {
296
+ failure = err instanceof Error ? err : new Error(String(err));
297
+ console.error(`[smoke-acp-overlay-live] stderr tail:\n${stderrTail.slice(-20).join("")}`);
298
+ console.error(`[smoke-acp-overlay-live] raw NDJSON tail:\n${rawBytes.slice(-2048)}`);
299
+ } finally {
300
+ connection.close?.();
301
+ await terminateChild(child);
302
+ for (const dir of [scratch, overlayDir]) {
303
+ try {
304
+ await rm(dir, { recursive: true, force: true });
305
+ } catch {
306
+ // best-effort cleanup
307
+ }
308
+ }
309
+ }
310
+
311
+ if (failure) fail(failure.message);
312
+ }
313
+
314
+ await main();
@@ -0,0 +1,162 @@
1
+ // S2c — full provider-path acceptance. LIVE-gated, OUT of `pnpm check`.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-acp-provider-live
4
+ //
5
+ // This is the S2c acceptance: it drives the REAL pi provider path end to end,
6
+ // not the raw ACP pipe (S2a) or the overlay-aware raw driver (S2b). A real `pi`
7
+ // process loads THIS checkout's extension, selects `entwurf/<model>`, and
8
+ // pi's runner calls our `streamSimple` (lib/acp/backend.ts) — which spawns
9
+ // claude-agent-acp under the overlay, drives one turn, and maps the result back
10
+ // through the S2c event mapper. A unique nonce in the requested reply proves the
11
+ // answer came from a live model turn, and the absence of the removed S0 stub
12
+ // error proves the provider path actually opened.
13
+ //
14
+ // Chain proved (and only this): registration → model selection → pi runner →
15
+ // streamShellAcp → overlay spawn → ACP turn → event mapping → assistant text.
16
+ //
17
+ // Deliberately NOT here (later cuts): no session reuse / signature (S2d), no
18
+ // identity carrier / engraving / first-user augment (S2d), no MCP/entwurf tool
19
+ // wiring into the backend (mcpServers:[] in S2c). Tool-execution behavior is not
20
+ // exercised — the prompt is tool-free; the deterministic event-mapper gate owns
21
+ // the tool→notice contract.
22
+
23
+ import { strict as assert } from "node:assert";
24
+ import { spawnSync } from "node:child_process";
25
+ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import { dirname, join, resolve } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
32
+ const PROVIDER = "entwurf";
33
+ const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
34
+
35
+ function fail(msg: string): never {
36
+ console.error(`[smoke-acp-provider-live] FAIL: ${msg}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ 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);
43
+ }
44
+
45
+ // A nonce makes the expected reply unique so a stray "OK" elsewhere in the
46
+ // output cannot pass the assertion. Date.now is fine in a normal smoke script.
47
+ const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
48
+ const expected = `OK_${nonce}`;
49
+ // A known session id + an isolated session dir let us inspect the on-disk JSONL
50
+ // after the turn (S2f Amber 1 — marker persistence).
51
+ const sessionId = `psa-s2f-${nonce}`;
52
+
53
+ const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2c-provider-"));
54
+ const sessionDir = mkdtempSync(join(tmpdir(), "entwurf-s2f-sessions-"));
55
+ try {
56
+ // --no-extensions + explicit -e REPO_ROOT: load ONLY this checkout's
57
+ // extensions (matches the S1 smoke). --mode text prints the assistant reply
58
+ // to stdout; -p is one-shot non-interactive; --approve auto-approves any
59
+ // pi-level permission (the turn is tool-free, but keep it unattended-safe).
60
+ const args = [
61
+ "--no-extensions",
62
+ "-e",
63
+ REPO_ROOT,
64
+ "--mode",
65
+ "text",
66
+ "--session-id",
67
+ sessionId,
68
+ "--session-dir",
69
+ sessionDir,
70
+ "-p",
71
+ "--approve",
72
+ "--provider",
73
+ PROVIDER,
74
+ "--model",
75
+ MODEL,
76
+ `Reply with exactly ${expected} and nothing else.`,
77
+ ];
78
+
79
+ console.error(`[smoke-acp-provider-live] repo: ${REPO_ROOT}`);
80
+ console.error(`[smoke-acp-provider-live] cwd: ${scratch}`);
81
+ console.error(`[smoke-acp-provider-live] model: ${PROVIDER}/${MODEL}`);
82
+ console.error(`[smoke-acp-provider-live] expects: ${expected}`);
83
+
84
+ const res = spawnSync("pi", args, { cwd: scratch, encoding: "utf8", timeout: TURN_TIMEOUT_MS, env: process.env });
85
+
86
+ const stdout = res.stdout ?? "";
87
+ const stderr = res.stderr ?? "";
88
+ const combined = `${stdout}\n${stderr}`;
89
+ const tail = combined.split("\n").slice(-25).join("\n");
90
+
91
+ if (res.error) {
92
+ console.error(`[smoke-acp-provider-live] output tail:\n${tail}`);
93
+ fail(`pi spawn failed: ${res.error.message}`);
94
+ }
95
+
96
+ // The removed S0 stub must never reappear — its error is the canary that the
97
+ // provider path did NOT actually open.
98
+ assert.ok(
99
+ !/AcpBackendNotImplementedError|not implemented in S0/i.test(combined),
100
+ "S0 fail-loud stub fired — the provider path did not open (streamSimple is still the stub)",
101
+ );
102
+
103
+ if (res.status !== 0) {
104
+ console.error(`[smoke-acp-provider-live] output tail:\n${tail}`);
105
+ fail(`pi exited ${res.status} (signal=${res.signal ?? "none"})`);
106
+ }
107
+
108
+ // The live model reply must carry the unique nonce.
109
+ assert.ok(
110
+ stdout.includes(expected),
111
+ `assistant reply did not contain ${expected} (stdout tail: ${JSON.stringify(stdout.slice(-300))})`,
112
+ );
113
+
114
+ // S2f: turn-lifecycle progress must be VISIBLE on a real turn (always-on, no
115
+ // gate). The bootstrap (overlay → spawn → init → newSession → setModel → first
116
+ // token) is otherwise silent and reads as a hang. The deterministic gate owns
117
+ // order/replay/signature; this proves the notices actually surface live.
118
+ for (const marker of ["[acp: preparing claude session]", "[acp: session ready model=", "[acp: sending prompt]"]) {
119
+ assert.ok(
120
+ combined.includes(marker),
121
+ `S2f lifecycle notice "${marker}" missing — turn progress must always be visible (output tail: ${JSON.stringify(tail)})`,
122
+ );
123
+ }
124
+
125
+ // S2f Amber 1 (L3 marker persistence): the lifecycle marker must SURVIVE pi's
126
+ // JSONL serialization. If pi dropped the textSignature on save, a later `new`
127
+ // rebuild from a reloaded Context would lose the filter and replay the notice
128
+ // into the ACP prompt. The deterministic Section F proves the in-memory filter;
129
+ // this closes the on-disk round-trip the filter depends on. Inspect the saved
130
+ // transcript directly (the marker string is used ONLY as a textSignature, so
131
+ // its presence in the JSONL is proof the field was persisted).
132
+ const jsonl = readdirSync(sessionDir, { recursive: true })
133
+ .filter((f): f is string => typeof f === "string" && f.endsWith(".jsonl"))
134
+ .map((f) => readFileSync(join(sessionDir, f), "utf8"))
135
+ .join("\n");
136
+ assert.ok(
137
+ jsonl.length > 0,
138
+ `no session JSONL written under ${sessionDir} — cannot verify marker persistence (check --session-dir support)`,
139
+ );
140
+ assert.ok(
141
+ jsonl.includes("[acp: preparing claude session]"),
142
+ "lifecycle notice text reached the persisted JSONL transcript (display-only, but still saved as assistant text)",
143
+ );
144
+ assert.ok(
145
+ jsonl.includes("entwurf:lifecycle-notice-v1"),
146
+ "the textSignature marker SURVIVED pi JSONL serialization — a `new` rebuild from a reloaded Context will still filter it (no replay, no signature drift)",
147
+ );
148
+
149
+ console.log("[smoke-acp-provider-live] PASS — pi provider path drove a live ACP turn through streamShellAcp");
150
+ console.log(` model: ${PROVIDER}/${MODEL}`);
151
+ console.log(` nonce: ${expected} present in assistant reply`);
152
+ console.log(" progress: [acp: preparing…] → [acp: session ready…] → [acp: sending prompt] all visible");
153
+ console.log(" persist: textSignature marker survived JSONL serialization (L3 round-trip closed)");
154
+ } finally {
155
+ for (const d of [scratch, sessionDir]) {
156
+ try {
157
+ rmSync(d, { recursive: true, force: true });
158
+ } catch {
159
+ // best-effort cleanup
160
+ }
161
+ }
162
+ }