@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,261 @@
|
|
|
1
|
+
// S2a-2 — raw ACP backend pipe, one live turn. LIVE-gated, OUT of `pnpm check`.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-acp-raw-turn-live
|
|
4
|
+
//
|
|
5
|
+
// What this proves (and ONLY this): the pinned Claude ACP adapter
|
|
6
|
+
// (@agentclientprotocol/claude-agent-acp@0.39.0) spawns, speaks the ACP wire
|
|
7
|
+
// protocol over stdio NDJSON, and returns one real model turn. It is the
|
|
8
|
+
// bytes-flow proof that the S2a dep surface is not just installable but
|
|
9
|
+
// actually drivable — before any provider/overlay/streamSimple code (S2b+).
|
|
10
|
+
//
|
|
11
|
+
// Deliberately NOT here (S2b+ / forbidden ahead of the raw pipe):
|
|
12
|
+
// - no pi provider path, no streamSimple (backend-stub stays fail-loud);
|
|
13
|
+
// - no config overlay, no tool-narrowing, no identity carrier / engraving;
|
|
14
|
+
// - no _meta.systemPrompt (kept absent so the billing carrier never grows);
|
|
15
|
+
// - no mailbox-absence / overlay-active assertions (those need S2b).
|
|
16
|
+
//
|
|
17
|
+
// Boundary notes (GPT-reviewed):
|
|
18
|
+
// - launch source MUST be the resolved package bin. A silent PATH fallback
|
|
19
|
+
// would hide a dep-pin / pack miss, so it FAILS acceptance unless the
|
|
20
|
+
// operator sets ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1 (then the run
|
|
21
|
+
// is stamped "debug/non-acceptance").
|
|
22
|
+
// - cwd is a fresh mkdtemp scratch — this isolates the repo, NOT ~/.claude
|
|
23
|
+
// side effects. Without an overlay the adapter reads the operator's own
|
|
24
|
+
// local Claude auth/config; entwurf neither copies nor proxies it.
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from "node:assert";
|
|
27
|
+
import { type ChildProcessByStdio, spawn } from "node:child_process";
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { Readable, Writable } from "node:stream";
|
|
34
|
+
import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
|
35
|
+
import { connectAcpClient } from "../pi-extensions/lib/acp/acp-client.ts";
|
|
36
|
+
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
37
|
+
|
|
38
|
+
const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_RAW_TURN_MODEL ?? "claude-sonnet-4-6";
|
|
39
|
+
const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK === "1";
|
|
40
|
+
const RAW_TAIL_CAP = 64 * 1024; // cap captured raw NDJSON to 64KB tail on report.
|
|
41
|
+
|
|
42
|
+
function fail(msg: string): never {
|
|
43
|
+
console.error(`[smoke-acp-raw-turn-live] FAIL: ${msg}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
48
|
+
|
|
49
|
+
// Guard each RPC so a hung backend fails this smoke instead of the outer
|
|
50
|
+
// process timeout — keeps the failure attributable to the right step.
|
|
51
|
+
function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
|
|
52
|
+
return Promise.race([
|
|
53
|
+
p,
|
|
54
|
+
sleep(ms).then((): never => {
|
|
55
|
+
throw new Error(`${label} timed out after ${ms}ms`);
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (process.env.LIVE !== "1") {
|
|
61
|
+
console.error("[smoke-acp-raw-turn-live] skipped — set LIVE=1 to run the real ACP turn.");
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Launch resolution — package bin only (acceptance), PATH fallback is debug.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function resolveLaunch(): { command: string; args: string[]; source: string; acceptance: boolean } {
|
|
69
|
+
const require = createRequire(import.meta.url);
|
|
70
|
+
try {
|
|
71
|
+
const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
72
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { bin?: string | Record<string, string> };
|
|
73
|
+
const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
|
|
74
|
+
if (binPath) {
|
|
75
|
+
return {
|
|
76
|
+
command: process.execPath,
|
|
77
|
+
args: [join(dirname(pkgJsonPath), binPath)],
|
|
78
|
+
source: "package:@agentclientprotocol/claude-agent-acp",
|
|
79
|
+
acceptance: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
fail("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (!ALLOW_PATH_FALLBACK) {
|
|
85
|
+
fail(
|
|
86
|
+
`could not resolve @agentclientprotocol/claude-agent-acp package bin (${(err as Error).message}). ` +
|
|
87
|
+
"This is an acceptance failure — the dep pin / install is broken. Set " +
|
|
88
|
+
"ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1 only for debug.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
command: "claude-agent-acp",
|
|
94
|
+
args: [],
|
|
95
|
+
source: "PATH:claude-agent-acp (debug/non-acceptance)",
|
|
96
|
+
acceptance: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Drive one turn.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
async function main(): Promise<void> {
|
|
104
|
+
const launch = resolveLaunch();
|
|
105
|
+
const scratch = await mkdtemp(join(tmpdir(), "entwurf-s2a-raw-"));
|
|
106
|
+
console.error(`[smoke-acp-raw-turn-live] launch source: ${launch.source}`);
|
|
107
|
+
console.error(`[smoke-acp-raw-turn-live] scratch cwd: ${scratch}`);
|
|
108
|
+
console.error(`[smoke-acp-raw-turn-live] model request: ${REQUESTED_MODEL_ID}`);
|
|
109
|
+
|
|
110
|
+
const child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(launch.command, launch.args, {
|
|
111
|
+
cwd: scratch,
|
|
112
|
+
env: { ...process.env },
|
|
113
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
114
|
+
}) as ChildProcessByStdio<Writable, Readable, Readable>;
|
|
115
|
+
|
|
116
|
+
const stderrTail: string[] = [];
|
|
117
|
+
child.stderr.on("data", (c) => {
|
|
118
|
+
stderrTail.push(c.toString());
|
|
119
|
+
if (stderrTail.length > 200) stderrTail.shift();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Tee stdout so we capture the raw NDJSON bytes the SDK consumes.
|
|
123
|
+
let rawBytes = "";
|
|
124
|
+
child.stdout.on("data", (c) => {
|
|
125
|
+
rawBytes += c.toString();
|
|
126
|
+
if (rawBytes.length > RAW_TAIL_CAP * 2) rawBytes = rawBytes.slice(-RAW_TAIL_CAP);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const stdoutWeb = Readable.toWeb(child.stdout) as unknown as ReadableStream<Uint8Array>;
|
|
130
|
+
const stdinWeb = Writable.toWeb(child.stdin) as unknown as WritableStream<Uint8Array>;
|
|
131
|
+
const stream = ndJsonStream(stdinWeb, stdoutWeb);
|
|
132
|
+
|
|
133
|
+
let collectedText = "";
|
|
134
|
+
let unexpectedPermission = 0;
|
|
135
|
+
let unexpectedFileOp = 0;
|
|
136
|
+
|
|
137
|
+
const connection = connectAcpClient(stream as any, {
|
|
138
|
+
// agent text chunks land here as sessionUpdate notifications.
|
|
139
|
+
sessionUpdate: async (notification: any) => {
|
|
140
|
+
const u = notification?.update;
|
|
141
|
+
if (u?.sessionUpdate === "agent_message_chunk") {
|
|
142
|
+
const t = u?.content?.text;
|
|
143
|
+
if (typeof t === "string") collectedText += t;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
// A raw "say OK" turn should need no tools / files. Anything else is
|
|
147
|
+
// unexpected — refuse and count it, then fail at the end.
|
|
148
|
+
requestPermission: async (): Promise<any> => {
|
|
149
|
+
unexpectedPermission++;
|
|
150
|
+
return { outcome: { outcome: "cancelled" } };
|
|
151
|
+
},
|
|
152
|
+
readTextFile: async (): Promise<any> => {
|
|
153
|
+
unexpectedFileOp++;
|
|
154
|
+
throw new Error("unexpected readTextFile in raw OK turn");
|
|
155
|
+
},
|
|
156
|
+
writeTextFile: async (): Promise<any> => {
|
|
157
|
+
unexpectedFileOp++;
|
|
158
|
+
throw new Error("unexpected writeTextFile in raw OK turn");
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let failure: Error | null = null;
|
|
163
|
+
try {
|
|
164
|
+
// Acceptance precondition: the launch must be the resolved package bin.
|
|
165
|
+
if (!launch.acceptance) {
|
|
166
|
+
throw new Error("launch was a PATH fallback (debug) — not an acceptance PASS");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 1) initialize
|
|
170
|
+
const init = await withTimeout(
|
|
171
|
+
"initialize",
|
|
172
|
+
connection.initialize({
|
|
173
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
174
|
+
clientCapabilities: {},
|
|
175
|
+
clientInfo: { name: "entwurf-smoke", version: "s2a-raw" },
|
|
176
|
+
} as any),
|
|
177
|
+
30_000,
|
|
178
|
+
);
|
|
179
|
+
assert.ok(init, "initialize returned no result");
|
|
180
|
+
console.error(`[smoke-acp-raw-turn-live] initialize ok (protocolVersion=${(init as any)?.protocolVersion})`);
|
|
181
|
+
|
|
182
|
+
// 2) newSession — minimal: scratch cwd, no MCP, NO _meta (carrier absent).
|
|
183
|
+
const created = (await withTimeout(
|
|
184
|
+
"newSession",
|
|
185
|
+
connection.newSession({ cwd: scratch, mcpServers: [] } as any),
|
|
186
|
+
30_000,
|
|
187
|
+
)) as any;
|
|
188
|
+
const sessionId = created?.sessionId;
|
|
189
|
+
assert.ok(sessionId, "newSession returned no sessionId");
|
|
190
|
+
const available = Array.isArray(created?.models)
|
|
191
|
+
? created.models
|
|
192
|
+
: Array.isArray(created?.availableModels)
|
|
193
|
+
? created.availableModels
|
|
194
|
+
: undefined;
|
|
195
|
+
console.error(
|
|
196
|
+
`[smoke-acp-raw-turn-live] newSession ok (sessionId=${String(sessionId).slice(0, 12)}…` +
|
|
197
|
+
(available ? `, models=${available.map((m: any) => m?.modelId ?? m?.id ?? m).join(",")}` : "") +
|
|
198
|
+
")",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// 3) force the requested model — REQUIRED. If the adapter cannot honor
|
|
202
|
+
// the switch, the "sonnet" claim would be a lie; fail loudly instead
|
|
203
|
+
// of silently running the session default.
|
|
204
|
+
const setConfig = (connection as any).setSessionConfigOption;
|
|
205
|
+
if (typeof setConfig !== "function") {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`setSessionConfigOption unsupported — cannot enforce ${REQUESTED_MODEL_ID}; S2a requires honest model enforcement`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
await withTimeout(
|
|
211
|
+
"setSessionConfigOption",
|
|
212
|
+
setConfig.call(connection, { sessionId, configId: "model", value: REQUESTED_MODEL_ID }),
|
|
213
|
+
30_000,
|
|
214
|
+
);
|
|
215
|
+
console.error(`[smoke-acp-raw-turn-live] model set -> ${REQUESTED_MODEL_ID}`);
|
|
216
|
+
|
|
217
|
+
// 4) prompt — one tiny turn, no carrier.
|
|
218
|
+
const promptResult = (await withTimeout(
|
|
219
|
+
"prompt",
|
|
220
|
+
connection.prompt({
|
|
221
|
+
sessionId,
|
|
222
|
+
prompt: [{ type: "text", text: "Reply with exactly OK and nothing else." }],
|
|
223
|
+
} as any),
|
|
224
|
+
120_000,
|
|
225
|
+
)) as any;
|
|
226
|
+
console.error(`[smoke-acp-raw-turn-live] prompt returned (stopReason=${promptResult?.stopReason})`);
|
|
227
|
+
|
|
228
|
+
// ---- assertions ----
|
|
229
|
+
assert.ok(promptResult, "prompt returned no result");
|
|
230
|
+
assert.equal(unexpectedPermission, 0, `unexpected permission request(s): ${unexpectedPermission}`);
|
|
231
|
+
assert.equal(unexpectedFileOp, 0, `unexpected file op(s): ${unexpectedFileOp}`);
|
|
232
|
+
assert.ok(rawBytes.trim().length > 0, "no raw NDJSON bytes captured from backend stdout");
|
|
233
|
+
const normalized = collectedText.replace(/\s+/g, " ").trim().toUpperCase();
|
|
234
|
+
assert.ok(
|
|
235
|
+
normalized.includes("OK"),
|
|
236
|
+
`agent text did not contain "OK" (got: ${JSON.stringify(collectedText.slice(0, 200))})`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
console.log("[smoke-acp-raw-turn-live] PASS — raw ACP turn drove a live model reply");
|
|
240
|
+
console.log(` launch: ${launch.source}`);
|
|
241
|
+
console.log(` model: ${REQUESTED_MODEL_ID}`);
|
|
242
|
+
console.log(` reply: ${JSON.stringify(collectedText.trim().slice(0, 120))}`);
|
|
243
|
+
console.log(` rawBytes: ${rawBytes.length} captured (NDJSON)`);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
failure = err instanceof Error ? err : new Error(String(err));
|
|
246
|
+
console.error(`[smoke-acp-raw-turn-live] stderr tail:\n${stderrTail.slice(-20).join("")}`);
|
|
247
|
+
console.error(`[smoke-acp-raw-turn-live] raw NDJSON tail:\n${rawBytes.slice(-2048)}`);
|
|
248
|
+
} finally {
|
|
249
|
+
connection.close?.();
|
|
250
|
+
await terminateChild(child);
|
|
251
|
+
try {
|
|
252
|
+
await rm(scratch, { recursive: true, force: true });
|
|
253
|
+
} catch {
|
|
254
|
+
// scratch cleanup is best-effort
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (failure) fail(failure.message);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await main();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// S2d-1b-2b — live in-memory session reuse acceptance. LIVE-gated, OUT of check.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-acp-session-reuse-live
|
|
4
|
+
//
|
|
5
|
+
// The deterministic gate (check-acp-session-reuse) proves the prompt SCOPE
|
|
6
|
+
// (delta-only) and the wiring with a fake seam. This proves the REAL thing: a
|
|
7
|
+
// process-scoped pi process drives TWO real ACP turns over ONE reused
|
|
8
|
+
// claude-agent-acp child, and turn 2 — which sends ONLY the latest user delta —
|
|
9
|
+
// gets an answer that depends on a codeword introduced in turn 1. That is only
|
|
10
|
+
// possible if (a) the child/connection were actually reused (not respawned) and
|
|
11
|
+
// (b) the live ACP session kept turn 1 in its own history (so the delta was
|
|
12
|
+
// enough). A respawn-per-turn backend (S2c) would forget the codeword.
|
|
13
|
+
//
|
|
14
|
+
// It forces process-scoped by pushing the real `--entwurf-control` resident flag
|
|
15
|
+
// into argv (resolveLifecyclePolicy reads process.argv), then calls the REAL
|
|
16
|
+
// streamShellAcp (real overlay + real spawn) twice with the same sessionId.
|
|
17
|
+
//
|
|
18
|
+
// The one-shot exit0 half (a `pi -p` turn-scoped session tears down cleanly) is
|
|
19
|
+
// owned by smoke-acp-provider-live — reuse must not regress it.
|
|
20
|
+
|
|
21
|
+
import { strict as assert } from "node:assert";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { dirname, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
+
import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
|
+
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
|
|
31
|
+
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
32
|
+
|
|
33
|
+
function fail(msg: string): never {
|
|
34
|
+
console.error(`[smoke-acp-session-reuse-live] FAIL: ${msg}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.env.LIVE !== "1") {
|
|
39
|
+
console.error("[smoke-acp-session-reuse-live] skipped — set LIVE=1 to run the real 2-turn reuse acceptance.");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Stream = AsyncIterable<AssistantMessageEvent> & {
|
|
44
|
+
result: () => Promise<{ stopReason: string; errorMessage?: string }>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async function consume(stream: Stream): Promise<{ text: string; error?: string; done: boolean }> {
|
|
48
|
+
let text = "";
|
|
49
|
+
let error: string | undefined;
|
|
50
|
+
let done = false;
|
|
51
|
+
for await (const ev of stream) {
|
|
52
|
+
if (ev.type === "text_delta") text += ev.delta;
|
|
53
|
+
else if (ev.type === "done") done = true;
|
|
54
|
+
else if (ev.type === "error") error = ev.error.errorMessage ?? "error";
|
|
55
|
+
}
|
|
56
|
+
return { text, error, done };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`.toUpperCase();
|
|
60
|
+
const codeword = `KIWI${nonce}`;
|
|
61
|
+
const scratch = mkdtempSync(resolve(tmpdir(), "entwurf-reuse-"));
|
|
62
|
+
const TMP_EMIT = resolve(REPO_ROOT, ".tmp-verify/acp-reuse-live");
|
|
63
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
64
|
+
|
|
65
|
+
async function main(): Promise<void> {
|
|
66
|
+
// Compile so backend.js's `.js` sibling imports resolve (same trick as gates).
|
|
67
|
+
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
68
|
+
cwd: REPO_ROOT,
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
});
|
|
71
|
+
// tsc emits only .ts→.js; the engraving carrier is a .md asset that ships
|
|
72
|
+
// alongside engraving.js in the real package, and backend.js reads it at its
|
|
73
|
+
// default path. Copy it into the emit tree so the carrier resolves (the
|
|
74
|
+
// deterministic check-acp-session-reuse gate does the same copyFileSync).
|
|
75
|
+
const promptsOut = resolve(TMP_EMIT, "pi-extensions/lib/acp/prompts");
|
|
76
|
+
mkdirSync(promptsOut, { recursive: true });
|
|
77
|
+
copyFileSync(resolve(REPO_ROOT, "pi-extensions/lib/acp/prompts/engraving.md"), resolve(promptsOut, "engraving.md"));
|
|
78
|
+
const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
|
|
79
|
+
const backend = (await import(backendUrl)) as any;
|
|
80
|
+
|
|
81
|
+
// Force process-scoped: the real resident flag makes resolveLifecyclePolicy
|
|
82
|
+
// pick `process-scoped`, so turn 2 reuses the in-memory child.
|
|
83
|
+
if (!process.argv.includes("--entwurf-control")) process.argv.push("--entwurf-control");
|
|
84
|
+
|
|
85
|
+
const model = { id: MODEL } as unknown as Model<Api>;
|
|
86
|
+
const options = { cwd: scratch, sessionId: "reuse-smoke" };
|
|
87
|
+
|
|
88
|
+
console.error(`[smoke-acp-session-reuse-live] repo: ${REPO_ROOT}`);
|
|
89
|
+
console.error(`[smoke-acp-session-reuse-live] cwd: ${scratch}`);
|
|
90
|
+
console.error(`[smoke-acp-session-reuse-live] model: entwurf/${MODEL}`);
|
|
91
|
+
console.error(`[smoke-acp-session-reuse-live] codeword: ${codeword}`);
|
|
92
|
+
|
|
93
|
+
// --- turn 1 (new): introduce the codeword, full transcript ---
|
|
94
|
+
const turn1: Context = {
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
role: "user",
|
|
98
|
+
content: `Remember this codeword: ${codeword}. Reply with exactly OK and nothing else.`,
|
|
99
|
+
timestamp: 0,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const r1 = await Promise.race([
|
|
104
|
+
consume(backend.streamShellAcp(model, turn1, options) as Stream),
|
|
105
|
+
new Promise<never>((_, rej) => setTimeout(() => rej(new Error("turn 1 timed out")), TURN_TIMEOUT_MS)),
|
|
106
|
+
]);
|
|
107
|
+
if (r1.error) fail(`turn 1 errored: ${r1.error}`);
|
|
108
|
+
if (!r1.done) fail("turn 1 did not complete");
|
|
109
|
+
console.error(`[smoke-acp-session-reuse-live] turn 1 reply: ${JSON.stringify(r1.text.slice(-120))}`);
|
|
110
|
+
|
|
111
|
+
// --- turn 2 (reuse): ask for the codeword, DELTA ONLY ---
|
|
112
|
+
const turn2: Context = {
|
|
113
|
+
messages: [
|
|
114
|
+
...turn1.messages,
|
|
115
|
+
{
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: [{ type: "text", text: r1.text || "OK" }],
|
|
118
|
+
api: "entwurf",
|
|
119
|
+
provider: "entwurf",
|
|
120
|
+
model: MODEL,
|
|
121
|
+
usage: {
|
|
122
|
+
input: 0,
|
|
123
|
+
output: 0,
|
|
124
|
+
cacheRead: 0,
|
|
125
|
+
cacheWrite: 0,
|
|
126
|
+
totalTokens: 0,
|
|
127
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
128
|
+
},
|
|
129
|
+
stopReason: "stop",
|
|
130
|
+
timestamp: 0,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
role: "user",
|
|
134
|
+
content: "What codeword did I ask you to remember? Reply with exactly that codeword and nothing else.",
|
|
135
|
+
timestamp: 0,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
const r2 = await Promise.race([
|
|
140
|
+
consume(backend.streamShellAcp(model, turn2, options) as Stream),
|
|
141
|
+
new Promise<never>((_, rej) => setTimeout(() => rej(new Error("turn 2 timed out")), TURN_TIMEOUT_MS)),
|
|
142
|
+
]);
|
|
143
|
+
if (r2.error) fail(`turn 2 errored: ${r2.error}`);
|
|
144
|
+
if (!r2.done) fail("turn 2 did not complete");
|
|
145
|
+
console.error(`[smoke-acp-session-reuse-live] turn 2 reply: ${JSON.stringify(r2.text.slice(-120))}`);
|
|
146
|
+
|
|
147
|
+
// The codeword only lives in turn 1. Turn 2 sent only the delta question, so a
|
|
148
|
+
// correct answer proves the reused live ACP session remembered turn 1.
|
|
149
|
+
assert.ok(
|
|
150
|
+
r2.text.includes(codeword),
|
|
151
|
+
`turn 2 reply did not contain the turn-1 codeword ${codeword} — reuse/memory failed (reply: ${JSON.stringify(r2.text.slice(-200))})`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
console.log(
|
|
155
|
+
"[smoke-acp-session-reuse-live] PASS — 2-turn reuse over one ACP child; turn-2 delta recalled the turn-1 codeword",
|
|
156
|
+
);
|
|
157
|
+
console.log(` model: entwurf/${MODEL}`);
|
|
158
|
+
console.log(` codeword: ${codeword} recalled in turn 2 (delta-only)`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main()
|
|
162
|
+
.then(() => {
|
|
163
|
+
// retained child is SIGKILLed by the backend's process 'exit' hook.
|
|
164
|
+
process.exit(0);
|
|
165
|
+
})
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
168
|
+
})
|
|
169
|
+
.finally(() => {
|
|
170
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
171
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
172
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// S2g LIVE 2 — operator skillPlugins passthrough acceptance. LIVE-gated, OUT of
|
|
2
|
+
// `pnpm check`.
|
|
3
|
+
//
|
|
4
|
+
// LIVE=1 ./run.sh smoke-acp-skill-live
|
|
5
|
+
//
|
|
6
|
+
// The other half of the GLG baseline ("the model knows no skills either"). The
|
|
7
|
+
// deterministic gate proved the resolved config carries `skillPlugins` +
|
|
8
|
+
// auto-adds `Skill`/`Skill(*)` and that `_meta.claudeCode.options.plugins`
|
|
9
|
+
// reaches `newSession`. This live smoke proves the last mile: a real ACP child
|
|
10
|
+
// loads the plugin and the model can SURFACE/USE the skill.
|
|
11
|
+
//
|
|
12
|
+
// Method (GPT `…2f9325` LIVE 2): a temp skill plugin (.claude-plugin/plugin.json
|
|
13
|
+
// + skills/<name>/SKILL.md) whose SKILL.md body carries a per-run nonce and an
|
|
14
|
+
// instruction to emit it. A scratch `.pi/settings.json` points
|
|
15
|
+
// `entwurfProvider.skillPlugins` at the plugin dir. The prompt asks the model
|
|
16
|
+
// to use that skill and report the nonce. The nonce lives ONLY in the skill body,
|
|
17
|
+
// so it cannot appear unless the skill reached the session's tool schema and the
|
|
18
|
+
// model activated it.
|
|
19
|
+
//
|
|
20
|
+
// Model-in-loop skill activation is inherently a bit more brittle than the MCP
|
|
21
|
+
// probe (the model must choose to invoke the skill), so the failure message
|
|
22
|
+
// surfaces whether the Skill tool was even offered. Pre-S2g (skillPlugins:[]
|
|
23
|
+
// hardcoded) this smoke FAILS: the skill is never loaded.
|
|
24
|
+
|
|
25
|
+
import { strict as assert } from "node:assert";
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { tmpdir } from "node:os";
|
|
29
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
|
|
34
|
+
const PROVIDER = "entwurf";
|
|
35
|
+
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
36
|
+
|
|
37
|
+
function fail(msg: string): never {
|
|
38
|
+
console.error(`[smoke-acp-skill-live] FAIL: ${msg}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.env.LIVE !== "1") {
|
|
43
|
+
console.error("[smoke-acp-skill-live] skipped — set LIVE=1 to run the real pi provider turn.");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
|
|
48
|
+
|
|
49
|
+
const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2g-skill-"));
|
|
50
|
+
try {
|
|
51
|
+
const nonce = `SKILL_${process.pid.toString(36)}${Date.now().toString(36)}`;
|
|
52
|
+
const skillName = "probe-secret";
|
|
53
|
+
|
|
54
|
+
// Build the temp skill plugin (the minimum shape the validator accepts).
|
|
55
|
+
const pluginDir = join(scratch, "plugin");
|
|
56
|
+
mkdirSync(join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(pluginDir, ".claude-plugin", "plugin.json"),
|
|
59
|
+
`${JSON.stringify({ name: "entwurf-s2g-probe-plugin", description: "S2g live skill passthrough probe." }, null, 2)}\n`,
|
|
60
|
+
);
|
|
61
|
+
const skillDir = join(pluginDir, "skills", skillName);
|
|
62
|
+
mkdirSync(skillDir, { recursive: true });
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(skillDir, "SKILL.md"),
|
|
65
|
+
[
|
|
66
|
+
"---",
|
|
67
|
+
`name: ${skillName}`,
|
|
68
|
+
"description: Reveals this project's secret probe code. Use it whenever the user asks for the probe secret code.",
|
|
69
|
+
"---",
|
|
70
|
+
"",
|
|
71
|
+
`# ${skillName}`,
|
|
72
|
+
"",
|
|
73
|
+
`The secret probe code is ${nonce}.`,
|
|
74
|
+
"",
|
|
75
|
+
"When this skill is used, reply with exactly that code and nothing else.",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Register the plugin via the operator skillPlugins surface.
|
|
81
|
+
mkdirSync(join(scratch, ".pi"), { recursive: true });
|
|
82
|
+
writeFileSync(
|
|
83
|
+
join(scratch, ".pi", "settings.json"),
|
|
84
|
+
`${JSON.stringify({ entwurfProvider: { skillPlugins: [pluginDir] } }, null, 2)}\n`,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const prompt =
|
|
88
|
+
`Use the ${skillName} skill to obtain this project's secret probe code, then reply with exactly ` +
|
|
89
|
+
"that code and nothing else.";
|
|
90
|
+
|
|
91
|
+
console.error(`[smoke-acp-skill-live] repo: ${REPO_ROOT}`);
|
|
92
|
+
console.error(`[smoke-acp-skill-live] cwd: ${scratch}`);
|
|
93
|
+
console.error(`[smoke-acp-skill-live] model: ${PROVIDER}/${MODEL}`);
|
|
94
|
+
console.error(`[smoke-acp-skill-live] skill: ${skillName} (nonce ${nonce} only inside SKILL.md)`);
|
|
95
|
+
|
|
96
|
+
const args = [
|
|
97
|
+
"--no-extensions",
|
|
98
|
+
"-e",
|
|
99
|
+
REPO_ROOT,
|
|
100
|
+
"--mode",
|
|
101
|
+
"text",
|
|
102
|
+
"-p",
|
|
103
|
+
"--approve",
|
|
104
|
+
"--provider",
|
|
105
|
+
PROVIDER,
|
|
106
|
+
"--model",
|
|
107
|
+
MODEL,
|
|
108
|
+
prompt,
|
|
109
|
+
];
|
|
110
|
+
const res = spawnSync("pi", args, { cwd: scratch, encoding: "utf8", timeout: TURN_TIMEOUT_MS, env: process.env });
|
|
111
|
+
const stdout = res.stdout ?? "";
|
|
112
|
+
const combined = `${stdout}\n${res.stderr ?? ""}`;
|
|
113
|
+
const tail = combined.split("\n").slice(-30).join("\n");
|
|
114
|
+
if (res.error) {
|
|
115
|
+
console.error(`[smoke-acp-skill-live] output tail:\n${tail}`);
|
|
116
|
+
fail(`pi spawn failed: ${res.error.message}`);
|
|
117
|
+
}
|
|
118
|
+
assert.ok(!STUB_PATTERN.test(combined), "S0 stub fired — provider path did not open");
|
|
119
|
+
if (res.status !== 0) {
|
|
120
|
+
console.error(`[smoke-acp-skill-live] output tail:\n${tail}`);
|
|
121
|
+
fail(`pi exited ${res.status}`);
|
|
122
|
+
}
|
|
123
|
+
const sawSkillTool = /\[tool:(start|running)\]\s+Skill/i.test(combined);
|
|
124
|
+
assert.ok(
|
|
125
|
+
stdout.includes(nonce),
|
|
126
|
+
`reply did not carry the skill nonce ${nonce} — the operator skill plugin was not loaded/usable ` +
|
|
127
|
+
`(S2g skillPlugins passthrough failure). Skill tool notice ${sawSkillTool ? "WAS" : "was NOT"} seen. ` +
|
|
128
|
+
`stdout tail: ${JSON.stringify(stdout.slice(-300))}`,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
console.log(
|
|
132
|
+
"[smoke-acp-skill-live] PASS — the operator-declared skill plugin reached the live ACP session: the model",
|
|
133
|
+
);
|
|
134
|
+
console.log(` used the ${skillName} skill and returned ${nonce} (the nonce lives only inside SKILL.md).`);
|
|
135
|
+
console.log(
|
|
136
|
+
` model: ${PROVIDER}/${MODEL}; Skill tool notice ${sawSkillTool ? "observed" : "not observed (reply-only evidence)"}`,
|
|
137
|
+
);
|
|
138
|
+
} finally {
|
|
139
|
+
try {
|
|
140
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
// best-effort cleanup
|
|
143
|
+
}
|
|
144
|
+
}
|