@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,452 @@
|
|
|
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 run check:full.
|
|
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 and matrix ` +
|
|
247
|
+
"both 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. (spawn-live was
|
|
273
|
+
// deleted with its transport in the visible-first cut; the incident is kept
|
|
274
|
+
// verbatim because it is what this rule was derived from.)
|
|
275
|
+
//
|
|
276
|
+
// So: every LIVE smoke is either WIRED into release_gate or EXCLUDED for a
|
|
277
|
+
// reason an operator can read in the docs. The exclusion half is checked
|
|
278
|
+
// against the doc text, not against a list in this file — an exclusion that
|
|
279
|
+
// only this gate believes in is how the omission would come back.
|
|
280
|
+
// ===========================================================================
|
|
281
|
+
{
|
|
282
|
+
const runSh = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
283
|
+
const gateBody = runSh.slice(runSh.indexOf("release_gate() {"), runSh.indexOf("# 5. Summary"));
|
|
284
|
+
assert.ok(gateBody.length > 1000, "located the release_gate body");
|
|
285
|
+
|
|
286
|
+
// name → the sentence in the docs that carries its exclusion, and where.
|
|
287
|
+
const DOCUMENTED_EXCLUSIONS: Record<string, [file: string, sentence: string]> = {
|
|
288
|
+
"smoke-acp-long-turn-live": ["VERIFY.md", "on-demand, not part of `release-gate`"],
|
|
289
|
+
"smoke-mux-fresh-call-live": ["VERIFY.md", "Fresh-call LIVE is on-demand, not part of `release-gate`"],
|
|
290
|
+
"smoke-agy-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own an agy conversation id"],
|
|
291
|
+
"smoke-acp-ordering-probe-live": ["docs/acp-backend-rail.md", "opt-in paired observation"],
|
|
292
|
+
// Cortex needs an external Snowflake connection the HOST owns, so an aggregate
|
|
293
|
+
// that required it would block every cut taken without that account. Excluded —
|
|
294
|
+
// NOT waived: its direct call stays required for a Cortex-rail cut, and running
|
|
295
|
+
// it without the connection still reports protocol SKIP rather than a pass.
|
|
296
|
+
"smoke-acp-cortex-live": ["VERIFY.md", "The release aggregate does not re-certify Cortex"],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })
|
|
300
|
+
.map((p) => basename(p).replace(/\.(ts|sh)$/, ""))
|
|
301
|
+
.sort();
|
|
302
|
+
assert.ok(allLive.length >= 18, `expected the full LIVE smoke family, found ${allLive.length}`);
|
|
303
|
+
|
|
304
|
+
for (const name of allLive) {
|
|
305
|
+
const wired = gateBody.includes(`"$self" ${name}\n`) || gateBody.includes(`"$self" ${name} `);
|
|
306
|
+
const excused = DOCUMENTED_EXCLUSIONS[name];
|
|
307
|
+
if (wired) {
|
|
308
|
+
assert.ok(!excused, `${name} is both wired and excused — pick one`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
assert.ok(
|
|
312
|
+
excused,
|
|
313
|
+
`[QK:NO-SILENT-AGGREGATE-OMISSION] ${name} is neither wired into release_gate nor excluded in the docs. ` +
|
|
314
|
+
"A LIVE smoke that exists but is never listed makes a green cut silent about the axis it covers — exactly " +
|
|
315
|
+
"how cortex (the second shipped backend) and claude-native-resume went unrun for releases. " +
|
|
316
|
+
"Wire it, or state the exclusion where an operator reads it.",
|
|
317
|
+
);
|
|
318
|
+
const [file, sentence] = excused as [string, string];
|
|
319
|
+
assert.ok(
|
|
320
|
+
readFileSync(join(REPO_DIR, file), "utf8").includes(sentence),
|
|
321
|
+
`${name} claims a documented exclusion, but ${file} no longer says "${sentence}" — an exclusion only this ` +
|
|
322
|
+
"gate believes in is not documented",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
// 8. qualification scheduling topology — the subtraction has its own oracle.
|
|
329
|
+
// check-gate-qualification left the default check chains (operator
|
|
330
|
+
// inner-loop cost, 2026-08 subtraction). That move is a gate/release
|
|
331
|
+
// contract: the step must stay REACHABLE on the axes that now own it — the
|
|
332
|
+
// CI check job on every push and release_gate as its own MUST step — and
|
|
333
|
+
// must not silently return to the default chain. Without this cell,
|
|
334
|
+
// deleting the release_gate qualification block or the CI line leaves every
|
|
335
|
+
// focused gate green while a cut quietly loses its discriminating-power
|
|
336
|
+
// step.
|
|
337
|
+
//
|
|
338
|
+
// TWO claims, because #70 added a second independent contract here. 8a is
|
|
339
|
+
// REACHABILITY (absent from the default chain; present exactly once in CI and
|
|
340
|
+
// exactly once as a wired release_gate MUST step; named in VERIFY). 8b is what
|
|
341
|
+
// the CI step qualifies (the FULL floor, before the qualification run). Each
|
|
342
|
+
// carries its own replant — one mutant must never stand in for both.
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
{
|
|
345
|
+
// The default chain is tiered (#70): `check` (core) and `check:full` compose the
|
|
346
|
+
// named group scripts, so the reachability scan covers EVERY check* script —
|
|
347
|
+
// qualification sneaking into any group re-doubles the closure floor.
|
|
348
|
+
const pkgScripts = (
|
|
349
|
+
JSON.parse(readFileSync(join(REPO_DIR, "package.json"), "utf8")) as { scripts: Record<string, string> }
|
|
350
|
+
).scripts;
|
|
351
|
+
const pkgCheck = Object.entries(pkgScripts)
|
|
352
|
+
.filter(([name]) => name === "check" || name.startsWith("check:"))
|
|
353
|
+
.map(([, body]) => body)
|
|
354
|
+
.join(" && ");
|
|
355
|
+
const ciYml = readFileSync(join(REPO_DIR, ".github/workflows/ci.yml"), "utf8");
|
|
356
|
+
const ciHits = ciYml.split("- run: ./run.sh check-gate-qualification").length - 1;
|
|
357
|
+
const runShQ = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
358
|
+
const qualGateBody = runShQ.slice(runShQ.indexOf("release_gate() {"), runShQ.indexOf("# 5. Summary"));
|
|
359
|
+
const invocations = qualGateBody.split('bash "$self" check-gate-qualification').length - 1;
|
|
360
|
+
const verifyDoc = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
361
|
+
|
|
362
|
+
// One claim token, one assert (the qualification runner requires the exact-once
|
|
363
|
+
// signature); each broken axis names itself in the joined message.
|
|
364
|
+
const holes: string[] = [];
|
|
365
|
+
if (pkgCheck.includes("check-gate-qualification"))
|
|
366
|
+
holes.push(
|
|
367
|
+
"package.json's default `check` chain contains check-gate-qualification again (doubles every closure floor)",
|
|
368
|
+
);
|
|
369
|
+
if (ciHits !== 1) holes.push(`the CI check job runs check-gate-qualification ${ciHits}x (need exactly once)`);
|
|
370
|
+
if (invocations !== 1)
|
|
371
|
+
holes.push(`release_gate invokes check-gate-qualification ${invocations}x (need exactly one MUST step)`);
|
|
372
|
+
if (
|
|
373
|
+
!qualGateBody.includes('results+=("PASS check-gate-qualification")') ||
|
|
374
|
+
!qualGateBody.includes('results+=("FAIL check-gate-qualification")')
|
|
375
|
+
)
|
|
376
|
+
holes.push("the release_gate qualification step does not wire PASS/FAIL into the MUST counters");
|
|
377
|
+
if (!verifyDoc.includes("in the CI `check` job on every push, and as a release-gate MUST step"))
|
|
378
|
+
holes.push("VERIFY.md no longer names the owners of the moved qualification step");
|
|
379
|
+
assert.ok(
|
|
380
|
+
holes.length === 0,
|
|
381
|
+
"[QK:QUALIFICATION-SCHEDULING-REACHABLE] check-gate-qualification left the default check chains " +
|
|
382
|
+
"deliberately, so it must stay REACHABLE on the axes that own it now — absent from the default chain, " +
|
|
383
|
+
"exactly once in the CI check job, exactly once as a release_gate MUST step with its " +
|
|
384
|
+
`outcome wired, and named in VERIFY. Broken: ${holes.join("; ")}`,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// 8b. WHAT the CI qualification step qualifies — its own claim, not a branch
|
|
388
|
+
// folded into the one above. #70 split the deterministic floor into tiers,
|
|
389
|
+
// so "CI runs qualification once" stopped being sufficient evidence on its
|
|
390
|
+
// own: qualification on top of the ≤60s core would certify kill-power over
|
|
391
|
+
// a floor no candidate ships on. This is ONE contract with one condition —
|
|
392
|
+
// the full floor is present in the CI check job AND qualification follows
|
|
393
|
+
// it — because a floor that is absent and a floor that runs afterwards
|
|
394
|
+
// break the same promise identically. The committed replant qualifies the
|
|
395
|
+
// downgrade/omission axis (the tier rename downgrading CI back to core);
|
|
396
|
+
// ordering stays directly asserted here and has no incident-earned mutant.
|
|
397
|
+
const ciFloorAt = ciYml.indexOf("- run: pnpm run check:full");
|
|
398
|
+
const ciQualAt = ciYml.indexOf("- run: ./run.sh check-gate-qualification");
|
|
399
|
+
assert.ok(
|
|
400
|
+
ciFloorAt !== -1 && ciQualAt > ciFloorAt,
|
|
401
|
+
"[QK:CI-FULL-FLOOR-QUALIFIED] the CI check job owns the FULL deterministic floor (#70): it must run " +
|
|
402
|
+
"`pnpm run check:full` — not the ≤60s core — and check-gate-qualification must come AFTER it, so the " +
|
|
403
|
+
"kill-power proof covers the floor a candidate actually ships on. The committed replant qualifies the " +
|
|
404
|
+
"downgrade/omission axis; ordering is directly asserted by this same oracle. " +
|
|
405
|
+
`Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ===========================================================================
|
|
410
|
+
// The integrated mux lifecycle is a MUST, and the focused fresh-call LIVE is not
|
|
411
|
+
//
|
|
412
|
+
// Two LIVE smokes now cover the mux rail and they are NOT interchangeable:
|
|
413
|
+
// `smoke-mux-fresh-call-live` drives the composition from SOURCE and is on-demand,
|
|
414
|
+
// while `smoke-mux-lifecycle-live` enters through the real MCP surface and follows a
|
|
415
|
+
// citizen through resume and recall. A cut that ran only the focused one would be
|
|
416
|
+
// green about the axis nobody proved. So the MUST wiring is pinned here, exactly
|
|
417
|
+
// once, together with the doc sentence that keeps the two apart for an operator.
|
|
418
|
+
// ===========================================================================
|
|
419
|
+
{
|
|
420
|
+
const runSh = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
421
|
+
const gateBody = runSh.slice(runSh.indexOf("release_gate() {"), runSh.indexOf("# 5. Summary"));
|
|
422
|
+
const verify = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
423
|
+
const mustSteps = gateBody.split("\n").filter((l) => l.includes('"$self" smoke-mux-lifecycle-live'));
|
|
424
|
+
const gaps: string[] = [];
|
|
425
|
+
if (mustSteps.length !== 1)
|
|
426
|
+
gaps.push(`release_gate runs smoke-mux-lifecycle-live ${mustSteps.length}x (need exactly one MUST step)`);
|
|
427
|
+
if (mustSteps.length === 1 && !mustSteps[0]?.includes("run_live_step"))
|
|
428
|
+
gaps.push("the lifecycle step does not go through run_live_step, so its SKIP would not reach the classifier");
|
|
429
|
+
if (!verify.includes("Fresh-call LIVE is on-demand, not part of `release-gate`"))
|
|
430
|
+
gaps.push("VERIFY.md no longer excuses the focused fresh-call LIVE from the aggregate");
|
|
431
|
+
if (!verify.includes("smoke-mux-lifecycle-live"))
|
|
432
|
+
gaps.push("VERIFY.md does not name the integrated lifecycle MUST at all");
|
|
433
|
+
assert.ok(
|
|
434
|
+
gaps.length === 0,
|
|
435
|
+
"[QK:MUX-LIFECYCLE-IS-RELEASE-MUST] the integrated mux lifecycle LIVE must be a release-gate MUST exactly once " +
|
|
436
|
+
"and go through run_live_step (so a missing prerequisite is a SKIP the cut refuses, never a silent pass), " +
|
|
437
|
+
"while the focused fresh-call LIVE stays on-demand and VERIFY keeps the two distinguishable — a cut that " +
|
|
438
|
+
`ran only the source-level smoke would be green about the surface no one entered. Broken: ${gaps.join("; ")}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log(
|
|
443
|
+
"[check-release-gate-outcomes] ok — STEP OUTCOME protocol: one skip exit code shared by the shell and TS halves " +
|
|
444
|
+
`(${LIVE_SKIP_EXIT}, clear of the per-tool 0..4 and shell 126+ bands), classifier maps 0→PASS / skip→SKIP / ` +
|
|
445
|
+
"everything else→FAIL (never rounding a skip up), `--cut` refuses a MUST SKIP while a bare diagnostic run stays " +
|
|
446
|
+
"exit 0, no LIVE smoke still carries the pre-P1 exit-0 skip shape, and both real skip surfaces were INVOKED and " +
|
|
447
|
+
"observed to propagate the code: a smoke with LIVE unset (through run_ts, with its operator-readable marker) and " +
|
|
448
|
+
"a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
|
|
449
|
+
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
|
|
450
|
+
"check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
|
|
451
|
+
"exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it",
|
|
452
|
+
);
|