@junghanacs/entwurf 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +125 -0
- package/README.md +8 -11
- package/demo/README.md +1 -1
- package/docs/setup-clean-host.md +24 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +19 -10
- package/package.json +8 -8
- package/pi-extensions/lib/acp/backend-adapter.ts +19 -9
- package/pi-extensions/lib/acp/backend.ts +125 -7
- package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
- package/run.sh +108 -32
- package/scripts/check-acp-launch-namespace.ts +127 -0
- package/scripts/check-acp-prompt-lifecycle.ts +145 -2
- package/scripts/check-copilot-birth-hook.ts +28 -1
- package/scripts/check-gate-qualification.ts +3 -2
- package/scripts/check-omp-fresh-preflight.ts +27 -0
- package/scripts/check-setup-qualification.sh +40 -2
- package/scripts/copilot-bridge-oracle.sh +14 -6
- package/scripts/fake-copilot-vendor.sh +4 -2
- package/scripts/mutants/acp-launch-namespace.json +34 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
- package/scripts/mutants/copilot-birth.json +3 -5
- package/scripts/mutants/pack-install.json +2 -2
- package/scripts/mutants/setup-verdict.json +35 -0
- package/scripts/omp-config-xdev.py +310 -0
- package/scripts/omp-config-xdev.sh +76 -0
- package/scripts/omp-tool-surface.py +61 -10
- package/scripts/raw-acp-child-exit-measure/README.md +285 -0
- package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
- package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
- package/scripts/smoke-setup-verdict.sh +48 -3
|
@@ -119,7 +119,13 @@ export interface AcpChildLike {
|
|
|
119
119
|
signalCode: NodeJS.Signals | null;
|
|
120
120
|
stdin: { destroy(): void; unref?(): void };
|
|
121
121
|
stdout: { destroy(): void; unref?(): void };
|
|
122
|
-
stderr: {
|
|
122
|
+
stderr: {
|
|
123
|
+
on(event: "data", listener: (chunk: Buffer) => void): void;
|
|
124
|
+
/** Optional: absent on minimal fakes; the flush is best-effort, never load-bearing for liveness. */
|
|
125
|
+
once?(event: "close", listener: () => void): void;
|
|
126
|
+
destroy(): void;
|
|
127
|
+
unref?(): void;
|
|
128
|
+
};
|
|
123
129
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
124
130
|
unref(): void;
|
|
125
131
|
once(event: "exit" | "error", listener: (...args: unknown[]) => void): void;
|
|
@@ -210,6 +216,11 @@ interface BridgeSession {
|
|
|
210
216
|
* nothing to diagnose it by (observed 2026-07-30 on a live sonnet reuse turn).
|
|
211
217
|
*/
|
|
212
218
|
stderrTail: string[];
|
|
219
|
+
/**
|
|
220
|
+
* SESSION-scoped like `stderrTail`, and for the same reason: the launcher's
|
|
221
|
+
* frame can arrive on a turn later than the one that spawned the child.
|
|
222
|
+
*/
|
|
223
|
+
launchObservation: AcpLaunchObservation;
|
|
213
224
|
/** How the child ended, once it has — folded into the prompt-phase error. */
|
|
214
225
|
exit?: { code: number | null; signal: NodeJS.Signals | null };
|
|
215
226
|
/**
|
|
@@ -367,6 +378,85 @@ const CHILD_END_SETTLE_MS = 500;
|
|
|
367
378
|
*/
|
|
368
379
|
const ACP_CONNECTION_CLOSED_TEXT = "ACP connection closed";
|
|
369
380
|
|
|
381
|
+
/**
|
|
382
|
+
* The launcher's control frame — see `claude-acp-launch.js`.
|
|
383
|
+
*
|
|
384
|
+
* Matched as an EXACT FULL LINE and nothing else. The vendor writes prose that
|
|
385
|
+
* mentions signals; a substring test would let vendor text manufacture an
|
|
386
|
+
* entwurf observation, which is precisely the confusion #72 cost three
|
|
387
|
+
* diagnosis passes. Our own frame is a fixed enum, so exact-line matching is
|
|
388
|
+
* sufficient AND necessary.
|
|
389
|
+
*/
|
|
390
|
+
const LAUNCH_SIGNAL_FRAME_PREFIX = "ENTWURF_ACP_LAUNCH_SIGNAL=";
|
|
391
|
+
const LAUNCH_SIGNAL_FRAME_VALUES: ReadonlySet<string> = new Set(["SIGTERM", "SIGINT"]);
|
|
392
|
+
|
|
393
|
+
/** A mutable, session-scoped record of a terminating signal the LAUNCHER caught. */
|
|
394
|
+
export type AcpLaunchObservation = { signal?: "SIGTERM" | "SIGINT" };
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Split a stderr chunk stream into lines, consuming our own frames and passing
|
|
398
|
+
* everything else through to the tail untouched.
|
|
399
|
+
*
|
|
400
|
+
* Line-buffered because a chunk boundary can fall inside a frame; the trailing
|
|
401
|
+
* partial line is held, not emitted, so a frame split across two reads is still
|
|
402
|
+
* recognised exactly.
|
|
403
|
+
*/
|
|
404
|
+
function makeLaunchFrameFilter(observation: AcpLaunchObservation, onText: (text: string) => void) {
|
|
405
|
+
let held = "";
|
|
406
|
+
return {
|
|
407
|
+
write(chunk: string): void {
|
|
408
|
+
held += chunk;
|
|
409
|
+
let nl = held.indexOf("\n");
|
|
410
|
+
let passed = "";
|
|
411
|
+
while (nl !== -1) {
|
|
412
|
+
const line = held.slice(0, nl);
|
|
413
|
+
if (
|
|
414
|
+
line.startsWith(LAUNCH_SIGNAL_FRAME_PREFIX) &&
|
|
415
|
+
LAUNCH_SIGNAL_FRAME_VALUES.has(line.slice(LAUNCH_SIGNAL_FRAME_PREFIX.length))
|
|
416
|
+
) {
|
|
417
|
+
// FIRST caught signal wins: a later one is our own teardown racing
|
|
418
|
+
// the external kill, and reporting that would bury the cause.
|
|
419
|
+
observation.signal ??= line.slice(LAUNCH_SIGNAL_FRAME_PREFIX.length) as "SIGTERM" | "SIGINT";
|
|
420
|
+
} else {
|
|
421
|
+
passed += `${line}\n`;
|
|
422
|
+
}
|
|
423
|
+
held = held.slice(nl + 1);
|
|
424
|
+
nl = held.indexOf("\n");
|
|
425
|
+
}
|
|
426
|
+
if (passed) onText(passed);
|
|
427
|
+
},
|
|
428
|
+
/**
|
|
429
|
+
* The child's LAST words may arrive without a trailing newline — a process
|
|
430
|
+
* dying mid-write is exactly when that happens, and it is exactly when the
|
|
431
|
+
* tail matters most. Line buffering would otherwise hold that fragment
|
|
432
|
+
* forever, so the stream's close flushes it VERBATIM.
|
|
433
|
+
*
|
|
434
|
+
* No frame check here, deliberately: the launcher writes its frame with a
|
|
435
|
+
* single `writeSync` including the newline, well under PIPE_BUF, so a
|
|
436
|
+
* complete frame can never be the un-terminated remainder. Anything left
|
|
437
|
+
* without a newline is vendor text by construction.
|
|
438
|
+
*/
|
|
439
|
+
flush(): void {
|
|
440
|
+
if (!held) return;
|
|
441
|
+
onText(held);
|
|
442
|
+
held = "";
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* What the operator is told about a caught signal — and what they are NOT told.
|
|
449
|
+
*
|
|
450
|
+
* Absence is reported as "not observed", never as "no signal": the launcher can
|
|
451
|
+
* only see what reached it, and an override launch (`CLAUDE_AGENT_ACP_COMMAND`)
|
|
452
|
+
* has no launcher at all. Attribution of the SENDER is not claimed here either —
|
|
453
|
+
* that needs the host's journal, which this process cannot read.
|
|
454
|
+
*/
|
|
455
|
+
function launchSignalLine(observation: AcpLaunchObservation | undefined): string | undefined {
|
|
456
|
+
if (!observation?.signal) return undefined;
|
|
457
|
+
return `[acp] launch observed ${observation.signal} before child exit (sender not attributed)`;
|
|
458
|
+
}
|
|
459
|
+
|
|
370
460
|
function isAcpConnectionClosure(err: unknown): boolean {
|
|
371
461
|
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
372
462
|
return message.trim() === ACP_CONNECTION_CLOSED_TEXT;
|
|
@@ -921,14 +1011,24 @@ export function streamAcpTurn(
|
|
|
921
1011
|
stream.end();
|
|
922
1012
|
}
|
|
923
1013
|
|
|
924
|
-
function finishError(
|
|
1014
|
+
function finishError(
|
|
1015
|
+
err: unknown,
|
|
1016
|
+
aborted: boolean,
|
|
1017
|
+
stderrTail?: string[],
|
|
1018
|
+
lifecycle?: string,
|
|
1019
|
+
launchObservation?: AcpLaunchObservation,
|
|
1020
|
+
): void {
|
|
925
1021
|
finalizeAcpStreamState(state);
|
|
926
1022
|
state.output.stopReason = aborted ? "aborted" : "error";
|
|
927
1023
|
const base = err instanceof Error ? err.message : String(err);
|
|
928
1024
|
// The FIRST failure stays first and verbatim (it is what the backend
|
|
929
1025
|
// actually said); the lifecycle line is added, never substituted, so a
|
|
930
1026
|
// reader can still match the transport's own text.
|
|
931
|
-
const
|
|
1027
|
+
const withLifecycle = lifecycle ? `${base}\n${lifecycle}` : base;
|
|
1028
|
+
// Its OWN line, above the vendor tail: an entwurf-owned observation must not
|
|
1029
|
+
// have to be recovered by reading vendor prose.
|
|
1030
|
+
const observed = launchSignalLine(launchObservation);
|
|
1031
|
+
const diagnosed = observed ? `${withLifecycle}\n${observed}` : withLifecycle;
|
|
932
1032
|
const tail = (stderrTail ?? []).join("").trim().slice(-1_000);
|
|
933
1033
|
const full = tail ? `${diagnosed}\n--- backend stderr (tail) ---\n${tail}` : diagnosed;
|
|
934
1034
|
// A-c: a real failure (not an abort) that looks like a context-window
|
|
@@ -1124,6 +1224,8 @@ export function streamAcpTurn(
|
|
|
1124
1224
|
/** Which phase a failure belongs to — flipped once the prompt is on the wire. */
|
|
1125
1225
|
let phase: "pre-prompt" | "prompt" = "pre-prompt";
|
|
1126
1226
|
const stderrTail: string[] = [];
|
|
1227
|
+
// Session-scoped alongside the tail — see the field's note on the session type.
|
|
1228
|
+
const launchObservation: AcpLaunchObservation = {};
|
|
1127
1229
|
const sessionKey = resolveSessionKey(opts, cwd);
|
|
1128
1230
|
try {
|
|
1129
1231
|
if (signal?.aborted) throw new Error("aborted before launch");
|
|
@@ -1156,10 +1258,18 @@ export function streamAcpTurn(
|
|
|
1156
1258
|
const spawned = child;
|
|
1157
1259
|
|
|
1158
1260
|
// Drain stderr (an unconsumed pipe can backpressure-deadlock a long turn).
|
|
1159
|
-
|
|
1160
|
-
|
|
1261
|
+
// The launcher's own control frames are consumed here and kept OUT of the
|
|
1262
|
+
// tail: the tail is vendor evidence, the observation is an entwurf fact,
|
|
1263
|
+
// and mixing the two is the overloading #72 was made of.
|
|
1264
|
+
const consumeStderr = makeLaunchFrameFilter(launchObservation, (text) => {
|
|
1265
|
+
stderrTail.push(text);
|
|
1161
1266
|
if (stderrTail.length > 50) stderrTail.shift();
|
|
1162
1267
|
});
|
|
1268
|
+
spawned.stderr.on("data", (c: Buffer) => consumeStderr.write(c.toString()));
|
|
1269
|
+
// `close` rather than `end`: it covers the destroy path our own teardown
|
|
1270
|
+
// takes, and on the EOF-first death it lands inside the post-mortem
|
|
1271
|
+
// settle window, so the flushed fragment is in the tail before we seal.
|
|
1272
|
+
spawned.stderr.once?.("close", () => consumeStderr.flush());
|
|
1163
1273
|
|
|
1164
1274
|
// Abort during BOOTSTRAP (spawn → initialize → newSession → set-model):
|
|
1165
1275
|
// there is no prompt turn for the agent to cancel yet, so the child is
|
|
@@ -1211,6 +1321,8 @@ export function streamAcpTurn(
|
|
|
1211
1321
|
// this turn with the session, so a later reuse turn can still report
|
|
1212
1322
|
// the child's dying words.
|
|
1213
1323
|
stderrTail,
|
|
1324
|
+
// SAME object the frame filter writes into, for the same reason.
|
|
1325
|
+
launchObservation,
|
|
1214
1326
|
// Armed at spawn, before ANY turn can fail on this child — the latch
|
|
1215
1327
|
// must already exist when the `exit` listener below can fire.
|
|
1216
1328
|
childEnd: makeChildEndLatch(),
|
|
@@ -1345,7 +1457,13 @@ export function streamAcpTurn(
|
|
|
1345
1457
|
// own SIGTERM; the tail collected by then is sealed before our cleanup
|
|
1346
1458
|
// touches the stderr pipe.
|
|
1347
1459
|
const lifecycle = await diagnoseTransportClosure({ err, session, phase, aborted });
|
|
1348
|
-
finishError(
|
|
1460
|
+
finishError(
|
|
1461
|
+
err,
|
|
1462
|
+
aborted,
|
|
1463
|
+
session?.stderrTail ?? stderrTail,
|
|
1464
|
+
lifecycle,
|
|
1465
|
+
session?.launchObservation ?? launchObservation,
|
|
1466
|
+
);
|
|
1349
1467
|
// error/abort → drop the (uncertain) session and close its child; an
|
|
1350
1468
|
// uncertain connection must never be reused (GPT ④).
|
|
1351
1469
|
if (child) {
|
|
@@ -1412,7 +1530,7 @@ export function streamAcpTurn(
|
|
|
1412
1530
|
// Without the session-scoped tail a mid-turn child death on a resident
|
|
1413
1531
|
// session surfaced as a bare "ACP connection closed" with nothing to read
|
|
1414
1532
|
// it by.
|
|
1415
|
-
finishError(err, aborted, session.stderrTail, lifecycle);
|
|
1533
|
+
finishError(err, aborted, session.stderrTail, lifecycle, session.launchObservation);
|
|
1416
1534
|
// error/abort on a reused session → drop it and close the child (GPT ④).
|
|
1417
1535
|
session.retiring = true;
|
|
1418
1536
|
retainedChildren.delete(session.child);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// entwurf-owned launcher for the Claude ACP backend (#72).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS — it is not a wrapper for its own sake.
|
|
4
|
+
//
|
|
5
|
+
// The vendor bin is named `claude-agent-acp`, and that name is not ours: any
|
|
6
|
+
// harness on the host that spawns the same package produces a process with the
|
|
7
|
+
// same name. On GLG's oracle host a janitor installed for a DIFFERENT harness
|
|
8
|
+
// (openclaw's acpx, upstream PR #245) selects `claude-agent-acp` by argv
|
|
9
|
+
// SUBSTRING and SIGTERMs anything older than 900s. entwurf's child is retained
|
|
10
|
+
// across turns, so its age is the age of the SESSION, not of a turn — every
|
|
11
|
+
// session older than 15 minutes was shot at every 5 minutes. Measured: 12 of 12
|
|
12
|
+
// anomalous terminations across two boots, pid- and timestamp-locked (receipts
|
|
13
|
+
// in `scripts/raw-acp-child-exit-measure/README.md` §ANSWERED).
|
|
14
|
+
//
|
|
15
|
+
// So this launcher does two things, and deliberately nothing else:
|
|
16
|
+
//
|
|
17
|
+
// 1. NAMESPACE. It carries a name that is ours, so a name-matching janitor
|
|
18
|
+
// stops selecting our process. The vendor is `import`ed INTO this same
|
|
19
|
+
// process — not spawned as a child. That distinction is what keeps this
|
|
20
|
+
// inside #72's repair fence: with no child to restart, this cannot become
|
|
21
|
+
// the supervisor/watcher/retry the issue forbids. Any variant that spawns
|
|
22
|
+
// the vendor breaks the fence and must not be written.
|
|
23
|
+
//
|
|
24
|
+
// 2. OBSERVE A CAUGHT SIGNAL. The vendor's own handler turns SIGTERM/SIGINT
|
|
25
|
+
// into `dispose(); process.exit(0)`, so by the time entwurf sees the child
|
|
26
|
+
// end there is an exit code 0 and NO signal — a clean external kill and a
|
|
27
|
+
// vendor fault are indistinguishable. Registering first lets us record
|
|
28
|
+
// that a terminating signal was caught, before the vendor erases it.
|
|
29
|
+
//
|
|
30
|
+
// WHAT THIS COSTS, STATED SO NOBODY DISCOVERS IT LATER. Once the name split is
|
|
31
|
+
// in place, a host janitor that scans for the vendor name can no longer see
|
|
32
|
+
// entwurf's children AT ALL — including ones that genuinely leaked. If pi is
|
|
33
|
+
// SIGKILLed so `teardownChild` never runs, this adapter reparents to PID 1 and
|
|
34
|
+
// matches neither that janitor's name phase nor its orphan phase (which looks
|
|
35
|
+
// for a bare `claude`). That leak class now belongs to entwurf: after the split,
|
|
36
|
+
// we own our own cleanup story and cannot expect someone else's timer to cover
|
|
37
|
+
// it. This is a deliberate trade — being killed mid-turn is worse than leaking
|
|
38
|
+
// a process on an abnormal exit — but it is a trade, not a free win.
|
|
39
|
+
//
|
|
40
|
+
// ARGV IS NOT OURS TO TOUCH. The vendor reads `--cli` / `--version` /
|
|
41
|
+
// `--hide-claude-auth` from `process.argv`, and builds its own re-invocation
|
|
42
|
+
// command from `process.argv.slice(1)` for the terminal-auth advert. Under this
|
|
43
|
+
// launcher that advert becomes `node <this file> --cli auth login …`, which
|
|
44
|
+
// keeps working ONLY because we consume no flags and resolve the vendor
|
|
45
|
+
// ourselves. Never give this file an option of its own.
|
|
46
|
+
|
|
47
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
48
|
+
import { createRequire } from "node:module";
|
|
49
|
+
import { dirname, join } from "node:path";
|
|
50
|
+
import { pathToFileURL } from "node:url";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The one line entwurf's stderr drain looks for, matched as an EXACT full line.
|
|
54
|
+
* A fixed enum, never interpolated from vendor text: the backend must be unable
|
|
55
|
+
* to mistake vendor prose (which mentions signals) for our own observation.
|
|
56
|
+
*/
|
|
57
|
+
const SIGNAL_FRAME_PREFIX = "ENTWURF_ACP_LAUNCH_SIGNAL=";
|
|
58
|
+
|
|
59
|
+
/** Signals the vendor normalizes to exit 0, and which are therefore invisible downstream. */
|
|
60
|
+
const OBSERVED_SIGNALS = /** @type {const} */ (["SIGTERM", "SIGINT"]);
|
|
61
|
+
|
|
62
|
+
for (const signal of OBSERVED_SIGNALS) {
|
|
63
|
+
process.on(signal, () => {
|
|
64
|
+
// writeSync, not console.error: a handler may run while the process is
|
|
65
|
+
// tearing down and an async write can be dropped.
|
|
66
|
+
try {
|
|
67
|
+
writeSync(2, `${SIGNAL_FRAME_PREFIX}${signal}\n`);
|
|
68
|
+
} catch {
|
|
69
|
+
// stderr already gone — the observation is best-effort, never fatal.
|
|
70
|
+
}
|
|
71
|
+
// THE SINK GUARD. Registering a handler SUPPRESSES node's default
|
|
72
|
+
// termination. If we are still the only listener — the vendor has not
|
|
73
|
+
// registered yet, or its import failed — this observer would make the
|
|
74
|
+
// process immune to the very signal it is observing, including entwurf's
|
|
75
|
+
// own teardown SIGTERM. Stand down and let the signal land for real.
|
|
76
|
+
if (process.listenerCount(signal) === 1) {
|
|
77
|
+
process.removeAllListeners(signal);
|
|
78
|
+
process.kill(process.pid, signal);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Resolution lives INSIDE the same try as the import so both failures speak with
|
|
84
|
+
// one voice: a missing package and a broken package are the same event to an
|
|
85
|
+
// operator reading stderr, and only one of them would otherwise be legible.
|
|
86
|
+
try {
|
|
87
|
+
const require = createRequire(import.meta.url);
|
|
88
|
+
const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
89
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
90
|
+
const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
|
|
91
|
+
if (!binPath) throw new Error("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
|
|
92
|
+
// Same process, no argv touched. The vendor bin has no main-module guard, so
|
|
93
|
+
// importing it starts the agent exactly as executing it would.
|
|
94
|
+
await import(pathToFileURL(join(dirname(pkgJsonPath), binPath)).href);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Fail loud and DIE. No retry, no fallback: a launcher that survives its own
|
|
97
|
+
// failure is the hidden supervisor #72 forbids.
|
|
98
|
+
writeSync(2, `entwurf acp launcher: vendor import failed: ${err instanceof Error ? err.stack : String(err)}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
package/run.sh
CHANGED
|
@@ -119,7 +119,7 @@ run_vitest() {
|
|
|
119
119
|
usage() {
|
|
120
120
|
cat <<'EOF'
|
|
121
121
|
Usage:
|
|
122
|
-
./run.sh setup [project-dir] # ONE presence-driven composition (#86): mode-first (source bootstrap only on a checkout), then per-component PASS/SKIP/FAIL for pi (presence+floor)/claude/agy/copilot (all four units: birth→MCP→receiver→footer, independently) + stable dev bins + v2 install smoke; absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness or touches credentials
|
|
122
|
+
./run.sh setup [project-dir] # ONE presence-driven composition (#86): mode-first (source bootstrap only on a checkout), then per-component PASS/SKIP/FAIL for pi (presence+floor)/claude/agy/copilot (all four units: birth→MCP→receiver→footer, independently)/omp (four units: birth→MCP→tools.xdev setting→receiver, independently) + stable dev bins + v2 install smoke; absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness or touches credentials
|
|
123
123
|
./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
|
|
124
124
|
./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
|
|
125
125
|
./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and entwurf_resume_call registered/schema + the tools/list surface is EXACTLY the seven shipped garden verbs; tools/list only, no auth/side-effect
|
|
@@ -218,6 +218,8 @@ Usage:
|
|
|
218
218
|
./run.sh install-copilot-mcp # #82 RAIL 5: register ONE entwurf-bridge server in ~/.copilot/mcp-config.json (adopt / create / REFUSE symlink), type:local, install-state under $XDG_DATA_HOME/entwurf/copilot-mcp/
|
|
219
219
|
./run.sh uninstall-copilot-mcp # honest inverse of install-copilot-mcp from install-state
|
|
220
220
|
./run.sh doctor-copilot-mcp # static ownership/config/boot doctor; RED only when install-state exists
|
|
221
|
+
./run.sh install-omp-config # #87 follow-on: write the ONE operator setting omp's tool hand requires — `tools.xdev: false` in <omp agent dir>/config.yml. Owns exactly the line(s) it adds (recorded in install-state), refuses a symlinked config, refuses a config it cannot parse, and refuses an EXPLICIT operator `xdev: true` by name rather than overwriting a decision. Without it the vendor default wraps every MCP tool behind `xd://` and the doorbell announces a tool the model cannot call
|
|
222
|
+
./run.sh uninstall-omp-config # honest inverse from install-state: takes back exactly the recorded line(s), removes the file only when entwurf created it, REFUSES when the config changed since install
|
|
221
223
|
./run.sh install-omp-bridge # #87: install the OMP BIRTH extension into <omp agent dir>/extensions/entwurf-meta-omp (index.ts|js + lib + capability registry). No launcher and no bake — an omp hook is an in-process extension. Refuses when an inherited PI_CODING_AGENT_DIR/PI_CONFIG_DIR/PI_PROFILE makes the target agent dir ambiguous (ledger M6), and refuses ANY pre-existing artifact at the unit path that entwurf holds no ownership state for — a shape is not a proof of ownership
|
|
222
224
|
./run.sh uninstall-omp-bridge # honest inverse from install-state (exact unit dir + recorded entry; no-state host REFUSES; state deleted LAST). Records already minted are preserved
|
|
223
225
|
./run.sh doctor-omp-bridge # #87: runtime axis (importable unit, writer/registry parity, mint vs sender-marker errors on SEPARATE axes, scope-fence receipts, a root-grammar preflight that goes RED on a relative ENTWURF_META_* override instead of reporting on some other directory, CERTIFIED omp record count via meta-facts under the omp root policy — never a text grep, live omp processes carrying inherited PI_SESSION_ID/PI_AGENT_ID) + ownership axis. PI_CODING_AGENT_DIR is the vendor's own agent dir here and is reported as ignored, never as contamination. Zero records = NOT-YET, never red
|
|
@@ -250,7 +252,7 @@ Usage:
|
|
|
250
252
|
./run.sh check-dep-versions # local deterministic check that the pi pin agrees across package.json (devDeps + peer range), run.sh (peer-install pins), and the baseline docs (AGENTS/README/ROADMAP/setup-clean-host/demo)
|
|
251
253
|
./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
|
|
252
254
|
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
|
|
253
|
-
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes (version boundary: @0.84.
|
|
255
|
+
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes (version boundary: @0.84.40 must leak, @0.84.4 bare/peer-hash must pass); snapshot-safe qualification oracle, also run first inside check-pack-install
|
|
254
256
|
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
|
|
255
257
|
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for)
|
|
256
258
|
./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
|
|
@@ -1825,13 +1827,13 @@ assert.equal(peerTui, piAi,
|
|
|
1825
1827
|
// floor tracks the devDep pin so a consumer can't install against a pi lacking
|
|
1826
1828
|
// the public trust exports the bridge imports at the pinned minor, AND an upper
|
|
1827
1829
|
// bound at the next minor stops a fresh install from silently pulling a future
|
|
1828
|
-
// pi (past the declared ceiling — 0.85+ at the current 0.84.
|
|
1830
|
+
// pi (past the declared ceiling — 0.85+ at the current 0.84.4 pin) whose
|
|
1829
1831
|
// internal export surface has drifted from the one we typecheck against.
|
|
1830
1832
|
// pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
|
|
1831
1833
|
// factory churn is exactly this), so an open `>=` floor is exactly how the next
|
|
1832
1834
|
// installer re-acquires the drift. The floor is also the HARD MINIMUM a consumer
|
|
1833
|
-
// install resolves: at `>=0.84.
|
|
1834
|
-
// Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.84.
|
|
1835
|
+
// install resolves: at `>=0.84.4` an existing 0.84.3 host is upgraded, not kept.
|
|
1836
|
+
// Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.84.4 <0.85`).
|
|
1835
1837
|
const [piMaj, piMin] = piAi.split('.').map(Number);
|
|
1836
1838
|
assert.equal(piMaj, 0,
|
|
1837
1839
|
`pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
|
|
@@ -2729,6 +2731,17 @@ check_acp_prompt_lifecycle() {
|
|
|
2729
2731
|
run_ts scripts/check-acp-prompt-lifecycle.ts
|
|
2730
2732
|
}
|
|
2731
2733
|
|
|
2734
|
+
check_acp_launch_namespace() {
|
|
2735
|
+
# #72: the Claude ACP child must launch under a name entwurf OWNS. A janitor
|
|
2736
|
+
# installed on the host for ANOTHER harness selects the vendor process name
|
|
2737
|
+
# `claude-agent-acp` by argv substring and SIGTERMs it by age; entwurf retains
|
|
2738
|
+
# its child across turns, so its age is the session's. This gate holds the
|
|
2739
|
+
# name split, holds the launcher transparent (the vendor still answers
|
|
2740
|
+
# --version through it), and keeps an explicit operator override verbatim.
|
|
2741
|
+
section "ACP launch namespace (#72 — no vendor process name in our argv)"
|
|
2742
|
+
run_ts scripts/check-acp-launch-namespace.ts
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2732
2745
|
check_acp_stream_hooks() {
|
|
2733
2746
|
# Deterministic gate for the pi 0.84 streamSimple hook contract on the ACP rail
|
|
2734
2747
|
# (#63; upstream pi-mono #7372 → doc-only #7576). before_provider_request
|
|
@@ -3067,15 +3080,15 @@ check_pack() {
|
|
|
3067
3080
|
# The pin-leak filter for the install tree's .pnpm listing, shared by the real scan and its
|
|
3068
3081
|
# self-test below. The version BOUNDARY is load-bearing: a pnpm .pnpm entry is
|
|
3069
3082
|
# `<name>@<version>` followed by either `_<peer-hash>` or end-of-name (measured pnpm 11.20.0
|
|
3070
|
-
# on this tree), so an unbounded substring match would bless a lookalike such as `@0.84.
|
|
3083
|
+
# on this tree), so an unbounded substring match would bless a lookalike such as `@0.84.40`
|
|
3071
3084
|
# while announcing the pinned floor — a false-green oracle (found by independent review,
|
|
3072
3085
|
# 2026-08-25).
|
|
3073
3086
|
pack_install_leaked_pi() {
|
|
3074
|
-
grep '^@earendil-works+pi-' | grep -Ev '@0\.84\.
|
|
3087
|
+
grep '^@earendil-works+pi-' | grep -Ev '@0\.84\.4(_|$)' || true
|
|
3075
3088
|
}
|
|
3076
3089
|
|
|
3077
3090
|
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise the
|
|
3078
|
-
# false-green shape (it contains no 0.84.
|
|
3091
|
+
# false-green shape (it contains no 0.84.40), so the oracle is proven against a fixture
|
|
3079
3092
|
# listing. Expected: the two lookalikes leak, the pinned version passes bare and with a
|
|
3080
3093
|
# peer-hash suffix. Exposed as its own snapshot-safe subcommand because the heavy
|
|
3081
3094
|
# check-pack-install cannot run inside the qualification snapshot (no install environment),
|
|
@@ -3084,13 +3097,13 @@ pack_install_leaked_pi() {
|
|
|
3084
3097
|
check_pack_pin_matcher() {
|
|
3085
3098
|
local matcher_probe
|
|
3086
3099
|
matcher_probe=$(printf '%s\n' \
|
|
3087
|
-
'@earendil-works+pi-ai@0.84.
|
|
3088
|
-
'@earendil-works+pi-ai@0.84.
|
|
3089
|
-
'@earendil-works+pi-ai@0.84.
|
|
3090
|
-
'@earendil-works+pi-agent-core@0.84.
|
|
3091
|
-
if [ "$matcher_probe" != '@earendil-works+pi-ai@0.84.
|
|
3092
|
-
@earendil-works+pi-agent-core@0.84.
|
|
3093
|
-
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the 0.84.
|
|
3100
|
+
'@earendil-works+pi-ai@0.84.4' \
|
|
3101
|
+
'@earendil-works+pi-ai@0.84.4_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
3102
|
+
'@earendil-works+pi-ai@0.84.40' \
|
|
3103
|
+
'@earendil-works+pi-agent-core@0.84.3' | pack_install_leaked_pi)
|
|
3104
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@0.84.40
|
|
3105
|
+
@earendil-works+pi-agent-core@0.84.3' ]; then
|
|
3106
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the 0.84.40/0.84.3 lookalikes and pass 0.84.4 bare or with a peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
3094
3107
|
return 1
|
|
3095
3108
|
fi
|
|
3096
3109
|
echo "[check-pack-pin-matcher] ok — the pin-leak matcher is version-bounded (lookalikes leak, pinned version passes bare and with a peer-hash)"
|
|
@@ -3320,7 +3333,7 @@ _check_pack_install_impl() {
|
|
|
3320
3333
|
printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
|
|
3321
3334
|
|
|
3322
3335
|
# pi-agent-core is pinned even though we never import it: pi-coding-agent depends
|
|
3323
|
-
# on it by CARET (`^0.84.
|
|
3336
|
+
# on it by CARET (`^0.84.x`), so with no lockfile in this fresh temp project it
|
|
3324
3337
|
# floats to whatever pi published last — and that newer core then drags a NESTED
|
|
3325
3338
|
# pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
|
|
3326
3339
|
# pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
|
|
@@ -3335,24 +3348,24 @@ _check_pack_install_impl() {
|
|
|
3335
3348
|
# first time. Unpinned they would float exactly like pi-agent-core did in the
|
|
3336
3349
|
# 2026-07-21 incident above. pi-telemetry joined the explicit pin list on
|
|
3337
3350
|
# 2026-08-31: it arrives transitively (pi-agent-core and pi-ai both carry
|
|
3338
|
-
# `^0.84.
|
|
3351
|
+
# `^0.84.x` carets on it), and upstream's 0.84.4 patch publish (2026-08-28
|
|
3339
3352
|
# 22:04Z) floated that caret in this lockfile-less temp install, turning CI
|
|
3340
|
-
# red through the leak assertion below.
|
|
3341
|
-
#
|
|
3342
|
-
#
|
|
3353
|
+
# red through the leak assertion below. That explicit pin is what held the
|
|
3354
|
+
# line until the bump lane ran; the verified floor is 0.84.4 as of
|
|
3355
|
+
# 2026-09-01, and the pin moved WITH it rather than being retired. The leak
|
|
3343
3356
|
# assertion below still covers every other pi package, including any package
|
|
3344
3357
|
# a future pi bump adds to the closure.
|
|
3345
3358
|
echo "[check-pack-install] pnpm add into $tmp (with 0.84.x peers + typebox)"
|
|
3346
3359
|
local install_log
|
|
3347
3360
|
install_log=$(cd "$tmp" && pnpm add \
|
|
3348
3361
|
"$tgz_path" \
|
|
3349
|
-
"@earendil-works/pi-ai@0.84.
|
|
3350
|
-
"@earendil-works/pi-coding-agent@0.84.
|
|
3351
|
-
"@earendil-works/pi-tui@0.84.
|
|
3352
|
-
"@earendil-works/pi-agent-core@0.84.
|
|
3353
|
-
"@earendil-works/pi-client@0.84.
|
|
3354
|
-
"@earendil-works/pi-protocol@0.84.
|
|
3355
|
-
"@earendil-works/pi-telemetry@0.84.
|
|
3362
|
+
"@earendil-works/pi-ai@0.84.4" \
|
|
3363
|
+
"@earendil-works/pi-coding-agent@0.84.4" \
|
|
3364
|
+
"@earendil-works/pi-tui@0.84.4" \
|
|
3365
|
+
"@earendil-works/pi-agent-core@0.84.4" \
|
|
3366
|
+
"@earendil-works/pi-client@0.84.4" \
|
|
3367
|
+
"@earendil-works/pi-protocol@0.84.4" \
|
|
3368
|
+
"@earendil-works/pi-telemetry@0.84.4" \
|
|
3356
3369
|
"typebox@latest" \
|
|
3357
3370
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
3358
3371
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -3362,17 +3375,17 @@ _check_pack_install_impl() {
|
|
|
3362
3375
|
|
|
3363
3376
|
# A pin is a wish until the resolved tree is read back. Assert it: EVERY
|
|
3364
3377
|
# @earendil-works pi package present — direct or transitive, top level or nested —
|
|
3365
|
-
# must be the pinned 0.84.
|
|
3378
|
+
# must be the pinned 0.84.4. Anything else means an unpinned caret floated and the
|
|
3366
3379
|
# rest of this gate would be exercising a runtime nobody verified, while still
|
|
3367
|
-
# printing "pinned pi 0.84.
|
|
3380
|
+
# printing "pinned pi 0.84.4". Fail loud instead of proving the wrong floor.
|
|
3368
3381
|
local leaked_pi
|
|
3369
3382
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
3370
3383
|
if [ -n "$leaked_pi" ]; then
|
|
3371
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.84.
|
|
3384
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.84.4):"
|
|
3372
3385
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
3373
3386
|
return 1
|
|
3374
3387
|
fi
|
|
3375
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.84.
|
|
3388
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.84.4"
|
|
3376
3389
|
|
|
3377
3390
|
# Resolve the installed package.json and confirm pi.extensions
|
|
3378
3391
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -4983,6 +4996,52 @@ setup_all() {
|
|
|
4983
4996
|
fi
|
|
4984
4997
|
fi
|
|
4985
4998
|
|
|
4999
|
+
# ── omp (oh-my-pi) ── presence-driven composition, same shape as copilot. OMP
|
|
5000
|
+
# was admitted as a garden citizen in v0.16.0 with installers, doctors and
|
|
5001
|
+
# inverses for every unit — but it was never composed HERE, so the one-command
|
|
5002
|
+
# surface left the fifth backend to a hand-run verb list and the operator setting
|
|
5003
|
+
# to a documentation step. That gap is what `docs/adding-a-harness.md` step 10
|
|
5004
|
+
# now closes for every future harness: an onboarding is not finished until setup
|
|
5005
|
+
# composes it. Four units, each independent: birth (who the citizen is) → MCP
|
|
5006
|
+
# hand (what it can call) → the tools.xdev operator setting (whether those calls
|
|
5007
|
+
# are REACHABLE) → receiver (whether a reply can land). OMP_BIN pins the PROBE
|
|
5008
|
+
# for hermetic gates; the unit scripts address `omp` on PATH.
|
|
5009
|
+
local omp_rc
|
|
5010
|
+
if ! command -v "${OMP_BIN:-omp}" >/dev/null 2>&1; then
|
|
5011
|
+
setup_result omp SKIP "omp not on PATH — zero OMP wiring written"
|
|
5012
|
+
else
|
|
5013
|
+
section "omp units (native harness detected: oh-my-pi)"
|
|
5014
|
+
omp_rc=0; (cd "$REPO_DIR" && bash scripts/omp-bridge-install.sh) || omp_rc=$?
|
|
5015
|
+
if [ "$omp_rc" -eq 0 ]; then
|
|
5016
|
+
setup_result omp-birth PASS "birth extension installed — verify: ./run.sh doctor-omp-bridge"
|
|
5017
|
+
else
|
|
5018
|
+
setup_result omp-birth FAIL "detected omp, but the birth extension install did not complete (see above) — repair, then re-run setup"
|
|
5019
|
+
fi
|
|
5020
|
+
omp_rc=0; (cd "$REPO_DIR" && bash scripts/omp-mcp-bridge.sh install) || omp_rc=$?
|
|
5021
|
+
if [ "$omp_rc" -eq 0 ]; then
|
|
5022
|
+
setup_result omp-mcp PASS "MCP server registered — verify: ./run.sh doctor-omp-mcp"
|
|
5023
|
+
else
|
|
5024
|
+
setup_result omp-mcp FAIL "detected omp, but the MCP registration did not complete (see above) — repair, then re-run setup"
|
|
5025
|
+
fi
|
|
5026
|
+
# The operator setting is a component of its own because its FAIL is a real
|
|
5027
|
+
# disagreement, not a broken install: an explicit `tools.xdev: true` is the
|
|
5028
|
+
# operator's decision and the writer refuses it by name. Naming that as a
|
|
5029
|
+
# component FAIL puts the choice in front of the operator instead of silently
|
|
5030
|
+
# shipping a citizen whose tools nobody can call.
|
|
5031
|
+
omp_rc=0; (cd "$REPO_DIR" && bash scripts/omp-config-xdev.sh install) || omp_rc=$?
|
|
5032
|
+
if [ "$omp_rc" -eq 0 ]; then
|
|
5033
|
+
setup_result omp-config PASS "tools.xdev: false written — verify: ./run.sh doctor-omp-mcp"
|
|
5034
|
+
else
|
|
5035
|
+
setup_result omp-config FAIL "detected omp, but the tools.xdev operator setting did not land (see above) — resolve it, then re-run setup"
|
|
5036
|
+
fi
|
|
5037
|
+
omp_rc=0; (cd "$REPO_DIR" && bash scripts/omp-receive-install.sh) || omp_rc=$?
|
|
5038
|
+
if [ "$omp_rc" -eq 0 ]; then
|
|
5039
|
+
setup_result omp-receive PASS "receiver extension installed — verify: ./run.sh doctor-omp-receive"
|
|
5040
|
+
else
|
|
5041
|
+
setup_result omp-receive FAIL "detected omp, but the receiver extension install did not complete (see above) — repair, then re-run setup"
|
|
5042
|
+
fi
|
|
5043
|
+
fi
|
|
5044
|
+
|
|
4986
5045
|
# ── core bridge boundary ── deterministic preflight lives in `pnpm run
|
|
4987
5046
|
# check:full`; live substrate acceptance lives in `LIVE=1 ./run.sh
|
|
4988
5047
|
# release-gate <scratch> --cut`. Setup is the install path, so it verifies the
|
|
@@ -5003,7 +5062,7 @@ setup_all() {
|
|
|
5003
5062
|
local entry rest s_name s_verdict s_fails=""
|
|
5004
5063
|
for entry in "${SETUP_RESULTS[@]}"; do
|
|
5005
5064
|
s_name="${entry%%|*}"; rest="${entry#*|}"; s_verdict="${rest%%|*}"
|
|
5006
|
-
printf ' %-
|
|
5065
|
+
printf ' %-18s %-4s %s\n' "$s_name" "$s_verdict" "${rest#*|}"
|
|
5007
5066
|
if [ "$s_verdict" = "FAIL" ]; then s_fails="$s_fails $s_name"; fi
|
|
5008
5067
|
done
|
|
5009
5068
|
echo ""
|
|
@@ -6121,6 +6180,20 @@ case "$cmd" in
|
|
|
6121
6180
|
smoke-omp-mcp-state)
|
|
6122
6181
|
(cd "$REPO_DIR" && bash scripts/smoke-omp-mcp-state.sh)
|
|
6123
6182
|
;;
|
|
6183
|
+
install-omp-config)
|
|
6184
|
+
# #87 follow-on: the ONE operator setting omp's tool hand requires (`tools.xdev: false`).
|
|
6185
|
+
# A separate unit from install-omp-mcp because it answers a different question — the MCP
|
|
6186
|
+
# hand registers the server, this decides whether the registered tools are REACHABLE. It
|
|
6187
|
+
# owns exactly the line it adds and refuses an explicit operator `xdev: true` by name.
|
|
6188
|
+
shift || true
|
|
6189
|
+
(cd "$REPO_DIR" && bash scripts/omp-config-xdev.sh install "$@")
|
|
6190
|
+
;;
|
|
6191
|
+
uninstall-omp-config)
|
|
6192
|
+
# honest inverse from install-state: takes back exactly the recorded line(s), removes the
|
|
6193
|
+
# file only when entwurf created it, and REFUSES when the config changed since install.
|
|
6194
|
+
shift || true
|
|
6195
|
+
(cd "$REPO_DIR" && bash scripts/omp-config-xdev.sh uninstall "$@")
|
|
6196
|
+
;;
|
|
6124
6197
|
install-omp-bridge)
|
|
6125
6198
|
# #87: the OMP BIRTH install. Not a mode of the Claude or Copilot installer, and for a
|
|
6126
6199
|
# structural reason rather than a stylistic one: an omp "hook" IS an in-process
|
|
@@ -6453,6 +6526,9 @@ case "$cmd" in
|
|
|
6453
6526
|
check-acp-prompt-lifecycle)
|
|
6454
6527
|
check_acp_prompt_lifecycle
|
|
6455
6528
|
;;
|
|
6529
|
+
check-acp-launch-namespace)
|
|
6530
|
+
check_acp_launch_namespace
|
|
6531
|
+
;;
|
|
6456
6532
|
check-acp-stream-hooks)
|
|
6457
6533
|
check_acp_stream_hooks
|
|
6458
6534
|
;;
|