@junghanacs/entwurf 0.13.0 → 0.13.1
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 +5 -3
- package/BASELINE.md +57 -203
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +117 -261
- package/README.md +68 -408
- package/VERIFY.md +58 -87
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +231 -0
- package/docs/external-mcp-host.md +132 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +123 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
- 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 +3 -3
- package/mcp/entwurf-bridge/src/index.ts +21 -48
- package/package.json +12 -9
- package/pi-extensions/entwurf-control.ts +20 -49
- 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 +305 -36
- 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 +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +244 -108
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-sdk-surface.ts +60 -21
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-entwurf-v2-surface.ts +41 -0
- package/scripts/check-gate-qualification.ts +6 -3
- package/scripts/check-probe-ordering.ts +39 -7
- package/scripts/check-release-gate-outcomes.ts +332 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/probe-ordering.json +27 -5
- package/scripts/mutants/release-gate.json +105 -0
- package/scripts/mutants/v2-surface.json +26 -15
- 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 +4 -3
- 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 +2 -2
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
- package/scripts/smoke-meta-install-state.sh +4 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Deterministic gate for the release-gate STEP OUTCOME protocol (P1).
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT THIS OWNS. The aggregate release gate documented — in run.sh's own
|
|
4
|
+
// usage, README, and VERIFY — that a cut needs `LIVE=1` and `SKIP=0`. Nothing
|
|
5
|
+
// enforced it. Exit authority read the FAIL counter alone, so
|
|
6
|
+
// `./run.sh release-gate <dir>` returned 0 while printing 14 SKIPs and the words
|
|
7
|
+
// "all green". And a step that WAS invoked could decline a prerequisite and exit
|
|
8
|
+
// 0 — Cortex without `ENTWURF_ACP_CORTEX_CONNECTION` is the measured case — which
|
|
9
|
+
// the aggregate counted as PASS. Both holes are the same shape: a skip that
|
|
10
|
+
// cannot be told apart from an acceptance, which means a release summary cannot
|
|
11
|
+
// prove the calls it claims.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS GATE PINS, in the order the cells run (the order is load-bearing:
|
|
14
|
+
// each mutant must die on ITS claim, so a cell that would fire first on another
|
|
15
|
+
// cell's mutation is deliberately kept narrow):
|
|
16
|
+
// 1. the protocol is ONE number, agreed across the shell and TS halves;
|
|
17
|
+
// 2. the classifier never rounds a skip up to a pass;
|
|
18
|
+
// 3. `--cut` refuses a MUST skip while a bare diagnostic run does not, AND the
|
|
19
|
+
// refusal names its cause — a step that RAN AND BROKE is a different fact
|
|
20
|
+
// from one that NEVER RAN, and the counters are never fudged to carry it;
|
|
21
|
+
// 4. no LIVE smoke still carries the pre-P1 exit-0 skip shape;
|
|
22
|
+
// 5. a real smoke invoked with LIVE unset propagates the protocol code out
|
|
23
|
+
// through run_ts (the "direct LIVE!=1" case);
|
|
24
|
+
// 6. a run.sh wrapper that declines its own prerequisite does the same (the
|
|
25
|
+
// "internal prerequisite" case, Cortex being the one that was measured).
|
|
26
|
+
// 7. every LIVE smoke is either wired into the aggregate or excluded for a
|
|
27
|
+
// reason the docs actually state — the protocol cannot vouch for a step the
|
|
28
|
+
// gate never lists.
|
|
29
|
+
//
|
|
30
|
+
// Cells 5-6 SPAWN the real subcommands rather than reasoning about them: the
|
|
31
|
+
// whole defect was an assumption about what a step would do, so an assumption is
|
|
32
|
+
// exactly what this gate must not make. They are cheap — each smoke declines
|
|
33
|
+
// before it does any work.
|
|
34
|
+
//
|
|
35
|
+
// Pure + subprocess, no network/model — IN pnpm check.
|
|
36
|
+
|
|
37
|
+
import { strict as assert } from "node:assert";
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
39
|
+
import { globSync, readFileSync } from "node:fs";
|
|
40
|
+
import { basename, join } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
import { LIVE_SKIP_EXIT, LIVE_SKIP_MARKER } from "./lib/live-skip.ts";
|
|
43
|
+
|
|
44
|
+
const REPO_DIR = fileURLToPath(new URL("..", import.meta.url));
|
|
45
|
+
const SHELL_LIB = "scripts/lib/step-outcome.sh";
|
|
46
|
+
|
|
47
|
+
/** Run a snippet with the shell half sourced; returns trimmed stdout. */
|
|
48
|
+
function inShell(snippet: string): string {
|
|
49
|
+
return execFileSync("bash", ["-c", `. "${SHELL_LIB}"; ${snippet}`], {
|
|
50
|
+
cwd: REPO_DIR,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
timeout: 20_000,
|
|
53
|
+
}).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Invoke a real run.sh subcommand; returns its exit code + combined output. */
|
|
57
|
+
function runSubcommand(sub: string, env: Record<string, string | undefined>): { code: number; output: string } {
|
|
58
|
+
const childEnv: Record<string, string> = {};
|
|
59
|
+
for (const [k, v] of Object.entries({ ...process.env, ...env })) {
|
|
60
|
+
if (v !== undefined) childEnv[k] = v;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const out = execFileSync("bash", ["run.sh", sub], {
|
|
64
|
+
cwd: REPO_DIR,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 120_000,
|
|
67
|
+
env: childEnv,
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
return { code: 0, output: out };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const e = err as { status?: number | null; stdout?: string; stderr?: string };
|
|
73
|
+
return { code: e.status ?? -1, output: `${e.stdout ?? ""}${e.stderr ?? ""}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
// 1) ONE protocol, two languages. A drifted constant does not degrade
|
|
79
|
+
// gracefully — it silently reclassifies every skip in the aggregate.
|
|
80
|
+
// ===========================================================================
|
|
81
|
+
{
|
|
82
|
+
const shellValue = inShell('echo "$ENTWURF_STEP_SKIP_EXIT"');
|
|
83
|
+
assert.equal(
|
|
84
|
+
shellValue,
|
|
85
|
+
String(LIVE_SKIP_EXIT),
|
|
86
|
+
`[QK:SKIP-EXIT-ONE-PROTOCOL] ${SHELL_LIB} and scripts/lib/live-skip.ts must name the SAME skip exit code. ` +
|
|
87
|
+
"The smokes exit with the TS constant and the aggregate classifies with the shell one, so a drift between " +
|
|
88
|
+
"them turns every honest skip into a FAIL (or, the other way, into a PASS) with nothing in the summary " +
|
|
89
|
+
`saying so. shell=${shellValue} ts=${LIVE_SKIP_EXIT}`,
|
|
90
|
+
);
|
|
91
|
+
// The code must also stay clear of the ranges that already mean something
|
|
92
|
+
// else, or a dependency's unrelated verdict reads as a skip.
|
|
93
|
+
assert.ok(
|
|
94
|
+
LIVE_SKIP_EXIT > 4 && LIVE_SKIP_EXIT < 126,
|
|
95
|
+
"the skip code must avoid the per-tool contract band (0..4) and the shell's signal band (126+)",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ===========================================================================
|
|
100
|
+
// 2) The classifier. A skip is its own outcome, never rounded up or down.
|
|
101
|
+
// ===========================================================================
|
|
102
|
+
{
|
|
103
|
+
const table: Array<[string, string]> = [
|
|
104
|
+
["0", "PASS"],
|
|
105
|
+
[String(LIVE_SKIP_EXIT), "SKIP"],
|
|
106
|
+
["1", "FAIL"],
|
|
107
|
+
["2", "FAIL"],
|
|
108
|
+
["127", "FAIL"],
|
|
109
|
+
["137", "FAIL"],
|
|
110
|
+
];
|
|
111
|
+
for (const [code, expected] of table) {
|
|
112
|
+
const got = inShell(`entwurf_step_outcome ${code}`);
|
|
113
|
+
assert.equal(
|
|
114
|
+
got,
|
|
115
|
+
expected,
|
|
116
|
+
`[QK:STEP-OUTCOME-SKIP-NOT-PASS] exit ${code} must classify as ${expected}, got ${got}. A skip rounded up to ` +
|
|
117
|
+
"PASS is exactly the pre-P1 defect: the aggregate then reports acceptance for a step that told it, in the " +
|
|
118
|
+
"only channel it has, that it never ran. A skip rounded down to FAIL is the mirror error and makes the " +
|
|
119
|
+
"unattended diagnostic unusable.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ===========================================================================
|
|
125
|
+
// 3) Cut authority. `--cut` is the executable half of "a CUT needs SKIP=0";
|
|
126
|
+
// without it the diagnostic must stay green so an unattended run is usable.
|
|
127
|
+
// ===========================================================================
|
|
128
|
+
{
|
|
129
|
+
const releasable = (failc: number, skipc: number, cut: number): boolean =>
|
|
130
|
+
inShell(`if entwurf_release_releasable ${failc} ${skipc} ${cut}; then echo YES; else echo NO; fi`) === "YES";
|
|
131
|
+
|
|
132
|
+
assert.ok(releasable(0, 0, 1), "a cut with no failures and no skips is releasable");
|
|
133
|
+
assert.ok(!releasable(1, 0, 1), "a failure blocks a cut");
|
|
134
|
+
assert.ok(!releasable(1, 0, 0), "a failure blocks the diagnostic too — FAIL was always blocking");
|
|
135
|
+
assert.ok(
|
|
136
|
+
releasable(0, 3, 0),
|
|
137
|
+
"a DIAGNOSTIC run with skips stays exit 0 — an unattended `./run.sh release-gate` must remain runnable, and " +
|
|
138
|
+
"turning it red was never the ask",
|
|
139
|
+
);
|
|
140
|
+
assert.ok(
|
|
141
|
+
!releasable(0, 3, 1),
|
|
142
|
+
"[QK:CUT-REFUSES-SKIP] `--cut` must refuse a MUST SKIP. This is the whole point: the release procedure said " +
|
|
143
|
+
'"a CUT needs LIVE=1, SKIP=0" in prose while the code returned 0 with 14 skips, so a summary could be quoted ' +
|
|
144
|
+
"as acceptance for calls that never happened. It also removes the need for a separate LIVE assertion — with " +
|
|
145
|
+
"LIVE unset every LIVE-gated step skips, and the skip count is what blocks.",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// …and the refusal must SAY WHICH of the two it is. A blocked cut caused by a
|
|
149
|
+
// broken call and one caused by an absent prerequisite need different actions
|
|
150
|
+
// from whoever reads the record, and the counters must not be fudged to carry
|
|
151
|
+
// that (a synthesized FAIL=1 for a policy block erases the distinction).
|
|
152
|
+
const verdict = (failc: number, skipc: number, cut: number): string =>
|
|
153
|
+
inShell(`entwurf_release_verdict ${failc} ${skipc} ${cut}`);
|
|
154
|
+
|
|
155
|
+
assert.equal(verdict(0, 0, 1), "cut: OK", "a clean cut says so in one token");
|
|
156
|
+
assert.equal(
|
|
157
|
+
verdict(0, 3, 0),
|
|
158
|
+
"cut: n/a (diagnostic, 3 SKIP)",
|
|
159
|
+
"a diagnostic run names its skips without claiming a cut",
|
|
160
|
+
);
|
|
161
|
+
assert.equal(verdict(0, 0, 0), "cut: n/a (diagnostic)", "a clean diagnostic run still does not claim a cut");
|
|
162
|
+
assert.equal(
|
|
163
|
+
verdict(1, 0, 1),
|
|
164
|
+
"cut: BLOCKED (MUST FAIL)",
|
|
165
|
+
"a step that RAN AND BROKE must be named as a failure — that is a defect to fix",
|
|
166
|
+
);
|
|
167
|
+
assert.equal(
|
|
168
|
+
verdict(0, 3, 1),
|
|
169
|
+
"cut: BLOCKED (MUST SKIP)",
|
|
170
|
+
"[QK:CUT-VERDICT-NAMES-CAUSE] a cut blocked ONLY by skips must say so in its own token, distinct from a failure " +
|
|
171
|
+
"block. An operator (and the P5 release record) reads two different actions out of these: a MUST FAIL is a " +
|
|
172
|
+
"broken call to fix, a MUST SKIP is a prerequisite to supply. Collapsing them into one string — or worse, " +
|
|
173
|
+
"synthesizing FAIL=1 for the policy block — throws away the exact distinction this protocol was built to make.",
|
|
174
|
+
);
|
|
175
|
+
assert.equal(
|
|
176
|
+
verdict(2, 5, 1),
|
|
177
|
+
"cut: BLOCKED (MUST FAIL)",
|
|
178
|
+
"when both are present the FAILURE is the headline — a broken call outranks a missing prerequisite",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
// 4) No LIVE smoke still carries the pre-P1 skip shape (static, all of them).
|
|
184
|
+
// A future smoke that hand-rolls `exit 0` on a skip re-opens the hole for
|
|
185
|
+
// one lane only, which is precisely how this survived so long.
|
|
186
|
+
// ===========================================================================
|
|
187
|
+
{
|
|
188
|
+
const smokes = globSync("scripts/smoke-*live*.ts", { cwd: REPO_DIR }).sort();
|
|
189
|
+
assert.ok(smokes.length >= 15, `expected the LIVE smoke family, found ${smokes.length}`);
|
|
190
|
+
let liveGated = 0;
|
|
191
|
+
for (const rel of smokes) {
|
|
192
|
+
const src = readFileSync(join(REPO_DIR, rel), "utf8");
|
|
193
|
+
if (!src.includes("process.env.LIVE")) continue; // gated in run.sh instead (cell 6 owns that surface)
|
|
194
|
+
liveGated++;
|
|
195
|
+
// ONE assertion, three ways to fail it: no protocol import, or either of
|
|
196
|
+
// the two pre-P1 shapes (exit 0 / bare return) still inside the LIVE gate.
|
|
197
|
+
// Kept as one so the claim owns every way a smoke can go back to being
|
|
198
|
+
// indistinguishable from success — a split would let a mutation die on an
|
|
199
|
+
// unclaimed sibling assertion instead of here.
|
|
200
|
+
const importsProtocol = src.includes('from "./lib/live-skip.ts"');
|
|
201
|
+
const exitsZero = /LIVE !== "1"[\s\S]{0,400}?process\.exit\(0\)/.test(src);
|
|
202
|
+
const bareReturns = /LIVE !== "1"[\s\S]{0,400}?\n\t+return;/.test(src);
|
|
203
|
+
assert.ok(
|
|
204
|
+
importsProtocol && !exitsZero && !bareReturns,
|
|
205
|
+
`[QK:NO-SMOKE-SKIPS-WITH-ZERO] ${basename(rel)} gates on LIVE but does not decline through skipLive ` +
|
|
206
|
+
`(importsProtocol=${importsProtocol} exitsZero=${exitsZero} bareReturns=${bareReturns}). Every LIVE smoke ` +
|
|
207
|
+
"must take the one protocol exit — a hand-rolled `process.exit(0)` or bare `return` is indistinguishable " +
|
|
208
|
+
"from success, which is exactly what let the aggregate count a never-run step as PASS.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
assert.ok(liveGated >= 15, `expected most LIVE smokes to gate on LIVE, got ${liveGated}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ===========================================================================
|
|
215
|
+
// 5) REAL propagation — the direct `LIVE!=1` case, end to end through run_ts.
|
|
216
|
+
// Static source pins cannot see a transport that swallows the code.
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
{
|
|
219
|
+
const { code, output } = runSubcommand("smoke-acp-raw-turn-live", { LIVE: undefined });
|
|
220
|
+
assert.equal(
|
|
221
|
+
code,
|
|
222
|
+
LIVE_SKIP_EXIT,
|
|
223
|
+
`[QK:LIVE-SKIP-IS-PROTOCOL-EXIT] a LIVE smoke invoked with LIVE unset must leave the protocol's SKIP code on ` +
|
|
224
|
+
`the process, all the way out through run_ts — got exit ${code}. Exit 0 here is the original defect: the ` +
|
|
225
|
+
`aggregate cannot tell "I declined" from "I passed", so it reports acceptance. Output: ` +
|
|
226
|
+
`${JSON.stringify(output.slice(-300))}`,
|
|
227
|
+
);
|
|
228
|
+
assert.ok(
|
|
229
|
+
output.includes(LIVE_SKIP_MARKER),
|
|
230
|
+
`the skip must also be readable by a human in the log — ${LIVE_SKIP_MARKER} names the missing prerequisite so an ` +
|
|
231
|
+
"operator who hits a red --cut run knows what to supply. Output: " +
|
|
232
|
+
JSON.stringify(output.slice(-300)),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ===========================================================================
|
|
237
|
+
// 6) REAL propagation — a run.sh WRAPPER declining its own prerequisite. This
|
|
238
|
+
// is the second skip surface: some smokes never reach their .ts file at all.
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
{
|
|
241
|
+
const viaWrapper = runSubcommand("smoke-acp-cortex-live", { LIVE: undefined });
|
|
242
|
+
assert.equal(
|
|
243
|
+
viaWrapper.code,
|
|
244
|
+
LIVE_SKIP_EXIT,
|
|
245
|
+
`[QK:WRAPPER-SKIP-IS-PROTOCOL-EXIT] a run.sh smoke WRAPPER that declines a prerequisite must return the protocol ` +
|
|
246
|
+
`SKIP code, not 0 — got exit ${viaWrapper.code}. The wrapper is a skip surface of its own (cortex, matrix, ` +
|
|
247
|
+
"spawn-live and spawn-resume all decline before their .ts is ever reached), so fixing only the TypeScript " +
|
|
248
|
+
`half would leave the aggregate counting those as PASS. Output: ${JSON.stringify(viaWrapper.output.slice(-300))}`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// The measured Cortex cell: LIVE IS set, but the connection the adapter needs
|
|
252
|
+
// is not. Which branch declines (the wrapper's `cortex` PATH check or the
|
|
253
|
+
// smoke's own connection check) depends on the host; the OUTCOME must not.
|
|
254
|
+
const missingPrereq = runSubcommand("smoke-acp-cortex-live", {
|
|
255
|
+
LIVE: "1",
|
|
256
|
+
ENTWURF_ACP_CORTEX_CONNECTION: undefined,
|
|
257
|
+
});
|
|
258
|
+
assert.equal(
|
|
259
|
+
missingPrereq.code,
|
|
260
|
+
LIVE_SKIP_EXIT,
|
|
261
|
+
"LIVE=1 with no ENTWURF_ACP_CORTEX_CONNECTION must be a SKIP, not a PASS — this is the exact cell that made a " +
|
|
262
|
+
"cortex-less host look like cortex acceptance. Output: " +
|
|
263
|
+
JSON.stringify(missingPrereq.output.slice(-300)),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ===========================================================================
|
|
268
|
+
// 7) NO SILENT AGGREGATE OMISSION. The protocol tells the truth about the steps
|
|
269
|
+
// the gate RUNS; it says nothing about steps the gate never lists. Three LIVE
|
|
270
|
+
// smokes (cortex, spawn-live, claude-native-resume) sat outside the aggregate
|
|
271
|
+
// with no stated reason until 2026-07-31, so a green cut was silent about the
|
|
272
|
+
// second backend, the spawn substrate, and native resume.
|
|
273
|
+
//
|
|
274
|
+
// So: every LIVE smoke is either WIRED into release_gate or EXCLUDED for a
|
|
275
|
+
// reason an operator can read in the docs. The exclusion half is checked
|
|
276
|
+
// against the doc text, not against a list in this file — an exclusion that
|
|
277
|
+
// only this gate believes in is how the omission would come back.
|
|
278
|
+
// ===========================================================================
|
|
279
|
+
{
|
|
280
|
+
const runSh = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
281
|
+
const gateBody = runSh.slice(runSh.indexOf("release_gate() {"), runSh.indexOf("# 5. Summary"));
|
|
282
|
+
assert.ok(gateBody.length > 1000, "located the release_gate body");
|
|
283
|
+
|
|
284
|
+
// name → the sentence in the docs that carries its exclusion, and where.
|
|
285
|
+
const DOCUMENTED_EXCLUSIONS: Record<string, [file: string, sentence: string]> = {
|
|
286
|
+
"smoke-acp-long-turn-live": ["VERIFY.md", "on-demand, not part of `release-gate`"],
|
|
287
|
+
"smoke-agy-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own an agy conversation id"],
|
|
288
|
+
"smoke-acp-ordering-probe-live": ["docs/acp-backend-rail.md", "opt-in paired observation"],
|
|
289
|
+
// Cortex needs an external Snowflake connection the HOST owns, so an aggregate
|
|
290
|
+
// that required it would block every cut taken without that account. Excluded —
|
|
291
|
+
// NOT waived: its direct call stays required for a Cortex-rail cut, and running
|
|
292
|
+
// it without the connection still reports protocol SKIP rather than a pass.
|
|
293
|
+
"smoke-acp-cortex-live": ["VERIFY.md", "The 0.13.1 aggregate does not re-certify Cortex"],
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })
|
|
297
|
+
.map((p) => basename(p).replace(/\.(ts|sh)$/, ""))
|
|
298
|
+
.sort();
|
|
299
|
+
assert.ok(allLive.length >= 18, `expected the full LIVE smoke family, found ${allLive.length}`);
|
|
300
|
+
|
|
301
|
+
for (const name of allLive) {
|
|
302
|
+
const wired = gateBody.includes(`"$self" ${name}\n`) || gateBody.includes(`"$self" ${name} `);
|
|
303
|
+
const excused = DOCUMENTED_EXCLUSIONS[name];
|
|
304
|
+
if (wired) {
|
|
305
|
+
assert.ok(!excused, `${name} is both wired and excused — pick one`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
assert.ok(
|
|
309
|
+
excused,
|
|
310
|
+
`[QK:NO-SILENT-AGGREGATE-OMISSION] ${name} is neither wired into release_gate nor excluded in the docs. ` +
|
|
311
|
+
"A LIVE smoke that exists but is never listed makes a green cut silent about the axis it covers — exactly " +
|
|
312
|
+
"how cortex (the second shipped backend), spawn-live and claude-native-resume went unrun for releases. " +
|
|
313
|
+
"Wire it, or state the exclusion where an operator reads it.",
|
|
314
|
+
);
|
|
315
|
+
const [file, sentence] = excused as [string, string];
|
|
316
|
+
assert.ok(
|
|
317
|
+
readFileSync(join(REPO_DIR, file), "utf8").includes(sentence),
|
|
318
|
+
`${name} claims a documented exclusion, but ${file} no longer says "${sentence}" — an exclusion only this ` +
|
|
319
|
+
"gate believes in is not documented",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log(
|
|
325
|
+
"[check-release-gate-outcomes] ok — STEP OUTCOME protocol: one skip exit code shared by the shell and TS halves " +
|
|
326
|
+
`(${LIVE_SKIP_EXIT}, clear of the per-tool 0..4 and shell 126+ bands), classifier maps 0→PASS / skip→SKIP / ` +
|
|
327
|
+
"everything else→FAIL (never rounding a skip up), `--cut` refuses a MUST SKIP while a bare diagnostic run stays " +
|
|
328
|
+
"exit 0, no LIVE smoke still carries the pre-P1 exit-0 skip shape, and both real skip surfaces were INVOKED and " +
|
|
329
|
+
"observed to propagate the code: a smoke with LIVE unset (through run_ts, with its operator-readable marker) and " +
|
|
330
|
+
"a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
|
|
331
|
+
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry",
|
|
332
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The TypeScript half of the release-gate STEP OUTCOME protocol
|
|
2
|
+
// (scripts/lib/step-outcome.sh owns the contract prose and the shell half).
|
|
3
|
+
//
|
|
4
|
+
// A LIVE smoke that lacks a prerequisite must be DISTINGUISHABLE from one that
|
|
5
|
+
// ran and passed. Before P1 every smoke exited 0 on a skip, so `run_live_step`
|
|
6
|
+
// counted it PASS and the release summary could not prove a required call had
|
|
7
|
+
// happened at all. `skipLive` is the single exit any smoke takes when it
|
|
8
|
+
// declines: one reserved code the aggregate classifies as SKIP, plus one marker
|
|
9
|
+
// line so an operator reading the log sees WHICH prerequisite was missing
|
|
10
|
+
// without decoding a number.
|
|
11
|
+
//
|
|
12
|
+
// The two halves must agree. `check-release-gate-outcomes` reads this constant
|
|
13
|
+
// and the shell one and refuses a mismatch — a protocol that drifted between
|
|
14
|
+
// languages would silently reclassify every skip as a failure (or worse).
|
|
15
|
+
|
|
16
|
+
/** SKIP. Must equal `ENTWURF_STEP_SKIP_EXIT` in scripts/lib/step-outcome.sh. */
|
|
17
|
+
export const LIVE_SKIP_EXIT = 97;
|
|
18
|
+
|
|
19
|
+
/** The machine-greppable prefix every skip line carries. */
|
|
20
|
+
export const LIVE_SKIP_MARKER = "[entwurf:skip]";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Decline this smoke: print the marker + reason on stderr and exit with the
|
|
24
|
+
* protocol's SKIP code. Never returns.
|
|
25
|
+
*
|
|
26
|
+
* `label` is the smoke's own name (so a multi-step aggregate log stays
|
|
27
|
+
* attributable); `reason` states the missing prerequisite AND how to supply it —
|
|
28
|
+
* an operator who hits a SKIP in a cut run needs the fix, not the diagnosis.
|
|
29
|
+
*/
|
|
30
|
+
export function skipLive(label: string, reason: string): never {
|
|
31
|
+
console.error(`${LIVE_SKIP_MARKER} ${label} — ${reason}`);
|
|
32
|
+
process.exit(LIVE_SKIP_EXIT);
|
|
33
|
+
}
|
|
@@ -29,20 +29,29 @@ export interface ProbePhaseTimeouts {
|
|
|
29
29
|
initializeMs: number;
|
|
30
30
|
newSessionMs: number;
|
|
31
31
|
setModelMs: number;
|
|
32
|
-
promptMs: number;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// a D verdict is only readable against
|
|
38
|
-
// invent its own.
|
|
34
|
+
// BOOTSTRAP phase timeouts — MUST equal backend.ts's INITIALIZE/NEW_SESSION/
|
|
35
|
+
// SET_MODEL_TIMEOUT_MS (check-probe-ordering pins them against the source).
|
|
36
|
+
// §11-7: a bootstrap-phase D verdict is only readable against production's own
|
|
37
|
+
// boundaries, so the probe may not invent its own.
|
|
39
38
|
export const PROBE_PHASE_TIMEOUTS: ProbePhaseTimeouts = {
|
|
40
39
|
initializeMs: 30_000,
|
|
41
40
|
newSessionMs: 30_000,
|
|
42
41
|
setModelMs: 30_000,
|
|
43
|
-
promptMs: 600_000,
|
|
44
42
|
};
|
|
45
43
|
|
|
44
|
+
// The prompt phase has NO production counterpart to match: backend.ts ends a
|
|
45
|
+
// prompt on lifecycle events only (resolve / abort / child gone) and carries no
|
|
46
|
+
// wall-clock cutoff, by policy — an active turn is not a failed turn for being
|
|
47
|
+
// long. This value is therefore the MEASUREMENT HARNESS's own bounded
|
|
48
|
+
// observation horizon: it stops a probe RUN from hanging forever. It is not a
|
|
49
|
+
// production contract, not an equivalence, and not a deadline any §11-7 verdict
|
|
50
|
+
// is read against. Horizon expiry means the run's prompt window is
|
|
51
|
+
// INCONCLUSIVE — the artifact is preserved and read as "not measured", never as
|
|
52
|
+
// a model of production killing a turn.
|
|
53
|
+
export const PROBE_PROMPT_OBSERVATION_MS = 600_000;
|
|
54
|
+
|
|
46
55
|
/** The two adapter methods the turn drives — satisfied by the REAL claudeAdapter
|
|
47
56
|
* (emitted) in the LIVE runner and by recording stubs in the gate. */
|
|
48
57
|
export interface ProbeAdapterSeam {
|
|
@@ -108,7 +117,10 @@ export interface ProbeTurnParams {
|
|
|
108
117
|
adapter: ProbeAdapterSeam;
|
|
109
118
|
enrichMcpServers: ProbeMcpEnricher;
|
|
110
119
|
log: (event: ProbeEventName, payload?: Record<string, unknown>) => void;
|
|
120
|
+
/** Bootstrap boundaries (production-pinned). Defaults to PROBE_PHASE_TIMEOUTS. */
|
|
111
121
|
timeouts?: ProbePhaseTimeouts;
|
|
122
|
+
/** Harness observation horizon for the prompt phase — NOT a production deadline. */
|
|
123
|
+
promptObservationMs?: number;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
export interface ProbeTurnResult {
|
|
@@ -158,7 +170,7 @@ export async function driveProbeTurn(connection: AcpConnectionLike, params: Prob
|
|
|
158
170
|
}),
|
|
159
171
|
);
|
|
160
172
|
|
|
161
|
-
// _meta + wire servers exactly as backend.ts assembles them (S2g
|
|
173
|
+
// _meta + wire servers exactly as backend.ts assembles them (S2g / carrier-less shape): the
|
|
162
174
|
// injected production buildSessionMeta (omitted key when undefined) + the
|
|
163
175
|
// injected production envelope enrichment over the resolved config.
|
|
164
176
|
const sessionMeta = params.adapter.buildSessionMeta(
|
|
@@ -196,11 +208,20 @@ export async function driveProbeTurn(connection: AcpConnectionLike, params: Prob
|
|
|
196
208
|
}),
|
|
197
209
|
);
|
|
198
210
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
// The horizon below bounds THIS MEASUREMENT RUN, not the turn: production has
|
|
212
|
+
// no prompt deadline, so an expiry here is an inconclusive observation.
|
|
213
|
+
const promptObservationMs = params.promptObservationMs ?? PROBE_PROMPT_OBSERVATION_MS;
|
|
214
|
+
const promptResult = await runPhase(
|
|
215
|
+
"prompt",
|
|
216
|
+
PROBE_EVENTS.promptStart,
|
|
217
|
+
PROBE_EVENTS.promptEnd,
|
|
218
|
+
log,
|
|
219
|
+
promptObservationMs,
|
|
220
|
+
() =>
|
|
221
|
+
connection.prompt({
|
|
222
|
+
sessionId: acpSessionId,
|
|
223
|
+
prompt: [{ type: "text", text: params.promptText }],
|
|
224
|
+
}),
|
|
204
225
|
);
|
|
205
226
|
|
|
206
227
|
return { acpSessionId, stopReason: promptResult?.stopReason };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# Release-gate STEP OUTCOME protocol — the one place PASS / SKIP / FAIL is decided.
|
|
3
|
+
#
|
|
4
|
+
# SOURCE-ONLY. It carries the executable bit because check-pack requires every
|
|
5
|
+
# shipped `.sh` to have one (a blanket packaging invariant worth more than an
|
|
6
|
+
# exception carved for this file), not because anyone should run it.
|
|
7
|
+
#
|
|
8
|
+
# Sourced by run.sh's `release_gate`; sourced directly by
|
|
9
|
+
# scripts/check-release-gate-outcomes.ts so the truth table is exercised as a
|
|
10
|
+
# unit instead of being re-derived by eye from the aggregate's output.
|
|
11
|
+
#
|
|
12
|
+
# WHY A PROTOCOL AND NOT A PER-BACKEND BRANCH (P1, 2026-07-31). The aggregate's
|
|
13
|
+
# prose said a cut needs `LIVE=1` and `SKIP=0`, but its exit authority read only
|
|
14
|
+
# the FAIL counter, so `./run.sh release-gate` returned 0 with 14 SKIPs and the
|
|
15
|
+
# summary still printed "all green". Worse, a step that WAS invoked could decide
|
|
16
|
+
# it lacked a prerequisite and exit 0 — Cortex with no
|
|
17
|
+
# `ENTWURF_ACP_CORTEX_CONNECTION` is the measured case — and the aggregate
|
|
18
|
+
# counted that as PASS. Both holes have the same shape: a skip that cannot be
|
|
19
|
+
# told from an acceptance. So the fix is one outcome protocol every step speaks,
|
|
20
|
+
# never a special case per backend or per prerequisite.
|
|
21
|
+
#
|
|
22
|
+
# EXIT CONTRACT (house style: the verdict IS the exit code)
|
|
23
|
+
# 0 PASS — the step ran and its assertions held.
|
|
24
|
+
# ENTWURF_STEP_SKIP_EXIT SKIP — the step declined a prerequisite it does
|
|
25
|
+
# not have. NOT acceptance, and never rounded up.
|
|
26
|
+
# anything else FAIL — including a crash or a signal death.
|
|
27
|
+
#
|
|
28
|
+
# WHY 97. The repo already spends 0..4 on per-tool exit contracts that mean
|
|
29
|
+
# different things per tool (meta-bridge-fresh-cut's 3 = "cut transition
|
|
30
|
+
# incomplete", meta-bridge-store-doctor's 3 = "unreadable store", dev-bin's 3 =
|
|
31
|
+
# "refused a foreign link"), and 126+ belongs to the shell. A LIVE smoke shells
|
|
32
|
+
# out to some of those tools, so reusing a low number would let a dependency's
|
|
33
|
+
# unrelated verdict surface as a false SKIP — the exact confusion this protocol
|
|
34
|
+
# exists to remove. 97 is reserved for THIS cross-tool protocol and nothing else.
|
|
35
|
+
ENTWURF_STEP_SKIP_EXIT=97
|
|
36
|
+
|
|
37
|
+
# Classify one step's exit code. Prints PASS | SKIP | FAIL on stdout.
|
|
38
|
+
entwurf_step_outcome() {
|
|
39
|
+
case "${1:-}" in
|
|
40
|
+
0) printf 'PASS\n' ;;
|
|
41
|
+
"$ENTWURF_STEP_SKIP_EXIT") printf 'SKIP\n' ;;
|
|
42
|
+
*) printf 'FAIL\n' ;;
|
|
43
|
+
esac
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# The release authority. Returns 0 when the run may be read as a cut floor.
|
|
47
|
+
#
|
|
48
|
+
# $1 MUST fail count $2 MUST skip count $3 cut mode (1 = a real cut)
|
|
49
|
+
#
|
|
50
|
+
# A FAIL always blocks. A SKIP blocks ONLY in cut mode: an ordinary diagnostic
|
|
51
|
+
# `./run.sh release-gate <dir>` must stay runnable unattended and still exit 0
|
|
52
|
+
# while honestly reporting what it did not call. `--cut` is the executable half
|
|
53
|
+
# of "a CUT needs LIVE=1, SKIP=0" — and it needs no separate LIVE assertion,
|
|
54
|
+
# because with LIVE unset every LIVE-gated step skips and the skip count blocks.
|
|
55
|
+
entwurf_release_releasable() {
|
|
56
|
+
local failc="${1:-0}" skipc="${2:-0}" cut="${3:-0}"
|
|
57
|
+
[ "$failc" -gt 0 ] && return 1
|
|
58
|
+
if [ "$cut" = "1" ] && [ "$skipc" -gt 0 ]; then
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# The same decision as ONE greppable token, because "why is this red" must be
|
|
65
|
+
# machine-readable and not parsed out of prose.
|
|
66
|
+
#
|
|
67
|
+
# $1 MUST fail count $2 MUST skip count $3 cut mode (1 = a real cut)
|
|
68
|
+
#
|
|
69
|
+
# A step that RAN AND BROKE and a step that NEVER RAN are different facts, and a
|
|
70
|
+
# release record that blurs them is worthless: the first is a defect to fix, the
|
|
71
|
+
# second is a prerequisite to supply. So the counters are never fudged either —
|
|
72
|
+
# a policy block keeps `FAIL=0 SKIP=n` and says `BLOCKED (MUST SKIP)`. Reporting
|
|
73
|
+
# a synthetic `FAIL=1` for the policy block would destroy exactly the
|
|
74
|
+
# distinction this protocol exists to create.
|
|
75
|
+
entwurf_release_verdict() {
|
|
76
|
+
local failc="${1:-0}" skipc="${2:-0}" cut="${3:-0}"
|
|
77
|
+
if [ "$failc" -gt 0 ]; then
|
|
78
|
+
printf 'cut: BLOCKED (MUST FAIL)\n'
|
|
79
|
+
elif [ "$cut" = "1" ] && [ "$skipc" -gt 0 ]; then
|
|
80
|
+
printf 'cut: BLOCKED (MUST SKIP)\n'
|
|
81
|
+
elif [ "$cut" = "1" ]; then
|
|
82
|
+
printf 'cut: OK\n'
|
|
83
|
+
elif [ "$skipc" -gt 0 ]; then
|
|
84
|
+
printf 'cut: n/a (diagnostic, %s SKIP)\n' "$skipc"
|
|
85
|
+
else
|
|
86
|
+
printf 'cut: n/a (diagnostic)\n'
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
@@ -25,6 +25,112 @@
|
|
|
25
25
|
"timeoutSeconds": 60,
|
|
26
26
|
"signature": "[QK:AUGMENT-TRUNC-MARKER]",
|
|
27
27
|
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"claim": "CARRIER-PROVENANCE-STATED",
|
|
31
|
+
"title": "the augment stops saying what it is — the model is back to reading a first-user prepend as its system prompt (the measured 2026-07-30 misattribution)",
|
|
32
|
+
"subject": "pi-extensions/lib/acp/augment.ts",
|
|
33
|
+
"find": [
|
|
34
|
+
"\t\"This block is prepended to the FIRST USER MESSAGE of this session. It is not your system prompt.\";"
|
|
35
|
+
],
|
|
36
|
+
"replace": ["\t\"\";"],
|
|
37
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
38
|
+
"timeoutSeconds": 60,
|
|
39
|
+
"signature": "[QK:CARRIER-PROVENANCE-STATED]",
|
|
40
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"claim": "CARRIER-FRAME-NAMES-CLAUDE-CARRIER",
|
|
44
|
+
"title": "the claude frame stops naming its real engraving-only carrier — the model is told what its system prompt is NOT, but never what it IS",
|
|
45
|
+
"subject": "pi-extensions/lib/acp/augment.ts",
|
|
46
|
+
"find": [
|
|
47
|
+
"\t\t\t\t\t`The ${params.backend} rail does have a system-prompt carrier (\\`_meta.systemPrompt\\`), and entwurf keeps it deliberately tiny: it carries the operator engraving only — never this narrative, never AGENTS.md.`,"
|
|
48
|
+
],
|
|
49
|
+
"replace": ["\t\t\t\t\t`The ${params.backend} rail carries no system-prompt carrier at all.`,"],
|
|
50
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
51
|
+
"timeoutSeconds": 60,
|
|
52
|
+
"signature": "[QK:CARRIER-FRAME-NAMES-CLAUDE-CARRIER]",
|
|
53
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"claim": "CARRIER-FRAME-DENIES-CORTEX-CARRIER",
|
|
57
|
+
"title": "the carrier-less rail is told it has a tiny system-prompt carrier — a flat lie, since cortex's buildSessionMeta sends no _meta at all",
|
|
58
|
+
"subject": "pi-extensions/lib/acp/augment.ts",
|
|
59
|
+
"find": [
|
|
60
|
+
"\t\t\t\t\t`The ${params.backend} rail carries no system-prompt carrier at all — entwurf sends no \\`_meta.systemPrompt\\`, so everything entwurf tells you, including any operator engraving above, arrives here as user-message text.`,"
|
|
61
|
+
],
|
|
62
|
+
"replace": [
|
|
63
|
+
"\t\t\t\t\t`The ${params.backend} rail does have a system-prompt carrier (\\`_meta.systemPrompt\\`), and entwurf keeps it deliberately tiny: it carries the operator engraving only.`,"
|
|
64
|
+
],
|
|
65
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
66
|
+
"timeoutSeconds": 60,
|
|
67
|
+
"signature": "[QK:CARRIER-FRAME-DENIES-CORTEX-CARRIER]",
|
|
68
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"claim": "CARRIER-RAIL-DIFF-IS-SOURCE-PINNED",
|
|
72
|
+
"title": "cortex grows a session carrier while the augment still tells it there is none — the frame becomes a lie and no prose check would notice",
|
|
73
|
+
"subject": "pi-extensions/lib/acp/backend-adapter.ts",
|
|
74
|
+
"find": ["\tbuildSessionMeta() {\n\t\treturn undefined;\n\t},"],
|
|
75
|
+
"replace": ["\tbuildSessionMeta() {\n\t\treturn { systemPrompt: \"x\" };\n\t},"],
|
|
76
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
77
|
+
"timeoutSeconds": 60,
|
|
78
|
+
"signature": "[QK:CARRIER-RAIL-DIFF-IS-SOURCE-PINNED]",
|
|
79
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"claim": "CARRIER-LEADS-ITS-OWN-BLOCK",
|
|
83
|
+
"title": "the carrier loses its leading boundary — the Claude SDK's fixed sentence swallows the operator engraving again (the measured 2026-07-31 A-join)",
|
|
84
|
+
"subject": "pi-extensions/lib/acp/engraving.ts",
|
|
85
|
+
"find": ["export const CARRIER_LEAD_SEPARATOR = \"\\n\\n\";"],
|
|
86
|
+
"replace": ["export const CARRIER_LEAD_SEPARATOR = \"\";"],
|
|
87
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
88
|
+
"timeoutSeconds": 60,
|
|
89
|
+
"signature": "[QK:CARRIER-LEADS-ITS-OWN-BLOCK]",
|
|
90
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"claim": "CARRIER-OPT-OUT-SURVIVES-SEPARATOR",
|
|
94
|
+
"title": "the boundary is attached before the emptiness test — an emptied engraving file stops being the opt-out and becomes a whitespace carrier that still replaces the claude_code preset",
|
|
95
|
+
"subject": "pi-extensions/lib/acp/engraving.ts",
|
|
96
|
+
"find": [
|
|
97
|
+
"\tconst body = interpolate(source, params).trim();",
|
|
98
|
+
"\tif (body.length === 0) return \"\";",
|
|
99
|
+
"\treturn `${CARRIER_LEAD_SEPARATOR}${body}`;"
|
|
100
|
+
],
|
|
101
|
+
"replace": [
|
|
102
|
+
"\tconst body = `${CARRIER_LEAD_SEPARATOR}${interpolate(source, params).trim()}`;",
|
|
103
|
+
"\tif (body.length === 0) return \"\";",
|
|
104
|
+
"\treturn body;"
|
|
105
|
+
],
|
|
106
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
107
|
+
"timeoutSeconds": 60,
|
|
108
|
+
"signature": "[QK:CARRIER-OPT-OUT-SURVIVES-SEPARATOR]",
|
|
109
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"claim": "CARRIER-STAYS-TINY",
|
|
113
|
+
"title": "the shipped carrier grows into a real prompt — size, not shape, is what routes an OAuth subscription call to metered extra usage",
|
|
114
|
+
"subject": "pi-extensions/lib/acp/prompts/engraving.md",
|
|
115
|
+
"find": ["# Engraving Here"],
|
|
116
|
+
"replace": [
|
|
117
|
+
"# Engraving Here\n\n## Operator standing orders\n\n- Address the operator by name and keep the reply language matched to the prompt.\n- Prefer surgical edits over rewrites; verify every claim you make about behavior.\n- Never bypass a commit hook, and never push without an explicit request.\n- Indentation is tabs unless the project's linter says otherwise.\n- When docs and behavior disagree, report it and fix the source of truth.\n- Long-running commands belong in tmux, not in a foreground shell.\n- Record the next concrete move before you stop working.\n\n## Project context\n\nThis checkout is the bridge itself, so treat the adapter contract in the rail doc\nas authoritative and re-read it before touching a gate. The qualification lane is\nthe oracle for every contract claim; a green assertion count proves nothing on\nits own."
|
|
118
|
+
],
|
|
119
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
120
|
+
"timeoutSeconds": 60,
|
|
121
|
+
"signature": "[QK:CARRIER-STAYS-TINY]",
|
|
122
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"claim": "CARRIER-META-SENDS-CARRIER-VERBATIM",
|
|
126
|
+
"title": "the meta hop normalizes the carrier — the boundary is stripped back off on the wire while bridgeConfigSignature still folds the bounded value, so the A-join returns and reuse keys on a string the backend never received",
|
|
127
|
+
"subject": "pi-extensions/lib/acp/tool-surface.ts",
|
|
128
|
+
"find": ["\t\tmeta.systemPrompt = normalizedSystemPrompt;"],
|
|
129
|
+
"replace": ["\t\tmeta.systemPrompt = normalizedSystemPrompt.trim();"],
|
|
130
|
+
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
131
|
+
"timeoutSeconds": 60,
|
|
132
|
+
"signature": "[QK:CARRIER-META-SENDS-CARRIER-VERBATIM]",
|
|
133
|
+
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
28
134
|
}
|
|
29
135
|
]
|
|
30
136
|
}
|