@junghanacs/entwurf 0.18.2 → 0.20.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 +2 -2
- package/BASELINE.md +23 -5
- package/CHANGELOG.md +415 -0
- package/DELIVERY.md +59 -6
- package/README.md +43 -12
- package/VERIFY.md +10 -2
- package/docs/acp-backend-rail.md +9 -1
- package/docs/external-mcp-host.md +8 -5
- package/docs/setup-clean-host.md +93 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +18 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +76 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +54 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resolve-tmux-session.js +114 -0
- package/mcp/entwurf-bridge/src/index.ts +22 -7
- package/package.json +2 -2
- package/pi-extensions/acp-provider.ts +1 -1
- package/pi-extensions/entwurf-control.ts +27 -3
- package/pi-extensions/lib/acp/models.ts +3 -3
- package/pi-extensions/lib/acp/overlay.ts +6 -2
- package/pi-extensions/lib/meta-session.ts +25 -0
- package/pi-extensions/lib/mux-fresh-call.ts +98 -11
- package/pi-extensions/lib/mux-placement.ts +65 -10
- package/pi-extensions/lib/resolve-tmux-session.ts +137 -0
- package/run.sh +71 -26
- package/scripts/agy-bridge.sh +1 -1
- package/scripts/agy-imprint.sh +10 -7
- package/scripts/check-acp-carrier-augment.ts +6 -5
- package/scripts/check-acp-cortex.ts +41 -0
- package/scripts/check-acp-provider-surface.ts +9 -6
- package/scripts/check-copilot-birth-hook.ts +3 -1
- package/scripts/check-copilot-receive-arm.ts +169 -21
- package/scripts/check-copilot-statusline.ts +3 -1
- package/scripts/check-fresh-cut-gate.sh +54 -2
- package/scripts/check-gate-qualification.ts +13 -12
- package/scripts/check-hook-launch-topology.ts +6 -5
- package/scripts/check-install-surface.ts +2 -1
- package/scripts/check-meta-facts.ts +6 -4
- package/scripts/check-meta-hook-session-switch.ts +4 -3
- package/scripts/check-meta-identity-consumers.ts +8 -7
- package/scripts/check-meta-receiver-marker.ts +2 -1
- package/scripts/check-mux-launch-tmux.ts +77 -2
- package/scripts/check-mux-launch.ts +17 -0
- package/scripts/check-mux-placement-tmux.ts +61 -1
- package/scripts/check-mux-placement.ts +33 -0
- package/scripts/check-omp-birth-hook.ts +3 -2
- package/scripts/check-probe-bridge-command.ts +3 -1
- package/scripts/check-setup-qualification.sh +21 -0
- package/scripts/copilot-bridge-doctor.sh +15 -5
- package/scripts/copilot-bridge-install.sh +6 -2
- package/scripts/copilot-launch.sh +16 -2
- package/scripts/copilot-receive-bridge.sh +80 -13
- package/scripts/lib/reclaim-on-exit.ts +86 -0
- package/scripts/meta-bridge-doctor.sh +56 -15
- package/scripts/meta-bridge-install.sh +13 -7
- package/scripts/meta-bridge-uninstall.sh +6 -4
- package/scripts/mutants/acp-cortex.json +26 -2
- package/scripts/mutants/copilot-receive.json +26 -0
- package/scripts/mutants/fresh-cut.json +26 -0
- package/scripts/mutants/mux-boundary.json +24 -0
- package/scripts/mutants/mux-fresh-call.json +81 -0
- package/scripts/mutants/omp-birth.json +33 -3
- package/scripts/mutants/setup-verdict.json +13 -0
- package/scripts/omp-bridge-doctor.sh +110 -31
- package/scripts/omp-bridge-install.sh +6 -2
- package/scripts/omp-receive-install.sh +7 -2
- package/scripts/raw-async-delivery/README.md +10 -1
- package/scripts/raw-codex-measure/README.md +689 -0
- package/scripts/raw-codex-measure/source-audit.md +243 -0
- package/scripts/raw-macos-measure/README.md +148 -0
- package/scripts/raw-macos-measure/probe.sh +389 -0
- package/scripts/smoke-meta-async-drift.sh +7 -5
- package/scripts/smoke-meta-install-state.sh +70 -19
- package/scripts/smoke-mux-fresh-call-live.ts +175 -3
- package/scripts/smoke-omp-bridge-state.sh +43 -2
- package/scripts/smoke-setup-verdict.sh +100 -0
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* is still listed on an immediate re-read — which is why no post-launch presence check is
|
|
25
25
|
* performed and why the precondition runs before the window exists
|
|
26
26
|
* - a precondition refusal opens NO window at all
|
|
27
|
+
* - the #105 seat, through the real `freshCall`: an absent seat refuses and creates nothing,
|
|
28
|
+
* an existing one puts the window in THAT session with a receipt naming the resolved
|
|
29
|
+
* target, and the resulting handle closes through `closeWindow` from outside that session
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
import assert from "node:assert/strict";
|
|
@@ -97,7 +100,7 @@ function alive(pid: string): boolean {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
function main(): void {
|
|
103
|
+
async function main(): Promise<void> {
|
|
101
104
|
if (spawnSync("tmux", ["-V"], { encoding: "utf8" }).status !== 0) {
|
|
102
105
|
skipLive(LABEL, "tmux is not installed — install tmux to run the launch acceptance");
|
|
103
106
|
}
|
|
@@ -298,6 +301,78 @@ function main(): void {
|
|
|
298
301
|
originalPanes.every((p) => panes().includes(p)),
|
|
299
302
|
);
|
|
300
303
|
ok("restored: focus never moved", windows().find((w) => w.endsWith("|1")) === activeBefore);
|
|
304
|
+
|
|
305
|
+
// ── the fresh-call composition, against a real server (#105) ──────────────────
|
|
306
|
+
// The seat's deterministic gate can prove the argv, the leaf and the refusals, but the
|
|
307
|
+
// two things that only exist once tmux has answered — WHICH session the window landed
|
|
308
|
+
// in, and what the receipt says about it — had no oracle independent of the production
|
|
309
|
+
// source. This cell is that oracle: the same hermetic runtime above, a SECOND session
|
|
310
|
+
// on this same private server, and the real `freshCall`, read through its return value
|
|
311
|
+
// and the server's own inventory.
|
|
312
|
+
{
|
|
313
|
+
const { freshCall } = await import("../pi-extensions/lib/mux-fresh-call.ts");
|
|
314
|
+
const { closeWindow } = await import("../pi-extensions/lib/mux-placement.ts");
|
|
315
|
+
const call = (placementInput?: { tmuxSession: string }) =>
|
|
316
|
+
freshCall(
|
|
317
|
+
{
|
|
318
|
+
backend: "pi",
|
|
319
|
+
model: "fixture/model",
|
|
320
|
+
task: "fixture task",
|
|
321
|
+
placement: placementInput,
|
|
322
|
+
callerGardenId: "20260101T000000-fixture",
|
|
323
|
+
},
|
|
324
|
+
inherited,
|
|
325
|
+
);
|
|
326
|
+
const inventory = (): string => fxLines("list-windows", "-a", "-F", "#{session_id}|#{window_id}").join(" ");
|
|
327
|
+
|
|
328
|
+
// (i) an ABSENT seat: named refusal, and the server is byte-identical afterwards.
|
|
329
|
+
const beforeAbsent = inventory();
|
|
330
|
+
const absent = call({ tmuxSession: "nosuchseat" });
|
|
331
|
+
ok(
|
|
332
|
+
"seat: an absent seat refuses as tmux-session-missing and creates NOTHING — no window, no session",
|
|
333
|
+
!absent.ok && absent.reason === "tmux-session-missing" && inventory() === beforeAbsent,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// (ii) an EXISTING seat on the same server: the window lands THERE, the caller's own
|
|
337
|
+
// session is untouched, and the receipt names the resolved target rather than the caller.
|
|
338
|
+
assert.equal(fx("new-session", "-d", "-s", `${SESSION}-seat`).status, 0, "fixture seat session");
|
|
339
|
+
const seatId = fxLines("list-windows", "-t", `=${SESSION}-seat`, "-F", "#{session_id}")[0];
|
|
340
|
+
const callerWindowsBefore = fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ");
|
|
341
|
+
const seated = call({ tmuxSession: `${SESSION}-seat` });
|
|
342
|
+
assert.ok(seated.ok, `the seated fresh call must succeed: ${seated.ok ? "" : seated.reason}`);
|
|
343
|
+
const receipt = seated.receipt;
|
|
344
|
+
ok(
|
|
345
|
+
"seat: the receipt reports the RESOLVED target session and echoes the REQUESTED name — not the caller's session",
|
|
346
|
+
receipt.sessionId === seatId &&
|
|
347
|
+
receipt.sessionId !== placement.sessionId &&
|
|
348
|
+
receipt.tmuxSession === `${SESSION}-seat`,
|
|
349
|
+
);
|
|
350
|
+
ok(
|
|
351
|
+
"seat: tmux agrees — the window is in the seat, and the caller's own session is byte-identical",
|
|
352
|
+
fxLines("list-windows", "-t", seatId, "-F", "#{window_id}").includes(receipt.windowId) &&
|
|
353
|
+
fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ") === callerWindowsBefore,
|
|
354
|
+
);
|
|
355
|
+
ok(
|
|
356
|
+
"seat: an omitted seat still lands in the caller's own session and the receipt names none",
|
|
357
|
+
(() => {
|
|
358
|
+
const own = call();
|
|
359
|
+
if (!own.ok) return false;
|
|
360
|
+
const here = own.receipt.sessionId === placement.sessionId && own.receipt.tmuxSession === undefined;
|
|
361
|
+
process.kill(Number(own.receipt.panePid), "SIGKILL");
|
|
362
|
+
return here;
|
|
363
|
+
})(),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// (iii) the close side: that handle closes through the production verb, in a session
|
|
367
|
+
// that is NOT the caller's — the reason close binds to the server half.
|
|
368
|
+
ok(
|
|
369
|
+
"seat: the placed window closes through its own handle and tmux stops listing it",
|
|
370
|
+
closeWindow(receipt, inherited) === "closed" &&
|
|
371
|
+
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
|
|
372
|
+
);
|
|
373
|
+
fx("kill-session", "-t", seatId);
|
|
374
|
+
ok("seat: the fixture is back to one session", sessionCount() === 1);
|
|
375
|
+
}
|
|
301
376
|
} finally {
|
|
302
377
|
fx("kill-server");
|
|
303
378
|
if (fs.existsSync(SOCKET)) fs.rmSync(SOCKET, { force: true });
|
|
@@ -313,4 +388,4 @@ function main(): void {
|
|
|
313
388
|
console.log(`\n${LABEL}: ${passed} checks passed`);
|
|
314
389
|
}
|
|
315
390
|
|
|
316
|
-
main();
|
|
391
|
+
await main();
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* MUX-LAUNCH-NO-SHELL-ARGV the fixed runtime is passed after `--`, never as a string
|
|
20
20
|
* MUX-LAUNCH-NO-CARRIER argv is the append shape plus the runtime, nothing else
|
|
21
21
|
* MUX-LAUNCH-CORE-IMPORT-FREE the launch module and entwurf delivery never import each other
|
|
22
|
+
* RESOLVE-TMUX-SESSION-IMPORT-FREE the session-lookup leaf imports nothing at all
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
import assert from "node:assert/strict";
|
|
@@ -270,6 +271,22 @@ function main(): void {
|
|
|
270
271
|
"boundary: the placement leaf does not import the launch module — the leaf stays deletable on its own",
|
|
271
272
|
!importsLaunch("pi-extensions/lib/mux-placement.ts"),
|
|
272
273
|
);
|
|
274
|
+
// ── the session-lookup leaf imports NOTHING (docs §11, #105) ─────────────────
|
|
275
|
+
// It is the one leaf whose entire safety argument is that it cannot acquire an
|
|
276
|
+
// opinion: no tmux of its own (the runner is injected), no mux module, no entwurf
|
|
277
|
+
// core, and not even a node builtin. §11 states that; nothing held it, and the first
|
|
278
|
+
// `import { runTmux } from "./mux-placement.ts"` would quietly turn a decision leaf
|
|
279
|
+
// into a second place that can run tmux.
|
|
280
|
+
ok(
|
|
281
|
+
"[QK:RESOLVE-TMUX-SESSION-IMPORT-FREE] the session-lookup leaf imports nothing at all — not mux, not entwurf core, not even a node builtin — so the only power it has is the runner its caller injects",
|
|
282
|
+
importsOf(fs.readFileSync("pi-extensions/lib/resolve-tmux-session.ts", "utf8")).length === 0,
|
|
283
|
+
);
|
|
284
|
+
ok(
|
|
285
|
+
"boundary: the fresh-call composition is the ONLY shipped source that imports that leaf",
|
|
286
|
+
PRODUCTION_SOURCES.filter((m) =>
|
|
287
|
+
importsOf(fs.readFileSync(m, "utf8")).some((spec) => spec.includes("resolve-tmux-session")),
|
|
288
|
+
).join(",") === FRESH_CALL_MODULE,
|
|
289
|
+
);
|
|
273
290
|
// Prose is not behaviour: the module header names identity vocabulary precisely to say
|
|
274
291
|
// it owns none of it, so this assertion reads CODE with the comments stripped. A check
|
|
275
292
|
// that failed on its own documentation would push the boundary out of the docs.
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* - handles are stable @window/%pane
|
|
28
28
|
* - the rc=0 trap is real, and inspectPlacement refuses it instead of guessing
|
|
29
29
|
* - close reports `closed` for a live window and `already-gone` after a natural exit
|
|
30
|
+
* - a window placed in ANOTHER session of the same server closes through its own handle,
|
|
31
|
+
* while a handle from another SERVER is still refused (#105 close-side binding)
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
34
|
import assert from "node:assert/strict";
|
|
@@ -259,9 +261,10 @@ function main(): void {
|
|
|
259
261
|
/context changed/,
|
|
260
262
|
"appendWindow must refuse a placement from another session",
|
|
261
263
|
);
|
|
264
|
+
// Close binds to the SERVER half (#105), so its refusal names that half by its own word.
|
|
262
265
|
assert.throws(
|
|
263
266
|
() => closeWindow({ ...w4, serverPid: "999999" }, inherited),
|
|
264
|
-
/
|
|
267
|
+
/server changed/,
|
|
265
268
|
"closeWindow must refuse a handle from another server",
|
|
266
269
|
);
|
|
267
270
|
ok("binding: append/close refuse a foreign server or session before mutating", windows().length === 4);
|
|
@@ -305,6 +308,63 @@ function main(): void {
|
|
|
305
308
|
originalPanes.every((p) => panes().includes(p)),
|
|
306
309
|
);
|
|
307
310
|
ok("restored: focus never moved", windows().find((w) => w.endsWith("|1")) === activeBefore);
|
|
311
|
+
|
|
312
|
+
// ── close binds to the SERVER, not the session (#105) ─────────────────────────
|
|
313
|
+
// Since #105 a launched window may live in a session that is not the caller's, so the
|
|
314
|
+
// close side had to stop requiring the session half. This is that decision judged
|
|
315
|
+
// against a real server: a SECOND session, a window opened into it from the caller's
|
|
316
|
+
// pane the way the fresh-call composition does, and a close through the same handle.
|
|
317
|
+
// Both foreign-server refusal and the two close outcomes above stay exactly as they
|
|
318
|
+
// were — the session half is what changed, and only for close.
|
|
319
|
+
assert.equal(fx("new-session", "-d", "-s", `${SESSION}-b`).status, 0, "fixture second session");
|
|
320
|
+
ok("cross-session: the fixture now holds two sessions", sessionCount() === 2);
|
|
321
|
+
const otherSessionId = fxLines("list-windows", "-t", `=${SESSION}-b`, "-F", "#{session_id}")[0];
|
|
322
|
+
ok("cross-session: the second session resolves to a native id by exact name", /^\$[0-9]+$/.test(otherSessionId));
|
|
323
|
+
const placedRow = fxLines(
|
|
324
|
+
"new-window",
|
|
325
|
+
"-d",
|
|
326
|
+
"-a",
|
|
327
|
+
"-t",
|
|
328
|
+
`${otherSessionId}:{end}`,
|
|
329
|
+
"-P",
|
|
330
|
+
"-F",
|
|
331
|
+
"#{window_id}|#{window_index}|#{pane_id}|#{pane_pid}",
|
|
332
|
+
)[0].split("|");
|
|
333
|
+
const placed = {
|
|
334
|
+
serverPid: placement.serverPid,
|
|
335
|
+
sessionId: otherSessionId,
|
|
336
|
+
windowId: placedRow[0],
|
|
337
|
+
windowIndex: placedRow[1],
|
|
338
|
+
paneId: placedRow[2],
|
|
339
|
+
panePid: placedRow[3],
|
|
340
|
+
};
|
|
341
|
+
ok(
|
|
342
|
+
"cross-session: the window really landed in the OTHER session, and the caller's session is untouched",
|
|
343
|
+
fxLines("list-windows", "-t", otherSessionId, "-F", "#{window_id}").includes(placed.windowId) &&
|
|
344
|
+
!fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").includes(placed.windowId),
|
|
345
|
+
);
|
|
346
|
+
ok(
|
|
347
|
+
"cross-session: the caller's own session still shows windows 1,2 and its focus is unmoved",
|
|
348
|
+
fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").length === 2 &&
|
|
349
|
+
windows().find((w) => w.endsWith("|1")) === activeBefore,
|
|
350
|
+
);
|
|
351
|
+
// The predicate change, stated as the two facts that had to move together.
|
|
352
|
+
assert.throws(
|
|
353
|
+
() => closeWindow({ ...placed, serverPid: "999999" }, inherited),
|
|
354
|
+
/server changed/,
|
|
355
|
+
"closeWindow must still refuse a handle from another server",
|
|
356
|
+
);
|
|
357
|
+
ok(
|
|
358
|
+
"cross-session: a window in another session of the SAME server closes through its own handle — the release lifecycle smoke's close path reaches a placed window",
|
|
359
|
+
closeWindow(placed, inherited) === "closed",
|
|
360
|
+
);
|
|
361
|
+
ok(
|
|
362
|
+
"cross-session: it is gone from the other session, and nothing else moved",
|
|
363
|
+
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(placed.windowId) &&
|
|
364
|
+
fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").length === 2,
|
|
365
|
+
);
|
|
366
|
+
fx("kill-session", "-t", otherSessionId);
|
|
367
|
+
ok("cross-session: the fixture is back to one session", sessionCount() === 1);
|
|
308
368
|
} finally {
|
|
309
369
|
fx("kill-server");
|
|
310
370
|
if (fs.existsSync(SOCKET)) fs.rmSync(SOCKET, { force: true });
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* MUX-TMUX-FAILURE-LOUD a nonzero/signalled tmux run is raised, never read as a fact
|
|
23
23
|
* MUX-APPEND-END-DETACHED append is `-d -a -t <session_id>:{end}`, no carrier
|
|
24
24
|
* MUX-CONTEXT-BOUND-MUTATION a mutation matches the server pid AND the session id
|
|
25
|
+
* MUX-CLOSE-SERVER-BOUND a close binds to the server half only — same server, any session
|
|
25
26
|
* MUX-CLOSE-BY-WINDOW-ID close targets `@window`, and absence needs positive proof
|
|
26
27
|
*/
|
|
27
28
|
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
isDecimal,
|
|
39
40
|
isPaneId,
|
|
40
41
|
isSameContext,
|
|
42
|
+
isSameServer,
|
|
41
43
|
isSessionId,
|
|
42
44
|
isWindowId,
|
|
43
45
|
parsePlacement,
|
|
@@ -254,6 +256,37 @@ function main(): void {
|
|
|
254
256
|
);
|
|
255
257
|
})(),
|
|
256
258
|
);
|
|
259
|
+
// ── a close binds to the server, and the session half is covered by id uniqueness ────
|
|
260
|
+
// `appendWindow` and `closeWindow` are answering different questions, so they bind to
|
|
261
|
+
// different halves of the same fact. Append asks "is this target still the caller's own
|
|
262
|
+
// seat?" and needs both. Close asks "is this handle still the window it was born as?" —
|
|
263
|
+
// and since #105 a launched window may legitimately live in a session that is not the
|
|
264
|
+
// caller's, so requiring the session half would refuse a legitimate close. `[측정
|
|
265
|
+
// 2026-09-07]` window ids are handed out monotonically and never recycled within one
|
|
266
|
+
// server's life (after `@2` was killed the next was `@3`; after a whole session holding
|
|
267
|
+
// `@4`/`@5` was killed the next was `@6`), so on a matching server the `@id` alone
|
|
268
|
+
// identifies the window. What must still be refused is a handle from a DIFFERENT or
|
|
269
|
+
// restarted server, where the same id names something else.
|
|
270
|
+
ok(
|
|
271
|
+
"[QK:MUX-CLOSE-SERVER-BOUND] a close-side context matches on the server pid ALONE — another session on the same server is accepted, and every foreign server is refused however familiar its session id looks",
|
|
272
|
+
(() => {
|
|
273
|
+
const origin = { serverPid: "8150", sessionId: "$11" };
|
|
274
|
+
return (
|
|
275
|
+
isSameServer(origin, { serverPid: "8150", sessionId: "$11" }) &&
|
|
276
|
+
isSameServer(origin, { serverPid: "8150", sessionId: "$12" }) &&
|
|
277
|
+
!isSameServer(origin, { serverPid: "9999", sessionId: "$11" }) &&
|
|
278
|
+
!isSameServer(origin, { serverPid: "9999", sessionId: "$12" })
|
|
279
|
+
);
|
|
280
|
+
})(),
|
|
281
|
+
);
|
|
282
|
+
ok(
|
|
283
|
+
"binding: the two predicates are different contracts — append's refuses a foreign session that close's accepts, so neither may be expressed in terms of the other",
|
|
284
|
+
(() => {
|
|
285
|
+
const origin = { serverPid: "8150", sessionId: "$11" };
|
|
286
|
+
const otherSession = { serverPid: "8150", sessionId: "$12" };
|
|
287
|
+
return !isSameContext(origin, otherSession) && isSameServer(origin, otherSession);
|
|
288
|
+
})(),
|
|
289
|
+
);
|
|
257
290
|
ok(
|
|
258
291
|
"binding: a window handle carries the context it was born in",
|
|
259
292
|
(() => {
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
processStartKey,
|
|
52
52
|
readMetaReceiverMarker,
|
|
53
53
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
54
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
54
55
|
|
|
55
56
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
56
57
|
const UNIT = "entwurf-meta-omp";
|
|
@@ -62,7 +63,8 @@ function ok(label: string, cond: boolean): void {
|
|
|
62
63
|
passed++;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-omp-birth."));
|
|
66
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-omp-birth.")));
|
|
67
|
+
|
|
66
68
|
const asm = path.join(root, "asm");
|
|
67
69
|
|
|
68
70
|
// ── 1. the real assembler ────────────────────────────────────────────────────
|
|
@@ -1045,5 +1047,4 @@ for (const cell of ROOT_CELLS) {
|
|
|
1045
1047
|
);
|
|
1046
1048
|
}
|
|
1047
1049
|
|
|
1048
|
-
rmSync(root, { recursive: true, force: true });
|
|
1049
1050
|
console.log(`[check-omp-birth-hook] ${passed} assertions ok`);
|
|
@@ -13,6 +13,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
16
17
|
import { EXPECTED_TOOLS, probeBridgeCommand } from "./probe-bridge-command.ts";
|
|
17
18
|
|
|
18
19
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -32,7 +33,8 @@ function ok(label: string, cond: boolean, detail?: string): void {
|
|
|
32
33
|
passed++;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const dir = mkdtempSync(join(tmpdir(), "entwurf-probe-gate-"));
|
|
36
|
+
const dir = reclaimOnExit(mkdtempSync(join(tmpdir(), "entwurf-probe-gate-")));
|
|
37
|
+
|
|
36
38
|
const stub = (name: string, body: string): string => {
|
|
37
39
|
const p = join(dir, name);
|
|
38
40
|
writeFileSync(p, body);
|
|
@@ -160,5 +160,26 @@ want "F: an EXPLICIT operator tools.xdev:true is refused by name, never overwrit
|
|
|
160
160
|
want "F control: the disagreement is a component FAIL that leaves the other omp units composed" \
|
|
161
161
|
"printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-receive: PASS' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
|
|
162
162
|
|
|
163
|
+
# ── Cell G: a COMPLETED harness install on a platform whose RAIL has no receipt ──
|
|
164
|
+
# 0.20.0 opened the harness installers to Darwin, which retired the only thing that
|
|
165
|
+
# used to keep a detected Mac host non-green — the installer's own refusal. Install
|
|
166
|
+
# portability and rail certification are separate evidence axes (Hard Rule 17) and
|
|
167
|
+
# `setup` measures only the first, so the completed install must land as the NAMED
|
|
168
|
+
# non-green, never as a PASS pointing at a doctor that refuses on that platform.
|
|
169
|
+
# `uname` is faked on PATH, the same seam smoke-meta-install-state.sh uses for the
|
|
170
|
+
# installer's platform gate; Cell E above is the certified-platform control (same
|
|
171
|
+
# stub omp, same sandbox, unfaked platform → PASS rows).
|
|
172
|
+
FAKE_UNAME="$SB/fake-uname"; mkdir -p "$FAKE_UNAME"
|
|
173
|
+
printf '#!%s\nprintf "%%s\\n" Darwin\n' "$(command -v bash)" > "$FAKE_UNAME/uname"
|
|
174
|
+
chmod +x "$FAKE_UNAME/uname"
|
|
175
|
+
run_setup "$SB/home-g" "$SB/proj-g" "$FAKE_UNAME:$STUB_OMP:$SB/bin:$PATH" "$ABSENT" "" "$STUB_OMP/omp"
|
|
176
|
+
# One assertion by design: the row, its REASON, and the ARTIFACT proving the install
|
|
177
|
+
# actually completed. Drop the artifact half and the cell would also pass if the fake
|
|
178
|
+
# platform had merely broken the installer — the opposite of what is being claimed.
|
|
179
|
+
want "G: a completed install whose rail has no receipt is the named non-green, never a cosmetic PASS [QK:SETUP-DARWIN-RAIL-COSMETIC-PASS]" \
|
|
180
|
+
"printf '%s' \"\$OUT\" | grep -q 'omp-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'NOT CERTIFIED — pending physical host on Darwin' && [ -d '$SB/home-g/.omp/agent/extensions/entwurf-meta-omp' ]"
|
|
181
|
+
want "G control: the reason reads as an evidence boundary, not an install refusal, and names the axis owner" \
|
|
182
|
+
"printf '%s' \"\$OUT\" | grep -q 'the wiring WAS written and nothing failed to install' && printf '%s' \"\$OUT\" | grep -qF \"owned by './run.sh doctor-omp-bridge'\" && ! printf '%s' \"\$OUT\" | grep -qi 'unsupported platform'"
|
|
183
|
+
|
|
163
184
|
echo ""
|
|
164
185
|
echo "check-setup-qualification: $PASS checks passed (mutation-attribution oracle only — behavior evidence lives in smoke-setup-verdict and check-pack-install)"
|
|
@@ -183,6 +183,8 @@ if [ -f "$HOOK_LOG" ]; then
|
|
|
183
183
|
#
|
|
184
184
|
# grep -c prints 0 AND exits 1 with no match, so a `|| echo 0` fallback would append a
|
|
185
185
|
# SECOND line and every numeric test below would die on "0\n0". Keep it to one line.
|
|
186
|
+
# That rule still governs the plain-literal marker counters further down; the mint
|
|
187
|
+
# selector no longer needs it, for the reason in the next paragraph.
|
|
186
188
|
#
|
|
187
189
|
# MINT ERRORS ONLY. Since #82 RAIL 5b this unit also writes a SENDER marker, and a
|
|
188
190
|
# failed marker write is an ERROR that lands AFTER the successful mint line — so the
|
|
@@ -190,10 +192,18 @@ if [ -f "$HOOK_LOG" ]; then
|
|
|
190
192
|
# sentence that is simply false (the record IS there). The two failures are separated
|
|
191
193
|
# here rather than downgraded in the payload, because a marker write that keeps
|
|
192
194
|
# breaking must stay loud somewhere; it just is not a birth failure.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
#
|
|
196
|
+
# PORTABLE SELECTOR, SAME CONTRACT. That separation is a negative lookahead, and BSD
|
|
197
|
+
# grep (macOS) has no `-P` at all — so the selector is awk, where two conditions ARE
|
|
198
|
+
# `(?!…)`. It also removes the `grep -c` exit-1 hazard the paragraph above works
|
|
199
|
+
# around: awk prints 0 and exits 0 on a zero-match file, so there is no second line
|
|
200
|
+
# to launder. The success selector moves from GNU BRE `\(a\|b\)` to ERE `(a|b)` for
|
|
201
|
+
# the same portability reason — BRE alternation is a GNU extension, so on BSD grep
|
|
202
|
+
# that pattern matches nothing and a RECOVERED host reads as unrecovered (false RED).
|
|
203
|
+
MINT_ERRORS='/ ERROR \[copilot\] / && !/ ERROR \[copilot\] sender-marker-/'
|
|
204
|
+
LAST_ERROR_LINE="$(awk "$MINT_ERRORS{print NR}" "$HOOK_LOG" 2>/dev/null | tail -1)"
|
|
205
|
+
LAST_OK_LINE="$(grep -nE ' INFO \[copilot\] (create|attach) ' "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
|
|
206
|
+
TOTAL_ERRORS="$(awk "$MINT_ERRORS{n++} END{print n+0}" "$HOOK_LOG" 2>/dev/null)"
|
|
197
207
|
TOTAL_ERRORS="${TOTAL_ERRORS:-0}"
|
|
198
208
|
if [ -z "$LAST_ERROR_LINE" ]; then
|
|
199
209
|
ok "no copilot ERROR lines in $HOOK_LOG"
|
|
@@ -201,7 +211,7 @@ if [ -f "$HOOK_LOG" ]; then
|
|
|
201
211
|
note "$TOTAL_ERRORS historical copilot ERROR line(s) in $HOOK_LOG, all followed by a successful mint (line $LAST_OK_LINE > $LAST_ERROR_LINE) — recovered, not red"
|
|
202
212
|
else
|
|
203
213
|
bad "the newest copilot line in $HOOK_LOG is an unrecovered ERROR — the hook RAN and did not mint:"
|
|
204
|
-
|
|
214
|
+
awk "$MINT_ERRORS" "$HOOK_LOG" | tail -3 | sed 's/^/ /'
|
|
205
215
|
fi
|
|
206
216
|
|
|
207
217
|
# WHO-SENT, judged on its own axis. Both outcomes leave a citizen that EXISTS and can
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
# first-party extension, installed by `run.sh install-copilot-receive`, which also owns
|
|
23
23
|
# the launch-flag check. Four surfaces, four installers, four failure modes.
|
|
24
24
|
#
|
|
25
|
-
# Platform: Linux
|
|
25
|
+
# Platform: Linux and Darwin, same fence as the Claude installer. Its own platform
|
|
26
|
+
# dependency is python3 + node (checked below); no /proc, no GNU-only tool.
|
|
26
27
|
set -euo pipefail
|
|
27
28
|
|
|
28
29
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -66,7 +67,10 @@ for arg in "$@"; do
|
|
|
66
67
|
esac
|
|
67
68
|
done
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
case "$(uname -s)" in
|
|
71
|
+
Linux | Darwin) ;;
|
|
72
|
+
*) die "unsupported platform '$(uname -s)'. This installer requires Linux or Darwin." ;;
|
|
73
|
+
esac
|
|
70
74
|
if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
|
|
71
75
|
command -v copilot >/dev/null 2>&1 || die "the 'copilot' CLI is not on PATH."
|
|
72
76
|
fi
|
|
@@ -117,9 +117,23 @@ copilot_bin="$(type -P "$VENDOR_CMD" 2>/dev/null || true)"
|
|
|
117
117
|
|
|
118
118
|
# Self-exec fence, the second half of the recursion guard: resolve symlinks and refuse
|
|
119
119
|
# anything that is one of our own entrypoints even if the sentinel was stripped.
|
|
120
|
-
|
|
120
|
+
# POSIX walk, same shape as mcp/entwurf-bridge/start.sh — BSD readlink has no -f.
|
|
121
|
+
resolve_path() {
|
|
122
|
+
local SOURCE="$1" DIR TARGET
|
|
123
|
+
while [ -L "$SOURCE" ]; do
|
|
124
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
125
|
+
TARGET="$(readlink "$SOURCE")"
|
|
126
|
+
case "$TARGET" in
|
|
127
|
+
/*) SOURCE="$TARGET" ;;
|
|
128
|
+
*) SOURCE="$DIR/$TARGET" ;;
|
|
129
|
+
esac
|
|
130
|
+
done
|
|
131
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
132
|
+
printf '%s/%s\n' "$DIR" "$(basename "$SOURCE")"
|
|
133
|
+
}
|
|
134
|
+
resolved_bin="$(resolve_path "$copilot_bin")"
|
|
121
135
|
for own in "$HERE/copilot-launch.sh" "$REPO_DIR/run.sh"; do
|
|
122
|
-
own_resolved="$(
|
|
136
|
+
own_resolved="$(resolve_path "$own")"
|
|
123
137
|
[ "$resolved_bin" = "$own_resolved" ] \
|
|
124
138
|
&& fail "'$VENDOR_CMD' on PATH resolves to entwurf's own '$own_resolved' — that is a launch loop, not the vendor CLI."
|
|
125
139
|
done
|
|
@@ -233,7 +233,9 @@ do_doctor() {
|
|
|
233
233
|
# THE INVISIBLE FAILURE. A Copilot launched without the flag never scans for
|
|
234
234
|
# extensions and prints nothing at all, so a perfectly installed unit stays inert
|
|
235
235
|
# with no symptom anywhere. Reading the live processes' own environment is the only
|
|
236
|
-
# place that silence becomes visible.
|
|
236
|
+
# place that silence becomes visible. That read is `/proc`-bound today; where the
|
|
237
|
+
# interface does not exist the axis is reported UNVERIFIABLE and non-green, never
|
|
238
|
+
# as a benign note (see the branch below).
|
|
237
239
|
#
|
|
238
240
|
# WHY NOT `pgrep -x copilot` (what this used to do). It matched nothing, ever, and
|
|
239
241
|
# reported that as the benign "no live copilot" note — so the detector built to break
|
|
@@ -255,7 +257,27 @@ do_doctor() {
|
|
|
255
257
|
# from OUR unit and carry `COPILOT_EXTENSION_PARENT_PID`; counting one as a session
|
|
256
258
|
# would report a receiver as its own missing session.
|
|
257
259
|
if [ ! -d /proc ]; then
|
|
258
|
-
|
|
260
|
+
# D1 — NON-GREEN, NOT A NOTE. This used to `note`, and a note lets this doctor end
|
|
261
|
+
# in PASS on a platform where the flag axis is blind: the exact false-success the
|
|
262
|
+
# paragraph at the unreadable-environ branch below refuses on Linux ("A note would
|
|
263
|
+
# let the doctor end in PASS while a session that can never arm is running"). The
|
|
264
|
+
# epistemic state here is the SAME one — the flag verdict's predicate INPUT is
|
|
265
|
+
# missing — so it gets the same verdict, and the reason is stated rather than
|
|
266
|
+
# rounded off.
|
|
267
|
+
#
|
|
268
|
+
# AND ABSENCE CANNOT BE CLAIMED EITHER. Without /proc there is no candidate
|
|
269
|
+
# enumeration at all, so this branch cannot say "no live Copilot CLI" the way the
|
|
270
|
+
# zero-count note below does; it can only say the axis was not measurable. That
|
|
271
|
+
# is a strictly weaker sentence than the note it replaces, which is the point.
|
|
272
|
+
#
|
|
273
|
+
# WHY NO `ps` SUBSTITUTE IS ATTEMPTED HERE. `ps -A -o pid=,args=` would recover the
|
|
274
|
+
# argv half, but macOS truncates argv for processes the caller does not own, and the
|
|
275
|
+
# environment half (`ps -E`/`ps eww`) succeeds there only for unrestricted targets —
|
|
276
|
+
# both are unmeasured (scripts/raw-macos-measure/probe.sh cell M6 is the receipt that
|
|
277
|
+
# would decide it). A guessed reader that silently identifies nothing would restore
|
|
278
|
+
# exactly the fail-open this branch just closed, so the read is not guessed. When M6
|
|
279
|
+
# lands, the portable read belongs right here, in place of this refusal.
|
|
280
|
+
bad "UNVERIFIABLE on this platform: there is no per-process environment interface, so the $FLAG_ENV state of a live Copilot CLI cannot be read — and without one this doctor cannot enumerate processes either, so it cannot claim that no CLI is running. The receiver IS installed, so a session launched without $FLAG_ENV=$FLAG_VALUE (never scans extensions, prints nothing, can never arm) is NOT ruled out. Launch through 'entwurf copilot', which sets the flag, and treat this axis as unmeasured until a native Darwin receipt exists."
|
|
259
281
|
else
|
|
260
282
|
local pid_seam="" scan_json
|
|
261
283
|
# WHICH PROCESSES ARE CANDIDATES. Normally every pid under /proc.
|
|
@@ -274,7 +296,7 @@ do_doctor() {
|
|
|
274
296
|
# stepped on once. The status is captured instead, and a broken scan becomes a verdict.
|
|
275
297
|
local scan_rc=0
|
|
276
298
|
scan_json="$(ENTWURF_PID_SEAM="${pid_seam}" ENTWURF_PID_SEAM_SET="${ENTWURF_COPILOT_RECEIVE_PIDS+1}" FLAG_ENV="$FLAG_ENV" FLAG_VALUE="$FLAG_VALUE" python3 - <<'PY'
|
|
277
|
-
import os, pathlib
|
|
299
|
+
import errno, os, pathlib
|
|
278
300
|
|
|
279
301
|
flag_env = os.environ["FLAG_ENV"]
|
|
280
302
|
flag_value = os.environ["FLAG_VALUE"]
|
|
@@ -284,17 +306,43 @@ if os.environ.get("ENTWURF_PID_SEAM_SET"):
|
|
|
284
306
|
else:
|
|
285
307
|
candidates = [p.name for p in pathlib.Path("/proc").iterdir() if p.name.isdigit()]
|
|
286
308
|
|
|
309
|
+
# THREE ANSWERS, NOT TWO. "The process is gone" and "the read was refused" are different
|
|
310
|
+
# facts about a live host, and collapsing them is how this scanner reported a process it
|
|
311
|
+
# could not read as a process that was not there. A returned list is the bytes; `None` is
|
|
312
|
+
# a PROVEN absence; `UNREADABLE` is "the input could not be obtained", which is unknown.
|
|
313
|
+
UNREADABLE = object()
|
|
314
|
+
|
|
287
315
|
def read_nul(pid, what):
|
|
288
316
|
try:
|
|
289
317
|
return pathlib.Path(f"/proc/{pid}/{what}").read_bytes().split(b"\0")
|
|
290
|
-
except OSError:
|
|
291
|
-
|
|
318
|
+
except OSError as exc:
|
|
319
|
+
# ONLY A PROVEN ABSENCE IS BENIGN. ENOENT/ESRCH is the process itself leaving
|
|
320
|
+
# between enumeration and this read — nothing is left to judge. EVERYTHING else
|
|
321
|
+
# (EACCES/EPERM under a `hidepid` /proc or a session owned by another user, and
|
|
322
|
+
# any other I/O failure) leaves the process THERE and the input MISSING. Unknown
|
|
323
|
+
# is the DEFAULT side on purpose: a new errno class must arrive as non-green
|
|
324
|
+
# rather than inherit the benign one.
|
|
325
|
+
if exc.errno in (errno.ENOENT, errno.ESRCH):
|
|
326
|
+
return None
|
|
327
|
+
return UNREADABLE
|
|
292
328
|
|
|
293
|
-
armed_ok, armed_missing, unreadable, contaminated = 0, 0, 0, 0
|
|
294
|
-
missing_pids, unreadable_pids, contaminated_pids = [], [], []
|
|
329
|
+
armed_ok, armed_missing, unreadable, contaminated, argv_unknown = 0, 0, 0, 0, 0
|
|
330
|
+
missing_pids, unreadable_pids, contaminated_pids, argv_unknown_pids = [], [], [], []
|
|
295
331
|
|
|
296
332
|
for pid in candidates:
|
|
297
333
|
argv = read_nul(pid, "cmdline")
|
|
334
|
+
if argv is UNREADABLE:
|
|
335
|
+
# WE CANNOT EVEN SAY WHAT THIS PROCESS IS. Identity is the FIRST predicate here, so
|
|
336
|
+
# a refused argv read is not "not a Copilot CLI" — it is "unknown", and a Copilot
|
|
337
|
+
# CLI is NOT ruled out. It carries its own counter because the unreadable-ENVIRON
|
|
338
|
+
# counter below means something strictly stronger: there, identity already
|
|
339
|
+
# succeeded and the process IS a Copilot CLI, so that verdict can name a session.
|
|
340
|
+
# This branch can only say the doctor was blinded — which is exactly why the `bad`
|
|
341
|
+
# path further down cannot cover it (that one is reachable only AFTER a successful
|
|
342
|
+
# argv match).
|
|
343
|
+
argv_unknown += 1
|
|
344
|
+
argv_unknown_pids.append(pid)
|
|
345
|
+
continue
|
|
298
346
|
# No cmdline at all is a kernel thread or a process that exited mid-scan; neither is
|
|
299
347
|
# a Copilot session and neither is evidence of anything.
|
|
300
348
|
if not argv:
|
|
@@ -322,9 +370,13 @@ for pid in candidates:
|
|
|
322
370
|
continue
|
|
323
371
|
|
|
324
372
|
env = read_nul(pid, "environ")
|
|
325
|
-
if env is None:
|
|
326
|
-
# FAIL-CLOSED
|
|
327
|
-
#
|
|
373
|
+
if env is None or env is UNREADABLE:
|
|
374
|
+
# FAIL-CLOSED, AND DELIBERATELY NOT SPLIT. We identified a native CLI and then did
|
|
375
|
+
# not learn its environment — whether it left mid-scan or refused the read, its
|
|
376
|
+
# flag state is unknown and it WAS a live Copilot CLI when identity matched. Both
|
|
377
|
+
# halves stay in this one reported bucket. Only the argv branch above needs the
|
|
378
|
+
# distinction, because there absence and refusal answer different questions
|
|
379
|
+
# ("is anything running at all?" vs "what is this?").
|
|
328
380
|
unreadable += 1
|
|
329
381
|
unreadable_pids.append(pid)
|
|
330
382
|
continue
|
|
@@ -351,24 +403,30 @@ for pid in candidates:
|
|
|
351
403
|
armed_missing += 1
|
|
352
404
|
missing_pids.append(pid)
|
|
353
405
|
|
|
354
|
-
print(f"{armed_ok} {armed_missing} {unreadable} {contaminated}")
|
|
406
|
+
print(f"{armed_ok} {armed_missing} {unreadable} {contaminated} {argv_unknown}")
|
|
355
407
|
print(" ".join(missing_pids))
|
|
356
408
|
print(" ".join(unreadable_pids))
|
|
357
409
|
print(" ".join(contaminated_pids))
|
|
410
|
+
print(" ".join(argv_unknown_pids))
|
|
358
411
|
PY
|
|
359
412
|
)" || scan_rc=$?
|
|
360
|
-
local counts armed_ok armed_missing unreadable contaminated
|
|
413
|
+
local counts armed_ok armed_missing unreadable contaminated argv_unknown
|
|
414
|
+
local missing_pids unreadable_pids contaminated_pids argv_unknown_pids
|
|
361
415
|
counts="$(printf '%s\n' "$scan_json" | sed -n '1p')"
|
|
362
416
|
missing_pids="$(printf '%s\n' "$scan_json" | sed -n '2p')"
|
|
363
417
|
unreadable_pids="$(printf '%s\n' "$scan_json" | sed -n '3p')"
|
|
364
418
|
contaminated_pids="$(printf '%s\n' "$scan_json" | sed -n '4p')"
|
|
419
|
+
argv_unknown_pids="$(printf '%s\n' "$scan_json" | sed -n '5p')"
|
|
365
420
|
armed_ok="$(printf '%s' "$counts" | awk '{print $1}')"
|
|
366
421
|
armed_missing="$(printf '%s' "$counts" | awk '{print $2}')"
|
|
367
422
|
unreadable="$(printf '%s' "$counts" | awk '{print $3}')"
|
|
368
423
|
contaminated="$(printf '%s' "$counts" | awk '{print $4}')"
|
|
424
|
+
argv_unknown="$(printf '%s' "$counts" | awk '{print $5}')"
|
|
369
425
|
if [ "$scan_rc" -ne 0 ] || [ -z "$armed_ok" ]; then
|
|
370
426
|
bad "the /proc scan for live Copilot CLIs FAILED (exit $scan_rc) — the flag axis is UNKNOWN, so an inert session cannot be ruled out. This is a broken doctor, not a clean host."
|
|
371
|
-
elif [ "$armed_ok" -eq 0 ] && [ "$armed_missing" -eq 0 ] && [ "$unreadable" -eq 0 ]; then
|
|
427
|
+
elif [ "$armed_ok" -eq 0 ] && [ "$armed_missing" -eq 0 ] && [ "$unreadable" -eq 0 ] && [ "${argv_unknown:-0}" -eq 0 ]; then
|
|
428
|
+
# The one branch that may claim ABSENCE, and it may only claim it because every
|
|
429
|
+
# candidate answered: each was enumerated and its argv was read (or proved gone).
|
|
372
430
|
note "no live GitHub Copilot CLI process — start one with 'entwurf copilot' (or $FLAG_ENV=$FLAG_VALUE copilot) to arm a receiver"
|
|
373
431
|
else
|
|
374
432
|
[ "$armed_ok" -gt 0 ] && ok "$armed_ok live Copilot CLI process(es) carry $FLAG_ENV=$FLAG_VALUE"
|
|
@@ -379,6 +437,15 @@ PY
|
|
|
379
437
|
# exact false-success the section was written to break.
|
|
380
438
|
bad "$unreadable live Copilot CLI process(es) have an unreadable environment (pids: $unreadable_pids) — their $FLAG_ENV state is UNKNOWN and is NOT assumed armed. Re-run this doctor as the user that owns those sessions."
|
|
381
439
|
fi
|
|
440
|
+
if [ "${argv_unknown:-0}" -gt 0 ]; then
|
|
441
|
+
# RED, and NOT the same finding as the line above. There, identity had already
|
|
442
|
+
# succeeded, so the doctor could name a Copilot session and say what was unknown
|
|
443
|
+
# about it. Here identity ITSELF failed to be decided: a live process would not
|
|
444
|
+
# let its argv be read, so whether it is a Copilot CLI is unknown and one is not
|
|
445
|
+
# ruled out. Rounding that to "no live CLI" is the false success this branch
|
|
446
|
+
# exists to break.
|
|
447
|
+
bad "UNVERIFIABLE — the argv of $argv_unknown live process(es) could not be read (pids: $argv_unknown_pids), so whether any of them is a GitHub Copilot CLI is UNKNOWN. This doctor therefore does NOT claim that no CLI is running, and an inert session cannot be ruled out. Re-run it as the user that owns those processes, or on a host whose /proc is not restricted."
|
|
448
|
+
fi
|
|
382
449
|
if [ "$armed_missing" -gt 0 ]; then
|
|
383
450
|
bad "$armed_missing live Copilot CLI process(es) lack $FLAG_ENV=$FLAG_VALUE while the receiver is installed (pids: $missing_pids) — relaunch them with 'entwurf copilot', or uninstall the receiver so nothing promises a doorbell"
|
|
384
451
|
fi
|