@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,130 @@
1
+ // Deterministic gate for the S2c runtime tool-surface preflight wiring.
2
+ //
3
+ // S2b proved assertExcludeToolsHonored as a pure predicate; this proves it is
4
+ // actually wired into the streamShellAcp PROVIDER path and fires BEFORE any
5
+ // backend spawn. We call streamShellAcp with a context whose declared tools
6
+ // exclude a built-in the Claude child still exposes (`read`); the turn must fail
7
+ // fast into the returned stream as an `error` event — never reach a spawn, never
8
+ // emit `done`. No live backend is launched (the preflight throws first), so this
9
+ // stays deterministic and IN pnpm check.
10
+ //
11
+ // backend.ts imports its siblings with `.js` suffixes (the root/jiti runtime
12
+ // convention), which `node --experimental-strip-types` cannot resolve directly.
13
+ // So — like check-acp-provider-surface — we tsc-emit the project to a temp dir
14
+ // and import the COMPILED backend.js, whose `.js` imports resolve to real
15
+ // emitted siblings.
16
+
17
+ import { strict as assert } from "node:assert";
18
+ import { execFileSync } from "node:child_process";
19
+ import { readFileSync, rmSync } from "node:fs";
20
+ import { resolve } from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+ import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
23
+
24
+ const model = { id: "claude-sonnet-4-6" } as unknown as Model<Api>;
25
+
26
+ // Declared tools exclude `read`, but the Claude child always exposes Read →
27
+ // declared != actual → the preflight must reject before spawning.
28
+ const context: Context = {
29
+ messages: [{ role: "user", content: "hi", timestamp: 0 }],
30
+ tools: [
31
+ { name: "bash", description: "", parameters: {} as never },
32
+ { name: "edit", description: "", parameters: {} as never },
33
+ { name: "write", description: "", parameters: {} as never },
34
+ ],
35
+ };
36
+
37
+ const TMP_EMIT = ".tmp-verify/acp-backend-preflight";
38
+ rmSync(TMP_EMIT, { recursive: true, force: true });
39
+ try {
40
+ execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
41
+ stdio: "pipe",
42
+ });
43
+ const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
44
+ const mod = (await import(backendUrl)) as {
45
+ streamShellAcp: (
46
+ m: Model<Api>,
47
+ c: Context,
48
+ ) => AsyncIterable<AssistantMessageEvent> & {
49
+ result: () => Promise<{ stopReason: string; errorMessage?: string }>;
50
+ };
51
+ actionableAcpBackendHint: (message: string) => string | undefined;
52
+ };
53
+
54
+ // Detour A (A-c): the pure context-overflow classifier turns a terse backend
55
+ // 400 into an actionable hint, WITHOUT changing routing or hiding the error.
56
+ // Resume must stay legitimate, so the hint names the turn-scoped full-transcript
57
+ // cause and never tells the operator to stop resuming.
58
+ const overflowHint = mod.actionableAcpBackendHint("prompt is too long: 215345 tokens > 200000 maximum");
59
+ assert.ok(overflowHint, "a 'prompt is too long' 400 must classify as a context-overflow hint");
60
+ assert.match(overflowHint, /context-window overflow/, "hint names the overflow");
61
+ assert.match(overflowHint, /persisted resume|window/, "hint names the follow-up root fix");
62
+ assert.match(overflowHint, /locks the model, not resume/, "hint keeps resume legitimate");
63
+ // AMBER1 (GPT review): the hint must NOT assert "turn-scoped" outright — finishError also
64
+ // runs on a resident's first/incompatible turn. It says "FRESH ACP backend session" instead.
65
+ assert.match(overflowHint, /fresh ACP backend session/i, "hint does not over-assert turn-scoped");
66
+ assert.doesNotMatch(overflowHint, /this is a turn-scoped session/, "hint dropped the false-on-resident wording");
67
+ // Positive corpus — real Anthropic overflow phrasings (FN coverage, GPT review).
68
+ assert.ok(
69
+ mod.actionableAcpBackendHint("Error: input is too long for the context window"),
70
+ "context-window phrasing also classifies",
71
+ );
72
+ assert.ok(
73
+ mod.actionableAcpBackendHint("Please reduce the length of the messages or completion."),
74
+ "the 'reduce the length' overflow guidance also classifies",
75
+ );
76
+ // Negative corpus — unrelated failures must NOT be misclassified as overflow. More than one
77
+ // case so the "no misclassification" claim is honestly bounded (GPT review: ECONNREFUSED alone
78
+ // over-claims). These deliberately include near-miss words (exceeded / too many / denied).
79
+ for (const benign of [
80
+ "connect ECONNREFUSED /tmp/acp.sock",
81
+ "rate limit exceeded (429 Too Many Requests)",
82
+ "spawn claude ENOENT",
83
+ "permission denied reading credentials",
84
+ ]) {
85
+ assert.equal(mod.actionableAcpBackendHint(benign), undefined, `must NOT misclassify: ${benign}`);
86
+ }
87
+
88
+ // Seam guard (GPT review): finishError must call the classifier and must GATE it on
89
+ // `aborted` (an aborted turn gets no overflow hint — abort is not an overflow). A source
90
+ // guard, not a behavioral harness: the wiring is a one-liner and the classifier itself is
91
+ // proven above, so this proves the integration without driving a fake overflowing turn.
92
+ const backendSrc = readFileSync(resolve("pi-extensions/lib/acp/backend.ts"), "utf8");
93
+ assert.match(
94
+ backendSrc,
95
+ /aborted\s*\?\s*undefined\s*:\s*actionableAcpBackendHint\(/,
96
+ "finishError gates the overflow hint on `aborted` (no hint on abort) and calls the classifier",
97
+ );
98
+
99
+ const stream = mod.streamShellAcp(model, context);
100
+ const events: AssistantMessageEvent[] = [];
101
+ for await (const ev of stream) events.push(ev);
102
+
103
+ const types = events.map((e) => e.type);
104
+ assert.ok(!types.includes("done"), `a tool-surface lie must NOT complete as done (got ${types.join(",")})`);
105
+ const errorEvent = events.find((e): e is Extract<AssistantMessageEvent, { type: "error" }> => e.type === "error");
106
+ assert.ok(errorEvent, `expected an error event (got ${types.join(",")})`);
107
+ assert.equal(errorEvent.reason, "error", "tool-surface divergence is a hard error, not aborted");
108
+ assert.equal(errorEvent.error.stopReason, "error", "final message stopReason must be error");
109
+ assert.match(
110
+ String(errorEvent.error.errorMessage ?? ""),
111
+ /cannot honor --exclude-tools \(read\)/,
112
+ "error must carry the runtime preflight message naming the unhonored tool",
113
+ );
114
+
115
+ // The stream result resolves to the same error (stream closed cleanly).
116
+ const final = await stream.result();
117
+ assert.match(
118
+ String(final.errorMessage ?? ""),
119
+ /cannot honor --exclude-tools/,
120
+ "stream result carries the preflight error",
121
+ );
122
+ } finally {
123
+ rmSync(TMP_EMIT, { recursive: true, force: true });
124
+ }
125
+
126
+ console.log(
127
+ "[check-acp-backend-preflight] ok — streamShellAcp runs assertExcludeToolsHonored before spawn; a declared-vs-actual " +
128
+ "tool-surface lie fails fast into the stream as an error event (no backend launched, no done); " +
129
+ "actionableAcpBackendHint (A-c) classifies a context-window 400 into an actionable hint without misclassifying unrelated failures",
130
+ );
@@ -0,0 +1,297 @@
1
+ // Deterministic gate for the S2d-1c billing carrier (engraving) + first-user
2
+ // augment. Separate axis from check-acp-session-reuse (GPT c32a6c8): reuse owns
3
+ // session lifecycle; THIS owns the two prompt-shaping surfaces.
4
+ //
5
+ // The two hard invariants it locks (NEXT §S2-scout 핀1 / §S2d gate ②③):
6
+ // - the carrier (`_meta.systemPrompt`) is SHORT, NON-EMPTY by default (the v1
7
+ // preset-replacement memory-containment lever), a PURE function of
8
+ // (template, backend, sorted mcpServerNames), and folds into
9
+ // bridgeConfigSignature so a carrier change invalidates reuse but a stable
10
+ // carrier never forces a per-turn rebuild;
11
+ // - the rich augment rides the `new` prompt on the WIRE only — never the pi
12
+ // Context — so it never enters contextMessageSignatures, and an entwurf
13
+ // prompt that already carries cwd/AGENTS.md gets exactly that one section
14
+ // de-duped (nothing else).
15
+ // Pure + temp-dir fs, no spawn/child — IN pnpm check.
16
+
17
+ import { strict as assert } from "node:assert";
18
+ import { mkdtempSync, writeFileSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+ import type { Context } from "@earendil-works/pi-ai";
22
+ import {
23
+ buildPiContextAugment,
24
+ prependNewPromptAugment,
25
+ promptCarriesEntwurfCwdContext,
26
+ removeCwdAgentsSectionFromAugment,
27
+ } from "../pi-extensions/lib/acp/augment.ts";
28
+ import { buildAcpPrompt, contextToAcpPrompt } from "../pi-extensions/lib/acp/context.ts";
29
+ import { loadEngraving } from "../pi-extensions/lib/acp/engraving.ts";
30
+ import { bridgeConfigSignature, contextMessageSignatures } from "../pi-extensions/lib/acp/session-store.ts";
31
+ import { buildClaudeSessionMeta } from "../pi-extensions/lib/acp/tool-surface.ts";
32
+ import { ENTWURF_PROJECT_CONTEXT_OPEN_TAG } from "../protocol.js";
33
+
34
+ const tmp = mkdtempSync(join(tmpdir(), "acp-carrier-augment-"));
35
+ const BRIDGE_MARK = "operating through entwurf";
36
+
37
+ // ===========================================================================
38
+ // 1) loadEngraving is pure/deterministic + interpolates backend/mcp (sorted)
39
+ // ===========================================================================
40
+ {
41
+ const file = join(tmp, "engraving.md");
42
+ writeFileSync(file, "backend={{backend}} mcp={{mcp_servers}}\n");
43
+ const prev = process.env.ENTWURF_ACP_ENGRAVING_PATH;
44
+ process.env.ENTWURF_ACP_ENGRAVING_PATH = file;
45
+ try {
46
+ const a = loadEngraving({ backend: "claude", mcpServerNames: ["zebra", "alpha"] });
47
+ const b = loadEngraving({ backend: "claude", mcpServerNames: ["zebra", "alpha"] });
48
+ assert.equal(a, b, "loadEngraving is deterministic: same inputs → same output");
49
+ assert.equal(a, "backend=claude mcp=alpha, zebra", "interpolates {{backend}} and SORTED {{mcp_servers}}");
50
+ // Order of the input must not change the render (signature-stability guard).
51
+ assert.equal(
52
+ loadEngraving({ backend: "claude", mcpServerNames: ["alpha", "zebra"] }),
53
+ a,
54
+ "mcpServerNames order does not drift the rendered carrier (sorted)",
55
+ );
56
+ assert.equal(
57
+ loadEngraving({ backend: "claude", mcpServerNames: [] }),
58
+ "backend=claude mcp=(none registered)",
59
+ "no mcp servers → (none registered)",
60
+ );
61
+ } finally {
62
+ if (prev === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
63
+ else process.env.ENTWURF_ACP_ENGRAVING_PATH = prev;
64
+ }
65
+ }
66
+
67
+ // ===========================================================================
68
+ // 2) empty/whitespace/missing → null; shipped default → the non-empty v1 lever;
69
+ // buildClaudeSessionMeta omits the systemPrompt key when carrier absent
70
+ // ===========================================================================
71
+ {
72
+ const metaParams = {
73
+ modelId: "claude-x",
74
+ tools: ["Read"],
75
+ permissionAllow: ["Read(*)"],
76
+ disallowedTools: [],
77
+ settingSources: [],
78
+ strictMcpConfig: false,
79
+ skillPlugins: [],
80
+ };
81
+
82
+ const whitespace = join(tmp, "blank.md");
83
+ writeFileSync(whitespace, " \n\t\n");
84
+ const prev = process.env.ENTWURF_ACP_ENGRAVING_PATH;
85
+ process.env.ENTWURF_ACP_ENGRAVING_PATH = whitespace;
86
+ try {
87
+ assert.equal(loadEngraving({ backend: "claude", mcpServerNames: [] }), null, "whitespace-only template → null");
88
+ } finally {
89
+ if (prev === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
90
+ else process.env.ENTWURF_ACP_ENGRAVING_PATH = prev;
91
+ }
92
+
93
+ process.env.ENTWURF_ACP_ENGRAVING_PATH = join(tmp, "does-not-exist.md");
94
+ try {
95
+ assert.equal(loadEngraving({ backend: "claude", mcpServerNames: [] }), null, "missing/unreadable template → null");
96
+ } finally {
97
+ if (prev === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
98
+ else process.env.ENTWURF_ACP_ENGRAVING_PATH = prev;
99
+ }
100
+
101
+ // No override → the shipped default prompts/engraving.md is NON-EMPTY (the v1
102
+ // engraving lever): a string carrier is emitted, which makes claude-agent-acp
103
+ // REPLACE its `claude_code` preset with this string (acp-agent.js: string-form
104
+ // `_meta.systemPrompt` → full preset replacement). That replacement is what
105
+ // strips the preset's auto-memory section so the model never learns it has a
106
+ // per-session memory store — the memory containment v1 shipped, restored here.
107
+ assert.equal(
108
+ loadEngraving({ backend: "claude", mcpServerNames: [] }),
109
+ "# Engraving Here",
110
+ "shipped default engraving is the non-empty v1 lever → string carrier (preset replaced, auto-memory stripped)",
111
+ );
112
+
113
+ // carrier absent (undefined) → NO systemPrompt key at all.
114
+ const metaAbsent = buildClaudeSessionMeta(metaParams, undefined);
115
+ assert.ok(!("systemPrompt" in metaAbsent), "carrier absent opt-out → _meta has NO systemPrompt key");
116
+ // carrier present → the exact string is the systemPrompt.
117
+ const metaPresent = buildClaudeSessionMeta(metaParams, "tiny carrier");
118
+ assert.equal(metaPresent.systemPrompt, "tiny carrier", "carrier present → _meta.systemPrompt is the rendered string");
119
+ }
120
+
121
+ // ===========================================================================
122
+ // 3) a carrier change changes bridgeConfigSignature (invalidates reuse)
123
+ // ===========================================================================
124
+ {
125
+ const base = {
126
+ backend: "claude" as const,
127
+ modelId: "claude-x",
128
+ nativeModelId: "claude-x",
129
+ mcpServersHash: "deadbeef",
130
+ settingSources: [],
131
+ strictMcpConfig: true,
132
+ tools: ["Read", "Bash", "Edit", "Write"],
133
+ skillPlugins: [],
134
+ permissionAllow: ["Read(*)"],
135
+ disallowedTools: [],
136
+ };
137
+ const absent = bridgeConfigSignature({ ...base, appendSystemPrompt: "" });
138
+ const present = bridgeConfigSignature({ ...base, appendSystemPrompt: "# carrier" });
139
+ assert.notEqual(absent, present, "carrier change → different config signature (a drifted carrier invalidates reuse)");
140
+ // stable carrier → stable signature (no per-turn rebuild)
141
+ assert.equal(
142
+ bridgeConfigSignature({ ...base, appendSystemPrompt: "# carrier" }),
143
+ present,
144
+ "same carrier → same signature (a stable carrier never forces a rebuild)",
145
+ );
146
+ }
147
+
148
+ // ===========================================================================
149
+ // helper: a context whose first user message is `firstUser`
150
+ // ===========================================================================
151
+ function ctxWith(firstUser: string): Context {
152
+ return {
153
+ messages: [
154
+ { role: "user", content: firstUser, timestamp: 0 },
155
+ {
156
+ role: "assistant",
157
+ content: [{ type: "text", text: "ok" }],
158
+ api: "x",
159
+ provider: "x",
160
+ model: "x",
161
+ usage: {
162
+ input: 0,
163
+ output: 0,
164
+ cacheRead: 0,
165
+ cacheWrite: 0,
166
+ totalTokens: 0,
167
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
168
+ },
169
+ stopReason: "stop",
170
+ timestamp: 0,
171
+ },
172
+ { role: "user", content: "latest turn", timestamp: 0 },
173
+ ],
174
+ };
175
+ }
176
+
177
+ // ===========================================================================
178
+ // 4) augment is prepended on the `new` prompt; the `reuse` delta has none
179
+ // ===========================================================================
180
+ {
181
+ const ctx = ctxWith("hello");
182
+ const newBlocks = prependNewPromptAugment(buildAcpPrompt(ctx, "new"), {
183
+ backend: "claude",
184
+ cwd: tmp,
185
+ mcpServerNames: [],
186
+ homeDir: tmp,
187
+ });
188
+ assert.ok(newBlocks.length >= 2, "new prompt = augment block + transcript block(s)");
189
+ assert.ok(newBlocks[0].text.includes(BRIDGE_MARK), "new prompt block 0 is the bridge-identity augment");
190
+ assert.ok(
191
+ newBlocks.some((b) => b.text.includes("latest turn")),
192
+ "the original transcript is preserved after the augment",
193
+ );
194
+
195
+ // The reuse path (backend never calls prependNewPromptAugment on reuse) is the
196
+ // bare delta — no augment. Proven by the builder output itself.
197
+ const reuse = buildAcpPrompt(ctx, "reuse");
198
+ assert.ok(
199
+ reuse.every((b) => !b.text.includes(BRIDGE_MARK)),
200
+ "reuse delta carries NO augment (once-only: augment rides `new` only)",
201
+ );
202
+ }
203
+
204
+ // ===========================================================================
205
+ // 5) the augment NEVER enters contextMessageSignatures (wire-only, no mutation)
206
+ // ===========================================================================
207
+ {
208
+ const ctx = ctxWith("hello");
209
+ const before = contextMessageSignatures(ctx);
210
+ prependNewPromptAugment(buildAcpPrompt(ctx, "new"), {
211
+ backend: "claude",
212
+ cwd: tmp,
213
+ mcpServerNames: [],
214
+ homeDir: tmp,
215
+ });
216
+ const after = contextMessageSignatures(ctx);
217
+ assert.deepEqual(after, before, "prepending the augment does NOT mutate the Context → sigs unchanged (gate ②)");
218
+ // And the full transcript builder is itself augment-free (augment is added later).
219
+ assert.ok(
220
+ contextToAcpPrompt(ctx).every((b) => !b.text.includes(BRIDGE_MARK)),
221
+ "buildAcpPrompt(new) alone carries no augment — it is a pure wire-level prepend",
222
+ );
223
+ }
224
+
225
+ // ===========================================================================
226
+ // 6) & 7) entwurf cwd/AGENTS.md de-dup — present → drop ONLY that section;
227
+ // absent → keep it. Home AGENTS.md always survives.
228
+ // ===========================================================================
229
+ {
230
+ const home = mkdtempSync(join(tmpdir(), "acp-home-"));
231
+ const proj = mkdtempSync(join(tmpdir(), "acp-proj-"));
232
+ writeFileSync(join(home, "AGENTS.md"), "HOME-AGENTS-CONTENT");
233
+ writeFileSync(join(proj, "AGENTS.md"), "CWD-AGENTS-CONTENT");
234
+
235
+ const cwdHeading = `## ${join(proj, "AGENTS.md")}`;
236
+ const homeHeading = `## ${join(home, "AGENTS.md")}`;
237
+ const augParams = { backend: "claude" as const, cwd: proj, mcpServerNames: [], homeDir: home };
238
+
239
+ // Full augment carries BOTH AGENTS sections.
240
+ const full = buildPiContextAugment(augParams);
241
+ assert.ok(full.includes(homeHeading), "augment carries the home AGENTS.md section");
242
+ assert.ok(full.includes(cwdHeading), "augment carries the cwd AGENTS.md section");
243
+
244
+ // (7) plain prompt, no entwurf marker → cwd section kept.
245
+ const plain = prependNewPromptAugment(buildAcpPrompt(ctxWith("just a question"), "new"), augParams);
246
+ assert.ok(plain[0].text.includes(cwdHeading), "no entwurf marker → cwd AGENTS.md section is kept");
247
+
248
+ // (6) entwurf-spawned first user → marker present → cwd section dropped, home kept.
249
+ // Marker built from the SAME constant production uses (entwurf-core enrich +
250
+ // augment promptCarriesEntwurfCwdContext), so a future ENTWURF_PROJECT_CONTEXT_OPEN_TAG
251
+ // change cannot leave this gate green while real de-dup breaks (GPT c32a6c8 amber B).
252
+ const entwurfFirst = `${ENTWURF_PROJECT_CONTEXT_OPEN_TAG} path="${join(proj, "AGENTS.md")}">\nCWD-AGENTS-CONTENT\n</project-context>\n\ndo the task`;
253
+ const ctxE = ctxWith(entwurfFirst);
254
+ const promptText = contextToAcpPrompt(ctxE)
255
+ .map((b) => b.text)
256
+ .join("\n");
257
+ assert.ok(promptCarriesEntwurfCwdContext(promptText, proj), "detects the entwurf cwd project-context marker");
258
+ const deduped = prependNewPromptAugment(buildAcpPrompt(ctxE, "new"), augParams);
259
+ assert.ok(
260
+ !deduped[0].text.includes(cwdHeading),
261
+ "entwurf marker present → cwd AGENTS.md section de-duped from augment",
262
+ );
263
+ assert.ok(deduped[0].text.includes(homeHeading), "de-dup keeps the home AGENTS.md section");
264
+ assert.ok(deduped[0].text.includes(BRIDGE_MARK), "de-dup keeps the bridge identity");
265
+
266
+ // the pure remover is a no-op when the section is absent
267
+ assert.equal(
268
+ removeCwdAgentsSectionFromAugment("no project context here", proj),
269
+ "no project context here",
270
+ "removeCwdAgentsSectionFromAugment is a no-op when there is no cwd section",
271
+ );
272
+ }
273
+
274
+ // ===========================================================================
275
+ // 8) day-granularity date (no clock time) + 50KB truncation
276
+ // ===========================================================================
277
+ {
278
+ const aug = buildPiContextAugment({ backend: "claude", cwd: tmp, mcpServerNames: [], homeDir: tmp });
279
+ const dateLine = aug.split("\n").find((l) => l.startsWith("Current date:"));
280
+ assert.ok(dateLine, "augment carries a Current date line");
281
+ assert.match(dateLine as string, /^Current date: \d{4}-\d{2}-\d{2}$/, "date is day-granularity only (no clock time)");
282
+
283
+ // > 50KB cwd AGENTS.md → augment truncated with the marker.
284
+ const bigHome = mkdtempSync(join(tmpdir(), "acp-big-"));
285
+ writeFileSync(join(bigHome, "AGENTS.md"), "X".repeat(80 * 1024));
286
+ const big = buildPiContextAugment({ backend: "claude", cwd: tmp, mcpServerNames: [], homeDir: bigHome });
287
+ assert.ok(Buffer.byteLength(big, "utf8") <= 50 * 1024, "augment is truncated to the 50KB cap");
288
+ assert.match(big, /context augment truncated to \d+ bytes/, "truncation leaves an honest marker");
289
+ }
290
+
291
+ console.log(
292
+ "[check-acp-carrier-augment] ok — engraving carrier: pure/deterministic, sorted mcp interpolation, " +
293
+ "empty/whitespace/missing → null, shipped-default → non-empty v1 lever (preset replaced), carrier absent → no _meta.systemPrompt key, carrier change → " +
294
+ "signature change (stable carrier → stable signature); augment: prepended on `new` only (reuse delta has none), " +
295
+ "wire-only so it never enters contextMessageSignatures, entwurf cwd/AGENTS.md de-dup (present → drop only that " +
296
+ "section, home kept; absent → kept), day-granularity date, 50KB truncation marker",
297
+ );