@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,168 @@
1
+ // S1 acceptance smoke — ACP-model session is a first-class socket-citizen.
2
+ //
3
+ // Proves, end-to-end against a REAL pi resident, the S1 claim: a
4
+ // `pi --entwurf-control` session whose model is an ACP model
5
+ // (entwurf/claude-*) is a first-class v2 socket-citizen — peers-visible and
6
+ // get_info-answerable — WITHOUT running a backend turn. Citizenship is supplied
7
+ // by the host control socket (model-agnostic), not by the ACP plugin.
8
+ //
9
+ // It also clears the two S1 question marks live:
10
+ // QM1 model-lock.ts does NOT revert a launch-with-ACP-model: get_info still
11
+ // reports provider "entwurf" after the resident anchors.
12
+ // QM2 the fail-loud streamSimple stub does NOT fire at launch: the resident
13
+ // stands its socket up and stays alive with no turn (no stub error on
14
+ // stderr); the stub is a turn-only hard stop, never a launch-time crash.
15
+ //
16
+ // No prompt is ever sent, so streamSimple is never invoked — selecting the model
17
+ // and standing the control socket up is turn-free by construction. The real ACP
18
+ // backend turn is S2; this smoke must NOT exercise it.
19
+ //
20
+ // LIVE-only (spawns a real pi + opens a real socket) — kept OUT of `pnpm check`;
21
+ // honest skip when LIVE!=1 (skip = CI safety, NOT an acceptance PASS).
22
+ // LIVE=1 ./run.sh smoke-acp-socket-citizen-live
23
+ // override model via ENTWURF_S1_MODEL (default claude-opus-4-8).
24
+
25
+ import { type ChildProcess, spawn } from "node:child_process";
26
+ import { existsSync } from "node:fs";
27
+ import * as os from "node:os";
28
+ import * as path from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
31
+ import { generateSessionId } from "../pi-extensions/lib/entwurf-core.ts";
32
+ import { scanSocketProbes } from "../pi-extensions/lib/socket-discovery.ts";
33
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
34
+
35
+ const ACP_PROVIDER = "entwurf";
36
+ const ACP_MODEL = process.env.ENTWURF_S1_MODEL?.trim() || "claude-opus-4-8";
37
+
38
+ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
39
+ const SOCKET_SUFFIX = ".sock";
40
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
41
+ // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
42
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
43
+
44
+ const BOOT_TIMEOUT_MS = 30_000;
45
+ const POLL_MS = 100;
46
+
47
+ let passed = 0;
48
+ function ok(label: string, cond: boolean): void {
49
+ if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
50
+ console.log(` ok ${label}`);
51
+ passed++;
52
+ }
53
+
54
+ function sleep(ms: number): Promise<void> {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
57
+
58
+ async function waitForSocket(sockPath: string, timeoutMs: number): Promise<boolean> {
59
+ const deadline = Date.now() + timeoutMs;
60
+ while (Date.now() < deadline) {
61
+ if (existsSync(sockPath)) return true;
62
+ await sleep(POLL_MS);
63
+ }
64
+ return false;
65
+ }
66
+
67
+ async function waitForGone(sockPath: string, timeoutMs: number): Promise<boolean> {
68
+ const deadline = Date.now() + timeoutMs;
69
+ while (Date.now() < deadline) {
70
+ if (!existsSync(sockPath)) return true;
71
+ await sleep(POLL_MS);
72
+ }
73
+ return !existsSync(sockPath);
74
+ }
75
+
76
+ async function main(): Promise<void> {
77
+ if (process.env.LIVE !== "1") {
78
+ console.log(
79
+ "[smoke-acp-socket-citizen-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).",
80
+ );
81
+ return;
82
+ }
83
+
84
+ const gid = generateSessionId();
85
+ const sockPath = path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`);
86
+ const tmp = os.tmpdir();
87
+
88
+ // B3: a fresh gid must not collide with a pre-existing socket (else teardown would
89
+ // delete someone else's live socket). Fail loud rather than risk it.
90
+ ok("fresh gid has no pre-existing control socket", !existsSync(sockPath));
91
+
92
+ let stderrTail = "";
93
+ let resident: ChildProcess | null = null;
94
+ try {
95
+ // Launch a real resident on the ACP model, in rpc mode (non-TTY safe, turn-free).
96
+ resident = spawn(
97
+ "pi",
98
+ [
99
+ ...REPO_EXTENSION_ARGS,
100
+ "--session-id",
101
+ gid,
102
+ "--entwurf-control",
103
+ "--provider",
104
+ ACP_PROVIDER,
105
+ "--model",
106
+ ACP_MODEL,
107
+ "--mode",
108
+ "rpc",
109
+ ],
110
+ { cwd: tmp, stdio: ["pipe", "ignore", "pipe"], detached: false },
111
+ );
112
+ resident.stderr?.on("data", (b: Buffer) => {
113
+ stderrTail = (stderrTail + b.toString()).slice(-4000);
114
+ });
115
+
116
+ // QM2 (part 1): launch with an ACP model does not die — the socket comes up.
117
+ const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
118
+ ok(`ACP-model resident stood up a control socket (${ACP_PROVIDER}/${ACP_MODEL})`, up);
119
+
120
+ // Citizenship + get_info: the resident answers the control RPC like a native sibling.
121
+ const info = await fetchControlSocketRuntimeInfo(sockPath, { timeout: 3_000 });
122
+ ok("get_info answers (control RPC reachable)", info !== undefined);
123
+
124
+ // QM1: model-lock did NOT revert the launch model — provider is still entwurf.
125
+ ok(
126
+ `get_info model is the ACP model, not reverted (got ${formatRuntimeModel(info) ?? "none"})`,
127
+ info.modelProvider === ACP_PROVIDER && info.modelId === ACP_MODEL,
128
+ );
129
+
130
+ // Citizen facts present: idle + cwd are get_info-enriched fields the peers surface shows.
131
+ ok("get_info reports idle=true (no turn running)", info.idle === true);
132
+ ok("get_info reports the resident cwd", info.cwd === tmp);
133
+
134
+ // Peers-visible: the gid shows up in the REAL socket-discovery scan (the socket
135
+ // axis that feeds entwurf_peers' socketOnly section) as a live citizen — not just
136
+ // reachable by a direct get_info, but discoverable on the production fact surface.
137
+ const scan = await scanSocketProbes([]);
138
+ const probe = scan.probes.find((p) => p.gardenId === gid);
139
+ ok("gid is peers-visible on the socket-discovery fact surface, liveness=alive", probe?.liveness === "alive");
140
+
141
+ // QM2 (part 2): the fail-loud streamSimple stub never fired — no turn ran, so no
142
+ // AcpBackendNotImplementedError on the resident's stderr.
143
+ ok(
144
+ "fail-loud backend stub did NOT fire at launch (turn-free)",
145
+ !/AcpBackendNotImplementedError|not implemented in S0/i.test(stderrTail),
146
+ );
147
+
148
+ // Still alive after the probe (the socket is a live citizen, not a one-shot).
149
+ ok(
150
+ "resident still alive after get_info (live socket-citizen)",
151
+ resident.exitCode === null && resident.signalCode === null,
152
+ );
153
+ } finally {
154
+ if (resident) await terminateChild(resident);
155
+ }
156
+
157
+ // Hygiene (B): after teardown the resident's control socket file is gone — the
158
+ // smoke leaves no process/socket residue. (The 0-turn session JSONL is the
159
+ // denote-id memory layer and is intentionally NOT scrubbed — that is data, not
160
+ // process residue.)
161
+ ok("control socket file removed after teardown (no socket residue)", await waitForGone(sockPath, 5_000));
162
+
163
+ console.log(
164
+ `[smoke-acp-socket-citizen-live] PASS — ${passed} checks (ACP-model session is a first-class socket-citizen, turn-free)`,
165
+ );
166
+ }
167
+
168
+ await main();
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-claude-native-resume-live — LIVE-only Claude Code native resume + meta-bridge neutrality probe.
3
+ #
4
+ # This smoke is deliberately NOT a entwurf ACP backend test. It exercises
5
+ # Claude Code's own native persistence/resume path while the meta-bridge hook is
6
+ # installed, then checks that entwurf only records the native identity:
7
+ # - a fresh `claude -p --output-format=json` turn returns a native session_id;
8
+ # - exactly one meta-record body binds backend=claude-code + nativeSessionId;
9
+ # - the recorded transcriptPath exists and stays attached to that native id;
10
+ # - `claude -p --resume <session_id>` can continue the native conversation;
11
+ # - the meta-record remains unique after resume (no duplicate garden citizen).
12
+ #
13
+ # It creates real Claude Code turns and writes normal Claude transcripts plus
14
+ # entwurf meta-records. It never edits Claude config, credentials, or
15
+ # transcripts, and it does not go through the ACP provider.
16
+ set -euo pipefail
17
+
18
+ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+
20
+ if [ "${LIVE:-0}" != "1" ]; then
21
+ echo "[smoke-claude-native-resume-live] skipped — set LIVE=1 to run (spawns two real Claude Code native turns)."
22
+ exit 0
23
+ fi
24
+
25
+ command -v claude >/dev/null || { echo "FAIL: claude not on PATH" >&2; exit 1; }
26
+ command -v node >/dev/null || { echo "FAIL: node not on PATH" >&2; exit 1; }
27
+ command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH" >&2; exit 1; }
28
+
29
+ pass=0
30
+ fail=0
31
+ ok() { echo " ok $*"; pass=$((pass + 1)); }
32
+ bad() { echo " FAIL $*"; fail=$((fail + 1)); }
33
+ section() { echo; echo "== $* =="; }
34
+
35
+ TMP="$(mktemp -d -t psa-cc-native-resume.XXXXXX)"
36
+ cleanup() {
37
+ if [ "${KEEP_SMOKE_TMP:-0}" != "1" ]; then rm -rf "$TMP"; else echo " keep tmp: $TMP"; fi
38
+ }
39
+ trap cleanup EXIT
40
+
41
+ SCRATCH="$TMP/project"
42
+ mkdir -p "$SCRATCH"
43
+
44
+ MODEL_ARGS=()
45
+ # Pin the smoke to Sonnet by default so an operator's Opus default does not make
46
+ # a tiny continuity probe fail the budget gate. Set CLAUDE_CODE_NATIVE_RESUME_MODEL=""
47
+ # to use Claude Code's configured default, or any explicit alias/model id.
48
+ if [ "${CLAUDE_CODE_NATIVE_RESUME_MODEL-sonnet}" != "" ]; then
49
+ MODEL_ARGS+=(--model "${CLAUDE_CODE_NATIVE_RESUME_MODEL-sonnet}")
50
+ fi
51
+
52
+ FALLBACK_ARGS=()
53
+ if [ -n "${CLAUDE_CODE_NATIVE_RESUME_FALLBACK_MODEL:-}" ]; then
54
+ FALLBACK_ARGS+=(--fallback-model "$CLAUDE_CODE_NATIVE_RESUME_FALLBACK_MODEL")
55
+ fi
56
+
57
+ BUDGET_ARGS=()
58
+ if [ -n "${CLAUDE_CODE_NATIVE_RESUME_MAX_BUDGET_USD:-0.20}" ]; then
59
+ BUDGET_ARGS+=(--max-budget-usd "${CLAUDE_CODE_NATIVE_RESUME_MAX_BUDGET_USD:-0.20}")
60
+ fi
61
+
62
+ EFFECTIVE_MODEL="${CLAUDE_CODE_NATIVE_RESUME_MODEL-sonnet}"
63
+ EFFECTIVE_FALLBACK="${CLAUDE_CODE_NATIVE_RESUME_FALLBACK_MODEL:-}"
64
+ EFFECTIVE_BUDGET="${CLAUDE_CODE_NATIVE_RESUME_MAX_BUDGET_USD:-0.20}"
65
+ echo "[smoke-claude-native-resume-live] model=${EFFECTIVE_MODEL:-<claude-default>} fallback=${EFFECTIVE_FALLBACK:-<none>} max_budget_usd=${EFFECTIVE_BUDGET:-<none>} scratch=$SCRATCH"
66
+
67
+ # Keep the prompts tiny and tool-free. Do not use "secret" wording; this is a
68
+ # continuity marker, not a safety-sensitive secret.
69
+ NONCE="native-resume-$(date +%Y%m%dT%H%M%S)-$RANDOM"
70
+ FIRST_PROMPT="Remember the marker word ${NONCE}. Reply exactly READY ${NONCE}."
71
+ SECOND_PROMPT="What marker word did I ask you to remember? Reply exactly WORD ${NONCE}."
72
+
73
+ classify_error() {
74
+ python3 - "$@" <<'PY'
75
+ import re, sys
76
+ text = "\n".join(open(p, errors='replace').read() for p in sys.argv[1:] if p)
77
+ lo = text.lower()
78
+ if 'api error: 400 messages' in lo or '400 messages:' in lo:
79
+ if 'cache_control cannot be set for empty text blocks' in lo or 'text content blocks must be non-empty' in lo:
80
+ print('classification=transcript-poison-empty-text-400')
81
+ elif 'thinking' in lo and 'cannot be modified' in lo:
82
+ print('classification=transcript-poison-thinking-block-400')
83
+ else:
84
+ print('classification=anthropic-messages-400')
85
+ elif 'prompt is too long' in lo or 'context window' in lo or 'reduce the length' in lo:
86
+ print('classification=context-overflow')
87
+ elif 'out of extra usage' in lo or 'usage' in lo and 'claude.ai/settings/usage' in lo:
88
+ print('classification=usage-or-billing')
89
+ elif 'overloaded' in lo or 'rate_limit' in lo or 'rate limit' in lo or '529' in lo:
90
+ print('classification=service-overload-or-rate-limit')
91
+ elif 'error_max_budget_usd' in lo or 'reached maximum budget' in lo:
92
+ print('classification=max-budget-too-low')
93
+ elif text.strip():
94
+ print('classification=unknown-error')
95
+ else:
96
+ print('classification=no-output')
97
+ for line in text.splitlines()[-12:]:
98
+ if line.strip(): print('output-tail: ' + line[:500])
99
+ PY
100
+ }
101
+
102
+ run_claude_json() {
103
+ local out="$1" err="$2"; shift 2
104
+ (cd "$SCRATCH" && claude -p --output-format=json --tools "" "${MODEL_ARGS[@]}" "${FALLBACK_ARGS[@]}" "${BUDGET_ARGS[@]}" "$@") >"$out" 2>"$err"
105
+ }
106
+
107
+ json_field() {
108
+ local file="$1" expr="$2"
109
+ node -e 'const fs=require("fs"); const o=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const keys=process.argv[2].split("."); let v=o; for (const k of keys) v=v?.[k]; if (v !== undefined && v !== null) process.stdout.write(String(v));' "$file" "$expr"
110
+ }
111
+
112
+ model_usage_keys() {
113
+ node -e 'const fs=require("fs"); const o=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(Object.keys(o.modelUsage || {}).join(",") || "<none>");' "$1"
114
+ }
115
+
116
+ find_meta_record_json() {
117
+ local native_id="$1"
118
+ node - "$native_id" <<'JS'
119
+ const fs = require('fs');
120
+ const path = require('path');
121
+ const nativeId = process.argv[2];
122
+ const dir = process.env.ENTWURF_META_SESSIONS_DIR || path.join(process.env.HOME, '.pi/agent/meta-sessions');
123
+ const matches = [];
124
+ if (fs.existsSync(dir)) {
125
+ for (const name of fs.readdirSync(dir)) {
126
+ if (!name.endsWith('.meta.json')) continue;
127
+ const file = path.join(dir, name);
128
+ try {
129
+ const r = JSON.parse(fs.readFileSync(file, 'utf8'));
130
+ if (r && r.backend === 'claude-code' && r.nativeSessionId === nativeId) matches.push({ file, record: r });
131
+ } catch {}
132
+ }
133
+ }
134
+ console.log(JSON.stringify({ dir, count: matches.length, matches }));
135
+ JS
136
+ }
137
+
138
+ FIRST_OUT="$TMP/first.json"
139
+ FIRST_ERR="$TMP/first.err"
140
+ SECOND_OUT="$TMP/second.json"
141
+ SECOND_ERR="$TMP/second.err"
142
+
143
+ section "A. first native Claude Code turn"
144
+ if run_claude_json "$FIRST_OUT" "$FIRST_ERR" "$FIRST_PROMPT"; then
145
+ ok "first native claude -p completed"
146
+ else
147
+ bad "first native claude -p failed"
148
+ classify_error "$FIRST_ERR" "$FIRST_OUT"
149
+ exit 1
150
+ fi
151
+
152
+ SESSION_ID="$(json_field "$FIRST_OUT" session_id)"
153
+ if [ -z "$SESSION_ID" ]; then SESSION_ID="$(json_field "$FIRST_OUT" sessionId)"; fi
154
+ RESULT1="$(json_field "$FIRST_OUT" result || true)"
155
+ [ -n "$SESSION_ID" ] && ok "first turn returned session_id=$SESSION_ID" || bad "first turn JSON did not include session_id"
156
+ if printf '%s' "$RESULT1" | grep -Fq "$NONCE"; then ok "first response echoed marker"; else bad "first response did not echo marker"; fi
157
+ ok "first turn modelUsage=$(model_usage_keys "$FIRST_OUT")"
158
+
159
+ section "B. meta-record after first turn (record-only, no transcript mutation)"
160
+ META1="$TMP/meta1.json"
161
+ find_meta_record_json "$SESSION_ID" > "$META1"
162
+ COUNT1="$(json_field "$META1" count)"
163
+ if [ "$COUNT1" = "1" ]; then ok "exactly one meta-record binds nativeSessionId after first turn"; else bad "expected 1 meta-record after first turn, got $COUNT1"; fi
164
+ META_FILE="$(node -e 'const o=require(process.argv[1]); process.stdout.write(o.matches?.[0]?.file || "")' "$META1")"
165
+ TRANSCRIPT_PATH="$(node -e 'const o=require(process.argv[1]); process.stdout.write(o.matches?.[0]?.record?.transcriptPath || "")' "$META1")"
166
+ GARDEN_ID="$(node -e 'const o=require(process.argv[1]); process.stdout.write(o.matches?.[0]?.record?.gardenId || "")' "$META1")"
167
+ [ -n "$GARDEN_ID" ] && ok "meta-record gardenId=$GARDEN_ID" || bad "meta-record missing gardenId"
168
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then ok "meta-record transcriptPath exists"; else bad "meta-record transcriptPath missing or absent: $TRANSCRIPT_PATH"; fi
169
+
170
+ section "C. native Claude Code resume"
171
+ if run_claude_json "$SECOND_OUT" "$SECOND_ERR" --resume "$SESSION_ID" "$SECOND_PROMPT"; then
172
+ ok "claude --resume $SESSION_ID completed"
173
+ else
174
+ bad "claude --resume $SESSION_ID failed"
175
+ classify_error "$SECOND_ERR" "$SECOND_OUT"
176
+ echo " first-json: $FIRST_OUT"
177
+ echo " meta-json: $META1"
178
+ exit 1
179
+ fi
180
+ RESULT2="$(json_field "$SECOND_OUT" result || true)"
181
+ if printf '%s' "$RESULT2" | grep -Fq "$NONCE"; then ok "resume response recalled marker"; else bad "resume response did not recall marker"; fi
182
+ ok "resume turn modelUsage=$(model_usage_keys "$SECOND_OUT")"
183
+
184
+ section "D. meta-record after resume"
185
+ META2="$TMP/meta2.json"
186
+ find_meta_record_json "$SESSION_ID" > "$META2"
187
+ COUNT2="$(json_field "$META2" count)"
188
+ if [ "$COUNT2" = "1" ]; then ok "resume did not create duplicate meta-records for nativeSessionId"; else bad "expected 1 meta-record after resume, got $COUNT2"; fi
189
+ TRANSCRIPT_PATH2="$(node -e 'const o=require(process.argv[1]); process.stdout.write(o.matches?.[0]?.record?.transcriptPath || "")' "$META2")"
190
+ if [ "$TRANSCRIPT_PATH2" = "$TRANSCRIPT_PATH" ]; then ok "resume kept the same transcriptPath binding"; else bad "transcriptPath drifted: first=$TRANSCRIPT_PATH second=$TRANSCRIPT_PATH2"; fi
191
+ if [ -n "$META_FILE" ] && [ -f "$META_FILE" ]; then ok "meta-record file remains on disk: $META_FILE"; else bad "meta-record file missing after resume"; fi
192
+
193
+ section "SUMMARY"
194
+ echo " pass=$pass fail=$fail session_id=$SESSION_ID garden_id=$GARDEN_ID"
195
+ echo " scratch=$SCRATCH"
196
+ echo " transcript=$TRANSCRIPT_PATH"
197
+ if [ "$fail" -gt 0 ]; then exit 1; fi
198
+ echo " OK: Claude Code native resume works while meta-bridge only records identity."