@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,33 @@
|
|
|
1
|
+
// Tiny stdio MCP server used ONLY by smoke-acp-mcp-live (S2g LIVE 1).
|
|
2
|
+
//
|
|
3
|
+
// It exposes ONE tool — `probe_nonce` — that returns a per-run secret nonce
|
|
4
|
+
// supplied via the PROBE_NONCE env var. The smoke registers this server in a
|
|
5
|
+
// scratch `.pi/settings.json` under `entwurfProvider.mcpServers` and asks the
|
|
6
|
+
// ACP model to CALL the tool and echo the nonce. If the operator mcpServers
|
|
7
|
+
// passthrough (S2g) works, the ACP child spawns this server, the tool is visible
|
|
8
|
+
// in the session schema, and the model's reply carries the nonce. If passthrough
|
|
9
|
+
// is broken (the pre-S2g hardcoded `mcpServers:[]`), the tool never exists and
|
|
10
|
+
// the nonce cannot appear.
|
|
11
|
+
//
|
|
12
|
+
// Lives under the repo's scripts/fixtures/ so its `@modelcontextprotocol/sdk`
|
|
13
|
+
// import resolves against the repo node_modules even though the server is spawned
|
|
14
|
+
// (by the claude ACP child) with an arbitrary scratch cwd. Deliberately minimal:
|
|
15
|
+
// no identity / env coupling beyond PROBE_NONCE, so a failure isolates to "did
|
|
16
|
+
// the operator mcpServers reach newSession" — not to entwurf-bridge wiring.
|
|
17
|
+
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
|
|
21
|
+
const server = new McpServer({ name: "probe", version: "0.0.1" });
|
|
22
|
+
|
|
23
|
+
server.tool(
|
|
24
|
+
"probe_nonce",
|
|
25
|
+
"Return this session's secret probe nonce. Call this when asked for the probe nonce.",
|
|
26
|
+
{},
|
|
27
|
+
async () => ({
|
|
28
|
+
content: [{ type: "text", text: `PROBE_NONCE=${process.env.PROBE_NONCE ?? "MISSING"}` }],
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const transport = new StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
/**
|
|
3
|
+
* gnew-rpc-drive — sequential RPC driver for the /gnew E2E smoke.
|
|
4
|
+
*
|
|
5
|
+
* pi's native RPC `prompt` is async (it `void session.prompt(...)` and returns
|
|
6
|
+
* immediately), so a blind printf pipe can race a follow-up get_state ahead of
|
|
7
|
+
* the switch. This driver enforces ordering: it waits for each response before
|
|
8
|
+
* sending the next command.
|
|
9
|
+
*
|
|
10
|
+
* Base flow (0 tokens — `/gnew` is a slash command intercepted by session.prompt
|
|
11
|
+
* BEFORE any model turn, so NO agent_start should appear):
|
|
12
|
+
* g1: get_state → capture BEFORE sessionId
|
|
13
|
+
* p1: prompt "/gnew" → wait for the prompt response (the in-process switch
|
|
14
|
+
* completes inside the command handler, before this
|
|
15
|
+
* response fires)
|
|
16
|
+
* g2: get_state → capture AFTER sessionId / sessionFile / msgCount
|
|
17
|
+
*
|
|
18
|
+
* Optional self turn (T3 backend identity, ~1 turn — pass a 5th arg `selfPrompt`):
|
|
19
|
+
* p2: prompt <selfPrompt> → drive one model turn that calls entwurf_self; scan
|
|
20
|
+
* the stream for the identity envelope and capture
|
|
21
|
+
* every sessionId it reports (proves PI_SESSION_ID
|
|
22
|
+
* reached the backend MCP child AFTER the switch).
|
|
23
|
+
*
|
|
24
|
+
* Emits a single JSON object on stdout for the bash smoke to assert against.
|
|
25
|
+
*
|
|
26
|
+
* Usage: node --experimental-strip-types gnew-rpc-drive.ts <sessionId> <provider> <model> [timeoutMs] [selfPrompt]
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import os from "node:os";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import readline from "node:readline";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
const ENTWURF_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
36
|
+
const listSockets = (): string[] => {
|
|
37
|
+
try {
|
|
38
|
+
return fs.readdirSync(ENTWURF_DIR).filter((f) => f.endsWith(".sock"));
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const [sessionId, provider, model, timeoutMsArg, selfPrompt] = process.argv.slice(2);
|
|
45
|
+
if (!sessionId || !provider || !model) {
|
|
46
|
+
process.stderr.write("usage: gnew-rpc-drive.ts <sessionId> <provider> <model> [timeoutMs] [selfPrompt]\n");
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
const timeoutMs = Number(timeoutMsArg) || 90_000;
|
|
50
|
+
const wantSelf = typeof selfPrompt === "string" && selfPrompt.length > 0;
|
|
51
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
52
|
+
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
53
|
+
|
|
54
|
+
interface DriveResult {
|
|
55
|
+
before: string | null;
|
|
56
|
+
after: string | null;
|
|
57
|
+
afterFile: string | null;
|
|
58
|
+
afterMsgCount: number | null;
|
|
59
|
+
promptOk: boolean;
|
|
60
|
+
agentStartSeen: boolean;
|
|
61
|
+
extensionErrors: Array<{ path: unknown; event: unknown; error: unknown }>;
|
|
62
|
+
socketsAfterSwitch: string[];
|
|
63
|
+
selfEnvelopeSessionIds: string[];
|
|
64
|
+
selfTurnEnded: boolean;
|
|
65
|
+
timedOut: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result: DriveResult = {
|
|
69
|
+
before: null,
|
|
70
|
+
after: null,
|
|
71
|
+
afterFile: null,
|
|
72
|
+
afterMsgCount: null,
|
|
73
|
+
promptOk: false,
|
|
74
|
+
agentStartSeen: false,
|
|
75
|
+
extensionErrors: [],
|
|
76
|
+
// Control sockets present right after the switch, while pi is still alive (the
|
|
77
|
+
// socket vanishes on shutdown, so it can only be observed mid-run). Proves
|
|
78
|
+
// startControlServer rebound to the new garden id and dropped the old one.
|
|
79
|
+
socketsAfterSwitch: [],
|
|
80
|
+
selfEnvelopeSessionIds: [],
|
|
81
|
+
selfTurnEnded: false,
|
|
82
|
+
timedOut: false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const child = spawn(
|
|
86
|
+
"pi",
|
|
87
|
+
[
|
|
88
|
+
...REPO_EXTENSION_ARGS,
|
|
89
|
+
"--session-id",
|
|
90
|
+
sessionId,
|
|
91
|
+
"--entwurf-control",
|
|
92
|
+
"--provider",
|
|
93
|
+
provider,
|
|
94
|
+
"--model",
|
|
95
|
+
model,
|
|
96
|
+
"--mode",
|
|
97
|
+
"rpc",
|
|
98
|
+
],
|
|
99
|
+
{ stdio: ["pipe", "pipe", "inherit"] },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const send = (obj: Record<string, unknown>): void => {
|
|
103
|
+
child.stdin?.write(`${JSON.stringify(obj)}\n`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// phases: 0 await g1 · 1 await prompt/gnew · 2 await g2 · 3 self turn (optional) · 4 done
|
|
107
|
+
let phase = 0;
|
|
108
|
+
// finish() can be reached from three racing paths — the timeout timer, child
|
|
109
|
+
// `exit`, and the normal phase-2/phase-3 completion. Guard so the single result
|
|
110
|
+
// JSON is emitted exactly once; a double emit would tear the bash JSON parse.
|
|
111
|
+
let finished = false;
|
|
112
|
+
const finish = (): void => {
|
|
113
|
+
if (finished) return;
|
|
114
|
+
finished = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
try {
|
|
117
|
+
child.stdin?.end();
|
|
118
|
+
} catch {
|
|
119
|
+
/* best-effort */
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
try {
|
|
124
|
+
child.kill("SIGTERM");
|
|
125
|
+
} catch {
|
|
126
|
+
/* best-effort */
|
|
127
|
+
}
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}, 1500);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
result.timedOut = true;
|
|
134
|
+
finish();
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
|
|
137
|
+
const rl = readline.createInterface({ input: child.stdout! });
|
|
138
|
+
rl.on("line", (line: string) => {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed) return;
|
|
141
|
+
let evt: Record<string, unknown>;
|
|
142
|
+
try {
|
|
143
|
+
evt = JSON.parse(trimmed) as Record<string, unknown>;
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (evt.type === "agent_start") result.agentStartSeen = true;
|
|
149
|
+
if (evt.type === "extension_error") {
|
|
150
|
+
result.extensionErrors.push({ path: evt.extensionPath, event: evt.event, error: evt.error });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// entwurf_self identity envelope: streamed as a tool result embedded as a STRING
|
|
154
|
+
// inside the message event, so its JSON quotes are escaped (\"sessionId\":...) —
|
|
155
|
+
// match the garden-id TOKEN instead (alphanumerics, never escaped). The envelope
|
|
156
|
+
// is marked by `agentId` (get_state never emits it); the only garden-shaped token
|
|
157
|
+
// in it is the sessionId (= the backend MCP child's PI_SESSION_ID after the switch).
|
|
158
|
+
if (phase === 3 && trimmed.includes("agentId")) {
|
|
159
|
+
const m = trimmed.match(/\d{8}T\d{6}-[0-9a-f]{6}/g);
|
|
160
|
+
if (m) {
|
|
161
|
+
for (const id of m) {
|
|
162
|
+
if (!result.selfEnvelopeSessionIds.includes(id)) result.selfEnvelopeSessionIds.push(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (phase === 3 && evt.type === "agent_end") {
|
|
167
|
+
result.selfTurnEnded = true;
|
|
168
|
+
finish();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (evt.type !== "response") return;
|
|
173
|
+
const data = (evt.data ?? null) as { sessionId?: string; sessionFile?: string; messageCount?: number } | null;
|
|
174
|
+
|
|
175
|
+
if (phase === 0 && evt.command === "get_state") {
|
|
176
|
+
result.before = data?.sessionId ?? null;
|
|
177
|
+
phase = 1;
|
|
178
|
+
send({ type: "prompt", message: "/gnew", id: "p1" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (phase === 1 && evt.command === "prompt") {
|
|
182
|
+
result.promptOk = evt.success === true;
|
|
183
|
+
phase = 2;
|
|
184
|
+
send({ type: "get_state", id: "g2" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (phase === 2 && evt.command === "get_state") {
|
|
188
|
+
result.after = data?.sessionId ?? null;
|
|
189
|
+
result.afterFile = data?.sessionFile ?? null;
|
|
190
|
+
result.afterMsgCount = data?.messageCount ?? null;
|
|
191
|
+
result.socketsAfterSwitch = listSockets(); // pi still alive here — snapshot now
|
|
192
|
+
if (wantSelf) {
|
|
193
|
+
phase = 3;
|
|
194
|
+
send({ type: "prompt", message: selfPrompt, id: "p2" });
|
|
195
|
+
} else {
|
|
196
|
+
phase = 4;
|
|
197
|
+
finish();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
child.on("error", (err: Error) => {
|
|
203
|
+
process.stderr.write(`gnew-rpc-drive: spawn failed: ${err.message}\n`);
|
|
204
|
+
process.exit(2);
|
|
205
|
+
});
|
|
206
|
+
child.on("exit", () => {
|
|
207
|
+
if (phase < 4) finish();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// kick off
|
|
211
|
+
send({ type: "get_state", id: "g1" });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Shared bounded child-process teardown for the LIVE smokes (ACP + entwurf-v2).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (test-infra, not a Phase-1/Phase-2 feature):
|
|
4
|
+
// Every `smoke-acp-*-live.ts` carried its own copy of `terminateChild`, and
|
|
5
|
+
// each copy ended its SIGKILL path with an UNBOUNDED `await exited`. When the
|
|
6
|
+
// spawned child (or a grandchild ACP backend that still holds the stdio pipes)
|
|
7
|
+
// never emits an "exit" event, that await hangs FOREVER — *after* the smoke has
|
|
8
|
+
// already printed its PASS line. Individually the smokes pass; it is only this
|
|
9
|
+
// post-PASS cleanup that wedges, which is exactly what froze the aggregate
|
|
10
|
+
// `release-gate` runner (observed 2026-06-24: raw-turn PASS, then no further
|
|
11
|
+
// output for >25min while the runner sat in cleanup).
|
|
12
|
+
//
|
|
13
|
+
// Crash-don't-warn (AGENTS.md): every wait here is BOUNDED, and if the child is
|
|
14
|
+
// genuinely still alive after SIGKILL + a bounded grace we THROW. A leaked live
|
|
15
|
+
// backend is broken tool state, not a warning. The only swallowed errors are
|
|
16
|
+
// liveness PROBES (kill() / signal-0 / stdio destroy) — the one sanctioned use
|
|
17
|
+
// of `catch {}` (environment probing), never control flow we lie about.
|
|
18
|
+
|
|
19
|
+
import type { ChildProcess } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
22
|
+
|
|
23
|
+
function destroyChildStdio(child: ChildProcess): void {
|
|
24
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) {
|
|
25
|
+
try {
|
|
26
|
+
stream?.destroy();
|
|
27
|
+
} catch {
|
|
28
|
+
// best-effort teardown; never load-bearing
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TerminateOptions {
|
|
34
|
+
/** Grace after SIGTERM before escalating to SIGKILL. */
|
|
35
|
+
graceMs?: number;
|
|
36
|
+
/** Bounded wait after SIGKILL before the signal-0 liveness probe. */
|
|
37
|
+
killWaitMs?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Terminate a spawned smoke child with bounded waits and a loud failure.
|
|
42
|
+
*
|
|
43
|
+
* SIGTERM → grace → SIGKILL → bounded wait → signal-0 liveness probe.
|
|
44
|
+
* Returns when the child is gone (or was never alive); throws only if the
|
|
45
|
+
* child is provably STILL alive after SIGKILL + the bounded wait.
|
|
46
|
+
*/
|
|
47
|
+
export async function terminateChild(
|
|
48
|
+
child: ChildProcess,
|
|
49
|
+
{ graceMs = 2_000, killWaitMs = 2_000 }: TerminateOptions = {},
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
// Already reaped, but still close our side of the stdio pipes. With the SDK
|
|
52
|
+
// fluent connection a smoke can otherwise print PASS and keep Node alive on
|
|
53
|
+
// a retained pipe/read handle.
|
|
54
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
55
|
+
destroyChildStdio(child);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const exited = new Promise<void>((resolve) => child.once("exit", () => resolve()));
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
child.kill("SIGTERM");
|
|
63
|
+
} catch {
|
|
64
|
+
// kill() throws ESRCH when the process is already gone — treat as exited.
|
|
65
|
+
destroyChildStdio(child);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const afterTerm = await Promise.race([
|
|
70
|
+
exited.then(() => "exited" as const),
|
|
71
|
+
sleep(graceMs).then(() => "timeout" as const),
|
|
72
|
+
]);
|
|
73
|
+
if (afterTerm === "exited") {
|
|
74
|
+
destroyChildStdio(child);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Grace elapsed — escalate. (The old bug: the wait AFTER this kill was
|
|
79
|
+
// unbounded `await exited`.)
|
|
80
|
+
try {
|
|
81
|
+
child.kill("SIGKILL");
|
|
82
|
+
} catch {
|
|
83
|
+
destroyChildStdio(child);
|
|
84
|
+
return; // raced to death between the grace race and this kill
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Release any stdio the helper may be pinning, so a missed "exit" event does
|
|
88
|
+
// not keep the event loop (and the pipes) alive. Probe-only — best effort.
|
|
89
|
+
destroyChildStdio(child);
|
|
90
|
+
|
|
91
|
+
const afterKill = await Promise.race([
|
|
92
|
+
exited.then(() => "exited" as const),
|
|
93
|
+
sleep(killWaitMs).then(() => "timeout" as const),
|
|
94
|
+
]);
|
|
95
|
+
if (afterKill === "exited") {
|
|
96
|
+
destroyChildStdio(child);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The "exit" event may simply have been missed even though the process is
|
|
101
|
+
// dead. Probe with signal 0: a throw (ESRCH/EPERM on a reaped pid) means the
|
|
102
|
+
// process is gone, so proceed; a clean return means it is genuinely alive.
|
|
103
|
+
const pid = child.pid;
|
|
104
|
+
if (pid === undefined) {
|
|
105
|
+
destroyChildStdio(child);
|
|
106
|
+
return; // never acquired a pid; nothing can leak
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 0);
|
|
110
|
+
} catch {
|
|
111
|
+
destroyChildStdio(child);
|
|
112
|
+
return; // gone — the exit event was just missed
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(`terminateChild: child pid=${pid} still alive after SIGKILL + ${killWaitMs}ms bounded wait`);
|
|
116
|
+
}
|