@junghanacs/entwurf 0.13.0 → 0.14.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 +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Deterministic gate for the ACP stop-reason contract (backend.ts + event-mapper.ts).
|
|
2
|
+
//
|
|
3
|
+
// WHAT THIS EXISTS TO STOP. `mapPromptStopReason` used to be a bare
|
|
4
|
+
// `switch` returning a StopReason with `default: return "stop"`. The ACP terminal
|
|
5
|
+
// set is closed — `@agentclientprotocol/sdk` 1.3.0 `schema/types.gen` declares
|
|
6
|
+
// `end_turn | max_tokens | max_turn_requests | refusal | cancelled` — so that
|
|
7
|
+
// default silently collapsed THREE distinct non-success outcomes into a clean
|
|
8
|
+
// successful turn:
|
|
9
|
+
//
|
|
10
|
+
// refusal the model declined; the answer is absent
|
|
11
|
+
// max_turn_requests the backend's per-turn budget ran out; the answer is cut
|
|
12
|
+
// <anything new> a reason this build has never seen
|
|
13
|
+
//
|
|
14
|
+
// plus a fourth: a prompt result carrying NO stopReason at all. pi rendered all
|
|
15
|
+
// four as a finished assistant message. pi 0.83 closed the identical hole in its
|
|
16
|
+
// own providers (#7272 — unmapped terminal reasons surface as provider errors,
|
|
17
|
+
// never successful stops) and added `AssistantMessage.rawStopReason` so the wire
|
|
18
|
+
// value survives mapping; this gate pins our side of that same contract.
|
|
19
|
+
//
|
|
20
|
+
// A pure-function check of the mapper is NOT enough — the defect people actually
|
|
21
|
+
// feel is "pi showed a truncated answer as done". So every cell is driven
|
|
22
|
+
// BEHAVIORALLY through `streamAcpTurn` against a fake ACP connection, and each
|
|
23
|
+
// asserts all four observable facts together:
|
|
24
|
+
//
|
|
25
|
+
// (a) the emitted event KIND done vs error
|
|
26
|
+
// (b) the emitted event REASON stop / length / aborted / error
|
|
27
|
+
// (c) the final message stopReason
|
|
28
|
+
// (d) rawStopReason + errorMessage on that same final message
|
|
29
|
+
//
|
|
30
|
+
// SEED CLAIM (separate, and deliberately not folded into the cells above).
|
|
31
|
+
// `createAcpStreamState` seeds `stopReason: "pending"` — pi 0.83's sentinel for
|
|
32
|
+
// "no terminal reason observed yet". Seeding "stop" would pre-claim success for
|
|
33
|
+
// the entire in-flight turn. That claim is asserted on the value returned by
|
|
34
|
+
// `createAcpStreamState` DIRECTLY: `state.output` is a mutable object that every
|
|
35
|
+
// terminal path overwrites, so reading it after a turn would only ever show the
|
|
36
|
+
// terminal value and could never fail. Reading it late is how this assertion
|
|
37
|
+
// would quietly stop testing anything.
|
|
38
|
+
//
|
|
39
|
+
// backend.ts imports its siblings with `.js` suffixes (the root/jiti runtime
|
|
40
|
+
// convention), so — like check-acp-session-reuse — we tsc-emit the project and
|
|
41
|
+
// import the COMPILED backend.js whose `.js` imports resolve to real siblings.
|
|
42
|
+
|
|
43
|
+
import { strict as assert } from "node:assert";
|
|
44
|
+
import { execFileSync } from "node:child_process";
|
|
45
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmdirSync, rmSync } from "node:fs";
|
|
46
|
+
import { tmpdir } from "node:os";
|
|
47
|
+
import { resolve } from "node:path";
|
|
48
|
+
import { pathToFileURL } from "node:url";
|
|
49
|
+
import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
|
|
50
|
+
|
|
51
|
+
const sonnet = { id: "claude-sonnet-5" } as unknown as Model<Api>;
|
|
52
|
+
|
|
53
|
+
type Stream = AsyncIterable<AssistantMessageEvent> & {
|
|
54
|
+
result: () => Promise<{ stopReason: string; errorMessage?: string }>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
async function collect(stream: Stream): Promise<AssistantMessageEvent[]> {
|
|
58
|
+
const events: AssistantMessageEvent[] = [];
|
|
59
|
+
for await (const ev of stream) events.push(ev);
|
|
60
|
+
return events;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeFakeChild() {
|
|
64
|
+
const pipe = () => ({ destroy() {}, unref() {} });
|
|
65
|
+
return {
|
|
66
|
+
pid: undefined as number | undefined,
|
|
67
|
+
exitCode: null as number | null,
|
|
68
|
+
signalCode: null as NodeJS.Signals | null,
|
|
69
|
+
stdin: pipe(),
|
|
70
|
+
stdout: pipe(),
|
|
71
|
+
stderr: { on() {}, destroy() {}, unref() {} },
|
|
72
|
+
kill() {
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
unref() {},
|
|
76
|
+
once() {},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const EMPTY_MCP_HASH = "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945";
|
|
81
|
+
const DEFAULT_RESOLVED_CONFIG: any = {
|
|
82
|
+
settingSources: [],
|
|
83
|
+
strictMcpConfig: true,
|
|
84
|
+
showToolNotifications: true,
|
|
85
|
+
mcpServers: [],
|
|
86
|
+
mcpServersHash: EMPTY_MCP_HASH,
|
|
87
|
+
tools: ["Read"],
|
|
88
|
+
skillPlugins: [],
|
|
89
|
+
permissionAllow: ["Read(*)"],
|
|
90
|
+
disallowedTools: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A harness whose fake ACP `prompt` resolves with EXACTLY the terminal shape the
|
|
95
|
+
* cell is about. `wireReason === null` models a result object with no stopReason
|
|
96
|
+
* key at all (the "absent" cell) rather than a literal `undefined` value.
|
|
97
|
+
*/
|
|
98
|
+
function makeHarness(recordDir: string, wireReason: string | null) {
|
|
99
|
+
return {
|
|
100
|
+
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
101
|
+
ensureOverlay: () => {},
|
|
102
|
+
spawnChild: () => makeFakeChild(),
|
|
103
|
+
createConnection: (_child: any, handlers: any) => ({
|
|
104
|
+
initialize: async () => ({ agentCapabilities: {} }),
|
|
105
|
+
newSession: async () => ({ sessionId: "ACP-1" }),
|
|
106
|
+
setSessionConfigOption: async () => ({}),
|
|
107
|
+
prompt: async ({ sessionId }: any) => {
|
|
108
|
+
await handlers.sessionUpdate({
|
|
109
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "PARTIAL" } },
|
|
110
|
+
sessionId,
|
|
111
|
+
});
|
|
112
|
+
return wireReason === null ? {} : { stopReason: wireReason };
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
lifecyclePolicy: () => "process-scoped",
|
|
116
|
+
loadConfig: () => DEFAULT_RESOLVED_CONFIG,
|
|
117
|
+
now: () => "2026-07-30T00:00:00Z",
|
|
118
|
+
sessionDir: recordDir,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const TMP_EMIT = ".tmp-verify/acp-stop-reason";
|
|
123
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
124
|
+
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-stop-rec-"));
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One driven turn, described by everything an operator could observe.
|
|
128
|
+
*
|
|
129
|
+
* `claim` is the qualification token this cell's SEALING assertions carry. The
|
|
130
|
+
* mutant harness does not accept "the gate went red" — it requires the gate to
|
|
131
|
+
* go red *at the token of the claim under test*, so a cell that guards a claim
|
|
132
|
+
* must print that claim's `[QK:…]` in the very assertion the mutant trips.
|
|
133
|
+
* Cells whose sealing no mutant targets carry no token.
|
|
134
|
+
*/
|
|
135
|
+
type Cell = {
|
|
136
|
+
wire: string | null;
|
|
137
|
+
kind: "done" | "error";
|
|
138
|
+
reason: string;
|
|
139
|
+
stopReason: string;
|
|
140
|
+
rawStopReason?: string;
|
|
141
|
+
/** Substring the errorMessage must carry; absent = there must be no errorMessage. */
|
|
142
|
+
errorContains?: string;
|
|
143
|
+
claim?: string;
|
|
144
|
+
why: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Qualification token for the claim that the raw ACP reason survives mapping. */
|
|
148
|
+
const RAW_CLAIM = "[QK:STOP-RAW-PRESERVED]";
|
|
149
|
+
|
|
150
|
+
/** Prefix an assertion message with a claim token when the cell guards one. */
|
|
151
|
+
function tag(cell: Cell, message: string): string {
|
|
152
|
+
return cell.claim ? `${cell.claim} ${message}` : message;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const CELLS: Cell[] = [
|
|
156
|
+
{
|
|
157
|
+
wire: "end_turn",
|
|
158
|
+
kind: "done",
|
|
159
|
+
reason: "stop",
|
|
160
|
+
stopReason: "stop",
|
|
161
|
+
rawStopReason: "end_turn",
|
|
162
|
+
why: "a normal completion is the only clean success",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
wire: "max_tokens",
|
|
166
|
+
kind: "done",
|
|
167
|
+
reason: "length",
|
|
168
|
+
stopReason: "length",
|
|
169
|
+
rawStopReason: "max_tokens",
|
|
170
|
+
why: "output-cap truncation is honestly reported as length, not stop",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
wire: "cancelled",
|
|
174
|
+
kind: "error",
|
|
175
|
+
reason: "aborted",
|
|
176
|
+
stopReason: "aborted",
|
|
177
|
+
rawStopReason: "cancelled",
|
|
178
|
+
why: "a cancelled turn is an abort, never a done",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
wire: "refusal",
|
|
182
|
+
kind: "error",
|
|
183
|
+
reason: "error",
|
|
184
|
+
stopReason: "error",
|
|
185
|
+
rawStopReason: "refusal",
|
|
186
|
+
errorContains: "refusal",
|
|
187
|
+
claim: "[QK:STOP-REFUSAL-NOT-SUCCESS]",
|
|
188
|
+
why: "a model refusal has no answer to show — reporting it as done is the original defect",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
wire: "max_turn_requests",
|
|
192
|
+
kind: "error",
|
|
193
|
+
reason: "error",
|
|
194
|
+
stopReason: "error",
|
|
195
|
+
rawStopReason: "max_turn_requests",
|
|
196
|
+
errorContains: "max_turn_requests",
|
|
197
|
+
claim: "[QK:STOP-TURNBUDGET-NOT-SUCCESS]",
|
|
198
|
+
why: "an exhausted per-turn request budget leaves the answer cut short",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
wire: "teapot",
|
|
202
|
+
kind: "error",
|
|
203
|
+
reason: "error",
|
|
204
|
+
stopReason: "error",
|
|
205
|
+
rawStopReason: "teapot",
|
|
206
|
+
errorContains: "teapot",
|
|
207
|
+
claim: "[QK:STOP-UNKNOWN-NOT-SUCCESS]",
|
|
208
|
+
why: "a reason this build has never seen must fail loud, carrying the raw value",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
wire: null,
|
|
212
|
+
kind: "error",
|
|
213
|
+
reason: "error",
|
|
214
|
+
stopReason: "error",
|
|
215
|
+
errorContains: "without a stop reason",
|
|
216
|
+
claim: "[QK:STOP-ABSENT-NOT-SUCCESS]",
|
|
217
|
+
why: "a prompt result with no stopReason at all is not a successful turn",
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
223
|
+
stdio: "pipe",
|
|
224
|
+
});
|
|
225
|
+
const promptsOut = resolve(TMP_EMIT, "pi-extensions/lib/acp/prompts");
|
|
226
|
+
mkdirSync(promptsOut, { recursive: true });
|
|
227
|
+
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
228
|
+
|
|
229
|
+
const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
|
|
230
|
+
const eventUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/event-mapper.js")).href;
|
|
231
|
+
const backend = (await import(backendUrl)) as any;
|
|
232
|
+
const eventMod = (await import(eventUrl)) as any;
|
|
233
|
+
|
|
234
|
+
// ----------------------------------------------------------------------
|
|
235
|
+
// (1) SEED — asserted on the freshly returned state, never after a turn.
|
|
236
|
+
// ----------------------------------------------------------------------
|
|
237
|
+
{
|
|
238
|
+
const seeded = eventMod.createAcpStreamState(
|
|
239
|
+
{ push() {}, end() {} },
|
|
240
|
+
{
|
|
241
|
+
api: "entwurf",
|
|
242
|
+
provider: "entwurf",
|
|
243
|
+
model: sonnet.id,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
assert.equal(
|
|
247
|
+
seeded.output.stopReason,
|
|
248
|
+
"pending",
|
|
249
|
+
"[QK:STOP-PENDING-SEED] a freshly created ACP stream state must seed stopReason 'pending' (pi 0.83's " +
|
|
250
|
+
"no-terminal-reason-yet sentinel) — seeding 'stop' pre-claims success for the whole in-flight turn",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ----------------------------------------------------------------------
|
|
255
|
+
// (2) The seven terminal cells, driven end to end. Each cell's qualification
|
|
256
|
+
// token lives in its `claim` field above — exactly one occurrence per
|
|
257
|
+
// token in this file, which is what the mutant manifest requires.
|
|
258
|
+
// ----------------------------------------------------------------------
|
|
259
|
+
for (const cell of CELLS) {
|
|
260
|
+
const label = cell.wire === null ? "<absent>" : cell.wire;
|
|
261
|
+
const events = await collect(
|
|
262
|
+
backend.streamAcpTurn(
|
|
263
|
+
sonnet,
|
|
264
|
+
{ messages: [{ role: "user", content: `drive ${label}`, timestamp: 0 }] } as Context,
|
|
265
|
+
{ sessionId: `stop-${label}` },
|
|
266
|
+
makeHarness(recordDir, cell.wire),
|
|
267
|
+
) as Stream,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const done = events.find((e) => e.type === "done") as any;
|
|
271
|
+
const errored = events.find((e) => e.type === "error") as any;
|
|
272
|
+
|
|
273
|
+
if (cell.kind === "done") {
|
|
274
|
+
assert.ok(done, tag(cell, `[${label}] must seal as a done event — ${cell.why}`));
|
|
275
|
+
assert.ok(!errored, tag(cell, `[${label}] must not also emit an error event`));
|
|
276
|
+
} else {
|
|
277
|
+
assert.ok(errored, tag(cell, `[${label}] must seal as an ERROR event, not a done — ${cell.why}`));
|
|
278
|
+
assert.ok(
|
|
279
|
+
!done,
|
|
280
|
+
tag(cell, `[${label}] must NOT emit a done event — that is exactly the collapse this gate forbids`),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sealed = cell.kind === "done" ? done : errored;
|
|
285
|
+
assert.equal(sealed.reason, cell.reason, tag(cell, `[${label}] sealed event reason must be "${cell.reason}"`));
|
|
286
|
+
|
|
287
|
+
const final = cell.kind === "done" ? sealed.message : sealed.error;
|
|
288
|
+
assert.equal(
|
|
289
|
+
final.stopReason,
|
|
290
|
+
cell.stopReason,
|
|
291
|
+
tag(cell, `[${label}] final message stopReason must be "${cell.stopReason}"`),
|
|
292
|
+
);
|
|
293
|
+
// Deliberately carries no claim token: the seed mutant is caught by the
|
|
294
|
+
// direct assertion in (1), and this one is the standing guarantee that no
|
|
295
|
+
// terminal path ever leaks the sentinel out to a consumer.
|
|
296
|
+
assert.notEqual(
|
|
297
|
+
final.stopReason,
|
|
298
|
+
"pending",
|
|
299
|
+
`[${label}] the seed must never survive a sealed turn — every terminal path overwrites it`,
|
|
300
|
+
);
|
|
301
|
+
assert.equal(
|
|
302
|
+
final.rawStopReason,
|
|
303
|
+
cell.rawStopReason,
|
|
304
|
+
`${RAW_CLAIM} [${label}] rawStopReason must preserve the wire value (${cell.rawStopReason ?? "absent"}) so ` +
|
|
305
|
+
"the raw ACP reason survives the mapping",
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (cell.errorContains) {
|
|
309
|
+
assert.ok(
|
|
310
|
+
typeof final.errorMessage === "string" && final.errorMessage.includes(cell.errorContains),
|
|
311
|
+
tag(
|
|
312
|
+
cell,
|
|
313
|
+
`[${label}] errorMessage must name the reason (expected to contain "${cell.errorContains}", got ` +
|
|
314
|
+
`${JSON.stringify(final.errorMessage)})`,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
assert.equal(final.errorMessage, undefined, tag(cell, `[${label}] a clean end must not carry an errorMessage`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} finally {
|
|
322
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
323
|
+
try {
|
|
324
|
+
// The qualification harness's work-surface hash walks ignored paths too, so
|
|
325
|
+
// a leftover EMPTY parent dir reads as IMPURE tree drift even though git
|
|
326
|
+
// porcelain is clean. Remove it when empty; a concurrent sibling gate's
|
|
327
|
+
// emit keeps it alive and this rmdir simply fails.
|
|
328
|
+
rmdirSync(".tmp-verify");
|
|
329
|
+
} catch {
|
|
330
|
+
// non-empty or already gone — fine either way
|
|
331
|
+
}
|
|
332
|
+
rmSync(recordDir, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(
|
|
336
|
+
"[check-acp-stop-reason] ok — the ACP terminal set is driven end to end through streamAcpTurn: end_turn→done/stop " +
|
|
337
|
+
"and max_tokens→done/length are the only clean seals; cancelled→error/aborted; refusal, max_turn_requests, an " +
|
|
338
|
+
"unrecognized reason, and an ABSENT reason each seal as an ERROR event with no done (the collapse-to-stop defect " +
|
|
339
|
+
"pi 0.83 #7272 closed on its own providers); rawStopReason preserves the wire value on every cell that carried " +
|
|
340
|
+
"one; and the stream state seeds 'pending' (asserted on the fresh state, not after a turn) so no in-flight turn " +
|
|
341
|
+
"pre-claims success",
|
|
342
|
+
);
|