@junghanacs/entwurf 0.32.0 → 0.34.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 +4 -4
- package/CHANGELOG.md +103 -0
- package/README.md +57 -119
- package/VERIFY.md +25 -12
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +37 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +1 -1
- package/package.json +10 -10
- package/pi/pi-durable/bootstrap.mjs +111 -11
- package/pi/pi-durable/carrier/runtime.js +11 -14
- package/pi/pi-durable/carrier/tui.js +76 -11
- package/pi/pi-durable/overlay/runtime-contacts.patch +199 -7
- package/pi/pi-durable/overlay/upstream-pin.json +15 -15
- package/pi-extensions/meta-bridge-pi-durable.ts +4 -2
- package/run.sh +83 -53
- package/scripts/check-gate-qualification.ts +60 -3
- package/scripts/check-release-gate-outcomes.ts +22 -18
- package/scripts/emit-pi-durable-carrier.ts +61 -21
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/gate-qualification.json +24 -0
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +178 -11
- package/scripts/mutants/pi-floor.json +6 -6
- package/scripts/mutants/release-gate.json +5 -3
|
@@ -49,7 +49,7 @@ import { createHash } from "node:crypto";
|
|
|
49
49
|
import * as fs from "node:fs";
|
|
50
50
|
import * as os from "node:os";
|
|
51
51
|
import * as path from "node:path";
|
|
52
|
-
import { fileURLToPath } from "node:url";
|
|
52
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
53
53
|
import {
|
|
54
54
|
classifyMutantRun,
|
|
55
55
|
countOccurrences,
|
|
@@ -1178,6 +1178,63 @@ function buildDeclaredOutputFixture(): string {
|
|
|
1178
1178
|
parsedCompose.expectPath === "/e.txt",
|
|
1179
1179
|
);
|
|
1180
1180
|
|
|
1181
|
+
// The operator's door to this parser is `run.sh check-gate-qualification` (#133): it
|
|
1182
|
+
// once dropped every argument, so a requested PARTIAL started the whole body. The
|
|
1183
|
+
// cells run a byte copy of this checkout's run.sh whose runner is a fixture: the REAL
|
|
1184
|
+
// parser, then a print where the body would start — so neither a refusal nor a mutated
|
|
1185
|
+
// wrapper can launch a body or a snapshot from here. Mutation acceptance stays the
|
|
1186
|
+
// runner's own; this proves only the door's control flow.
|
|
1187
|
+
const door = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualification-door-")));
|
|
1188
|
+
fs.mkdirSync(path.join(door, "scripts"));
|
|
1189
|
+
fs.copyFileSync(path.join(REPO_DIR, "run.sh"), path.join(door, "run.sh"));
|
|
1190
|
+
const parserUrl = pathToFileURL(path.join(REPO_DIR, "scripts", "lib", "qualification-receipt.ts")).href;
|
|
1191
|
+
fs.writeFileSync(
|
|
1192
|
+
path.join(door, "scripts", "check-gate-qualification.ts"),
|
|
1193
|
+
[
|
|
1194
|
+
`import { parseQualificationArgs } from ${JSON.stringify(parserUrl)};`,
|
|
1195
|
+
"const argv = process.argv.slice(2);",
|
|
1196
|
+
"let parsed;",
|
|
1197
|
+
"try { parsed = parseQualificationArgs(argv); }",
|
|
1198
|
+
'catch (err) { process.stderr.write("refused: " + (err instanceof Error ? err.message : String(err)) + "\\n"); process.exit(2); }',
|
|
1199
|
+
'process.stdout.write(JSON.stringify({ argv, parsed }) + "\\n");',
|
|
1200
|
+
"",
|
|
1201
|
+
].join("\n"),
|
|
1202
|
+
);
|
|
1203
|
+
const viaDoor = (argv: string[]) => {
|
|
1204
|
+
const r = spawnSync("bash", [path.join(door, "run.sh"), "check-gate-qualification", ...argv], {
|
|
1205
|
+
cwd: door,
|
|
1206
|
+
encoding: "utf8",
|
|
1207
|
+
env: { PATH: process.env.PATH ?? "", HOME: door },
|
|
1208
|
+
timeout: 30_000,
|
|
1209
|
+
});
|
|
1210
|
+
const started = r.stdout.trim() === "" ? undefined : JSON.parse(r.stdout);
|
|
1211
|
+
return { status: r.status, stderr: r.stderr, started };
|
|
1212
|
+
};
|
|
1213
|
+
const requested = ["--group", '["bash","run.sh","check pi *"]', "--receipt", "/x/r.json"];
|
|
1214
|
+
const partial = viaDoor(requested);
|
|
1215
|
+
const bare = viaDoor([]);
|
|
1216
|
+
ok(
|
|
1217
|
+
"[QK:QUALIFY-DOOR-FORWARDS-ARGV] run.sh check-gate-qualification hands --group/--receipt to the parser unchanged (a space and a glob inside the argv JSON included) and the parser reads one PARTIAL group; called bare it still reads the whole body — the prepared SHA's CI call",
|
|
1218
|
+
partial.status === 0 &&
|
|
1219
|
+
JSON.stringify(partial.started?.argv) === JSON.stringify(requested) &&
|
|
1220
|
+
partial.started?.parsed.mode === "group" &&
|
|
1221
|
+
partial.started.parsed.receiptPath === "/x/r.json" &&
|
|
1222
|
+
bare.status === 0 &&
|
|
1223
|
+
JSON.stringify(bare.started?.argv) === "[]" &&
|
|
1224
|
+
bare.started?.parsed.mode === "full",
|
|
1225
|
+
);
|
|
1226
|
+
const doorRefusals: Array<[string[], string]> = [
|
|
1227
|
+
[["--group", '["bash","run.sh","check-x"]'], "--group needs --receipt"],
|
|
1228
|
+
[["--group", '["a"]', "--group", '["a"]', "--receipt", "/x/r.json"], "repeated flag"],
|
|
1229
|
+
[["--grop", '["a"]', "--receipt", "/x/r.json"], "unknown argument"],
|
|
1230
|
+
[["--group", "check-x", "--receipt", "/x/r.json"], "JSON array"],
|
|
1231
|
+
];
|
|
1232
|
+
const refused = doorRefusals.map(([argv, naming]) => ({ naming, run: viaDoor(argv) }));
|
|
1233
|
+
ok(
|
|
1234
|
+
"[QK:QUALIFY-DOOR-REFUSES-BEFORE-BODY] through run.sh, a missing --receipt, a repeated flag, a misspelled flag and a non-array --group are each refused by name and nothing starts — never a whole body in place of the PARTIAL that was asked for",
|
|
1235
|
+
refused.every(({ naming, run }) => run.status === 2 && run.stderr.includes(naming) && run.started === undefined),
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1181
1238
|
const tmpRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-receipt-selftest-")));
|
|
1182
1239
|
const { dir: origin, externalTarget } = buildFixtureOrigin();
|
|
1183
1240
|
try {
|
|
@@ -3405,12 +3462,12 @@ let manifestCount: number;
|
|
|
3405
3462
|
"pi-bridge-sender": 4,
|
|
3406
3463
|
"pi-mcp-bridge": 2,
|
|
3407
3464
|
"pi-mcp-register": 3,
|
|
3408
|
-
"pi-durable":
|
|
3465
|
+
"pi-durable": 58,
|
|
3409
3466
|
"copilot-receive": 20,
|
|
3410
3467
|
"entwurf-peers": 1,
|
|
3411
3468
|
"fresh-call-dispatch": 11,
|
|
3412
3469
|
"fresh-cut": 3,
|
|
3413
|
-
"gate-qualification":
|
|
3470
|
+
"gate-qualification": 10,
|
|
3414
3471
|
"herdr-placement": 13,
|
|
3415
3472
|
"herdr-plugin": 25,
|
|
3416
3473
|
"herdr-plugin-profile": 14,
|
|
@@ -358,16 +358,18 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
358
358
|
// check-gate-qualification left the default check chains (operator
|
|
359
359
|
// inner-loop cost, 2026-08 subtraction). That move is a gate/release
|
|
360
360
|
// contract: the step must stay REACHABLE on the axes that now own it — the
|
|
361
|
-
// CI check job (on the pushes #103's filter still sends it) and
|
|
362
|
-
//
|
|
363
|
-
// must not silently return to the default chain
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
361
|
+
// CI check job (on the pushes #103's filter still sends it) and, for a cut,
|
|
362
|
+
// the exact-SHA release oracle that requires that CI step at the prepared SHA
|
|
363
|
+
// (8c) — and must not silently return to the default chain or to release_gate.
|
|
364
|
+
// #132/#133 moved the cut's one BODY off release_gate: a local BODY there re-ran
|
|
365
|
+
// the same proof on a SHA nobody ships, so release_gate now invokes it ZERO
|
|
366
|
+
// times. Without this cell, deleting the CI line leaves every focused gate green
|
|
367
|
+
// while a cut quietly loses its discriminating-power step, and re-adding the
|
|
368
|
+
// release_gate block silently doubles every cut again.
|
|
367
369
|
//
|
|
368
370
|
// THREE claims, because each is an independent contract. 8a is REACHABILITY
|
|
369
|
-
// (absent from the default chain; present exactly once in
|
|
370
|
-
//
|
|
371
|
+
// (absent from the default chain and from release_gate; present exactly once in
|
|
372
|
+
// CI; named in VERIFY). 8b is what the CI step
|
|
371
373
|
// qualifies (the FULL floor, before the qualification run), added by #70. 8c is
|
|
372
374
|
// what the RELEASE path ACCEPTS as evidence for one SHA — that the body step
|
|
373
375
|
// actually ran there (#103). Each carries its own replant — one mutant must
|
|
@@ -399,25 +401,27 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
399
401
|
"package.json's default `check` chain contains check-gate-qualification again (doubles every closure floor)",
|
|
400
402
|
);
|
|
401
403
|
if (ciHits !== 1) holes.push(`the CI check job runs check-gate-qualification ${ciHits}x (need exactly once)`);
|
|
402
|
-
if (invocations !==
|
|
403
|
-
holes.push(
|
|
404
|
+
if (invocations !== 0)
|
|
405
|
+
holes.push(
|
|
406
|
+
`release_gate invokes check-gate-qualification ${invocations}x (need none: the cut's one BODY is the prepared SHA's CI step)`,
|
|
407
|
+
);
|
|
404
408
|
if (
|
|
405
|
-
|
|
406
|
-
|
|
409
|
+
qualGateBody.includes("PASS check-gate-qualification") ||
|
|
410
|
+
qualGateBody.includes("FAIL check-gate-qualification")
|
|
407
411
|
)
|
|
408
|
-
holes.push("
|
|
412
|
+
holes.push("release_gate still counts a check-gate-qualification MUST row");
|
|
409
413
|
if (
|
|
410
414
|
!verifyDoc.includes(
|
|
411
|
-
"in the CI `check` job on a branch push that touched the qualification surface, and
|
|
415
|
+
"in the CI `check` job on a branch push that touched the qualification surface, and — for a cut — at the prepared SHA, where the exact-SHA release oracle requires that CI step",
|
|
412
416
|
)
|
|
413
417
|
)
|
|
414
418
|
holes.push("VERIFY.md no longer names the owners of the moved qualification step");
|
|
415
419
|
assert.ok(
|
|
416
420
|
holes.length === 0,
|
|
417
421
|
"[QK:QUALIFICATION-SCHEDULING-REACHABLE] check-gate-qualification left the default check chains " +
|
|
418
|
-
"deliberately, so it must stay REACHABLE on the axes that own it now — absent from the default chain
|
|
419
|
-
"
|
|
420
|
-
`
|
|
422
|
+
"deliberately, so it must stay REACHABLE on the axes that own it now — absent from the default chain " +
|
|
423
|
+
"and from release_gate (the cut's one BODY is the prepared SHA's CI step), exactly once in the CI check job, " +
|
|
424
|
+
`and named in VERIFY. Broken: ${holes.join("; ")}`,
|
|
421
425
|
);
|
|
422
426
|
|
|
423
427
|
// 8b. WHAT the CI qualification step qualifies — its own claim, not a branch
|
|
@@ -1094,7 +1098,7 @@ console.log(
|
|
|
1094
1098
|
"a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
|
|
1095
1099
|
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
|
|
1096
1100
|
"check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
|
|
1097
|
-
"
|
|
1101
|
+
"absent from release_gate: the one body is the prepared SHA's CI step) and the CI step qualifies the FULL floor, which runs before it, " +
|
|
1098
1102
|
"while the exact-SHA release oracle requires that BODY step to have concluded success at the release SHA, " +
|
|
1099
1103
|
"and the CI filter that decides when that body runs covers every mutant subject and still runs it for all " +
|
|
1100
1104
|
"five historical reds; and " +
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Inputs, and nothing else: the pinned commit's OBJECTS (each declared file and the license, read as
|
|
11
11
|
* `<commit>:<path>` — never a worktree, so the repository's checkout state cannot enter), the packaged
|
|
12
12
|
* overlay patch, and this Node's `stripTypeScriptTypes` in strip mode (upstream runs these exact
|
|
13
|
-
* sources under Node's strip-only TypeScript). The patch
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* sources under Node's strip-only TypeScript). The one patch carries exactly one section per file the
|
|
14
|
+
* pin declares `patched`, and no other section (#134). Each section is applied by git's own engine to a
|
|
15
|
+
* private scratch copy of its file; the base blob and the patched result must carry the blob ids that
|
|
16
|
+
* section's `index` line names. Each stripped file is then relocated by pi/pi-durable/carrier-relocation.mjs, and the targets
|
|
16
17
|
* the six files import must equal the pin's declared `distTargets` exactly.
|
|
17
18
|
*
|
|
18
19
|
* `--write` emits pi/pi-durable/carrier/{<files>,LICENSE} and fills the pin's generated fields
|
|
@@ -118,22 +119,60 @@ function readPin(): CarrierPin {
|
|
|
118
119
|
`the emitter is ${EMITTER_TOOL} in ${EMITTER_MODE} mode, not ${JSON.stringify(pin.carrier.emitter)}`,
|
|
119
120
|
);
|
|
120
121
|
}
|
|
121
|
-
if (pin.carrier.files.
|
|
122
|
-
throw refuse("carrier-pin-malformed", "
|
|
122
|
+
if (!pin.carrier.files.some((f) => f.patched)) {
|
|
123
|
+
throw refuse("carrier-pin-malformed", "at least one carrier file carries the overlay patch");
|
|
123
124
|
}
|
|
124
125
|
return pin;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
/** One file's section of the overlay patch: its own bytes and the two blob ids its `index` line names. */
|
|
129
|
+
export interface PatchSection {
|
|
130
|
+
bytes: Buffer;
|
|
131
|
+
base: string;
|
|
132
|
+
result: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The overlay patch split into its per-file sections, keyed by the existing file each one changes, and
|
|
137
|
+
* matched against the source paths the pin declares `patched`: exactly one section per declared file and
|
|
138
|
+
* no section for any other file. Anything else is the patch's shape, refused by name.
|
|
139
|
+
*/
|
|
140
|
+
export function patchSections(
|
|
141
|
+
text: string,
|
|
142
|
+
patched: readonly string[],
|
|
143
|
+
name = "the overlay patch",
|
|
144
|
+
): Map<string, PatchSection> {
|
|
145
|
+
const starts = [...text.matchAll(/^diff --git /gm)].map((m) => m.index);
|
|
146
|
+
if (starts.length === 0 || starts[0] !== 0) {
|
|
147
|
+
throw refuse("carrier-patch-shape", `${name} must start with its first file section`);
|
|
148
|
+
}
|
|
149
|
+
const sections = new Map<string, PatchSection>();
|
|
150
|
+
for (const [at, start] of starts.entries()) {
|
|
151
|
+
const chunk = text.slice(start, starts[at + 1] ?? text.length);
|
|
152
|
+
const files = [...chunk.matchAll(/^diff --git a\/(\S+) b\/(\S+)$/gm)];
|
|
153
|
+
const index = [...chunk.matchAll(/^index ([0-9a-f]+)\.\.([0-9a-f]+) 100644$/gm)];
|
|
154
|
+
if (files.length !== 1 || files[0][1] !== files[0][2] || index.length !== 1) {
|
|
155
|
+
throw refuse("carrier-patch-shape", `every section of ${name} changes exactly one existing file`);
|
|
156
|
+
}
|
|
157
|
+
const file = files[0][1];
|
|
158
|
+
if (sections.has(file)) throw refuse("carrier-patch-shape", `${name} changes ${file} in two sections`);
|
|
159
|
+
sections.set(file, { bytes: Buffer.from(chunk, "utf8"), base: index[0][1], result: index[0][2] });
|
|
135
160
|
}
|
|
136
|
-
|
|
161
|
+
const undeclared = [...sections.keys()].filter((file) => !patched.includes(file));
|
|
162
|
+
const missing = patched.filter((file) => !sections.has(file));
|
|
163
|
+
if (undeclared.length > 0 || missing.length > 0) {
|
|
164
|
+
throw refuse(
|
|
165
|
+
"carrier-patch-shape",
|
|
166
|
+
`${name} must change exactly the files the pin declares patched (undeclared: ${undeclared.join(", ") || "none"}; missing: ${missing.join(", ") || "none"})`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return sections;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readPatch(pin: CarrierPin): Map<string, PatchSection> {
|
|
173
|
+
const text = fs.readFileSync(path.join(DURABLE, "overlay", pin.patches[0]), "utf8");
|
|
174
|
+
const patched = pin.carrier.files.filter((f) => f.patched).map((f) => f.source);
|
|
175
|
+
return patchSections(text, patched, pin.patches[0]);
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
/** `base` with the patch applied by git, in a private scratch tree outside any repository. */
|
|
@@ -172,7 +211,7 @@ function emit(source: string, pin: CarrierPin): Emission {
|
|
|
172
211
|
if (git(source, ["cat-file", "-t", pin.commit]).toString().trim() !== "commit") {
|
|
173
212
|
throw refuse("carrier-source-unreadable", `${pin.commit} is not a commit in ${source}`);
|
|
174
213
|
}
|
|
175
|
-
const
|
|
214
|
+
const sections = readPatch(pin);
|
|
176
215
|
const names = pin.carrier.files.map((f) => f.name);
|
|
177
216
|
const targets = pin.carrier.distTargets;
|
|
178
217
|
if (new Set(targets).size !== targets.length) throw refuse("carrier-pin-malformed", "distTargets repeats a path");
|
|
@@ -183,18 +222,19 @@ function emit(source: string, pin: CarrierPin): Emission {
|
|
|
183
222
|
const original = readSourceObject(source, `${pin.commit}:${declared.source}`);
|
|
184
223
|
const blob = blobId(original);
|
|
185
224
|
let text = original;
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
const section = declared.patched ? sections.get(declared.source) : undefined;
|
|
226
|
+
if (section !== undefined) {
|
|
227
|
+
if (!blob.startsWith(section.base)) {
|
|
188
228
|
throw refuse(
|
|
189
229
|
"carrier-patch-base",
|
|
190
|
-
`the patch changes ${
|
|
230
|
+
`the patch changes ${declared.source} from ${section.base}; the pinned object is ${blob}`,
|
|
191
231
|
);
|
|
192
232
|
}
|
|
193
|
-
text = applyPatch(declared.source, original,
|
|
194
|
-
if (!blobId(text).startsWith(
|
|
233
|
+
text = applyPatch(declared.source, original, section.bytes);
|
|
234
|
+
if (!blobId(text).startsWith(section.result)) {
|
|
195
235
|
throw refuse(
|
|
196
236
|
"carrier-patch-result",
|
|
197
|
-
`patched ${declared.source} is ${blobId(text)}, the patch names ${
|
|
237
|
+
`patched ${declared.source} is ${blobId(text)}, the patch names ${section.result}`,
|
|
198
238
|
);
|
|
199
239
|
}
|
|
200
240
|
}
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"claim": "PI-CORE-PIN-TRACKS-FLOOR",
|
|
182
182
|
"title": "the queue oracle's pi-agent-core pin drifts off the floor the rest of the pi runtime is pinned to, so the oracle keeps answering honestly — about a pi nobody in this repo runs",
|
|
183
183
|
"subject": "package.json",
|
|
184
|
-
"find": ["\"@earendil-works/pi-agent-core\": \"1.0
|
|
184
|
+
"find": ["\"@earendil-works/pi-agent-core\": \"1.1.0\","],
|
|
185
185
|
"replace": ["\"@earendil-works/pi-agent-core\": \"0.87.1\","],
|
|
186
186
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
187
187
|
"timeoutSeconds": 300,
|
|
@@ -95,6 +95,30 @@
|
|
|
95
95
|
"timeoutSeconds": 120,
|
|
96
96
|
"signature": "[QK:QUALIFY-PRIOR-RUN-EVIDENCE-KEPT]",
|
|
97
97
|
"signatureSource": "scripts/check-gate-qualification.ts"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"claim": "QUALIFY-DOOR-FORWARDS-ARGV",
|
|
101
|
+
"title": "run.sh check-gate-qualification drops its arguments again — a requested --group/--receipt PARTIAL reaches the runner as a bare call and starts the whole ~900-mutant body nobody asked for (#133: the wrapper defect that made every development PARTIAL a whole run)",
|
|
102
|
+
"subject": "run.sh",
|
|
103
|
+
"find": [" run_ts scripts/check-gate-qualification.ts \"$@\""],
|
|
104
|
+
"replace": [" run_ts scripts/check-gate-qualification.ts"],
|
|
105
|
+
"gate": ["bash", "run.sh", "check-gate-manifests"],
|
|
106
|
+
"timeoutSeconds": 120,
|
|
107
|
+
"signature": "[QK:QUALIFY-DOOR-FORWARDS-ARGV]",
|
|
108
|
+
"signatureSource": "scripts/check-gate-qualification.ts"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"claim": "QUALIFY-DOOR-REFUSES-BEFORE-BODY",
|
|
112
|
+
"title": "run.sh check-gate-qualification retries bare when the runner refuses — a malformed, missing or repeated flag is named, then the whole body starts anyway in place of the PARTIAL that was asked for (#133)",
|
|
113
|
+
"subject": "run.sh",
|
|
114
|
+
"find": [" run_ts scripts/check-gate-qualification.ts \"$@\""],
|
|
115
|
+
"replace": [
|
|
116
|
+
" run_ts scripts/check-gate-qualification.ts \"$@\" || run_ts scripts/check-gate-qualification.ts"
|
|
117
|
+
],
|
|
118
|
+
"gate": ["bash", "run.sh", "check-gate-manifests"],
|
|
119
|
+
"timeoutSeconds": 120,
|
|
120
|
+
"signature": "[QK:QUALIFY-DOOR-REFUSES-BEFORE-BODY]",
|
|
121
|
+
"signatureSource": "scripts/check-gate-qualification.ts"
|
|
98
122
|
}
|
|
99
123
|
]
|
|
100
124
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
|
|
7
7
|
"title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
|
|
8
8
|
"subject": "run.sh",
|
|
9
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0
|
|
9
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.1\\.0(_|$)' || true"],
|
|
10
10
|
"replace": [" grep '^@earendil-works+' | grep -v '@0\\.87\\.1' || true"],
|
|
11
11
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
12
12
|
"timeoutSeconds": 120,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
|
|
18
18
|
"title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
|
|
19
19
|
"subject": "run.sh",
|
|
20
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0
|
|
21
|
-
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.0
|
|
20
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.1\\.0(_|$)' || true"],
|
|
21
|
+
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.1\\.0(_|$)' || true"],
|
|
22
22
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
23
23
|
"timeoutSeconds": 120,
|
|
24
24
|
"signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
|
|
@@ -53,12 +53,8 @@
|
|
|
53
53
|
"claim": "PI-DURABLE-BOOTSTRAP-ARGV-NO-BRIDGE-ENTRY",
|
|
54
54
|
"title": "the launch argv accepts --bridge-entry, so a caller can point the contact at a bridge outside this package",
|
|
55
55
|
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
56
|
-
"find": [
|
|
57
|
-
|
|
58
|
-
],
|
|
59
|
-
"replace": [
|
|
60
|
-
"const VALUE_FLAGS = [\"--bridge-entry\", \"--provider\", \"--model\", \"--width\", \"--entwurf-bootstrap\", \"--native-module\"];"
|
|
61
|
-
],
|
|
56
|
+
"find": ["const VALUE_FLAGS = ["],
|
|
57
|
+
"replace": ["const VALUE_FLAGS = [\n\t\"--bridge-entry\","],
|
|
62
58
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
63
59
|
"timeoutSeconds": 300,
|
|
64
60
|
"signature": "[QK:PI-DURABLE-BOOTSTRAP-ARGV-NO-BRIDGE-ENTRY]",
|
|
@@ -345,7 +341,7 @@
|
|
|
345
341
|
"claim": "PI-DURABLE-CARRIER-PIN-MATCHES",
|
|
346
342
|
"title": "the pin records a digest the committed carrier file does not carry, so a carrier edited after emission would still be the declared one",
|
|
347
343
|
"subject": "pi/pi-durable/overlay/upstream-pin.json",
|
|
348
|
-
"find": ["\"sha256\": \"
|
|
344
|
+
"find": ["\"sha256\": \"bfd88844fae61e8bd32d57b5b986f48056c64eebb5466d9826318a0783ac9005\""],
|
|
349
345
|
"replace": ["\"sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\""],
|
|
350
346
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
351
347
|
"timeoutSeconds": 300,
|
|
@@ -588,8 +584,8 @@
|
|
|
588
584
|
"claim": "PI-DURABLE-MODULE-ABSENT-UNCHANGED",
|
|
589
585
|
"title": "the module phase runs without a --native-module, so a launch that names no module still tries to import one",
|
|
590
586
|
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
591
|
-
"find": ["\tconst
|
|
592
|
-
"replace": ["\tconst
|
|
587
|
+
"find": ["\tconst { nativeModules = [], ...args } = parseArgs(argv);"],
|
|
588
|
+
"replace": ["\tconst { nativeModules = [undefined], ...args } = parseArgs(argv);"],
|
|
593
589
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
594
590
|
"timeoutSeconds": 300,
|
|
595
591
|
"signature": "[QK:PI-DURABLE-MODULE-ABSENT-UNCHANGED]",
|
|
@@ -619,7 +615,7 @@
|
|
|
619
615
|
"title": "the module initializes after the TUI is imported, so its environment arrives after the SDK modules have evaluated",
|
|
620
616
|
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
621
617
|
"find": [
|
|
622
|
-
"\tconst extensions =
|
|
618
|
+
"\tconst extensions = await loadNativeModules(nativeModules);",
|
|
623
619
|
"\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
|
|
624
620
|
"\t// load opens no session, mints no citizen and leaves nothing to clean up.",
|
|
625
621
|
"\tconst { runDurableTui } = await loadTui();"
|
|
@@ -628,7 +624,7 @@
|
|
|
628
624
|
"\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
|
|
629
625
|
"\t// load opens no session, mints no citizen and leaves nothing to clean up.",
|
|
630
626
|
"\tconst { runDurableTui } = await loadTui();",
|
|
631
|
-
"\tconst extensions =
|
|
627
|
+
"\tconst extensions = await loadNativeModules(nativeModules);"
|
|
632
628
|
],
|
|
633
629
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
634
630
|
"timeoutSeconds": 300,
|
|
@@ -718,6 +714,177 @@
|
|
|
718
714
|
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
719
715
|
"output": "mcp/entwurf-bridge/dist"
|
|
720
716
|
}
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
"claim": "PI-DURABLE-CARRIER-PATCH-SECTIONS",
|
|
720
|
+
"title": "a declared patched file with no section of its own passes the patch-shape check, so the emitter would ship that file unpatched",
|
|
721
|
+
"subject": "scripts/emit-pi-durable-carrier.ts",
|
|
722
|
+
"find": ["\tif (undeclared.length > 0 || missing.length > 0) {"],
|
|
723
|
+
"replace": ["\tif (undeclared.length > 0) {"],
|
|
724
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
725
|
+
"timeoutSeconds": 300,
|
|
726
|
+
"signature": "[QK:PI-DURABLE-CARRIER-PATCH-SECTIONS]",
|
|
727
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
728
|
+
"build": {
|
|
729
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
730
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"claim": "PI-DURABLE-FOOTER-GRAPH-LIFETIME",
|
|
735
|
+
"title": "/tasks closes the task-graph observation again, so hiding the panel also hides live background work from the badge",
|
|
736
|
+
"subject": "pi/pi-durable/carrier/runtime.js",
|
|
737
|
+
"find": ["\t\t\ttoggleTasks: () => command(async () => update({ tasksPanel: !state.tasksPanel })),"],
|
|
738
|
+
"replace": [
|
|
739
|
+
"\t\t\ttoggleTasks: () => command(async () => update({ tasksPanel: !state.tasksPanel, tasks: state.tasksPanel ? undefined : state.tasks })),"
|
|
740
|
+
],
|
|
741
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
742
|
+
"timeoutSeconds": 300,
|
|
743
|
+
"signature": "[QK:PI-DURABLE-FOOTER-GRAPH-LIFETIME]",
|
|
744
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
745
|
+
"build": {
|
|
746
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
747
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
"claim": "PI-DURABLE-FOOTER-BADGE-COUNT",
|
|
752
|
+
"title": "the badge counts every live task, foreground generation and tool tasks included, so an idle-but-turning session shows phantom background work",
|
|
753
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
754
|
+
"find": ["\t\tconst nodes = Object.values(graph?.tasks ?? {}).filter((node) => node.background);"],
|
|
755
|
+
"replace": ["\t\tconst nodes = Object.values(graph?.tasks ?? {});"],
|
|
756
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
757
|
+
"timeoutSeconds": 300,
|
|
758
|
+
"signature": "[QK:PI-DURABLE-FOOTER-BADGE-COUNT]",
|
|
759
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
760
|
+
"build": {
|
|
761
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
762
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"claim": "PI-DURABLE-FOOTER-BADGE-WIDTH",
|
|
767
|
+
"title": "the badge has no one- or two-cell form, so a very narrow terminal shows a bare marker with no count",
|
|
768
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
769
|
+
"find": ["\t\tconst forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, `*${n}`, \"*+\", \"*\"];"],
|
|
770
|
+
"replace": ["\t\tconst forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, \"*\"];"],
|
|
771
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
772
|
+
"timeoutSeconds": 300,
|
|
773
|
+
"signature": "[QK:PI-DURABLE-FOOTER-BADGE-WIDTH]",
|
|
774
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
775
|
+
"build": {
|
|
776
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
777
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
"claim": "PI-DURABLE-FOOTER-BADGE-PROTECTED",
|
|
782
|
+
"title": "the badge sits last in the dock, so the renderer's bottom clip drops it first on a short terminal",
|
|
783
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
784
|
+
"find": [
|
|
785
|
+
"\t\t{ component: new VStack([{ component: badge, shrink: 0 }, ...dock]), basis: \"auto\", grow: 0, shrink: 1, minSize: 1 },"
|
|
786
|
+
],
|
|
787
|
+
"replace": [
|
|
788
|
+
"\t\t{ component: new VStack([...dock, { component: badge, shrink: 0 }]), basis: \"auto\", grow: 0, shrink: 1, minSize: 1 },"
|
|
789
|
+
],
|
|
790
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
791
|
+
"timeoutSeconds": 300,
|
|
792
|
+
"signature": "[QK:PI-DURABLE-FOOTER-BADGE-PROTECTED]",
|
|
793
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
794
|
+
"build": {
|
|
795
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
796
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
"claim": "PI-DURABLE-FOOTER-ZERO-UPSTREAM",
|
|
801
|
+
"title": "a one-row terminal hides the transcript even with no background work, so the idle screen is no longer upstream's",
|
|
802
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
803
|
+
"find": ["\t\t\tvisible: (viewport) => badge.count === 0 || viewport.height > 1,"],
|
|
804
|
+
"replace": ["\t\t\tvisible: (viewport) => viewport.height > 1,"],
|
|
805
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
806
|
+
"timeoutSeconds": 300,
|
|
807
|
+
"signature": "[QK:PI-DURABLE-FOOTER-ZERO-UPSTREAM]",
|
|
808
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
809
|
+
"build": {
|
|
810
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
811
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"claim": "PI-DURABLE-MODULE-DIR-TOPLEVEL-FILTER",
|
|
816
|
+
"title": "a dotfile ending in .extension.mjs counts as a module, so a hidden file in the named directory is evaluated as operator code",
|
|
817
|
+
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
818
|
+
"find": ["const MODULE_ENTRY = /^[^.].*\\.extension\\.mjs$/;"],
|
|
819
|
+
"replace": ["const MODULE_ENTRY = /\\.extension\\.mjs$/;"],
|
|
820
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
821
|
+
"timeoutSeconds": 300,
|
|
822
|
+
"signature": "[QK:PI-DURABLE-MODULE-DIR-TOPLEVEL-FILTER]",
|
|
823
|
+
"signatureSource": "pi-extensions/lib/pi-durable-native-module.test.ts",
|
|
824
|
+
"build": {
|
|
825
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
826
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"claim": "PI-DURABLE-MODULE-DIR-ORDER-BYTEWISE",
|
|
831
|
+
"title": "modules are ordered by the locale's collation, so the initialization order depends on the host's language settings",
|
|
832
|
+
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
833
|
+
"find": ["\t\t.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));"],
|
|
834
|
+
"replace": ["\t\t.sort((a, b) => a.localeCompare(b));"],
|
|
835
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
836
|
+
"timeoutSeconds": 300,
|
|
837
|
+
"signature": "[QK:PI-DURABLE-MODULE-DIR-ORDER-BYTEWISE]",
|
|
838
|
+
"signatureSource": "pi-extensions/lib/pi-durable-native-module.test.ts",
|
|
839
|
+
"build": {
|
|
840
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
841
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
"claim": "PI-DURABLE-MODULE-DIR-REFUSED",
|
|
846
|
+
"title": "a named directory without a module launches with none, so a wrong path or an unrenamed plain index.mjs goes unnoticed",
|
|
847
|
+
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
848
|
+
"find": ["\tif (names.length === 0) {"],
|
|
849
|
+
"replace": ["\tif (false) {"],
|
|
850
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
851
|
+
"timeoutSeconds": 300,
|
|
852
|
+
"signature": "[QK:PI-DURABLE-MODULE-DIR-REFUSED]",
|
|
853
|
+
"signatureSource": "pi-extensions/lib/pi-durable-native-module.test.ts",
|
|
854
|
+
"build": {
|
|
855
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
856
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"claim": "PI-DURABLE-MODULE-DIR-EXCLUSIVE",
|
|
861
|
+
"title": "naming both the file and the directory silently keeps only the file",
|
|
862
|
+
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
863
|
+
"find": ["\tif (moduleArg !== undefined && dirArg !== undefined) {"],
|
|
864
|
+
"replace": ["\tif (false) {"],
|
|
865
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
866
|
+
"timeoutSeconds": 300,
|
|
867
|
+
"signature": "[QK:PI-DURABLE-MODULE-DIR-EXCLUSIVE]",
|
|
868
|
+
"signatureSource": "pi-extensions/lib/pi-durable-native-module.test.ts",
|
|
869
|
+
"build": {
|
|
870
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
871
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
"claim": "PI-DURABLE-MODULE-DIR-NAME-DUPLICATE",
|
|
876
|
+
"title": "a second module's same-named extension or tool is accepted, so the native registry replaces the first extension or keeps both tools without a word",
|
|
877
|
+
"subject": "pi/pi-durable/bootstrap.mjs",
|
|
878
|
+
"find": ["\t\t\tif (owners.has(name)) {"],
|
|
879
|
+
"replace": ["\t\t\tif (false) {"],
|
|
880
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
881
|
+
"timeoutSeconds": 300,
|
|
882
|
+
"signature": "[QK:PI-DURABLE-MODULE-DIR-NAME-DUPLICATE]",
|
|
883
|
+
"signatureSource": "pi-extensions/lib/pi-durable-native-module.test.ts",
|
|
884
|
+
"build": {
|
|
885
|
+
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
886
|
+
"output": "mcp/entwurf-bridge/dist"
|
|
887
|
+
}
|
|
721
888
|
}
|
|
722
889
|
]
|
|
723
890
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"title": "one row of the pnpm workspace closure is left on the previous floor while every other surface moved — the shape a version bump actually fails in. Nothing else reads that file, so the stranded row surfaces only at install time, in a different command, far from the change that stranded it",
|
|
8
8
|
"subject": "pnpm-workspace.yaml",
|
|
9
9
|
"find": [
|
|
10
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4'"
|
|
10
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0'"
|
|
11
11
|
],
|
|
12
12
|
"replace": [
|
|
13
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2'"
|
|
13
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4'"
|
|
14
14
|
],
|
|
15
15
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
16
16
|
"timeoutSeconds": 300,
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"claim": "PI-SDK-PRODUCTION-SET",
|
|
22
|
-
"title": "the durable library's production pin falls one
|
|
22
|
+
"title": "the durable library's production pin falls one release behind the SDK set the pin declares, so the carrier would bind a pi-durable nobody verified it against",
|
|
23
23
|
"subject": "package.json",
|
|
24
|
-
"find": ["\"@earendil-works/pi-durable\": \"1.0
|
|
25
|
-
"replace": ["\"@earendil-works/pi-durable\": \"1.0.
|
|
24
|
+
"find": ["\"@earendil-works/pi-durable\": \"1.1.0\","],
|
|
25
|
+
"replace": ["\"@earendil-works/pi-durable\": \"1.0.4\","],
|
|
26
26
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
27
27
|
"timeoutSeconds": 300,
|
|
28
28
|
"signature": "[QK:PI-SDK-PRODUCTION-SET]",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"title": "an @earendil-works package returns as a dev dependency beside its production pin, one name in two dependency classes",
|
|
34
34
|
"subject": "package.json",
|
|
35
35
|
"find": ["\"@biomejs/biome\": \"^2.4.13\","],
|
|
36
|
-
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.0
|
|
36
|
+
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.1.0\","],
|
|
37
37
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
38
38
|
"timeoutSeconds": 300,
|
|
39
39
|
"signature": "[QK:PI-SDK-NO-DUAL-DECLARATION]",
|
|
@@ -103,10 +103,12 @@
|
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
"claim": "QUALIFICATION-SCHEDULING-REACHABLE",
|
|
106
|
-
"title": "release_gate
|
|
106
|
+
"title": "release_gate starts invoking check-gate-qualification again — every cut silently pays a second ~100-minute BODY on a SHA nobody ships, the repetition #132/#133 moved to the prepared SHA's one CI step",
|
|
107
107
|
"subject": "run.sh",
|
|
108
|
-
"find": ["
|
|
109
|
-
"replace": [
|
|
108
|
+
"find": [" # 2. (gemini-availability step removed — claude-only floor; gemini CLI is"],
|
|
109
|
+
"replace": [
|
|
110
|
+
" (cd \"$REPO_DIR\" && bash \"$self\" check-gate-qualification) || true\n # 2. (gemini-availability step removed — claude-only floor; gemini CLI is"
|
|
111
|
+
],
|
|
110
112
|
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
111
113
|
"timeoutSeconds": 180,
|
|
112
114
|
"signature": "[QK:QUALIFICATION-SCHEDULING-REACHABLE]",
|