@junghanacs/entwurf 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-mux-launcher-fence — deterministic gate for the shared operator-launcher protection
|
|
3
|
+
* (`scripts/lib/claude-launcher-fence.ts`) both mux LIVE smokes must consume (issue #67).
|
|
4
|
+
*
|
|
5
|
+
* The closed incident: a Claude child launched with real HOME + fixture XDG_DATA_HOME
|
|
6
|
+
* self-updated into the fixture tree and rewrote the operator's real `claude` launcher to point
|
|
7
|
+
* there; fixture teardown then deleted the tree and left the launcher dangling. Everything below
|
|
8
|
+
* replants that shape into DISPOSABLE fixture paths only — a simulated "operator" bin/versions
|
|
9
|
+
* tree and a simulated fixture cleanup root, both under one mkdtemp. The gate never inspects,
|
|
10
|
+
* touches or even resolves the real operator launcher.
|
|
11
|
+
*
|
|
12
|
+
* LAUNCHFENCE-PREFLIGHT-FAIL-CLOSED the preflight refuses to pin a launcher whose resolved
|
|
13
|
+
* target already lives inside the fixture cleanup root
|
|
14
|
+
* LAUNCHFENCE-RETARGET-BLOCKS-CLEANUP a post-launch retarget into the fixture (the incident's
|
|
15
|
+
* exact end state: a dangling reference into a tree about
|
|
16
|
+
* to be deleted) makes the cleanup guard BLOCK removal
|
|
17
|
+
* LAUNCHFENCE-CONTENT-CHANGE-DETECTED an in-place rewrite of the resolved target — same path,
|
|
18
|
+
* same link — is still reported before cleanup
|
|
19
|
+
* LAUNCHFENCE-XDG-EXACT-RESTORE a real-HOME Claude cell gets EXACT operator XDG parity:
|
|
20
|
+
* original values restored, absent variables DELETED,
|
|
21
|
+
* never filled with a canonical default
|
|
22
|
+
* LAUNCHFENCE-WIRED-LIFECYCLE smoke-mux-lifecycle-live actually consumes the fence:
|
|
23
|
+
* LAUNCHFENCE-WIRED-FRESH-CALL preflight + integrity oracle BEFORE fixture removal +
|
|
24
|
+
* removal gated on cleanup verdict AND tracked-pane
|
|
25
|
+
* quiescence + XDG parity, in each smoke's own source
|
|
26
|
+
* LAUNCHFENCE-LIFECYCLE-CELL-BRANCH lifecycle keeps its two truthful cell branches: ACP pi
|
|
27
|
+
* retains the four MEASURED canonical REAL_XDG_* roots,
|
|
28
|
+
* the direct Claude cell gets exact-parity restore
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import assert from "node:assert/strict";
|
|
32
|
+
import fs from "node:fs";
|
|
33
|
+
import os from "node:os";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import {
|
|
37
|
+
assessLauncherCleanup,
|
|
38
|
+
resolveFromPath,
|
|
39
|
+
restoreOriginalXdg,
|
|
40
|
+
snapshotClaudeLauncher,
|
|
41
|
+
snapshotOriginalXdg,
|
|
42
|
+
verifyClaudeLauncher,
|
|
43
|
+
} from "./lib/claude-launcher-fence.ts";
|
|
44
|
+
|
|
45
|
+
let passed = 0;
|
|
46
|
+
function ok(label: string, cond: boolean): void {
|
|
47
|
+
assert.ok(cond, label);
|
|
48
|
+
console.log(` ok ${label}`);
|
|
49
|
+
passed++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
|
+
const read = (rel: string): string => fs.readFileSync(path.join(ROOT, rel), "utf8");
|
|
54
|
+
|
|
55
|
+
function refusal(fn: () => unknown): string {
|
|
56
|
+
try {
|
|
57
|
+
fn();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return err instanceof Error ? err.message : String(err);
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function relink(link: string, target: string): void {
|
|
65
|
+
fs.rmSync(link);
|
|
66
|
+
fs.symlinkSync(target, link);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function main(): void {
|
|
70
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "check-mux-launcher-fence-"));
|
|
71
|
+
try {
|
|
72
|
+
// ── a disposable stand-in for the operator's install: bin/claude → versions/1.0.0/claude ──
|
|
73
|
+
const opBin = path.join(tmp, "operator", "bin");
|
|
74
|
+
const opTarget = path.join(tmp, "operator", "versions", "1.0.0", "claude");
|
|
75
|
+
fs.mkdirSync(opBin, { recursive: true });
|
|
76
|
+
fs.mkdirSync(path.dirname(opTarget), { recursive: true });
|
|
77
|
+
fs.writeFileSync(opTarget, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
78
|
+
const launcher = path.join(opBin, "claude");
|
|
79
|
+
fs.symlinkSync(opTarget, launcher);
|
|
80
|
+
const fixtureRoot = path.join(tmp, "fixture");
|
|
81
|
+
fs.mkdirSync(fixtureRoot);
|
|
82
|
+
const env: NodeJS.ProcessEnv = { PATH: opBin };
|
|
83
|
+
|
|
84
|
+
// ── PATH resolution mirrors the child's exec ─────────────────────────────
|
|
85
|
+
ok(
|
|
86
|
+
"resolution: the launcher is selected from the exact child PATH, not from this process's",
|
|
87
|
+
resolveFromPath("claude", env) === launcher && resolveFromPath("claude", { PATH: tmp }) === null,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// ── the fail-closed preflight ────────────────────────────────────────────
|
|
91
|
+
const snap = snapshotClaudeLauncher({ env, fixtureRoot });
|
|
92
|
+
ok(
|
|
93
|
+
"preflight: a healthy launcher is pinned — kind, link text, resolved target and content hash",
|
|
94
|
+
snap.launcherPath === launcher &&
|
|
95
|
+
snap.kind === "symlink" &&
|
|
96
|
+
snap.linkText === opTarget &&
|
|
97
|
+
snap.resolvedPath === fs.realpathSync(opTarget) &&
|
|
98
|
+
snap.sha256.length === 64,
|
|
99
|
+
);
|
|
100
|
+
ok(
|
|
101
|
+
"preflight: no `claude` on the child PATH refuses loudly instead of launching unpinned",
|
|
102
|
+
refusal(() => snapshotClaudeLauncher({ env: { PATH: path.join(tmp, "empty") }, fixtureRoot })).includes(
|
|
103
|
+
"no executable",
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
{
|
|
107
|
+
// The launcher ITSELF inside the cleanup root: refused on the launcher-path axis.
|
|
108
|
+
const inBin = path.join(fixtureRoot, "in-bin");
|
|
109
|
+
fs.mkdirSync(inBin, { recursive: true });
|
|
110
|
+
fs.writeFileSync(path.join(inBin, "claude"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
111
|
+
ok(
|
|
112
|
+
"preflight: a launcher that itself lives inside the fixture cleanup root is refused",
|
|
113
|
+
refusal(() => snapshotClaudeLauncher({ env: { PATH: inBin }, fixtureRoot })).includes(
|
|
114
|
+
"INSIDE the fixture cleanup root",
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
{
|
|
119
|
+
// The incident's precondition, replanted disposably: a launcher OUTSIDE the fixture whose
|
|
120
|
+
// resolved target is INSIDE it — exactly what a prior self-update leaves behind.
|
|
121
|
+
const fixtureExec = path.join(fixtureRoot, "claude", "versions", "9.0.0", "claude");
|
|
122
|
+
fs.mkdirSync(path.dirname(fixtureExec), { recursive: true });
|
|
123
|
+
fs.writeFileSync(fixtureExec, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
124
|
+
const trapBin = path.join(tmp, "trap-bin");
|
|
125
|
+
fs.mkdirSync(trapBin);
|
|
126
|
+
fs.symlinkSync(fixtureExec, path.join(trapBin, "claude"));
|
|
127
|
+
ok(
|
|
128
|
+
"[QK:LAUNCHFENCE-PREFLIGHT-FAIL-CLOSED] preflight: a launcher whose RESOLVED TARGET lies inside the fixture cleanup root is refused before any child starts — launching would hand the self-updater a tree teardown is about to delete",
|
|
129
|
+
refusal(() => snapshotClaudeLauncher({ env: { PATH: trapBin }, fixtureRoot })).includes(
|
|
130
|
+
"INSIDE the fixture cleanup root",
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
ok(
|
|
134
|
+
"cleanup guard: a launcher retargeted at an EXISTING file inside the fixture blocks removal too, naming the reference",
|
|
135
|
+
(() => {
|
|
136
|
+
relink(launcher, fixtureExec);
|
|
137
|
+
const verdict = assessLauncherCleanup(snap);
|
|
138
|
+
relink(launcher, opTarget);
|
|
139
|
+
return (
|
|
140
|
+
!verdict.safeToRemove && verdict.problems.some((p) => p.includes("lies inside the fixture cleanup root"))
|
|
141
|
+
);
|
|
142
|
+
})(),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── the incident's END state: a retarget whose fixture path is already gone ──
|
|
147
|
+
{
|
|
148
|
+
relink(launcher, path.join(fixtureRoot, "claude", "versions", "9.9.9", "claude"));
|
|
149
|
+
const problems = verifyClaudeLauncher(snap);
|
|
150
|
+
const verdict = assessLauncherCleanup(snap);
|
|
151
|
+
ok(
|
|
152
|
+
"[QK:LAUNCHFENCE-RETARGET-BLOCKS-CLEANUP] teardown: a launcher rewritten to point into the fixture tree — even when that fixture path is already dangling — is caught BEFORE removal: the integrity oracle reports it and the cleanup guard blocks rm naming the KNOWN fixture reference, not a vague uncertainty",
|
|
153
|
+
problems.length > 0 &&
|
|
154
|
+
!verdict.safeToRemove &&
|
|
155
|
+
verdict.problems.some((p) => p.includes("lies inside the fixture cleanup root")),
|
|
156
|
+
);
|
|
157
|
+
ok(
|
|
158
|
+
"teardown: the retarget is named on the link axis, not laundered into a generic failure",
|
|
159
|
+
problems.some((p) => p.includes("link text changed")),
|
|
160
|
+
);
|
|
161
|
+
relink(launcher, opTarget);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── uncertainty is fail-closed AND honest: blocked, but never claimed as a known reference ──
|
|
165
|
+
{
|
|
166
|
+
relink(launcher, path.join(tmp, "operator", "versions", "gone", "claude"));
|
|
167
|
+
const verdict = assessLauncherCleanup(snap);
|
|
168
|
+
ok(
|
|
169
|
+
"cleanup guard: a launcher whose chain cannot be fully resolved OUTSIDE the fixture blocks removal as 'not proven safe' — fail-closed on uncertainty without minting a false fixture reference",
|
|
170
|
+
!verdict.safeToRemove &&
|
|
171
|
+
verdict.problems.some((p) => p.includes("not proven safe")) &&
|
|
172
|
+
!verdict.problems.some((p) => p.includes("lies inside the fixture cleanup root")),
|
|
173
|
+
);
|
|
174
|
+
relink(launcher, opTarget);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── change detection with NO path movement at all ────────────────────────
|
|
178
|
+
{
|
|
179
|
+
const fresh = snapshotClaudeLauncher({ env, fixtureRoot });
|
|
180
|
+
fs.appendFileSync(opTarget, "# self-update rewrote me in place\n");
|
|
181
|
+
const problems = verifyClaudeLauncher(fresh);
|
|
182
|
+
ok(
|
|
183
|
+
"[QK:LAUNCHFENCE-CONTENT-CHANGE-DETECTED] teardown: an in-place rewrite of the resolved target — same launcher path, same link text — is still reported before cleanup, because content identity is part of the pinned evidence",
|
|
184
|
+
problems.some((p) => p.includes("sha256 mismatch")),
|
|
185
|
+
);
|
|
186
|
+
ok(
|
|
187
|
+
"teardown: after the healthy state is restored, the oracle is quiet and the guard PROVES removal safe",
|
|
188
|
+
(() => {
|
|
189
|
+
fs.writeFileSync(opTarget, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
190
|
+
const verdict = assessLauncherCleanup(fresh);
|
|
191
|
+
return verifyClaudeLauncher(fresh).length === 0 && verdict.safeToRemove && verdict.problems.length === 0;
|
|
192
|
+
})(),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── exact operator-env XDG parity for a real-HOME Claude cell ────────────
|
|
197
|
+
{
|
|
198
|
+
const before: NodeJS.ProcessEnv = {
|
|
199
|
+
XDG_CONFIG_HOME: "/op/custom-config",
|
|
200
|
+
XDG_CACHE_HOME: "",
|
|
201
|
+
};
|
|
202
|
+
const originals = snapshotOriginalXdg(before);
|
|
203
|
+
ok(
|
|
204
|
+
"xdg: the snapshot records presence and value — a set variable (even empty) is a string, an absent one is null",
|
|
205
|
+
originals.XDG_CONFIG_HOME === "/op/custom-config" &&
|
|
206
|
+
originals.XDG_CACHE_HOME === "" &&
|
|
207
|
+
originals.XDG_DATA_HOME === null &&
|
|
208
|
+
originals.XDG_STATE_HOME === null,
|
|
209
|
+
);
|
|
210
|
+
const cell: NodeJS.ProcessEnv = {
|
|
211
|
+
HOME: "/real/home",
|
|
212
|
+
XDG_CONFIG_HOME: path.join(fixtureRoot, "xdg-config"),
|
|
213
|
+
XDG_DATA_HOME: path.join(fixtureRoot, "xdg-data"),
|
|
214
|
+
XDG_STATE_HOME: path.join(fixtureRoot, "xdg-state"),
|
|
215
|
+
XDG_CACHE_HOME: path.join(fixtureRoot, "xdg-cache"),
|
|
216
|
+
};
|
|
217
|
+
restoreOriginalXdg(cell, originals);
|
|
218
|
+
ok(
|
|
219
|
+
"[QK:LAUNCHFENCE-XDG-EXACT-RESTORE] xdg: the cell gets EXACT operator parity — original values restored byte-for-byte and originally ABSENT variables DELETED, never filled with a canonical default path, because real HOME + fixture XDG_DATA_HOME is the measured state that let self-update rewrite the real launcher",
|
|
220
|
+
cell.XDG_CONFIG_HOME === "/op/custom-config" &&
|
|
221
|
+
cell.XDG_CACHE_HOME === "" &&
|
|
222
|
+
!("XDG_DATA_HOME" in cell) &&
|
|
223
|
+
!("XDG_STATE_HOME" in cell) &&
|
|
224
|
+
cell.HOME === "/real/home",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── the fence is actually consumed by BOTH smokes ────────────────────────
|
|
229
|
+
const wired = (src: string): boolean => {
|
|
230
|
+
const verifyAt = src.indexOf("verifyClaudeLauncher(launcherSnapshot)");
|
|
231
|
+
const rmAt = src.indexOf("rmSync(root");
|
|
232
|
+
return (
|
|
233
|
+
src.includes('from "./lib/claude-launcher-fence.ts"') &&
|
|
234
|
+
src.includes("snapshotClaudeLauncher({ env: process.env, fixtureRoot: root })") &&
|
|
235
|
+
src.includes("assessLauncherCleanup(launcherSnapshot)") &&
|
|
236
|
+
src.includes(".safeToRemove || !panesGone") &&
|
|
237
|
+
src.includes("restoreOriginalXdg(") &&
|
|
238
|
+
verifyAt !== -1 &&
|
|
239
|
+
rmAt !== -1 &&
|
|
240
|
+
verifyAt < rmAt
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
ok(
|
|
244
|
+
"[QK:LAUNCHFENCE-WIRED-LIFECYCLE] wiring: smoke-mux-lifecycle-live consumes the shared fence — fail-closed preflight from the exact child env, the integrity oracle BEFORE fixture removal, and a removal gated on BOTH the cleanup verdict and tracked-pane quiescence",
|
|
245
|
+
wired(read("scripts/smoke-mux-lifecycle-live.ts")),
|
|
246
|
+
);
|
|
247
|
+
ok(
|
|
248
|
+
"[QK:LAUNCHFENCE-WIRED-FRESH-CALL] wiring: smoke-mux-fresh-call-live consumes the SAME shared fence rather than a private copy — one protection, two smokes, per the issue's shared-repair requirement",
|
|
249
|
+
wired(read("scripts/smoke-mux-fresh-call-live.ts")),
|
|
250
|
+
);
|
|
251
|
+
ok(
|
|
252
|
+
"[QK:LAUNCHFENCE-LIFECYCLE-CELL-BRANCH] wiring: lifecycle's real-HOME cell env keeps its two truthful branches — the ACP-backed pi cell retains all four MEASURED canonical REAL_XDG_* assignments, and the direct Claude Code cell (the else branch) gets exact-parity restoreOriginalXdg instead",
|
|
253
|
+
/if \(backend === "pi"\) \{\s*serverEnv\.XDG_CONFIG_HOME = REAL_XDG_CONFIG_HOME;\s*serverEnv\.XDG_DATA_HOME = REAL_XDG_DATA_HOME;\s*serverEnv\.XDG_STATE_HOME = REAL_XDG_STATE_HOME;\s*serverEnv\.XDG_CACHE_HOME = REAL_XDG_CACHE_HOME;\s*\} else \{\s*restoreOriginalXdg\(serverEnv, ORIGINAL_XDG\);\s*\}/.test(
|
|
254
|
+
read("scripts/smoke-mux-lifecycle-live.ts"),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
} finally {
|
|
258
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(`\ncheck-mux-launcher-fence: ${passed} checks passed`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
main();
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-mux-parent-artifact — deterministic gate for the scrubbed parent-transcript fixture
|
|
3
|
+
* (`scripts/fixtures/mux-parent-transcript.scrubbed.jsonl`).
|
|
4
|
+
*
|
|
5
|
+
* ── What the fixture is, and what it is NOT ──
|
|
6
|
+
*
|
|
7
|
+
* It is a version-pinned SAMPLE OF THE PARENT-SIDE SHAPE: what an `entwurf_fresh_call` and the
|
|
8
|
+
* sibling's nonce callback actually look like in the transcript of the citizen that made the
|
|
9
|
+
* call. Its structure was extracted from a fixture-only real Pi parent retake and then scrubbed —
|
|
10
|
+
* every id, uuid, toolCallId, timestamp, model, path, cwd and message body is a fixture value,
|
|
11
|
+
* while the event order, roles, content types, toolName, the `toolCallId` join on the result, the
|
|
12
|
+
* nonce flowing from the launch receipt into the callback body, and the `sender_info` envelope
|
|
13
|
+
* field names are real.
|
|
14
|
+
*
|
|
15
|
+
* It is NOT placement evidence. Nothing here says a window opened; that axis is the launch
|
|
16
|
+
* receipt and the LIVE lifecycle gate, and reading this file as proof of a window would be
|
|
17
|
+
* exactly the receipt-merging docs/mux-launch-rail.md §12 forbids.
|
|
18
|
+
*
|
|
19
|
+
* ── Why a gate for a static file ──
|
|
20
|
+
*
|
|
21
|
+
* The downstream that needs this shape (`entwurf-peek`'s repair) parses parent transcripts. A
|
|
22
|
+
* fixture whose only guarantee is "someone scrubbed it once" would rot silently into a shape
|
|
23
|
+
* nothing produces, and the next reader would go back to searching private transcripts — the one
|
|
24
|
+
* outcome the artifact exists to prevent. So the claims below pin the shape AND the scrub:
|
|
25
|
+
*
|
|
26
|
+
* MUXARTIFACT-EVENT-ORDER provenance → the fresh_call toolResult → the later callback
|
|
27
|
+
* MUXARTIFACT-RESULT-CARRIES-CALL-ID MEASURED: pi records NO separate toolCall row, so the
|
|
28
|
+
* join a parser follows is the `toolCallId` on the RESULT. A
|
|
29
|
+
* fixture that invented a toolCall row would send the next
|
|
30
|
+
* reader looking for an event pi never writes
|
|
31
|
+
* MUXARTIFACT-NONCE-FLOW the nonce in the launch receipt is the SAME string that comes
|
|
32
|
+
* back in the callback body — that flow is the whole correlation
|
|
33
|
+
* contract, and a fixture that broke it would teach the opposite
|
|
34
|
+
* MUXARTIFACT-SENDER-INFO-SHAPE the callback body embeds the <sender_info> JSON envelope whose
|
|
35
|
+
* field names a reader keys on
|
|
36
|
+
* MUXARTIFACT-RECEIPT-GRAMMAR the scrubbed launch receipt is still GRAMMAR-VALID: session
|
|
37
|
+
* `$N`, window `@N`, pane `%N`, a decimal pid, and a runtime
|
|
38
|
+
* that is an absolute path to an executable. A fixture that
|
|
39
|
+
* collapses those sigils documents a receipt no parser meets,
|
|
40
|
+
* which defeats the only reason the file is tracked
|
|
41
|
+
* MUXARTIFACT-SCRUBBED no operator path, no real garden id, no real uuid survives
|
|
42
|
+
* MUXARTIFACT-PROVENANCE-HONEST the file says where it came from and denies being placement
|
|
43
|
+
* evidence, in its own first record
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import assert from "node:assert/strict";
|
|
47
|
+
import fs from "node:fs";
|
|
48
|
+
import path from "node:path";
|
|
49
|
+
import { fileURLToPath } from "node:url";
|
|
50
|
+
|
|
51
|
+
let passed = 0;
|
|
52
|
+
function ok(label: string, cond: boolean, detail?: string): void {
|
|
53
|
+
assert.ok(cond, `${label}${detail ? `\n${detail}` : ""}`);
|
|
54
|
+
console.log(` ok ${label}`);
|
|
55
|
+
passed++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
59
|
+
const ARTIFACT = path.join(ROOT, "scripts/fixtures/mux-parent-transcript.scrubbed.jsonl");
|
|
60
|
+
|
|
61
|
+
/** The fixture values the scrub substitutes. Anything OUTSIDE this vocabulary that looks like an
|
|
62
|
+
* id, a path or a uuid is a leak. */
|
|
63
|
+
const FIXTURE_GID = /^\d{8}T\d{6}-[0-9a-f]{6}$/;
|
|
64
|
+
const FIXTURE_GIDS = new Set(["20260101T000000-aaaaaa", "20260101T000001-bbbbbb"]);
|
|
65
|
+
|
|
66
|
+
interface Event {
|
|
67
|
+
type?: string;
|
|
68
|
+
note?: string;
|
|
69
|
+
message?: {
|
|
70
|
+
role?: string;
|
|
71
|
+
toolName?: string;
|
|
72
|
+
toolCallId?: string;
|
|
73
|
+
content?: { type?: string; text?: string }[];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main(): void {
|
|
78
|
+
ok("the artifact exists at its pinned path", fs.existsSync(ARTIFACT), `--- expected ---\n${ARTIFACT}`);
|
|
79
|
+
const raw = fs.readFileSync(ARTIFACT, "utf8");
|
|
80
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
81
|
+
const events: Event[] = lines.map((l, i) => {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(l) as Event;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new Error(`line ${i + 1} is not JSON — ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
ok("the artifact is exactly three JSONL records", events.length === 3, `--- got ${events.length} ---`);
|
|
90
|
+
|
|
91
|
+
const [provenance, result, callback] = events as [Event, Event, Event];
|
|
92
|
+
|
|
93
|
+
// ── provenance ───────────────────────────────────────────────────────────────
|
|
94
|
+
ok(
|
|
95
|
+
"[QK:MUXARTIFACT-PROVENANCE-HONEST] the first record states where the shape came from AND denies being placement evidence — a reader who takes this for proof that a window opened has merged the two receipts this rail keeps apart",
|
|
96
|
+
provenance.type === "fixture-provenance" &&
|
|
97
|
+
/scrub/i.test(provenance.note ?? "") &&
|
|
98
|
+
/real Pi parent/i.test(provenance.note ?? "") &&
|
|
99
|
+
/NOT placement evidence/i.test(provenance.note ?? ""),
|
|
100
|
+
`--- record 1 ---\n${JSON.stringify(provenance)}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// ── order and linkage ────────────────────────────────────────────────────────
|
|
104
|
+
ok(
|
|
105
|
+
"[QK:MUXARTIFACT-EVENT-ORDER] the two transcript events are in the order a parent actually records them: the fresh_call RESULT first, then the callback that arrives later as its own event — an artifact with the callback before the launch would teach a reader to correlate backwards",
|
|
106
|
+
result.message?.role === "toolResult" && callback.type === "custom_message",
|
|
107
|
+
`--- shapes ---\n${events.map((e) => `${e.type}/${e.message?.role ?? "-"}`).join(" → ")}`,
|
|
108
|
+
);
|
|
109
|
+
ok(
|
|
110
|
+
"[QK:MUXARTIFACT-RESULT-CARRIES-CALL-ID] the RESULT carries both toolName and toolCallId — measured: pi writes no separate toolCall row, so that id on the result is the whole join, and a fixture that invented a toolCall event would send the next reader hunting for something pi never records",
|
|
111
|
+
result.message?.toolName === "entwurf_fresh_call" && Boolean(result.message?.toolCallId),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// ── the nonce flow ───────────────────────────────────────────────────────────
|
|
115
|
+
const resultText = (result.message?.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
116
|
+
// The callback is a custom_message: its body is a plain string, not a content array.
|
|
117
|
+
const callbackText = String((callback as unknown as { content?: string }).content ?? "");
|
|
118
|
+
const nonce = /nonce:\s+(mux-fresh-call-\S+)/.exec(resultText)?.[1] ?? "";
|
|
119
|
+
ok(
|
|
120
|
+
"the launch receipt in the toolResult carries a nonce and reads as a launch receipt",
|
|
121
|
+
nonce.length > 0 && /LAUNCH receipt/i.test(resultText),
|
|
122
|
+
`--- toolResult text ---\n${resultText}`,
|
|
123
|
+
);
|
|
124
|
+
ok(
|
|
125
|
+
"[QK:MUXARTIFACT-NONCE-FLOW] the SAME nonce string that the launch receipt printed comes back inside the callback body — that flow is the entire correlation contract, and a fixture with two different nonces would document a rail that does not exist",
|
|
126
|
+
nonce.length > 0 && callbackText.includes(nonce),
|
|
127
|
+
`--- nonce ---\n${nonce}\n--- callback text ---\n${callbackText}`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// ── the sender envelope ──────────────────────────────────────────────────────
|
|
131
|
+
const envRaw = /<sender_info>(\{[\s\S]*?\})<\/sender_info>/.exec(callbackText)?.[1] ?? "";
|
|
132
|
+
let envelope: Record<string, unknown> = {};
|
|
133
|
+
try {
|
|
134
|
+
envelope = JSON.parse(envRaw) as Record<string, unknown>;
|
|
135
|
+
} catch {
|
|
136
|
+
envelope = {};
|
|
137
|
+
}
|
|
138
|
+
ok(
|
|
139
|
+
"[QK:MUXARTIFACT-SENDER-INFO-SHAPE] the callback body embeds a <sender_info> JSON envelope carrying the exact field names a reader keys on — sessionId (the sibling's garden id), agentId, cwd, timestamp, origin and replyable. Those NAMES are the contract; a fixture that renamed one would document a parser that cannot exist",
|
|
140
|
+
["sessionId", "agentId", "cwd", "timestamp", "origin", "replyable"].every((k) => k in envelope),
|
|
141
|
+
`--- envelope keys ---\n${Object.keys(envelope).join(", ") || "(none)"}`,
|
|
142
|
+
);
|
|
143
|
+
ok(
|
|
144
|
+
"the callback event is the delivery custom_message pi actually writes (customType entwurf-message)",
|
|
145
|
+
(callback as unknown as { customType?: string }).customType === "entwurf-message",
|
|
146
|
+
);
|
|
147
|
+
const senderGid = String(envelope.sessionId ?? "");
|
|
148
|
+
ok(
|
|
149
|
+
"the envelope's sessionId holds a garden id in the canonical grammar — that is the field the correlation reads, so its SHAPE must survive scrubbing",
|
|
150
|
+
FIXTURE_GID.test(senderGid),
|
|
151
|
+
`--- session field ---\n${senderGid}`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ── the receipt is still grammar-valid after scrubbing ───────────────────────
|
|
155
|
+
const grammar: string[] = [];
|
|
156
|
+
const win = /^\s*window:\s+(@\d+) \(index (\d+)\) in session (\$\d+)/m.exec(resultText);
|
|
157
|
+
const pane = /^\s*pane:\s+(%\d+) pid (\d+)/m.exec(resultText);
|
|
158
|
+
const runtime = /^\s*backend:\s+\S+ \((\S+)\)/m.exec(resultText);
|
|
159
|
+
if (!win) grammar.push("no `window: @N (index N) in session $N` line survives");
|
|
160
|
+
if (!pane) grammar.push("no `pane: %N pid N` line survives");
|
|
161
|
+
if (!runtime) grammar.push("no `backend: <name> (<runtime>)` line survives");
|
|
162
|
+
if (runtime && !runtime[1]?.startsWith("/")) grammar.push(`the runtime is not an absolute path (${runtime[1]})`);
|
|
163
|
+
// The fixture vocabulary is fixed, so this is exact rather than heuristic: the scrub's runtime
|
|
164
|
+
// value is one known string. "Looks like an executable" is not something a regex can decide,
|
|
165
|
+
// and claiming it would promise more than the check performs.
|
|
166
|
+
if (runtime && runtime[1] !== "/fixture/bin/pi")
|
|
167
|
+
grammar.push(`the runtime is not the fixture runtime value /fixture/bin/pi (${runtime[1]})`);
|
|
168
|
+
ok(
|
|
169
|
+
"[QK:MUXARTIFACT-RECEIPT-GRAMMAR] the scrubbed launch receipt still parses as one: session `$N`, window `@N`, pane `%N`, a decimal pid, and the fixture runtime path `/fixture/bin/pi` — an executable-shaped value, not a project directory. Substituting a value is only safe while it stays inside its own grammar — a fixture whose session id reads `@0` teaches a parser to accept something tmux never emits",
|
|
170
|
+
grammar.length === 0,
|
|
171
|
+
`--- grammar problems ---\n${grammar.join("\n")}\n--- receipt ---\n${resultText}`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// ── the scrub ────────────────────────────────────────────────────────────────
|
|
175
|
+
const leaks: string[] = [];
|
|
176
|
+
if (/\/home\/(?!junghan-fixture)[a-z]/i.test(raw.replace(/\/fixture\/[^"\s]*/g, "")))
|
|
177
|
+
leaks.push("an operator home path");
|
|
178
|
+
if (/\.pi\/agent\/sessions/.test(raw)) leaks.push("a real pi sessions path");
|
|
179
|
+
if (/\.claude\/projects/.test(raw)) leaks.push("a real Claude projects path");
|
|
180
|
+
for (const gid of raw.match(/\d{8}T\d{6}-[0-9a-f]{6}/g) ?? []) {
|
|
181
|
+
if (!FIXTURE_GIDS.has(gid)) leaks.push(`a garden id outside the fixture vocabulary (${gid})`);
|
|
182
|
+
}
|
|
183
|
+
for (const uuid of raw.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g) ?? []) {
|
|
184
|
+
if (uuid !== "0199f000-0000-7000-8000-000000000000") leaks.push(`a uuid outside the fixture vocabulary (${uuid})`);
|
|
185
|
+
}
|
|
186
|
+
ok(
|
|
187
|
+
"[QK:MUXARTIFACT-SCRUBBED] nothing outside the fixture vocabulary survives — no operator path, no real garden id, no real session uuid. The artifact exists so downstream never has to open a private transcript; a leak here would defeat the reason it is tracked at all",
|
|
188
|
+
leaks.length === 0,
|
|
189
|
+
`--- leaks ---\n${leaks.join("\n")}`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
console.log(`\ncheck-mux-parent-artifact: ${passed} checks passed`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main();
|