@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,25 @@
1
+ /**
2
+ * resolve-acp-bridge — print the bridge extension path entwurf-core's
3
+ * getRegistryRouting() would inject for a provider=entwurf spawn under the
4
+ * CURRENT environment (PI_CODING_AGENT_DIR + settings.json package sources).
5
+ *
6
+ * Used by smoke-installed-entwurf-acp so the live smoke drives a real pi child
7
+ * with the resolver's OWN output: if package-source routing regresses (wrong
8
+ * path, or unresolved), this either prints the wrong -e or throws
9
+ * EntwurfRoutingError (non-zero exit), and the smoke fails instead of silently
10
+ * passing. Prints the `-e` value to stdout on success; throws on unresolved.
11
+ *
12
+ * argv[2]: "remote" selects the SSH remote path; anything else is local.
13
+ */
14
+
15
+ import type { ResolvedTarget } from "../pi-extensions/lib/entwurf-core.ts";
16
+ import { getRegistryRouting } from "../pi-extensions/lib/entwurf-core.ts";
17
+
18
+ const isRemote = process.argv[2] === "remote";
19
+ const target: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-4-6", explicitOnly: false };
20
+
21
+ // Throws EntwurfRoutingError (non-zero exit) when the bridge cannot be resolved —
22
+ // exactly the fail-fast the smoke wants to observe as a failure, not a pass.
23
+ const routing = getRegistryRouting(target, isRemote);
24
+ const i = routing.args.indexOf("-e");
25
+ process.stdout.write(i >= 0 ? routing.args[i + 1] : "");
@@ -0,0 +1,292 @@
1
+ // S2g LIVE 3 (axis 3) — the BUNDLED entwurf-bridge reaches the live ACP session
2
+ // via the 0.11.0 resident/RPC circuit. LIVE-gated, OUT of `pnpm check`.
3
+ //
4
+ // LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
5
+ //
6
+ // WHY a separate axis. smoke-acp-mcp-live proves the generic passthrough with a
7
+ // TINY isolated probe MCP (so a failure isolates to "did mcpServers reach
8
+ // newSession" without identity/env coupling). This smoke proves the LAST mile that
9
+ // probe deliberately skips: the operator's REAL bundled `entwurf-bridge` — the one
10
+ // that needs PI_SESSION_ID/PI_AGENT_ID envelope injection (enrichMcpServersWithEnvelope)
11
+ // to answer entwurf_self — reaches the live ACP session and can be CALLED by the model.
12
+ // GLG verified this by hand (entwurf_self → sessionId/agentId/socketState alive); this
13
+ // codifies that hand-check into a repeatable gate.
14
+ //
15
+ // WHY resident/RPC, not `pi -p` one-shot. A `pi -p` one-shot + bundled tool-call
16
+ // hangs on closeSession/teardown — that hang is diagnostic backlog, NOT this smoke's
17
+ // subject, and NOT the 0.11.0 release circuit (whose bundled-MCP evidence was always
18
+ // resident/RPC). So this smoke RESTORES the 0.11.0 circuit: it drives the bundled
19
+ // bridge through a long-lived `pi --entwurf-control --mode rpc` resident, the same
20
+ // stdin-RPC / stdout-event-stream driver as scripts/gnew-rpc-drive.ts.
21
+ //
22
+ // METHOD (gnew-rpc-drive shape): launch a real resident on an ACP model, send one
23
+ // `{type:"prompt"}` over stdin asking the model to call mcp__entwurf-bridge__entwurf_self,
24
+ // and capture — DIRECTLY from the stdout RPC event stream — the identity envelope
25
+ // (the resident's own freshly-minted gid, agentId entwurf/<model>, socketState
26
+ // alive) plus `agent_end`. The gid is never told to the model: it lives only as the
27
+ // resident's PI_SESSION_ID, injected into the bridge env, so the model can surface it
28
+ // ONLY by actually calling the tool. JSONL is an L3 backstop, never the primary proof.
29
+ //
30
+ // The bundled bridge is supplied by the operator's REAL
31
+ // entwurfProvider.mcpServers.entwurf-bridge (global ~/.pi/agent/settings.json) —
32
+ // this is the operator circuit, not a scratch-isolated probe. If the operator has not
33
+ // wired it, the smoke fails loud (the circuit is not installed).
34
+ //
35
+ // LIVE-only — kept OUT of `pnpm check`; honest skip when LIVE!=1 (skip = CI safety,
36
+ // NOT an acceptance PASS). Model override: ENTWURF_ACP_PROVIDER_MODEL (default sonnet).
37
+
38
+ import { type ChildProcess, spawn } from "node:child_process";
39
+ import { existsSync } from "node:fs";
40
+ import * as os from "node:os";
41
+ import * as path from "node:path";
42
+ import * as readline from "node:readline";
43
+ import { fileURLToPath } from "node:url";
44
+ import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
45
+ import { generateSessionId } from "../pi-extensions/lib/entwurf-core.ts";
46
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
47
+
48
+ const ACP_PROVIDER = "entwurf";
49
+ const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
50
+
51
+ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
52
+ const SOCKET_SUFFIX = ".sock";
53
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
54
+ // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
55
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
56
+
57
+ const BOOT_TIMEOUT_MS = 30_000;
58
+ const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
59
+ const POLL_MS = 100;
60
+
61
+ const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
62
+
63
+ let passed = 0;
64
+ function ok(label: string, cond: boolean): void {
65
+ if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
66
+ console.log(` ok ${label}`);
67
+ passed++;
68
+ }
69
+
70
+ function sleep(ms: number): Promise<void> {
71
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
+ }
73
+
74
+ async function waitForSocket(sockPath: string, timeoutMs: number): Promise<boolean> {
75
+ const deadline = Date.now() + timeoutMs;
76
+ while (Date.now() < deadline) {
77
+ if (existsSync(sockPath)) return true;
78
+ await sleep(POLL_MS);
79
+ }
80
+ return false;
81
+ }
82
+
83
+ async function waitForGone(sockPath: string, timeoutMs: number): Promise<boolean> {
84
+ const deadline = Date.now() + timeoutMs;
85
+ while (Date.now() < deadline) {
86
+ if (!existsSync(sockPath)) return true;
87
+ await sleep(POLL_MS);
88
+ }
89
+ return !existsSync(sockPath);
90
+ }
91
+
92
+ interface TurnCapture {
93
+ agentStartSeen: boolean;
94
+ agentEndSeen: boolean;
95
+ extensionErrors: Array<{ path: unknown; event: unknown; error: unknown }>;
96
+ promptAccepted: boolean;
97
+ // Every line the resident emitted AFTER the prompt was sent — the envelope (a tool
98
+ // result embedded as an escaped string) and the assistant's reply both land here.
99
+ stream: string;
100
+ }
101
+
102
+ // Drive exactly one model turn over the resident's stdin RPC and capture the stdout
103
+ // event stream until `agent_end` (or a hard turn timeout). Mirrors gnew-rpc-drive.ts.
104
+ function driveSelfTurn(child: ChildProcess, prompt: string): Promise<TurnCapture> {
105
+ return new Promise((resolve) => {
106
+ const cap: TurnCapture = {
107
+ agentStartSeen: false,
108
+ agentEndSeen: false,
109
+ extensionErrors: [],
110
+ promptAccepted: false,
111
+ stream: "",
112
+ };
113
+ let settled = false;
114
+ const finish = (): void => {
115
+ if (settled) return;
116
+ settled = true;
117
+ clearTimeout(timer);
118
+ rl.close();
119
+ resolve(cap);
120
+ };
121
+ const timer = setTimeout(finish, TURN_TIMEOUT_MS);
122
+ // If the resident dies before agent_end, settle now instead of burning the full
123
+ // turn timeout — a dead child can never emit agent_end (the assertions then fail loud).
124
+ child.once("exit", finish);
125
+
126
+ const rl = readline.createInterface({ input: child.stdout! });
127
+ rl.on("line", (line: string) => {
128
+ const trimmed = line.trim();
129
+ if (!trimmed) return;
130
+ cap.stream += `${trimmed}\n`;
131
+ let evt: Record<string, unknown>;
132
+ try {
133
+ evt = JSON.parse(trimmed) as Record<string, unknown>;
134
+ } catch {
135
+ return;
136
+ }
137
+ if (evt.type === "agent_start") cap.agentStartSeen = true;
138
+ if (evt.type === "extension_error") {
139
+ cap.extensionErrors.push({ path: evt.extensionPath, event: evt.event, error: evt.error });
140
+ }
141
+ if (evt.type === "response" && evt.command === "prompt") cap.promptAccepted = evt.success === true;
142
+ if (evt.type === "agent_end") {
143
+ cap.agentEndSeen = true;
144
+ finish();
145
+ }
146
+ });
147
+
148
+ child.stdin?.write(`${JSON.stringify({ type: "prompt", message: prompt, id: "self" })}\n`);
149
+ });
150
+ }
151
+
152
+ async function main(): Promise<void> {
153
+ if (process.env.LIVE !== "1") {
154
+ console.log(
155
+ "[smoke-acp-bundled-mcp-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control resident + drives one model turn).",
156
+ );
157
+ return;
158
+ }
159
+
160
+ const gid = generateSessionId();
161
+ const sockPath = path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`);
162
+ const tmp = os.tmpdir();
163
+
164
+ // A fresh gid must not collide with a pre-existing socket (else teardown deletes
165
+ // someone else's live socket). Fail loud rather than risk it.
166
+ ok("fresh gid has no pre-existing control socket", !existsSync(sockPath));
167
+
168
+ console.error(`[smoke-acp-bundled-mcp-live] repo: ${REPO_ROOT}`);
169
+ console.error(
170
+ `[smoke-acp-bundled-mcp-live] gid: ${gid} (never told to the model — only the bridge env carries it)`,
171
+ );
172
+ console.error(`[smoke-acp-bundled-mcp-live] model: ${ACP_PROVIDER}/${ACP_MODEL}`);
173
+
174
+ let stderrTail = "";
175
+ let resident: ChildProcess | null = null;
176
+ let cap: TurnCapture | null = null;
177
+ try {
178
+ resident = spawn(
179
+ "pi",
180
+ [
181
+ ...REPO_EXTENSION_ARGS,
182
+ "--session-id",
183
+ gid,
184
+ "--entwurf-control",
185
+ "--provider",
186
+ ACP_PROVIDER,
187
+ "--model",
188
+ ACP_MODEL,
189
+ "--mode",
190
+ "rpc",
191
+ ],
192
+ { cwd: tmp, stdio: ["pipe", "pipe", "pipe"], detached: false },
193
+ );
194
+ resident.stderr?.on("data", (b: Buffer) => {
195
+ stderrTail = (stderrTail + b.toString()).slice(-4000);
196
+ });
197
+
198
+ // The resident stands its control socket up (turn-free citizenship — S1).
199
+ const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
200
+ ok(`ACP-model resident stood up a control socket (${ACP_PROVIDER}/${ACP_MODEL})`, up);
201
+
202
+ // Citizen fact: get_info answers and reports the un-reverted ACP model (QM1).
203
+ const info = await fetchControlSocketRuntimeInfo(sockPath, { timeout: 3_000 });
204
+ ok(
205
+ `get_info reports the ACP model, not reverted (got ${formatRuntimeModel(info) ?? "none"})`,
206
+ info.modelProvider === ACP_PROVIDER && info.modelId === ACP_MODEL,
207
+ );
208
+
209
+ // Drive ONE model turn over the stdin RPC: call the BUNDLED bridge's entwurf_self.
210
+ const prompt =
211
+ "Call the mcp__entwurf-bridge__entwurf_self tool now. Then reply with exactly the " +
212
+ "sessionId, agentId, and socketState values it returned, one per line, and nothing else. " +
213
+ "Do not paraphrase or invent values — copy them verbatim from the tool result.";
214
+ // The prompt must NOT leak the gid — the gid in the envelope is the proof the bridge
215
+ // answered, so a gid in the prompt would make that proof circular (the model could echo
216
+ // the prompt). Assert it at the source, not just by convention.
217
+ ok("the prompt does not leak the resident gid (envelope-gid proof stays non-circular)", !prompt.includes(gid));
218
+ cap = await driveSelfTurn(resident, prompt);
219
+
220
+ // A real model turn ran AND completed over RPC — no hang (the whole point of the
221
+ // resident circuit vs. the deferred `pi -p` one-shot teardown hang).
222
+ ok("a real model turn ran over the stdin RPC (agent_start)", cap.agentStartSeen);
223
+ ok("the turn completed cleanly over RPC (agent_end — no hang)", cap.agentEndSeen);
224
+ ok("the prompt RPC command was accepted", cap.promptAccepted);
225
+
226
+ // No extension blew up, the S0 fail-loud stub never fired (the provider path is real).
227
+ ok("no extension_error during the turn", cap.extensionErrors.length === 0);
228
+ ok("S0 fail-loud backend stub did NOT fire", !STUB_PATTERN.test(`${cap.stream}\n${stderrTail}`));
229
+
230
+ // PRIMARY proof — narrow to the ENVELOPE lines, then assert the three identity
231
+ // values appear TOGETHER inside them. Asserting against the whole stream is weak:
232
+ // readline attaches before the prompt, so startup/buffered events could carry a
233
+ // stray match. The envelope is the entwurf_self tool result (carried in the
234
+ // [tool:done] notice and echoed in the assistant reply), so it is the lines bearing
235
+ // the envelope markers — restrict the match to those.
236
+ const envelopeText = cap.stream
237
+ .split("\n")
238
+ .filter((l) => /socketState|agentId|sessionId|entwurf_self/i.test(l))
239
+ .join("\n");
240
+
241
+ // The resident's OWN freshly-minted gid appears in the envelope. The gid is never
242
+ // told to the model (asserted above); it lives only as PI_SESSION_ID inside the
243
+ // bridge env. So its presence in the envelope proves entwurf_self was CALLED and its
244
+ // operator-injected identity reached the model — the bundled bridge's last mile.
245
+ ok(
246
+ "the entwurf_self envelope carried the resident's own gid (bundled bridge reached the session)",
247
+ envelopeText.includes(gid),
248
+ );
249
+
250
+ // socketState alive — entwurf_self computes this from the live --entwurf-control socket.
251
+ ok("the envelope reported socketState alive", /socketState[\\":\s]*"?alive/i.test(envelopeText));
252
+
253
+ // agentId is the ACP model identity, not reverted/blank.
254
+ ok(
255
+ `the envelope reported agentId ${ACP_PROVIDER}/${ACP_MODEL}`,
256
+ envelopeText.includes(`${ACP_PROVIDER}/${ACP_MODEL}`),
257
+ );
258
+
259
+ // SUPPLEMENTARY (not a hard gate) — the ACP event mapper emits a tool notice when
260
+ // showToolNotifications is on. S2g lets operator config turn notices OFF, in which
261
+ // case the tool can succeed with no notice — so the envelope above is the real proof
262
+ // and the notice is only corroborating. Log it; do not fail on its absence.
263
+ const sawToolCall = /\[tool:(start|running|done)\][^\n]*entwurf_self/i.test(cap.stream);
264
+ console.log(
265
+ ` info tool-call notice ${sawToolCall ? "observed" : "not observed (notices may be off; envelope is the proof)"}`,
266
+ );
267
+
268
+ // Still alive after the turn (a live socket-citizen, not a one-shot that exits).
269
+ ok(
270
+ "resident still alive after the turn (live socket-citizen)",
271
+ resident.exitCode === null && resident.signalCode === null,
272
+ );
273
+ } catch (err) {
274
+ if (cap)
275
+ console.error(`[smoke-acp-bundled-mcp-live] stream tail:\n${cap.stream.split("\n").slice(-30).join("\n")}`);
276
+ if (stderrTail)
277
+ console.error(`[smoke-acp-bundled-mcp-live] stderr tail:\n${stderrTail.split("\n").slice(-20).join("\n")}`);
278
+ throw err;
279
+ } finally {
280
+ if (resident) await terminateChild(resident);
281
+ }
282
+
283
+ // Hygiene: after teardown the control socket file is gone — no process/socket residue.
284
+ // (The session JSONL is the denote-id memory layer and is intentionally NOT scrubbed.)
285
+ ok("control socket file removed after teardown (no socket residue)", await waitForGone(sockPath, 5_000));
286
+
287
+ console.log(
288
+ `[smoke-acp-bundled-mcp-live] PASS — ${passed} checks (bundled entwurf-bridge reaches the live ACP session via the 0.11.0 resident/RPC circuit)`,
289
+ );
290
+ }
291
+
292
+ await main();
@@ -0,0 +1,192 @@
1
+ // S2e-1 — carrier/augment live acceptance. LIVE-gated, OUT of `pnpm check`.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-acp-carrier-augment-live
4
+ //
5
+ // S2d-1c shipped the billing carrier (engraving) + the rich first-user augment,
6
+ // but only the deterministic gate (check-acp-carrier-augment) proved their SHAPE.
7
+ // This is the S2e-1 live half: it drives the REAL pi provider path and proves the
8
+ // two 핀1-critical behaviors the gate cannot observe on a real model turn:
9
+ //
10
+ // 1. the augment reaches the model — a unique secret written ONLY into the
11
+ // scratch cwd's AGENTS.md (NEVER into the user prompt) comes back in the
12
+ // reply, so buildPiContextAugment's "## <cwd>/AGENTS.md" section actually rode
13
+ // the wire to the live model via streamShellAcp.
14
+ // 2. the default (EMPTY) carrier does not trip subscription billing — the turn
15
+ // exits 0 with no HTTP-400 / "extra usage" billing error. This is the 핀1
16
+ // live check: a carrier-absent run must bill like a normal subscription call.
17
+ //
18
+ // Read-tool caveat (deliberate — GPT c32a6c8 Q1): Claude ACP exposes Read, so a
19
+ // model COULD read AGENTS.md directly instead of answering from the augment. We do
20
+ // not forbid that at the wire — the deterministic gate already locks the augment
21
+ // SHAPE; this smoke asks the model to answer WITHOUT tools and treats the secret
22
+ // in the reply as evidence that the augment+provider path is live AND billing-
23
+ // clean. The honest claim is "the augment rode the live provider path and the
24
+ // empty-carrier turn billed fine", NOT "the model was physically unable to read
25
+ // the file". A wire-dump would over-build for this cut's purpose.
26
+ //
27
+ // Optional carrier-present path (SMOKE_ACP_CARRIER_PRESENT=1, non-blocking — GPT
28
+ // c32a6c8 Q2): a second turn with a TINY engraving via ENTWURF_ACP_ENGRAVING_PATH
29
+ // proves a small _meta.systemPrompt carrier also exits 0 (carrier present ≠ 400).
30
+ //
31
+ // NOT proved here: session reuse (smoke-acp-session-reuse-live) and RGG (S2e-2).
32
+
33
+ import { strict as assert } from "node:assert";
34
+ import { spawnSync } from "node:child_process";
35
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
36
+ import { tmpdir } from "node:os";
37
+ import { dirname, join, resolve } from "node:path";
38
+ import { fileURLToPath } from "node:url";
39
+
40
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
41
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
42
+ const PROVIDER = "entwurf";
43
+ const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
44
+
45
+ function fail(msg: string): never {
46
+ console.error(`[smoke-acp-carrier-augment-live] FAIL: ${msg}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ if (process.env.LIVE !== "1") {
51
+ console.error("[smoke-acp-carrier-augment-live] skipped — set LIVE=1 to run the real pi provider turn.");
52
+ process.exit(0);
53
+ }
54
+
55
+ // Billing-reclassification canaries (핀1). The empty carrier must NOT trip metered
56
+ // billing — if it ever did, the turn would surface one of these. We assert their
57
+ // ABSENCE in the output tail. Kept narrow so a normal reply cannot false-positive.
58
+ const BILLING_FAILURE_PATTERNS = [
59
+ /out of (extra )?usage/i,
60
+ /\bHTTP[\s/]*400\b/i,
61
+ /\b400 Bad Request\b/i,
62
+ /insufficient (credit|balance|quota)/i,
63
+ /metered.*(not|unavailable|denied)/i,
64
+ ];
65
+
66
+ // The S0 stub canary — its error means the provider path never opened.
67
+ const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
68
+
69
+ /** Run one real `pi` provider turn in `cwd`; return {status, stdout, combinedTail}. */
70
+ function runTurn(
71
+ cwd: string,
72
+ prompt: string,
73
+ extraEnv: Record<string, string>,
74
+ ): {
75
+ status: number | null;
76
+ stdout: string;
77
+ combined: string;
78
+ tail: string;
79
+ } {
80
+ const args = [
81
+ "--no-extensions",
82
+ "-e",
83
+ REPO_ROOT,
84
+ "--mode",
85
+ "text",
86
+ "-p",
87
+ "--approve",
88
+ "--provider",
89
+ PROVIDER,
90
+ "--model",
91
+ MODEL,
92
+ prompt,
93
+ ];
94
+ const res = spawnSync("pi", args, {
95
+ cwd,
96
+ encoding: "utf8",
97
+ timeout: TURN_TIMEOUT_MS,
98
+ env: { ...process.env, ...extraEnv },
99
+ });
100
+ const stdout = res.stdout ?? "";
101
+ const stderr = res.stderr ?? "";
102
+ const combined = `${stdout}\n${stderr}`;
103
+ const tail = combined.split("\n").slice(-25).join("\n");
104
+ if (res.error) {
105
+ console.error(`[smoke-acp-carrier-augment-live] output tail:\n${tail}`);
106
+ fail(`pi spawn failed: ${res.error.message}`);
107
+ }
108
+ return { status: res.status, stdout, combined, tail };
109
+ }
110
+
111
+ function assertCleanTurn(label: string, turn: { status: number | null; combined: string; tail: string }): void {
112
+ assert.ok(!STUB_PATTERN.test(turn.combined), `${label}: S0 stub fired — provider path did not open`);
113
+ for (const pat of BILLING_FAILURE_PATTERNS) {
114
+ assert.ok(!pat.test(turn.combined), `${label}: billing-reclassification canary matched ${pat} (핀1 violation?)`);
115
+ }
116
+ if (turn.status !== 0) {
117
+ console.error(`[smoke-acp-carrier-augment-live] ${label} output tail:\n${turn.tail}`);
118
+ fail(`${label}: pi exited ${turn.status}`);
119
+ }
120
+ }
121
+
122
+ const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2e1-"));
123
+ try {
124
+ // A nonce unique to this run; lives ONLY in the cwd AGENTS.md, never the prompt.
125
+ const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
126
+ const secret = `SAC_${nonce}`;
127
+ const agentsPath = join(scratch, "AGENTS.md");
128
+ writeFileSync(
129
+ agentsPath,
130
+ [
131
+ "# Scratch project — entwurf S2e-1 augment live check",
132
+ "",
133
+ `SECRET_PROJECT_CODE: ${secret}`,
134
+ "",
135
+ "When asked for the secret project code, reply with the value above.",
136
+ "",
137
+ ].join("\n"),
138
+ );
139
+
140
+ // The prompt never names the secret; only the cwd AGENTS.md (carried by the
141
+ // augment) holds it. "Without using any tool" pushes the model to answer from
142
+ // the provided instructions rather than reading the file (Read-tool caveat).
143
+ const prompt =
144
+ "Without using any tool or reading any file, reply with exactly the " +
145
+ "SECRET_PROJECT_CODE value from this project's instructions, and nothing else.";
146
+
147
+ console.error(`[smoke-acp-carrier-augment-live] repo: ${REPO_ROOT}`);
148
+ console.error(`[smoke-acp-carrier-augment-live] cwd: ${scratch}`);
149
+ console.error(`[smoke-acp-carrier-augment-live] model: ${PROVIDER}/${MODEL}`);
150
+ console.error(`[smoke-acp-carrier-augment-live] secret: ${secret} (only in cwd AGENTS.md)`);
151
+
152
+ // --- MUST: empty (default) carrier + augment behavior ---------------------
153
+ const turn1 = runTurn(scratch, prompt, {});
154
+ assertCleanTurn("empty-carrier turn", turn1);
155
+ assert.ok(
156
+ turn1.stdout.includes(secret),
157
+ `empty-carrier turn: reply did not carry the cwd-AGENTS secret ${secret} ` +
158
+ `(the augment did not reach the model). stdout tail: ${JSON.stringify(turn1.stdout.slice(-300))}`,
159
+ );
160
+ console.log("[smoke-acp-carrier-augment-live] PASS (MUST) — augment delivered the cwd AGENTS.md secret on a");
161
+ console.log(" live provider turn; the empty default carrier billed clean (exit 0, no 400 canary).");
162
+ console.log(` model: ${PROVIDER}/${MODEL}`);
163
+ console.log(` secret: ${secret} present in assistant reply`);
164
+
165
+ // --- OPTIONAL (non-blocking): tiny carrier-present billing -----------------
166
+ if (process.env.SMOKE_ACP_CARRIER_PRESENT === "1") {
167
+ const carrierMarker = `CARRIER_${nonce}`;
168
+ const carrierPath = join(scratch, "engraving.md");
169
+ writeFileSync(carrierPath, `Always end every reply with the exact marker <<${carrierMarker}>>.\n`);
170
+ const carrierPrompt = "Reply with the single word OK.";
171
+ const turn2 = runTurn(scratch, carrierPrompt, { ENTWURF_ACP_ENGRAVING_PATH: carrierPath });
172
+ assertCleanTurn("carrier-present turn", turn2);
173
+ // The billing claim (exit 0, no 400) is the point. The marker is advisory:
174
+ // it shows the small _meta.systemPrompt carrier influenced the reply.
175
+ const carrierFollowed = turn2.stdout.includes(`<<${carrierMarker}>>`);
176
+ console.log(
177
+ `[smoke-acp-carrier-augment-live] PASS (optional) — tiny carrier turn billed clean (exit 0); ` +
178
+ `carrier marker ${carrierFollowed ? "present" : "ABSENT (advisory, not blocking)"}.`,
179
+ );
180
+ } else {
181
+ console.log(
182
+ "[smoke-acp-carrier-augment-live] optional carrier-present path skipped " +
183
+ "(set SMOKE_ACP_CARRIER_PRESENT=1 to also check a tiny carrier bills clean).",
184
+ );
185
+ }
186
+ } finally {
187
+ try {
188
+ rmSync(scratch, { recursive: true, force: true });
189
+ } catch {
190
+ // best-effort cleanup
191
+ }
192
+ }
@@ -0,0 +1,129 @@
1
+ // S2g LIVE 1 — operator MCP passthrough acceptance. LIVE-gated, OUT of `pnpm check`.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-acp-mcp-live
4
+ //
5
+ // THE baseline proof. GLG's baseline finding was: an ACP model booted with
6
+ // `entwurf/<model>` saw 4 tools and NO MCP servers — the operator's
7
+ // `entwurfProvider.mcpServers` never reached the session. S2g wired that
8
+ // passthrough; the deterministic gate (check-acp-config / check-acp-session-reuse
9
+ // Section G) proved the config reaches `newSession` on a FAKE seam. This live
10
+ // smoke proves the LAST mile a fake cannot: a real ACP child spawns the operator
11
+ // MCP server and the live model can actually CALL its tool.
12
+ //
13
+ // Method (GPT `…2f9325` LIVE 1 — a TINY isolated probe, not entwurf-bridge, so a
14
+ // failure isolates to "did mcpServers reach newSession" without identity/env
15
+ // coupling): a scratch `.pi/settings.json` registers ONE stdio MCP server
16
+ // (scripts/fixtures/probe-mcp-server.ts) that exposes a single tool `probe_nonce`
17
+ // returning a per-run secret carried in its env. The prompt asks the model to
18
+ // CALL the tool and echo the nonce. The nonce lives ONLY inside the MCP server's
19
+ // env — the model cannot produce it unless the tool was visible AND callable.
20
+ //
21
+ // Pre-S2g (hardcoded `mcpServers:[]`) this smoke FAILS: the tool never exists.
22
+
23
+ import { strict as assert } from "node:assert";
24
+ import { spawnSync } from "node:child_process";
25
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } 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 PROBE_SERVER = join(REPO_ROOT, "scripts", "fixtures", "probe-mcp-server.ts");
32
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
33
+ const PROVIDER = "entwurf";
34
+ const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
35
+
36
+ function fail(msg: string): never {
37
+ console.error(`[smoke-acp-mcp-live] FAIL: ${msg}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ 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);
44
+ }
45
+
46
+ const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
47
+
48
+ const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2g-mcp-"));
49
+ try {
50
+ const nonce = `MCP_${process.pid.toString(36)}${Date.now().toString(36)}`;
51
+ // Register the probe MCP server in the scratch project settings — the exact
52
+ // `entwurfProvider.mcpServers` surface `./run.sh install` writes for real.
53
+ mkdirSync(join(scratch, ".pi"), { recursive: true });
54
+ writeFileSync(
55
+ join(scratch, ".pi", "settings.json"),
56
+ `${JSON.stringify(
57
+ {
58
+ entwurfProvider: {
59
+ mcpServers: {
60
+ probe: {
61
+ command: process.execPath,
62
+ args: ["--experimental-strip-types", PROBE_SERVER],
63
+ env: { PROBE_NONCE: nonce },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ null,
69
+ 2,
70
+ )}\n`,
71
+ );
72
+
73
+ const prompt =
74
+ "Call the probe_nonce tool now, then reply with exactly the PROBE_NONCE value it returns " +
75
+ "(the part after the '=' sign), and nothing else.";
76
+
77
+ console.error(`[smoke-acp-mcp-live] repo: ${REPO_ROOT}`);
78
+ console.error(`[smoke-acp-mcp-live] cwd: ${scratch}`);
79
+ console.error(`[smoke-acp-mcp-live] model: ${PROVIDER}/${MODEL}`);
80
+ console.error(`[smoke-acp-mcp-live] nonce: ${nonce} (only inside the probe MCP server env)`);
81
+
82
+ const args = [
83
+ "--no-extensions",
84
+ "-e",
85
+ REPO_ROOT,
86
+ "--mode",
87
+ "text",
88
+ "-p",
89
+ "--approve",
90
+ "--provider",
91
+ PROVIDER,
92
+ "--model",
93
+ MODEL,
94
+ prompt,
95
+ ];
96
+ const res = spawnSync("pi", args, { cwd: scratch, encoding: "utf8", timeout: TURN_TIMEOUT_MS, env: process.env });
97
+ const stdout = res.stdout ?? "";
98
+ const combined = `${stdout}\n${res.stderr ?? ""}`;
99
+ const tail = combined.split("\n").slice(-30).join("\n");
100
+ if (res.error) {
101
+ console.error(`[smoke-acp-mcp-live] output tail:\n${tail}`);
102
+ fail(`pi spawn failed: ${res.error.message}`);
103
+ }
104
+ assert.ok(!STUB_PATTERN.test(combined), "S0 stub fired — provider path did not open");
105
+ if (res.status !== 0) {
106
+ console.error(`[smoke-acp-mcp-live] output tail:\n${tail}`);
107
+ fail(`pi exited ${res.status}`);
108
+ }
109
+ // The tool-start notice is corroborating evidence the model invoked the MCP tool.
110
+ const sawToolCall = /\[tool:(start|running)\]\s+probe_nonce/i.test(combined);
111
+ assert.ok(
112
+ stdout.includes(nonce),
113
+ `reply did not carry the probe nonce ${nonce} — the operator MCP server was not visible/callable ` +
114
+ `(S2g passthrough failure). tool-call notice ${sawToolCall ? "WAS" : "was NOT"} seen. ` +
115
+ `stdout tail: ${JSON.stringify(stdout.slice(-300))}`,
116
+ );
117
+
118
+ console.log("[smoke-acp-mcp-live] PASS — the operator-declared MCP server reached the live ACP session: the model");
119
+ console.log(` called probe_nonce and returned ${nonce} (the nonce lives only inside the MCP server env).`);
120
+ console.log(
121
+ ` model: ${PROVIDER}/${MODEL}; tool-call notice ${sawToolCall ? "observed" : "not observed (reply-only evidence)"}`,
122
+ );
123
+ } finally {
124
+ try {
125
+ rmSync(scratch, { recursive: true, force: true });
126
+ } catch {
127
+ // best-effort cleanup
128
+ }
129
+ }