@junghanacs/entwurf 0.25.1 → 0.30.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 +1 -1
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +73 -0
- package/DELIVERY.md +1 -1
- package/README.md +178 -674
- package/VERIFY.md +16 -15
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +6 -2
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +346 -10
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +12 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
- package/mcp/entwurf-bridge/src/index.ts +30 -3
- package/package.json +11 -11
- package/pi-extensions/entwurf-control.ts +50 -637
- package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
- package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
- package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
- package/pi-extensions/lib/fresh-call-composition.ts +13 -6
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -3
- package/run.sh +163 -50
- package/scripts/check-entwurf-peers-surface.ts +8 -15
- package/scripts/check-entwurf-self-address.ts +7 -11
- package/scripts/check-entwurf-v2-surface.ts +20 -112
- package/scripts/check-fresh-call-dispatch.ts +12 -13
- package/scripts/check-gate-qualification.ts +248 -4
- package/scripts/check-install-surface.ts +3 -2
- package/scripts/check-mux-resume-call.ts +24 -35
- package/scripts/check-pi-bridge-sender.ts +130 -0
- package/scripts/check-pi-mcp-bridge.ts +251 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
- package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
- package/scripts/lib/mutation-qualify.ts +269 -44
- package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
- package/scripts/meta-bridge-doctor.sh +16 -5
- package/scripts/mutants/acp-foreground-only.json +17 -0
- package/scripts/mutants/codex-native.json +11 -11
- package/scripts/mutants/control-send-receipt.json +6 -4
- package/scripts/mutants/copilot-birth.json +2 -1
- package/scripts/mutants/fresh-call-dispatch.json +3 -14
- package/scripts/mutants/gate-qualification.json +59 -0
- package/scripts/mutants/mux-fresh-call.json +43 -45
- package/scripts/mutants/mux-resume-call.json +11 -35
- package/scripts/mutants/pack-install.json +15 -4
- package/scripts/mutants/pi-bridge-sender.json +54 -0
- package/scripts/mutants/pi-floor.json +6 -2
- package/scripts/mutants/pi-mcp-bridge.json +36 -0
- package/scripts/mutants/pi-mcp-register.json +51 -0
- package/scripts/mutants/v2-surface.json +6 -6
- package/scripts/resident-rpc-drive.ts +5 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
- package/scripts/smoke-acp-cortex-live.ts +3 -1
- package/scripts/smoke-acp-session-reuse-live.ts +29 -12
- package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
- package/scripts/smoke-acp-v2-send-live.ts +3 -1
- package/scripts/smoke-codex-fresh-live.ts +20 -1
- package/scripts/smoke-entwurf-chain-live.ts +6 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/smoke-resident-garden-guard.sh +3 -1
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* mutants (a gate already red at baseline can produce only fake KILLEDs).
|
|
20
20
|
* - Bounded: each gate run has a hard timeout and its process GROUP is killed.
|
|
21
21
|
* - Evidence is claim IDs + killed mutant IDs, never assertion counts.
|
|
22
|
+
* - The snapshot is SOURCE-ONLY. An ignored build output a gate consumes (#125: the
|
|
23
|
+
* compiled entwurf-bridge a pi citizen registers) enters only through a mutant's
|
|
24
|
+
* `build` declaration, and only for that gate's group: the snapshot builds it from its
|
|
25
|
+
* OWN bytes before the control, again after every mutation (so the gate judges the
|
|
26
|
+
* output of the source it is handed), and again from the restored bytes before the
|
|
27
|
+
* post-control, which must reproduce the baseline output exactly. A build red is never
|
|
28
|
+
* a kill, a gate that writes into the output voids its run, and the output is removed
|
|
29
|
+
* when the group ends — no other group ever sees it, and no origin artifact is copied.
|
|
22
30
|
*/
|
|
23
31
|
|
|
24
32
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -161,6 +169,20 @@ export interface MutantSpec {
|
|
|
161
169
|
signature: string;
|
|
162
170
|
/** Repo-relative gate source file that owns the claim token. */
|
|
163
171
|
signatureSource: string;
|
|
172
|
+
/** Optional: an ignored build output this mutant's gate consumes (see the module header). */
|
|
173
|
+
build?: MutantBuild;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A build whose OUTPUT a gate group consumes. Never a subject: the output is ignored, so
|
|
178
|
+
* the snapshot does not carry it, and a gate requiring it would be CONTROL-RED in every
|
|
179
|
+
* run. Every mutant of one gate declares the same build or none.
|
|
180
|
+
*/
|
|
181
|
+
export interface MutantBuild {
|
|
182
|
+
/** Build argv (no shell string), run with cwd = snapshot repo root, fenced and bounded like a gate. */
|
|
183
|
+
argv: string[];
|
|
184
|
+
/** Repo-relative directory the build writes; absent from the work surface, removed after the group. */
|
|
185
|
+
output: string;
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
export interface MutantManifest {
|
|
@@ -182,12 +204,14 @@ const MUTANT_KEYS = [
|
|
|
182
204
|
"signature",
|
|
183
205
|
"signatureSource",
|
|
184
206
|
];
|
|
207
|
+
const OPTIONAL_MUTANT_KEYS = ["build"];
|
|
208
|
+
const BUILD_KEYS = ["argv", "output"];
|
|
185
209
|
|
|
186
210
|
export class ManifestError extends Error {}
|
|
187
211
|
|
|
188
|
-
function requireExactKeys(obj: Record<string, unknown>, keys: string[], where: string): void {
|
|
212
|
+
function requireExactKeys(obj: Record<string, unknown>, keys: string[], where: string, optional: string[] = []): void {
|
|
189
213
|
for (const k of Object.keys(obj)) {
|
|
190
|
-
if (!keys.includes(k)) throw new ManifestError(`${where}: unknown key \`${k}\``);
|
|
214
|
+
if (!keys.includes(k) && !optional.includes(k)) throw new ManifestError(`${where}: unknown key \`${k}\``);
|
|
191
215
|
}
|
|
192
216
|
for (const k of keys) {
|
|
193
217
|
if (!(k in obj)) throw new ManifestError(`${where}: missing key \`${k}\``);
|
|
@@ -218,7 +242,7 @@ export function validateManifest(raw: unknown, name: string): MutantManifest {
|
|
|
218
242
|
const where = `${name} mutants[${i}]`;
|
|
219
243
|
if (typeof m !== "object" || m === null) throw new ManifestError(`${where}: must be an object`);
|
|
220
244
|
const rec = m as Record<string, unknown>;
|
|
221
|
-
requireExactKeys(rec, MUTANT_KEYS, where);
|
|
245
|
+
requireExactKeys(rec, MUTANT_KEYS, where, OPTIONAL_MUTANT_KEYS);
|
|
222
246
|
if (typeof rec.claim !== "string" || !CLAIM_RE.test(rec.claim)) {
|
|
223
247
|
throw new ManifestError(`${where}: claim must match ${CLAIM_RE}`);
|
|
224
248
|
}
|
|
@@ -251,6 +275,7 @@ export function validateManifest(raw: unknown, name: string): MutantManifest {
|
|
|
251
275
|
if (typeof sigSrc !== "string" || path.isAbsolute(sigSrc) || sigSrc.split(/[\\/]/).includes("..")) {
|
|
252
276
|
throw new ManifestError(`${where}: signatureSource must be a repo-relative path`);
|
|
253
277
|
}
|
|
278
|
+
const build = "build" in rec ? validateBuild(rec.build, `${where}.build`) : undefined;
|
|
254
279
|
mutants.push({
|
|
255
280
|
claim: rec.claim,
|
|
256
281
|
title: rec.title,
|
|
@@ -261,11 +286,54 @@ export function validateManifest(raw: unknown, name: string): MutantManifest {
|
|
|
261
286
|
timeoutSeconds: rec.timeoutSeconds,
|
|
262
287
|
signature: expectedSig,
|
|
263
288
|
signatureSource: sigSrc,
|
|
289
|
+
...(build ? { build } : {}),
|
|
264
290
|
});
|
|
265
291
|
}
|
|
266
292
|
return { schemaVersion: 1, lane: doc.lane, mutants };
|
|
267
293
|
}
|
|
268
294
|
|
|
295
|
+
function validateBuild(raw: unknown, where: string): MutantBuild {
|
|
296
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
297
|
+
throw new ManifestError(`${where}: must be an object`);
|
|
298
|
+
}
|
|
299
|
+
const rec = raw as Record<string, unknown>;
|
|
300
|
+
requireExactKeys(rec, BUILD_KEYS, where);
|
|
301
|
+
const argv = requireLines(rec.argv, `${where}.argv`);
|
|
302
|
+
const output = rec.output;
|
|
303
|
+
if (
|
|
304
|
+
typeof output !== "string" ||
|
|
305
|
+
path.isAbsolute(output) ||
|
|
306
|
+
output.split("/").some((seg) => seg === "" || seg === "." || seg === "..") ||
|
|
307
|
+
output.includes("\\")
|
|
308
|
+
) {
|
|
309
|
+
throw new ManifestError(`${where}: output must be a normalized repo-relative directory (got ${String(output)})`);
|
|
310
|
+
}
|
|
311
|
+
const top = output.split("/")[0];
|
|
312
|
+
if (top === "node_modules" || top === ".git") {
|
|
313
|
+
throw new ManifestError(`${where}: output may not live under ${top}`);
|
|
314
|
+
}
|
|
315
|
+
return { argv, output };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The one build a gate group declares, or none. A group is keyed by its exact gate argv;
|
|
320
|
+
* two mutants of it declaring different builds (or one declaring none) would hand the same
|
|
321
|
+
* gate different inputs depending on run order, so the set is refused — at manifest time and
|
|
322
|
+
* again at run time, because the self-test injects specs directly.
|
|
323
|
+
*/
|
|
324
|
+
export function groupBuild(specs: MutantSpec[]): MutantBuild | undefined {
|
|
325
|
+
const first = JSON.stringify(specs[0]?.build ?? null);
|
|
326
|
+
for (const m of specs) {
|
|
327
|
+
const mine = JSON.stringify(m.build ?? null);
|
|
328
|
+
if (mine !== first) {
|
|
329
|
+
throw new ManifestError(
|
|
330
|
+
`gate ${JSON.stringify(m.gate)} mixes build declarations (${first} vs ${mine} at ${m.claim}) — every mutant of one gate declares the same build or none`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return specs[0]?.build;
|
|
335
|
+
}
|
|
336
|
+
|
|
269
337
|
export interface OriginChecks {
|
|
270
338
|
/** claim → true when the subject is tracked in the origin index. */
|
|
271
339
|
subjectTracked: (subject: string) => boolean;
|
|
@@ -323,6 +391,12 @@ export function validateManifestSet(manifests: MutantManifest[], origin: OriginC
|
|
|
323
391
|
all.push(m);
|
|
324
392
|
}
|
|
325
393
|
}
|
|
394
|
+
const byGate = new Map<string, MutantSpec[]>();
|
|
395
|
+
for (const m of all) {
|
|
396
|
+
const key = JSON.stringify(m.gate);
|
|
397
|
+
byGate.set(key, [...(byGate.get(key) ?? []), m]);
|
|
398
|
+
}
|
|
399
|
+
for (const specs of byGate.values()) groupBuild(specs);
|
|
326
400
|
return all;
|
|
327
401
|
}
|
|
328
402
|
|
|
@@ -461,21 +535,26 @@ export function createRepoSnapshot(originDir: string, tmpRoot: string = os.tmpdi
|
|
|
461
535
|
* misses ignored-path writes; this does not (hardening #4).
|
|
462
536
|
*/
|
|
463
537
|
export function computeTreeManifest(repoDir: string): string {
|
|
538
|
+
return digestTree(repoDir, [".git", "node_modules"]);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** The same content manifest over any directory, skipping the named top-level entries. */
|
|
542
|
+
function digestTree(root: string, skipAtRoot: readonly string[]): string {
|
|
464
543
|
const lines: string[] = [];
|
|
465
544
|
const walk = (rel: string): void => {
|
|
466
|
-
const abs = path.join(
|
|
545
|
+
const abs = path.join(root, rel);
|
|
467
546
|
for (const name of fs.readdirSync(abs).sort()) {
|
|
468
|
-
if (rel === "" && (name
|
|
547
|
+
if (rel === "" && skipAtRoot.includes(name)) continue;
|
|
469
548
|
const childRel = rel === "" ? name : `${rel}/${name}`;
|
|
470
|
-
const st = fs.lstatSync(path.join(
|
|
549
|
+
const st = fs.lstatSync(path.join(root, childRel));
|
|
471
550
|
if (st.isSymbolicLink()) {
|
|
472
|
-
lines.push(`${childRel}\0link\0${fs.readlinkSync(path.join(
|
|
551
|
+
lines.push(`${childRel}\0link\0${fs.readlinkSync(path.join(root, childRel))}`);
|
|
473
552
|
} else if (st.isDirectory()) {
|
|
474
553
|
lines.push(`${childRel}\0dir`);
|
|
475
554
|
walk(childRel);
|
|
476
555
|
} else if (st.isFile()) {
|
|
477
556
|
const sha = createHash("sha256")
|
|
478
|
-
.update(fs.readFileSync(path.join(
|
|
557
|
+
.update(fs.readFileSync(path.join(root, childRel)))
|
|
479
558
|
.digest("hex");
|
|
480
559
|
lines.push(`${childRel}\0${(st.mode & 0o777).toString(8)}\0${sha}`);
|
|
481
560
|
} else {
|
|
@@ -487,6 +566,57 @@ export function computeTreeManifest(repoDir: string): string {
|
|
|
487
566
|
return createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
488
567
|
}
|
|
489
568
|
|
|
569
|
+
// ── declared build output (source-only snapshot, #125) ──────────────────────
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Absolute path of a group's declared build output, refused unless it is ABSENT from the
|
|
573
|
+
* snapshot: the snapshot is the work surface, so a present output would be source bytes the
|
|
574
|
+
* build (and the end-of-group removal) would overwrite. Every existing ancestor must be a
|
|
575
|
+
* real directory inside the snapshot, so neither the build nor the removal can follow a link out,
|
|
576
|
+
* and the parent must already exist: a build that created source-tree directories would leave
|
|
577
|
+
* them behind after the removal.
|
|
578
|
+
*/
|
|
579
|
+
function declaredBuildOutput(repoDir: string, build: MutantBuild): string {
|
|
580
|
+
const abs = path.join(repoDir, build.output);
|
|
581
|
+
const rel = path.relative(repoDir, abs);
|
|
582
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
583
|
+
throw new Error(`declared build output ${build.output} is not inside the snapshot — refusing`);
|
|
584
|
+
}
|
|
585
|
+
if (fs.existsSync(abs) || isLink(abs)) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`declared build output ${build.output} already exists in the snapshot — it is on the work surface, so building or removing it would touch source bytes; refusing`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const repoReal = fs.realpathSync(repoDir) + path.sep;
|
|
591
|
+
for (let dir = path.dirname(abs); dir !== repoDir; dir = path.dirname(dir)) {
|
|
592
|
+
if (isLink(dir) || (fs.existsSync(dir) && !(fs.realpathSync(dir) + path.sep).startsWith(repoReal))) {
|
|
593
|
+
throw new Error(`declared build output ${build.output} has an ancestor that leaves the snapshot — refusing`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!fs.existsSync(path.dirname(abs)) || !fs.lstatSync(path.dirname(abs)).isDirectory()) {
|
|
597
|
+
throw new Error(`declared build output ${build.output} has no parent directory in the snapshot — refusing`);
|
|
598
|
+
}
|
|
599
|
+
return abs;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function isLink(p: string): boolean {
|
|
603
|
+
try {
|
|
604
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Content digest of the build output, or null when it is not a real directory (a build that wrote nothing there). */
|
|
611
|
+
function buildOutputDigest(abs: string): string | null {
|
|
612
|
+
try {
|
|
613
|
+
if (!fs.lstatSync(abs).isDirectory()) return null;
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
return digestTree(abs, []);
|
|
618
|
+
}
|
|
619
|
+
|
|
490
620
|
// ── bounded gate execution ──────────────────────────────────────────────────
|
|
491
621
|
|
|
492
622
|
export interface GateRunResult {
|
|
@@ -670,6 +800,12 @@ export async function qualifyMutants(
|
|
|
670
800
|
list.push(m);
|
|
671
801
|
groups.set(key, list);
|
|
672
802
|
}
|
|
803
|
+
// Every group's declaration is judged before the first gate runs, so a bad one late in the
|
|
804
|
+
// set refuses the run up front instead of discarding an hour of earlier groups.
|
|
805
|
+
for (const specs of groups.values()) {
|
|
806
|
+
const build = groupBuild(specs);
|
|
807
|
+
if (build) declaredBuildOutput(snapshot.repoDir, build);
|
|
808
|
+
}
|
|
673
809
|
|
|
674
810
|
const results: GroupResult[] = [];
|
|
675
811
|
let invocationSeq = 0;
|
|
@@ -683,37 +819,79 @@ export async function qualifyMutants(
|
|
|
683
819
|
}
|
|
684
820
|
};
|
|
685
821
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
822
|
+
const markControlRed = (group: GroupResult, specs: MutantSpec[], detail: string): void => {
|
|
823
|
+
group.control = "pre-red";
|
|
824
|
+
for (const m of specs) {
|
|
825
|
+
group.mutants.push({
|
|
826
|
+
claim: m.claim,
|
|
827
|
+
verdict: classifyMutantRun({
|
|
828
|
+
controlPreOk: false,
|
|
829
|
+
matchCount: 1,
|
|
830
|
+
timedOut: false,
|
|
831
|
+
exitCode: null,
|
|
832
|
+
signatureOnFailureLine: false,
|
|
833
|
+
restoredOk: true,
|
|
834
|
+
}),
|
|
835
|
+
seconds: 0,
|
|
836
|
+
subjectSha256: "",
|
|
837
|
+
detail,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// The declared output, made from the snapshot's CURRENT bytes. Removed first, so the digest
|
|
843
|
+
// sees this build's output alone, whatever the build script does about stale files. Green only
|
|
844
|
+
// when the build exited 0 and left a real directory at the declared path; `digest` is null otherwise.
|
|
845
|
+
const buildOutput = async (
|
|
846
|
+
build: MutantBuild,
|
|
847
|
+
outputAbs: string,
|
|
848
|
+
timeoutSeconds: number,
|
|
849
|
+
): Promise<{ run: GateRunResult; digest: string | null }> => {
|
|
850
|
+
fs.rmSync(outputAbs, { recursive: true, force: true });
|
|
851
|
+
const run = await runOnce(build.argv, timeoutSeconds);
|
|
852
|
+
return { run, digest: !run.timedOut && run.exitCode === 0 ? buildOutputDigest(outputAbs) : null };
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const runGroup = async (
|
|
856
|
+
gate: string[],
|
|
857
|
+
groupMutants: MutantSpec[],
|
|
858
|
+
groupTimeout: number,
|
|
859
|
+
group: GroupResult,
|
|
860
|
+
build: MutantBuild | undefined,
|
|
861
|
+
outputAbs: string | null,
|
|
862
|
+
): Promise<void> => {
|
|
863
|
+
let baseline: string | null = null;
|
|
864
|
+
if (build && outputAbs) {
|
|
865
|
+
const b = await buildOutput(build, outputAbs, groupTimeout);
|
|
866
|
+
log(
|
|
867
|
+
` build ${build.argv.join(" ")} → ${build.output}: ${b.digest ? "built" : `RED (exit=${b.run.exitCode} timedOut=${b.run.timedOut})`} in ${b.run.seconds.toFixed(1)}s`,
|
|
868
|
+
);
|
|
869
|
+
if (!b.digest) {
|
|
870
|
+
logControlOutput(b.run.output, log);
|
|
871
|
+
markControlRed(
|
|
872
|
+
group,
|
|
873
|
+
groupMutants,
|
|
874
|
+
"declared build red at baseline — the gate's input could not be made from the snapshot's own bytes",
|
|
875
|
+
);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
baseline = b.digest;
|
|
879
|
+
}
|
|
880
|
+
// A gate that writes into its declared build output would judge the next run against
|
|
881
|
+
// bytes no build produced: every gate run below is bracketed by an output digest.
|
|
882
|
+
const wroteOutput = (expected: string | null): boolean =>
|
|
883
|
+
outputAbs !== null && buildOutputDigest(outputAbs) !== expected;
|
|
691
884
|
|
|
692
885
|
const pre = await runOnce(gate, groupTimeout);
|
|
693
|
-
const
|
|
886
|
+
const preWrote = wroteOutput(baseline);
|
|
887
|
+
const controlPreOk = !pre.timedOut && pre.exitCode === 0 && !preWrote;
|
|
694
888
|
log(
|
|
695
|
-
` control-pre ${gate.join(" ")}: ${controlPreOk ? "green" : `RED (exit=${pre.exitCode} timedOut=${pre.timedOut})`} in ${pre.seconds.toFixed(1)}s`,
|
|
889
|
+
` control-pre ${gate.join(" ")}: ${controlPreOk ? "green" : preWrote ? "RED (the gate wrote into its declared build output)" : `RED (exit=${pre.exitCode} timedOut=${pre.timedOut})`} in ${pre.seconds.toFixed(1)}s`,
|
|
696
890
|
);
|
|
697
891
|
if (!controlPreOk) {
|
|
698
892
|
logControlOutput(pre.output, log);
|
|
699
|
-
group
|
|
700
|
-
|
|
701
|
-
group.mutants.push({
|
|
702
|
-
claim: m.claim,
|
|
703
|
-
verdict: classifyMutantRun({
|
|
704
|
-
controlPreOk: false,
|
|
705
|
-
matchCount: 1,
|
|
706
|
-
timedOut: false,
|
|
707
|
-
exitCode: null,
|
|
708
|
-
signatureOnFailureLine: false,
|
|
709
|
-
restoredOk: true,
|
|
710
|
-
}),
|
|
711
|
-
seconds: 0,
|
|
712
|
-
subjectSha256: "",
|
|
713
|
-
detail: "control-pre red — no kill can be claimed against a baseline-red gate",
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
continue;
|
|
893
|
+
markControlRed(group, groupMutants, "control-pre red — no kill can be claimed against a baseline-red gate");
|
|
894
|
+
return;
|
|
717
895
|
}
|
|
718
896
|
|
|
719
897
|
let groupImpure = false;
|
|
@@ -770,18 +948,34 @@ export async function qualifyMutants(
|
|
|
770
948
|
source.replace(find, () => m.replace.join("\n")),
|
|
771
949
|
);
|
|
772
950
|
let run: GateRunResult;
|
|
951
|
+
let buildRed = false;
|
|
952
|
+
let outputWritten = false;
|
|
773
953
|
try {
|
|
774
|
-
|
|
954
|
+
if (build && outputAbs) {
|
|
955
|
+
// Rebuilt from the MUTATED bytes: the gate judges the output of the source it is handed.
|
|
956
|
+
const b = await buildOutput(build, outputAbs, m.timeoutSeconds);
|
|
957
|
+
if (b.digest === null) {
|
|
958
|
+
buildRed = true;
|
|
959
|
+
run = b.run;
|
|
960
|
+
} else {
|
|
961
|
+
run = await runOnce(m.gate, m.timeoutSeconds);
|
|
962
|
+
outputWritten = wroteOutput(b.digest);
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
run = await runOnce(m.gate, m.timeoutSeconds);
|
|
966
|
+
}
|
|
775
967
|
} finally {
|
|
776
968
|
fs.writeFileSync(subjectAbs, originalBytes);
|
|
777
969
|
}
|
|
778
|
-
const restoredOk = sha256File(subjectAbs) === originalSha;
|
|
970
|
+
const restoredOk = sha256File(subjectAbs) === originalSha && !outputWritten;
|
|
779
971
|
const verdict = classifyMutantRun({
|
|
780
972
|
controlPreOk: true,
|
|
781
973
|
matchCount: 1,
|
|
782
974
|
timedOut: run.timedOut,
|
|
783
|
-
|
|
784
|
-
|
|
975
|
+
// A build that exited 0 without leaving its declared output is a build red too.
|
|
976
|
+
exitCode: buildRed && run.exitCode === 0 ? 1 : run.exitCode,
|
|
977
|
+
// A build red is never a kill, whatever the build printed: the gate did not run.
|
|
978
|
+
signatureOnFailureLine: !buildRed && signatureAttributedToFailure(run.output, m.signature, run.failedTitles),
|
|
785
979
|
restoredOk,
|
|
786
980
|
});
|
|
787
981
|
if (!restoredOk) groupImpure = true;
|
|
@@ -790,27 +984,58 @@ export async function qualifyMutants(
|
|
|
790
984
|
verdict,
|
|
791
985
|
seconds: run.seconds,
|
|
792
986
|
subjectSha256: originalSha,
|
|
793
|
-
detail:
|
|
794
|
-
`exit=${run.exitCode} timedOut=${run.timedOut}
|
|
795
|
-
`
|
|
987
|
+
detail: buildRed
|
|
988
|
+
? `declared build red under the mutation (exit=${run.exitCode} timedOut=${run.timedOut}) — the gate did not run`
|
|
989
|
+
: `exit=${run.exitCode} timedOut=${run.timedOut} signature=${m.signature}` +
|
|
990
|
+
` attribution=${describeAttribution(run.failedTitles)}` +
|
|
991
|
+
(outputWritten ? " — the gate wrote into its declared build output" : ""),
|
|
796
992
|
});
|
|
797
993
|
log(
|
|
798
|
-
` claim ${m.claim}: ${verdict} in ${run.seconds.toFixed(1)}s (subject ${m.subject} sha256=${originalSha.slice(0, 12)}…)`,
|
|
994
|
+
` claim ${m.claim}: ${verdict} in ${run.seconds.toFixed(1)}s (subject ${m.subject} sha256=${originalSha.slice(0, 12)}…)${buildRed ? " [build red]" : ""}${outputWritten ? " [gate wrote build output]" : ""}`,
|
|
799
995
|
);
|
|
800
996
|
if (!restoredOk) break; // contaminated snapshot — stop the group
|
|
801
997
|
}
|
|
802
998
|
|
|
803
999
|
if (groupImpure) {
|
|
804
1000
|
group.control = "post-red";
|
|
805
|
-
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (build && outputAbs) {
|
|
1004
|
+
// From the RESTORED bytes the build must reproduce its baseline output exactly.
|
|
1005
|
+
const b = await buildOutput(build, outputAbs, groupTimeout);
|
|
1006
|
+
if (b.digest !== baseline) {
|
|
1007
|
+
group.control = "post-red";
|
|
1008
|
+
log(
|
|
1009
|
+
` control-post build ${build.argv.join(" ")}: RED (exit=${b.run.exitCode} timedOut=${b.run.timedOut}) — the restored bytes did not rebuild the baseline output`,
|
|
1010
|
+
);
|
|
1011
|
+
logControlOutput(b.run.output, log);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
806
1014
|
}
|
|
807
1015
|
const post = await runOnce(gate, groupTimeout);
|
|
808
|
-
const
|
|
1016
|
+
const postWrote = wroteOutput(baseline);
|
|
1017
|
+
const controlPostOk = !post.timedOut && post.exitCode === 0 && !postWrote;
|
|
809
1018
|
group.control = controlPostOk ? "ok" : "post-red";
|
|
810
1019
|
log(
|
|
811
|
-
` control-post ${gate.join(" ")}: ${controlPostOk ? "green" : `RED (exit=${post.exitCode} timedOut=${post.timedOut}) — restore contamination or gate state leak`} in ${post.seconds.toFixed(1)}s`,
|
|
1020
|
+
` control-post ${gate.join(" ")}: ${controlPostOk ? "green" : postWrote ? "RED (the gate wrote into its declared build output)" : `RED (exit=${post.exitCode} timedOut=${post.timedOut}) — restore contamination or gate state leak`} in ${post.seconds.toFixed(1)}s`,
|
|
812
1021
|
);
|
|
813
1022
|
if (!controlPostOk) logControlOutput(post.output, log);
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
for (const [key, groupMutants] of groups) {
|
|
1026
|
+
const gate = JSON.parse(key) as string[];
|
|
1027
|
+
const groupTimeout = Math.max(...groupMutants.map((m) => m.timeoutSeconds));
|
|
1028
|
+
const build = groupBuild(groupMutants);
|
|
1029
|
+
const outputAbs = build ? declaredBuildOutput(snapshot.repoDir, build) : null;
|
|
1030
|
+
const group: GroupResult = { gate, control: "skipped", mutants: [] };
|
|
1031
|
+
results.push(group);
|
|
1032
|
+
try {
|
|
1033
|
+
await runGroup(gate, groupMutants, groupTimeout, group, build, outputAbs);
|
|
1034
|
+
} finally {
|
|
1035
|
+
// The output is this group's alone: removed whatever happened, so no later group — and
|
|
1036
|
+
// not the post-run tree manifest — ever sees it. declaredBuildOutput pinned its ancestors.
|
|
1037
|
+
if (outputAbs) fs.rmSync(outputAbs, { recursive: true, force: true });
|
|
1038
|
+
}
|
|
814
1039
|
}
|
|
815
1040
|
|
|
816
1041
|
const postTree = computeTreeManifest(snapshot.repoDir);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Child probe for scripts/check-pi-mcp-bridge.ts (#125). Runs in its OWN process with a throwaway
|
|
2
|
+
// HOME / PI_CODING_AGENT_DIR set by the parent — entwurf-control reads HOME at module load and births
|
|
3
|
+
// into PI_CODING_AGENT_DIR, so this must never run inside an operator session.
|
|
4
|
+
//
|
|
5
|
+
// REAL: Pi's AgentSessionRuntime (createAgentSessionRuntime → newSession, the CLI /new contract), its
|
|
6
|
+
// extension loader, the built-in MCP extension registered exactly as the CLI registers it (a
|
|
7
|
+
// `builtin: true` entry → the `builtin:mcp` path), Pi's default stdio transport, entwurf-control's
|
|
8
|
+
// record birth and registration, and the COMPILED entwurf-bridge child. FAUX: only the model's
|
|
9
|
+
// responses (pi-ai `fauxProvider` on the public ModelRuntime seam). No network, no paid call.
|
|
10
|
+
//
|
|
11
|
+
// It REPORTS, it does not judge: every fact goes out as JSON on the last stdout line and the parent
|
|
12
|
+
// decides. The only thing it reads beyond Pi's own answers is /proc (children of THIS process).
|
|
13
|
+
//
|
|
14
|
+
// Usage: node pi-mcp-bridge-probe.mjs <repoRoot> <cwd>
|
|
15
|
+
import { readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { fauxAssistantMessage, fauxProvider, fauxText, fauxToolCall, getCurrentTools } from "@earendil-works/pi-ai";
|
|
18
|
+
import {
|
|
19
|
+
createAgentSessionFromServices,
|
|
20
|
+
createAgentSessionRuntime,
|
|
21
|
+
createAgentSessionServices,
|
|
22
|
+
createMcpExtension,
|
|
23
|
+
ModelRuntime,
|
|
24
|
+
SessionManager,
|
|
25
|
+
} from "@earendil-works/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
const [repo, cwd] = process.argv.slice(2);
|
|
28
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
29
|
+
const out = {
|
|
30
|
+
parentPid: process.pid,
|
|
31
|
+
execPath: process.execPath,
|
|
32
|
+
phases: {},
|
|
33
|
+
declaredPerCall: [],
|
|
34
|
+
toolResults: [],
|
|
35
|
+
errors: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function proc(pid) {
|
|
39
|
+
try {
|
|
40
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
41
|
+
const rest = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
42
|
+
return { state: rest[0], ppid: Number(rest[1]), starttime: rest[19] };
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Every direct child of this process, with the facts the kernel holds for it: argv, the two identity
|
|
48
|
+
// env keys, and its working directory.
|
|
49
|
+
function children() {
|
|
50
|
+
const kids = [];
|
|
51
|
+
for (const d of readdirSync("/proc")) {
|
|
52
|
+
if (!/^\d+$/.test(d)) continue;
|
|
53
|
+
const info = proc(Number(d));
|
|
54
|
+
if (!info || info.ppid !== process.pid) continue;
|
|
55
|
+
const kid = { pid: Number(d), ...info, argv: [], env: {}, cwd: null };
|
|
56
|
+
try {
|
|
57
|
+
kid.argv = readFileSync(`/proc/${d}/cmdline`, "utf8").split("\0").filter(Boolean);
|
|
58
|
+
for (const kv of readFileSync(`/proc/${d}/environ`, "utf8").split("\0")) {
|
|
59
|
+
const i = kv.indexOf("=");
|
|
60
|
+
const k = kv.slice(0, i);
|
|
61
|
+
if (k === "PI_SESSION_ID" || k === "PI_AGENT_ID") kid.env[k] = kv.slice(i + 1);
|
|
62
|
+
}
|
|
63
|
+
kid.cwd = readlinkSync(`/proc/${d}/cwd`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
kid.readError = String(err instanceof Error ? err.message : err);
|
|
66
|
+
}
|
|
67
|
+
kids.push(kid);
|
|
68
|
+
}
|
|
69
|
+
return kids;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const faux = fauxProvider();
|
|
73
|
+
const callSelf = (ctx) => {
|
|
74
|
+
out.declaredPerCall.push(getCurrentTools(ctx.messages).map((t) => t.name));
|
|
75
|
+
return fauxAssistantMessage(
|
|
76
|
+
[fauxToolCall("mcp__entwurf_bridge__entwurf_self", {}, { id: `pmb-self-${out.declaredPerCall.length}` })],
|
|
77
|
+
{
|
|
78
|
+
stopReason: "toolUse",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
faux.setResponses([
|
|
83
|
+
callSelf,
|
|
84
|
+
fauxAssistantMessage([fauxText("s1 done")]),
|
|
85
|
+
callSelf,
|
|
86
|
+
fauxAssistantMessage([fauxText("s2 done")]),
|
|
87
|
+
]);
|
|
88
|
+
const modelRuntime = await ModelRuntime.create({
|
|
89
|
+
authPath: join(agentDir, "auth.json"),
|
|
90
|
+
modelsPath: join(agentDir, "models.json"),
|
|
91
|
+
});
|
|
92
|
+
modelRuntime.registerNativeProvider(faux.provider);
|
|
93
|
+
|
|
94
|
+
const createRuntime = async ({ cwd: c, agentDir: a, sessionManager, sessionStartEvent }) => {
|
|
95
|
+
const services = await createAgentSessionServices({
|
|
96
|
+
cwd: c,
|
|
97
|
+
agentDir: a,
|
|
98
|
+
modelRuntime,
|
|
99
|
+
extensionFlagValues: new Map([["entwurf-control", true]]),
|
|
100
|
+
resourceLoaderOptions: {
|
|
101
|
+
additionalExtensionPaths: [join(repo, "pi-extensions", "entwurf-control.ts")],
|
|
102
|
+
extensionFactories: [{ name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true }],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const created = await createAgentSessionFromServices({
|
|
106
|
+
services,
|
|
107
|
+
sessionManager,
|
|
108
|
+
sessionStartEvent,
|
|
109
|
+
model: faux.getModel(),
|
|
110
|
+
});
|
|
111
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
let runtime;
|
|
115
|
+
try {
|
|
116
|
+
runtime = await createAgentSessionRuntime(createRuntime, {
|
|
117
|
+
cwd,
|
|
118
|
+
agentDir,
|
|
119
|
+
sessionManager: SessionManager.create(cwd),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
async function turn(tag) {
|
|
123
|
+
const session = runtime.session;
|
|
124
|
+
const unsubscribe = session.subscribe((ev) => {
|
|
125
|
+
if (ev.type === "tool_execution_end") {
|
|
126
|
+
const text = (ev.result?.content ?? [])
|
|
127
|
+
.filter((c) => c.type === "text")
|
|
128
|
+
.map((c) => c.text)
|
|
129
|
+
.join("\n");
|
|
130
|
+
out.toolResults.push({ tag, toolName: ev.toolName, isError: ev.isError === true, text: text.slice(0, 1200) });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
await session.bindExtensions({
|
|
135
|
+
mode: "rpc",
|
|
136
|
+
onError: (e) => out.errors.push(`${tag} ${e.event ?? ""}: ${e.error}`),
|
|
137
|
+
});
|
|
138
|
+
await session.prompt(`probe ${tag}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
out.errors.push(`${tag} prompt: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
} finally {
|
|
142
|
+
unsubscribe();
|
|
143
|
+
}
|
|
144
|
+
return session.sessionManager.getSessionId();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const native1 = await turn("s1");
|
|
148
|
+
const s1Children = children();
|
|
149
|
+
out.phases.s1 = { native: native1, children: s1Children };
|
|
150
|
+
out.phases.newSession = await runtime.newSession();
|
|
151
|
+
out.phases.afterNew = {
|
|
152
|
+
parentAlive: proc(process.pid) !== null,
|
|
153
|
+
p1: s1Children.map((k) => ({ pid: k.pid, starttime: k.starttime, now: proc(k.pid) })),
|
|
154
|
+
children: children(),
|
|
155
|
+
};
|
|
156
|
+
const native2 = await turn("s2");
|
|
157
|
+
out.phases.s2 = { native: native2, children: children() };
|
|
158
|
+
out.fauxCalls = faux.state.callCount;
|
|
159
|
+
out.fauxPending = faux.getPendingResponseCount();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
out.errors.push(`probe: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
162
|
+
} finally {
|
|
163
|
+
try {
|
|
164
|
+
await runtime?.dispose();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
out.errors.push(`dispose: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log(JSON.stringify(out));
|
|
170
|
+
process.exit(0);
|
|
@@ -1252,12 +1252,23 @@ V2_MCP_SRC="$REPO/mcp/entwurf-bridge/src/index.ts"
|
|
|
1252
1252
|
if [ -f "$V2_CONTROL_SRC" ] && grep -q 'ENTWURF_FLAG = "entwurf-control"' "$V2_CONTROL_SRC" && grep -q 'registerFlag(ENTWURF_FLAG' "$V2_CONTROL_SRC"; then
|
|
1253
1253
|
ok "pi-native --entwurf-control flag registered (ENTWURF_FLAG)"
|
|
1254
1254
|
else
|
|
1255
|
-
bad "pi-native --entwurf-control flag not found in entwurf-control.ts — the
|
|
1255
|
+
bad "pi-native --entwurf-control flag not found in entwurf-control.ts — record birth and the MCP registration depend on it"
|
|
1256
1256
|
fi
|
|
1257
|
-
|
|
1258
|
-
|
|
1257
|
+
# #125: a pi session no longer carries native Entwurf tools — the extension registers the compiled
|
|
1258
|
+
# entwurf-bridge with Pi's built-in MCP after record birth. This is STATIC DETECTION only: it proves
|
|
1259
|
+
# the shipped source makes that registration and the compiled entry it points at shipped. It does NOT
|
|
1260
|
+
# prove a running pi spawned the child or that a same-name mcp.json entry did not replace it (Pi's
|
|
1261
|
+
# `/mcp` reports that) — runtime reachability is the MCP verb check below and `doctor-pi-provider`.
|
|
1262
|
+
V2_BRIDGE_DIST="$REPO/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
|
|
1263
|
+
if [ -f "$V2_CONTROL_SRC" ] && grep -q 'pi.registerMcpServer(' "$V2_CONTROL_SRC" && grep -q 'ENTWURF_MCP_SERVER_NAME' "$V2_CONTROL_SRC"; then
|
|
1264
|
+
ok "pi-native: entwurf-control registers the entwurf-bridge MCP server (static detection)"
|
|
1259
1265
|
else
|
|
1260
|
-
bad "pi-native
|
|
1266
|
+
bad "pi-native: entwurf-control.ts does not register the entwurf-bridge MCP server — a pi session would have no Entwurf verbs"
|
|
1267
|
+
fi
|
|
1268
|
+
if [ -f "$V2_BRIDGE_DIST" ]; then
|
|
1269
|
+
ok "pi-native: compiled entwurf-bridge entry present (the path the registration points at)"
|
|
1270
|
+
else
|
|
1271
|
+
bad "pi-native: compiled entwurf-bridge entry missing at $V2_BRIDGE_DIST — run: pnpm run build-bridge"
|
|
1261
1272
|
fi
|
|
1262
1273
|
if [ -f "$V2_MCP_SRC" ] && grep -q 'server.tool(' "$V2_MCP_SRC" && grep -q '"entwurf_v2"' "$V2_MCP_SRC"; then
|
|
1263
1274
|
ok "MCP entwurf_v2 verb registered"
|
|
@@ -1282,7 +1293,7 @@ case "$REPO" in
|
|
|
1282
1293
|
;;
|
|
1283
1294
|
*)
|
|
1284
1295
|
if [ -f "$V2_SURFACE_SRC" ] && (cd "$REPO" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts >/dev/null 2>&1); then
|
|
1285
|
-
ok "check-entwurf-v2-surface gate passes (surface adapter +
|
|
1296
|
+
ok "check-entwurf-v2-surface gate passes (surface adapter + MCP wiring + pi receiver)"
|
|
1286
1297
|
else
|
|
1287
1298
|
bad "check-entwurf-v2-surface gate FAILED or surface adapter missing — run: ./run.sh check-entwurf-v2-surface"
|
|
1288
1299
|
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-foreground-only",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACP-CLAUDE-FOREGROUND-ONLY",
|
|
7
|
+
"title": "the claude launch env loses the background-task switch — a backgrounded command outlives an ACP cancel and is orphaned to init when the child exits",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/backend-adapter.ts",
|
|
9
|
+
"find": ["\t\treturn { ...claudeLaunchEnvDefaults(), ...CLAUDE_FOREGROUND_ONLY_ENV };"],
|
|
10
|
+
"replace": ["\t\treturn claudeLaunchEnvDefaults();"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-acp-foreground-only"],
|
|
12
|
+
"timeoutSeconds": 60,
|
|
13
|
+
"signature": "[QK:ACP-CLAUDE-FOREGROUND-ONLY]",
|
|
14
|
+
"signatureSource": "pi-extensions/lib/acp/backend-adapter.test.ts"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|