@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.
- package/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- 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."
|