review(rocko): row 72 round 1 packet, Claude pin tests (#1566)

Rocko's packet agents/rocko/work/queue-72/ (candidate manifest e6531a53,
base a0080da8) with their BUILD-LOG and SESSIONS appends, and Sage's
SESSIONS line for the jarvis review request (comment 27454).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 18:06:07 -05:00
co-authored by Claude Opus 5.5
parent 93e8c19327
commit 036afdb686
51 changed files with 1395 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# Row 72 (#1566): Claude pin tests, candidate packet, round 1
Author: Rocko. Reviewer: Filbert. Brief:
`docs/plans/2026-10-11_claude-pin-and-coder-grant.md`, section "Claude pin
tests: a directory pin, a symlink default, a pin with a space". Base:
`a0080da8` (`base.txt`). At 2026-10-11T23:03Z the checkout HEAD `ebd90554`
has no change since the base to either test file, `bus-service.sh` or
`adapter.sh`, and `build.patch` applies to it cleanly. Tests only: no
change to `bus-service.sh`, `adapter.sh`, the launcher or the session code.
The packet is uncommitted. I made no commits, pushes or Gitea calls and read
no token or private binding.
## Files (`files.txt`, `candidate-manifest.sha256`)
| File | Change |
|---|---|
| `packages/cli/tests/host.test.mjs` | +1 test: "bus-service.sh refuses a directory pin and keeps a symlink default unresolved" |
| `packages/harness/tests/claude-session.test.mjs` | +1 test: "claude adapter: a directory pin exits 2 before it runs; a pin with a space runs" |
| `agents/rocko/work/queue-72/mutants.sh` | the mutant rerun, so a reviewer can repeat it: `mutants.sh <tree>` |
## What the tests check
- `render --claude DIR` (a 0755 directory) exits 1 with "not an executable
file" and prints nothing on stdout.
- **Deviation from the brief: the default with a directory named `claude` on
`PATH`.** The brief says it refuses with "not an executable file". That
can't happen: bash's `command -v` never returns a directory. With
only a directory named `claude` on PATH it finds nothing (rc 1); with
`/usr/bin` behind it, it returns `/usr/bin/claude`. So the default goes down
the "no claude on PATH" branch: exit 0, a comment, no pin. The test
asserts that, and that the directory's path never reaches the unit. The
code is not wrong. The hazard the brief names, a unit carrying a directory
pin, can't come from the default, only from `--claude`, and the first case
covers that. I checked this by hand before writing the test (`env -i`, PATH
of only the directory plus the tools).
- The default with a symlink named `claude` on PATH, pointing to an
executable in another directory: `MOSAIC_CLAUDE_BIN` is the link's path,
and the target's path appears nowhere in the unit.
- Adapter, `MOSAIC_CLAUDE_BIN` a directory: exit 2 with "MOSAIC_CLAUDE_BIN is
not an executable file", and the stand-in on PATH never runs.
- Adapter, a stand-in at `<dir>/Claude Code/claude`: exit 0, and that
stand-in is the one that ran.
The host test runs bash with a PATH of only the test directory and a tools
directory (node, dirname, sed, readlink), as row 68's no-claude case does, so
the host's own claude can't stand in. No test spawns a real claude.
## Mutants (`out/mutants-candidate.txt`, `out/mutants-base.txt`)
The reviewers' comments name the mutants but don't write them out on the
checkout, so `mutants.sh` defines each from the brief's description:
| Mutant | Edit | Base `a0080da8` | Candidate |
|---|---|---|---|
| `bs-no-f` | bus-service `claude_line`: `[ -f "$bin" ]` → `[ -e "$bin" ]` | survived | killed (host directory-pin test) |
| `bs-x-only` | bus-service: only `[ -x "$bin" ]` | survived | killed (host) |
| `ad-no-f` | adapter: `[ -f "$MOSAIC_CLAUDE_BIN" ]` → `[ -e … ]` | survived | killed (adapter test) |
| `ad-x-only` | adapter: only `[ -x "$MOSAIC_CLAUDE_BIN" ]` | survived | killed (adapter) |
| `bs-resolve-default` | bus-service: default `bin` passed through `readlink -f` | survived | killed (host symlink case) |
| `ad-unquoted-run` | adapter: `"$CLAUDE" -p` → `$CLAUDE -p` | survived | killed (adapter space case) |
Per mutant, the script checks that the sed changed the file, saves the
diff, runs the five pin tests, then restores the file from git. Before
mutating, it refuses if either file is already modified or if the unmutated
run fails. `out/mutants-candidate/` and `out/mutants-base/` hold each
mutant's diff and log.
## Gate (`out/gate/`, sequential, scratch worktree at a0080da8 with the patch, `node_modules` linked)
| Suite | Result |
|---|---|
| cli `node --test 'packages/cli/tests/*.test.mjs'` | 113 pass, 0 fail |
| harness `node --test 'packages/harness/tests/*.test.mjs'` | 58 pass, 0 fail, 0 skipped (claude on PATH) |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed |
| test-release | 14 passed, 0 failed |
| test-task | 98 passed, 0 failed (Docker) |
After the gate I removed the `rocko-r72_default` network.
+1
View File
@@ -0,0 +1 @@
a0080da8c901c4db6a653598f14a692567bd832a
+168
View File
@@ -0,0 +1,168 @@
diff --git a/agents/rocko/work/queue-72/mutants.sh b/agents/rocko/work/queue-72/mutants.sh
new file mode 100755
index 00000000..ebd767b7
--- /dev/null
+++ b/agents/rocko/work/queue-72/mutants.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Row 72 (#1566): rerun the six mutants the row 68 reviewers named against a
+# tree with the candidate tests. Usage: mutants.sh <tree>. Each mutant edits
+# one line of bus-service.sh or adapter.sh, runs the pin tests, and restores
+# the file from git. Prints "killed" or "SURVIVED" per mutant; exit 1 if any
+# mutant survives, a sed doesn't change its file, or the unmutated run fails.
+set -euo pipefail
+TREE="$(cd "${1:?usage: mutants.sh <tree>}" && pwd)"
+cd "$TREE"
+BS=scripts/bus-service.sh
+AD=adapters/claude/adapter.sh
+PATTERN='directory pin|symlink default|pin with a space|pins the claude|MOSAIC_CLAUDE_BIN wins'
+TESTS=(packages/cli/tests/host.test.mjs packages/harness/tests/claude-session.test.mjs)
+run_tests() { node --test --test-name-pattern="$PATTERN" "${TESTS[@]}" >"$1" 2>&1; }
+
+# name | file | sed expression
+MUTANTS=(
+ 'bs-no-f|'"$BS"'|s/\[ -f "\$bin" \] \&\& \[ -x "\$bin" \]/[ -e "$bin" ] \&\& [ -x "$bin" ]/'
+ 'bs-x-only|'"$BS"'|s/\[ -f "\$bin" \] \&\& \[ -x "\$bin" \]/[ -x "$bin" ]/'
+ 'ad-no-f|'"$AD"'|s/\[ -f "\$MOSAIC_CLAUDE_BIN" \] \&\& \[ -x "\$MOSAIC_CLAUDE_BIN" \]/[ -e "$MOSAIC_CLAUDE_BIN" ] \&\& [ -x "$MOSAIC_CLAUDE_BIN" ]/'
+ 'ad-x-only|'"$AD"'|s/\[ -f "\$MOSAIC_CLAUDE_BIN" \] \&\& \[ -x "\$MOSAIC_CLAUDE_BIN" \]/[ -x "$MOSAIC_CLAUDE_BIN" ]/'
+ 'bs-resolve-default|'"$BS"'|s/^ bin="\$(command -v claude || true)"$/ bin="$(command -v claude || true)"; [ -z "$bin" ] || bin="$(readlink -f "$bin")"/'
+ 'ad-unquoted-run|'"$AD"'|s/^"\$CLAUDE" -p /$CLAUDE -p /'
+)
+
+OUT="${OUT:-$TREE/.mutants}"
+mkdir -p "$OUT"
+git diff --quiet -- "$BS" "$AD" || { echo "refusing: $BS or $AD already modified" >&2; exit 1; }
+run_tests "$OUT/baseline.log" || { echo "baseline run failed: $OUT/baseline.log" >&2; exit 1; }
+echo "baseline: pass"
+status=0
+for m in "${MUTANTS[@]}"; do
+ IFS='|' read -r name file expr <<<"$m"
+ sed -i -e "$expr" "$file"
+ if git diff --quiet -- "$file"; then
+ echo "$name: NOT APPLIED (sed matched nothing)"; status=1; continue
+ fi
+ git diff -- "$file" >"$OUT/$name.diff"
+ if run_tests "$OUT/$name.log"; then
+ echo "$name: SURVIVED"; status=1
+ else
+ echo "$name: killed ($(grep -E '^✖' "$OUT/$name.log" | sed 's/ ([0-9.]*ms)$//' | sort -u | tr '\n' ';' ))"
+ fi
+ git checkout -q -- "$file"
+done
+git diff --quiet -- "$BS" "$AD" || { echo "restore failed" >&2; exit 1; }
+exit "$status"
diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs
index 0a5d6825..13a621ae 100644
--- a/packages/cli/tests/host.test.mjs
+++ b/packages/cli/tests/host.test.mjs
@@ -559,3 +559,55 @@ test("bus-service.sh pins the claude binary, refuses one the unit can't carry, a
assert.match(none.stdout, /^# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time$/m);
assert.doesNotMatch(none.stdout, /@CLAUDE_ENV@/);
});
+
+// Row 72 (#1566): mutants row 68's tests left alive. A directory is -x, so
+// only the -f check keeps a directory pin out of the unit; the default pin
+// keeps a symlink as given, so a claude that updates through it is followed.
+test("bus-service.sh refuses a directory pin and keeps a symlink default unresolved", (t) => {
+ const dir = tmp(t);
+ const script = join(REPO, "scripts", "bus-service.sh");
+ const pinned = (out) => out.match(/^Environment=MOSAIC_CLAUDE_BIN=(.*)$/m)?.[1];
+ const tools = join(dir, "tools");
+ mkdirSync(tools);
+ symlinkSync(process.execPath, join(tools, "node"));
+ for (const tool of ["dirname", "sed", "readlink"]) {
+ const at = ["/usr/bin", "/bin"].map((d) => join(d, tool)).find((p) => existsSync(p));
+ symlinkSync(at, join(tools, tool));
+ }
+ const bash = ["/usr/bin/bash", "/bin/bash"].find((p) => existsSync(p));
+ const run = (args, bin) => spawnSync(bash, [script, ...args], { encoding: "utf8", env: { PATH: `${bin}:${tools}`, HOME: dir } });
+
+ // --claude DIR: a directory passes -x and fails -f.
+ const pinDir = join(dir, "claude-dir");
+ mkdirSync(pinDir);
+ chmodSync(pinDir, 0o755);
+ let r = run(["render", "--claude", pinDir], join(dir, "empty"));
+ assert.equal(r.status, 1);
+ assert.match(r.stderr, /not an executable file/);
+ assert.equal(r.stdout, "");
+
+ // A directory named claude on PATH: bash's `command -v` never returns a
+ // directory, so the default finds no claude and pins nothing. It never
+ // pins the directory.
+ const dirBin = join(dir, "dir-bin");
+ mkdirSync(join(dirBin, "claude"), { recursive: true });
+ chmodSync(join(dirBin, "claude"), 0o755);
+ r = run(["render"], dirBin);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stderr, /no claude on PATH; MOSAIC_CLAUDE_BIN left unset/);
+ assert.equal(pinned(r.stdout), undefined);
+ assert.ok(!r.stdout.includes(join(dirBin, "claude")));
+
+ // A symlink named claude on PATH: the pin is the link, not its target.
+ const real = join(dir, "versions", "claude-9.9.9");
+ mkdirSync(dirname(real));
+ writeFileSync(real, '#!/bin/sh\necho "9.9.9 (Claude Code)"\n');
+ chmodSync(real, 0o755);
+ const linkBin = join(dir, "link-bin");
+ mkdirSync(linkBin);
+ symlinkSync(real, join(linkBin, "claude"));
+ r = run(["render"], linkBin);
+ assert.equal(r.status, 0, r.stderr);
+ assert.equal(pinned(r.stdout), join(linkBin, "claude"));
+ assert.ok(!r.stdout.includes(real), "the target never reaches the unit");
+});
diff --git a/packages/harness/tests/claude-session.test.mjs b/packages/harness/tests/claude-session.test.mjs
index eb680836..213c811e 100644
--- a/packages/harness/tests/claude-session.test.mjs
+++ b/packages/harness/tests/claude-session.test.mjs
@@ -215,6 +215,50 @@ test("claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute e
}
});
+// Row 72 (#1566): a directory pin exits 2 with the adapter's own message,
+// not 126 at exec; a pin under a directory whose name has a space runs,
+// which needs "$CLAUDE" quoted.
+test("claude adapter: a directory pin exits 2 before it runs; a pin with a space runs", (t) => {
+ const dir = scratch(t);
+ mkdirSync(join(dir, "bin"));
+ const stand = (p, word) => {
+ writeFileSync(p, `#!/bin/sh\necho ${word} > "$WHO_OUT"\necho ok\n`);
+ chmodSync(p, 0o755);
+ return p;
+ };
+ stand(join(dir, "bin", "claude"), "path");
+ for (const f of ["prompt.md", "settings.json", "mcp.json"]) writeFileSync(join(dir, f), "{}");
+ const run = (bin) => spawnSync("/bin/sh", [ADAPTER], {
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ env: {
+ PATH: `${join(dir, "bin")}:/usr/bin:/bin`,
+ WHO_OUT: join(dir, "who"),
+ MOSAIC_SYSTEM_PROMPT_FILE: join(dir, "prompt.md"),
+ MOSAIC_REQUEST: "x",
+ MOSAIC_WORKSPACE: join(dir, "ws"),
+ MOSAIC_SESSION_DIR: join(dir, "session"),
+ MOSAIC_MODEL: "claude-sonnet-5-5",
+ MOSAIC_CLAUDE_SETTINGS: join(dir, "settings.json"),
+ MOSAIC_CLAUDE_MCP_CONFIG: join(dir, "mcp.json"),
+ MOSAIC_CLAUDE_BIN: bin,
+ },
+ });
+ const pinDir = join(dir, "claude-dir");
+ mkdirSync(pinDir);
+ chmodSync(pinDir, 0o755);
+ let r = run(pinDir);
+ assert.equal(r.status, 2, r.stderr);
+ assert.match(r.stderr, /MOSAIC_CLAUDE_BIN is not an executable file/);
+ assert.equal(existsSync(join(dir, "who")), false);
+
+ mkdirSync(join(dir, "Claude Code"));
+ const spaced = stand(join(dir, "Claude Code", "claude"), "spaced");
+ r = run(spaced);
+ assert.equal(r.status, 0, r.stderr);
+ assert.equal(readFileSync(join(dir, "who"), "utf8").trim(), "spaced");
+});
+
test("claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do", { skip }, async (t) => {
const { dir, workspace, env } = await session(t, []);
const slug = realpathSync(workspace).replace(/[/.]/g, "-");
@@ -0,0 +1,3 @@
88adfa55efb6da9ea575b2074ed788f69b5cfc73b637dad8f3943c48e7c1f83d agents/rocko/work/queue-72/mutants.sh
534ef435a3fab82616079000dc7c357e3630380e47ce2fb1384e294d388c98dd packages/cli/tests/host.test.mjs
9e8b5465f27665832f6c6c3872117f6d773b1ba6e699cd65da66ccda96297e94 packages/harness/tests/claude-session.test.mjs
+3
View File
@@ -0,0 +1,3 @@
agents/rocko/work/queue-72/mutants.sh
packages/cli/tests/host.test.mjs
packages/harness/tests/claude-session.test.mjs
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Row 72 (#1566): rerun the six mutants the row 68 reviewers named against a
# tree with the candidate tests. Usage: mutants.sh <tree>. Each mutant edits
# one line of bus-service.sh or adapter.sh, runs the pin tests, and restores
# the file from git. Prints "killed" or "SURVIVED" per mutant; exit 1 if any
# mutant survives, a sed doesn't change its file, or the unmutated run fails.
set -euo pipefail
TREE="$(cd "${1:?usage: mutants.sh <tree>}" && pwd)"
cd "$TREE"
BS=scripts/bus-service.sh
AD=adapters/claude/adapter.sh
PATTERN='directory pin|symlink default|pin with a space|pins the claude|MOSAIC_CLAUDE_BIN wins'
TESTS=(packages/cli/tests/host.test.mjs packages/harness/tests/claude-session.test.mjs)
run_tests() { node --test --test-name-pattern="$PATTERN" "${TESTS[@]}" >"$1" 2>&1; }
# name | file | sed expression
MUTANTS=(
'bs-no-f|'"$BS"'|s/\[ -f "\$bin" \] \&\& \[ -x "\$bin" \]/[ -e "$bin" ] \&\& [ -x "$bin" ]/'
'bs-x-only|'"$BS"'|s/\[ -f "\$bin" \] \&\& \[ -x "\$bin" \]/[ -x "$bin" ]/'
'ad-no-f|'"$AD"'|s/\[ -f "\$MOSAIC_CLAUDE_BIN" \] \&\& \[ -x "\$MOSAIC_CLAUDE_BIN" \]/[ -e "$MOSAIC_CLAUDE_BIN" ] \&\& [ -x "$MOSAIC_CLAUDE_BIN" ]/'
'ad-x-only|'"$AD"'|s/\[ -f "\$MOSAIC_CLAUDE_BIN" \] \&\& \[ -x "\$MOSAIC_CLAUDE_BIN" \]/[ -x "$MOSAIC_CLAUDE_BIN" ]/'
'bs-resolve-default|'"$BS"'|s/^ bin="\$(command -v claude || true)"$/ bin="$(command -v claude || true)"; [ -z "$bin" ] || bin="$(readlink -f "$bin")"/'
'ad-unquoted-run|'"$AD"'|s/^"\$CLAUDE" -p /$CLAUDE -p /'
)
OUT="${OUT:-$TREE/.mutants}"
mkdir -p "$OUT"
git diff --quiet -- "$BS" "$AD" || { echo "refusing: $BS or $AD already modified" >&2; exit 1; }
run_tests "$OUT/baseline.log" || { echo "baseline run failed: $OUT/baseline.log" >&2; exit 1; }
echo "baseline: pass"
status=0
for m in "${MUTANTS[@]}"; do
IFS='|' read -r name file expr <<<"$m"
sed -i -e "$expr" "$file"
if git diff --quiet -- "$file"; then
echo "$name: NOT APPLIED (sed matched nothing)"; status=1; continue
fi
git diff -- "$file" >"$OUT/$name.diff"
if run_tests "$OUT/$name.log"; then
echo "$name: SURVIVED"; status=1
else
echo "$name: killed ($(grep -E '^✖' "$OUT/$name.log" | sed 's/ ([0-9.]*ms)$//' | sort -u | tr '\n' ';' ))"
fi
git checkout -q -- "$file"
done
git diff --quiet -- "$BS" "$AD" || { echo "restore failed" >&2; exit 1; }
exit "$status"
+123
View File
@@ -0,0 +1,123 @@
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (8166.765346ms)
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (572.522763ms)
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (997.261698ms)
✔ NO_COLOR: no escape codes, except reverse video on the selected row (1366.594383ms)
✔ 1-9 shows the decide flow's own lines and question; only y resolves (324.985306ms)
✔ the app shows the same decide text as mosaic decide (105.472122ms)
✔ any key but y or Y at the question answers No; nothing is sent (979.98217ms)
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (537.613643ms)
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (139.952237ms)
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (907.979929ms)
✔ quitting at the question answers No and sends nothing, armed or not (115.13296ms)
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (124.787577ms)
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (110.884622ms)
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (1166.07854ms)
✔ at every size the decide box shows each line y depends on, or says it is hidden (31504.438386ms)
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (440.615391ms)
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (184.256448ms)
✔ keys: views, moving, trail of a selection, the command line, help and quit (437.25353ms)
✔ one option: the status line says Press 1, not 1-1 (56.747724ms)
✔ no business: the app says so and :business picks one (36.701946ms)
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (4.497651ms)
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (852.094435ms)
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (1.039678ms)
✔ decide with --note still asks; a No sends nothing (30.558987ms)
✔ the app's business comes from a live bus host only (4.039615ms)
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (48.387881ms)
✔ draw is pure: same state, same frame (4.095435ms)
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (70.100468ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (89.010174ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (131.922309ms)
✔ decide prints a declining choice as declining (136.78509ms)
✔ an unknown outcome is reported once and never resent (118.819165ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (137.479336ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (178.591685ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (91.800624ms)
✔ every human command refuses inside an agent run before it touches the bus (77.839866ms)
✔ usage errors exit 4; no business and no host is a usage error (138.875876ms)
✔ agents and tasks print through the broker (60.090991ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (5.32231ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (279.470054ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (517.50159ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (406.234323ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (222.948756ms)
✔ a business without tracker.baseUrl gets no trackers entry (257.879607ms)
✔ an unknown business and a broken system config refuse with exit 3 (352.978931ms)
✔ empty views say so (4.151914ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (5.793327ms)
✔ tasks print the tracker fields the snapshot carries (0.504836ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (2032.065812ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (1258.637038ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (504.679874ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1447.090939ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (1028.567302ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (626.653723ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (792.780229ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (827.588805ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (448.928319ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (959.559717ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (374.582964ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (1041.924964ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (272.326327ms)
✔ bus stop refuses to signal a live pid that is not a bus host (212.075795ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (1877.931965ms)
✔ bus start refuses with exit 3 without a notifier config (410.231009ms)
✔ bus start runs until bus stop; status reports it while it runs (2751.420173ms)
✔ bus-service.sh renders the unit and installs it into a given directory (271.391306ms)
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (286.635166ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (124.634862ms)
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (2202.417372ms)
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (2405.387947ms)
✔ a runner that stops at once ends its launch with the runner's reason (888.819534ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (2968.426852ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (5984.400948ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (297.688892ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (524.428233ms)
✔ a launch client that never closes its side doesn't hold the host's close (483.523483ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1622.655595ms)
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (36.065044ms)
✔ zoned uses the IANA zone across DST (73.655648ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.612806ms)
✔ two blocking decisions get two DMs with different nonces (164.020027ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (1.77021ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (107.037673ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (176.700103ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (163.738237ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (266.590884ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (606.701328ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (306.405387ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (93.252035ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (62.239756ms)
✔ an inbox read failure is logged and the next poll retries (5.72164ms)
✔ no Discord id reaches the journal or the log (67.389748ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (8.573637ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (43.865237ms)
✔ the journal: a whole file that is one torn line truncates to empty (28.752236ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (5.797293ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (2.434898ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (21.052982ms)
✔ the journal: a symlinked directory refuses and says it is a link (1.121274ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (1.177906ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.937541ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (1.23765ms)
✔ digest content stays within Discord's 2000 characters (6.139295ms)
✔ runLoop never overlaps ticks and stops after the one in flight (124.934955ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (1711.354145ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (537.477436ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (3013.392452ms)
✔ busExit and refuseInsideAgent (3.705285ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (325.919449ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1130.145307ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (127.353818ms)
✔ launches off and on go to the broker and change the business's launch state (89.503237ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (124.261264ms)
ℹ tests 113
ℹ suites 0
ℹ pass 113
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 49813.574688
@@ -0,0 +1 @@
2026-10-11T23:03:44Z
@@ -0,0 +1,66 @@
✔ sessionModel: agent vars win, then the system's execution settings (220.571303ms)
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (38.138242ms)
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (14.587866ms)
✔ a bundle is written once: an existing file refuses (11.013312ms)
✔ a path with a single quote can't go into the hook command (7.472368ms)
✔ allow exits 0, a deny exits 2 with the reason on stderr (1091.107942ms)
✔ a missing or wrong policy, or a bad event, exits 2 (370.064293ms)
✔ the bundle's wrapped command: a missing gate or node still blocks (1524.139979ms)
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (5239.695758ms)
✔ claude: the hook alone blocks a path outside the workspace (3505.114261ms)
✔ claude: a second turn resumes the first turn's session (7090.980712ms)
✔ claude adapter: --restricted is always passed (62.080941ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (346.323108ms)
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (158.882322ms)
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (5097.218294ms)
✔ claude: a missing hook or MCP file refuses before claude starts (199.143816ms)
✔ pi: policy tools and typed tools pass, anything else is blocked (16.151597ms)
✔ claude: builtins map from pi names, typed tools need the mcp prefix (10.439093ms)
✔ file tool paths must resolve inside the workspace (5.524028ms)
✔ pi's own path normalisation can't be used to step out (11.975103ms)
✔ a symlink inside the workspace that points out is outside (24.472476ms)
✔ a dangling symlink is refused at any depth, in both harnesses (36.341784ms)
✔ read is checked under every spelling pi's read would open, in both harnesses (134.447906ms)
✔ other spellings cover directories, dangling links and pi's cwd (23.176456ms)
✔ a relative path climbs from the workspace's real path, in both harnesses (41.832941ms)
✔ claude path fields per tool (1.844369ms)
✔ glob patterns stay inside the workspace (4.749344ms)
✔ a path that can't be checked is blocked (1.197203ms)
✔ initialize, ping and tools/list (512.567722ms)
✔ tools/call goes through the tool socket; a refusal is an isError result (290.30019ms)
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (191.976474ms)
✔ a missing argument is a usage error (186.292495ms)
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (2292.59352ms)
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (1843.612383ms)
✔ pi: a read is refused when pi would open another spelling outside (1603.970397ms)
✔ pi: a relative path climbs from the real path of a workspace behind a symlink (2127.541578ms)
✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (2310.586378ms)
✔ pi: a missing extension refuses before any model call (49.689892ms)
✔ pi: an extension without its configuration fails pi's start (1448.161485ms)
✔ founderCheck: founder variables, then a needed service without a usable token (13.603367ms)
✔ turnRequest names the sender, class, reply and decision (0.519624ms)
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (984.434421ms)
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (252.636326ms)
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (1356.107063ms)
✔ a RESULT gets no automatic reply; failed turns reply with the reason (2195.652619ms)
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (462.989047ms)
✔ founder credentials stop before the claim (20) (589.454895ms)
✔ a refused claim exits 21; an ended run's capability exits 22 (803.196677ms)
✔ the launch ending under a running session exits 22 (386.836143ms)
✔ a broker that stays unreachable exits 23 after brokerRetries polls (435.829225ms)
✔ a broker that is down at the claim exits 23, not 21 (675.200131ms)
✔ no capability, or a malformed one, on stdin exits 2 (999.096123ms)
✔ a missing or malformed policy exits 2 before the claim (496.057533ms)
✔ the PM gets launch, its task verbs and the reads (34.817657ms)
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (28.313991ms)
✔ launch only when the business's launch block names the instance as launcher (12.055749ms)
✔ an action outside the instance's authority has no tool (16.601564ms)
✔ callTool: one JSON line out, the result back, a refusal rejects (57.019666ms)
ℹ tests 58
ℹ suites 0
ℹ pass 58
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 22008.973676
@@ -0,0 +1 @@
2026-10-11T22:58:58Z
@@ -0,0 +1,11 @@
cli rc=0 ℹ pass 113 ℹ fail 0 ℹ skipped 0
harness rc=0 ℹ pass 58 ℹ fail 0 ℹ skipped 0
test-auth rc=0 selftest: 15 passed, 0 failed
test-conductor rc=0 selftest: 17 passed, 0 failed
test-config rc=0 selftest: 24 passed, 0 failed
test-discord rc=0 discord suite: 66 passed, 0 failed
test-extension-package rc=0 extension package selftest: 18 passed, 0 failed
test-foundation rc=0 selftest: 44 passed, 0 failed
test-queue rc=0 queue suite: 27 passed, 0 failed
test-release rc=0 selftest: 14 passed, 0 failed
test-task rc=0 selftest: 98 passed, 0 failed
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,23 @@
On branch refactor
Your branch is up to date with 'origin/refactor'.
nothing to commit, working tree clean
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 3959563 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 150
ℹ pass 150
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r72) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,16 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
selftest: 14 passed, 0 failed
@@ -0,0 +1,101 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
OK mock adapter: gate passes on matching mock response (exit 0)
OK mock adapter: expect-mismatch recorded (exit 1)
OK mismatch reason recorded
OK unknown adapter fails closed (exit 1)
OK mission task runs via mock adapter (exit 0)
OK mission section injected into generated prompt
OK retry of mission run succeeds (exit 0)
OK retriedFrom lineage recorded
OK mission section present after retry (relative path resolved)
OK retry of missing run exits 4 (exit 4)
OK skill install bundled ms-tools (exit 0)
OK installed to skills-available
OK double install refuses (exit 1)
OK activate enables skill (exit 0)
OK enabled dir populated
OK uninstall while enabled refuses (exit 1)
OK deactivate moves back to available (exit 0)
OK uninstall removes from available (exit 0)
OK seat with enabled skill launches
OK skill path delivered to adapter
OK shipped researcher contract resolves (ceiling + network)
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
OK resolve-role refuses name/filename mismatch (exit 2)
OK resolve-role refuses unknown network declaration (exit 2)
OK resolve-role refuses duplicate role tool (exit 2)
OK resolve-role refuses unsupported tool (exit 2)
OK resolve-role refuses missing contract file (exit 4)
OK version 1 role prints no contract line
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
OK resolve-role accepts a minimal version 2 role (exit 0)
OK resolve-role refuses an action outside the vocabulary (exit 2)
OK resolve-role refuses a gated-only action in a role file (exit 2)
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
OK resolve-role refuses a contract that is a symbolic link (exit 2)
OK seat launches under role ceiling
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
OK narrowing recorded loudly
OK missing role contract refuses launch (exit 2)
OK missing-contract refusal names the role
OK empty ceiling intersection -> tool-free seat
OK tool-free outcome recorded loudly
OK overridden agents dir without seat definition refuses (exit 4)
OK seat-resolution refusal names the seat
OK policy: mission only -> mission tools
OK policy: task only -> task tools
OK policy: both -> intersection (task narrowed)
OK policy: empty intersection -> tool-free
OK invalid mission capabilities rejected (exit 2)
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
OK task run with user layer present (exit 0)
OK user context dispatched into generated prompt
OK workspace+tools task runs via mock (exit 0)
OK workspace path + tools delivered to adapter
OK persistent workspace created on host
OK plain task still runs (no workspace/tools) (exit 0)
OK workspace var present but empty when absent
OK tools empty when absent
OK unknown tool exits 2 (exit 2)
OK workspace traversal exits 2 (exit 2)
OK live hello task succeeds with exact marker
OK result.json written in run dir
OK result.json contents are correct
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
OK each run gets a distinct run dir (no clobber)
OK list shows both runs
OK fork base: teach succeeds (exit 0)
OK fork child recalls ancestor context (exit 0)
OK forked child recalled ancestor code word
OK ancestor session untouched by fork
OK child session dir has its own branch file
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
OK user recall run succeeds
OK recalled user name (response: Jason)
OK no agent identity on headless run
selftest: 98 passed, 0 failed
@@ -0,0 +1,7 @@
baseline: pass
bs-no-f: SURVIVED
bs-x-only: SURVIVED
ad-no-f: SURVIVED
ad-x-only: SURVIVED
bs-resolve-default: SURVIVED
ad-unquoted-run: SURVIVED
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..40a43ddb 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -33,7 +33,7 @@ if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
/*) ;;
*) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;;
esac
- [ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
+ [ -e "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (776.728613ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (119.517434ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1066.686453
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..700291db 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -73,7 +73,7 @@ fi
ALLOWED=mcp__mosaic
[ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic"
PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")"
-"$CLAUDE" -p "$MOSAIC_REQUEST" \
+$CLAUDE -p "$MOSAIC_REQUEST" \
--output-format text \
--system-prompt "$PROMPT_CONTENT" \
--model "$MOSAIC_MODEL" \
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (966.484972ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (310.409084ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1429.026231
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..1b10f061 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -33,7 +33,7 @@ if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
/*) ;;
*) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;;
esac
- [ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
+ [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (1052.739669ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (316.749733ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1389.331619
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (943.671471ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (230.288669ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1178.042132
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..8ec66216 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -49,7 +49,7 @@ claude_line() {
fi
case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac
case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac
- [ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
+ [ -e "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
echo "Environment=MOSAIC_CLAUDE_BIN=$bin"
}
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (452.698199ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (109.619729ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 767.577742
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..3c9e47d1 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -40,7 +40,7 @@ claude_line() {
if [ "$CLAUDE_SET" = 1 ]; then
bin="$CLAUDE_BIN"
else
- bin="$(command -v claude || true)"
+ bin="$(command -v claude || true)"; [ -z "$bin" ] || bin="$(readlink -f "$bin")"
if [ -z "$bin" ]; then
echo "bus-service: no claude on PATH; MOSAIC_CLAUDE_BIN left unset (--claude PATH pins one)" >&2
echo "# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time"
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (1006.856047ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (376.512437ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1415.823841
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..c0f5c4d4 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -49,7 +49,7 @@ claude_line() {
fi
case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac
case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac
- [ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
+ [ -x "$bin" ] || die "claude path is not an executable file: $bin"
echo "Environment=MOSAIC_CLAUDE_BIN=$bin"
}
@@ -0,0 +1,10 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (1051.885653ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (119.000095ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1598.588034
@@ -0,0 +1,7 @@
baseline: pass
bs-no-f: killed (✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved;✖ failing tests:;)
bs-x-only: killed (✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved;✖ failing tests:;)
ad-no-f: killed (✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs;✖ failing tests:;)
ad-x-only: killed (✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs;✖ failing tests:;)
bs-resolve-default: killed (✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved;✖ failing tests:;)
ad-unquoted-run: killed (✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs;✖ failing tests:;)
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..40a43ddb 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -33,7 +33,7 @@ if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
/*) ;;
*) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;;
esac
- [ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
+ [ -e "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
@@ -0,0 +1,36 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (812.758403ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (114.825073ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (105.923033ms)
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (56.404674ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1223.641818
✖ failing tests:
test at packages/harness/tests/claude-session.test.mjs:221:1
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (56.404674ms)
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/rocko-r72/adapters/claude/adapter.sh: line 76: /mnt/storage/scratch/tmp/mosaic-harness-TaRYG3/claude-dir: Is a directory
126 !== 2
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/harness/tests/claude-session.test.mjs:251:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 126,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..700291db 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -73,7 +73,7 @@ fi
ALLOWED=mcp__mosaic
[ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic"
PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")"
-"$CLAUDE" -p "$MOSAIC_REQUEST" \
+$CLAUDE -p "$MOSAIC_REQUEST" \
--output-format text \
--system-prompt "$PROMPT_CONTENT" \
--model "$MOSAIC_MODEL" \
@@ -0,0 +1,36 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (624.572705ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (157.138428ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (228.077294ms)
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (53.77181ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1092.980179
✖ failing tests:
test at packages/harness/tests/claude-session.test.mjs:221:1
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (53.77181ms)
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/rocko-r72/adapters/claude/adapter.sh: line 76: /mnt/storage/scratch/tmp/mosaic-harness-0kAUdR/Claude: No such file or directory
127 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/harness/tests/claude-session.test.mjs:258:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 127,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,13 @@
diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh
index a0e04dae..1b10f061 100644
--- a/adapters/claude/adapter.sh
+++ b/adapters/claude/adapter.sh
@@ -33,7 +33,7 @@ if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
/*) ;;
*) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;;
esac
- [ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
+ [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
@@ -0,0 +1,36 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (1098.847282ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (127.567038ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (271.776218ms)
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (103.350808ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1656.594055
✖ failing tests:
test at packages/harness/tests/claude-session.test.mjs:221:1
✖ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (103.350808ms)
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/rocko-r72/adapters/claude/adapter.sh: line 76: /mnt/storage/scratch/tmp/mosaic-harness-Gqq6WT/claude-dir: Is a directory
126 !== 2
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/harness/tests/claude-session.test.mjs:251:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 126,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,12 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (975.231311ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (86.973344ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (238.744224ms)
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (69.864826ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1403.440301
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..8ec66216 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -49,7 +49,7 @@ claude_line() {
fi
case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac
case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac
- [ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
+ [ -e "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
echo "Environment=MOSAIC_CLAUDE_BIN=$bin"
}
@@ -0,0 +1,35 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (624.008311ms)
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (29.247918ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (248.164396ms)
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (56.412235ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 923.50351
✖ failing tests:
test at packages/cli/tests/host.test.mjs:566:1
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (29.247918ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
0 !== 1
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/cli/tests/host.test.mjs:585:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..3c9e47d1 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -40,7 +40,7 @@ claude_line() {
if [ "$CLAUDE_SET" = 1 ]; then
bin="$CLAUDE_BIN"
else
- bin="$(command -v claude || true)"
+ bin="$(command -v claude || true)"; [ -z "$bin" ] || bin="$(readlink -f "$bin")"
if [ -z "$bin" ]; then
echo "bus-service: no claude on PATH; MOSAIC_CLAUDE_BIN left unset (--claude PATH pins one)" >&2
echo "# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time"
@@ -0,0 +1,37 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (826.39241ms)
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (225.528412ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (108.00737ms)
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (37.548293ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1359.702788
✖ failing tests:
test at packages/cli/tests/host.test.mjs:566:1
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (225.528412ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ '/mnt/storage/scratch/tmp/mosaic-cli-ePhmQ9/versions/claude-9.9.9'
- '/mnt/storage/scratch/tmp/mosaic-cli-ePhmQ9/link-bin/claude'
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/cli/tests/host.test.mjs:611:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: '/mnt/storage/scratch/tmp/mosaic-cli-ePhmQ9/versions/claude-9.9.9',
expected: '/mnt/storage/scratch/tmp/mosaic-cli-ePhmQ9/link-bin/claude',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,13 @@
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
index f641c479..c0f5c4d4 100755
--- a/scripts/bus-service.sh
+++ b/scripts/bus-service.sh
@@ -49,7 +49,7 @@ claude_line() {
fi
case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac
case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac
- [ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
+ [ -x "$bin" ] || die "claude path is not an executable file: $bin"
echo "Environment=MOSAIC_CLAUDE_BIN=$bin"
}
@@ -0,0 +1,35 @@
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (763.740714ms)
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (33.151932ms)
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (270.747243ms)
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (45.638074ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1238.504414
✖ failing tests:
test at packages/cli/tests/host.test.mjs:566:1
✖ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (33.151932ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
0 !== 1
at TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r72/packages/cli/tests/host.test.mjs:585:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,48 @@
b1c1aa79de3438504f38f92b51af8aaff065f496039d516dcb9609e8ef5ccf73 ./base.txt
074d3dc8d21ad5130143cbd39b371295fe345184c5d826ec322ac4bdcd042a56 ./BUILD.md
917bb1be18d7a7261901044178567b0c7b6b4e097e6883ebe5765286308fff23 ./build.patch
e6531a5311f17ea147700e417dfdbd766a9c38acf3c3b44755267c285b78e006 ./candidate-manifest.sha256
68b2ea64112704a190be5dc42264b2500fbcd0fe00bab01a761957d3f97ceb54 ./files.txt
88adfa55efb6da9ea575b2074ed788f69b5cfc73b637dad8f3943c48e7c1f83d ./mutants.sh
691348f31e44be6f0fc5bab7e06c64888953c607db85cb267a75c92345ed5f38 ./out/gate/cli.log
fa23dbda2669e02a15422fd3bf306796641dd8fdd565213a85c82504153c84e2 ./out/gate/end.txt
c329c85886bcd30613cf9ca494f4fe8f00817f850e49e569c412030dbdfdf986 ./out/gate/harness.log
d06fc2614bb75bc7ff5a2c9ada5ac2c5177c3f850c4aaabe7fa089383e476731 ./out/gate/start.txt
4561294ef708fb023f56b47d50f818d23224dfb5b38d278295b22b6f395809c8 ./out/gate/summary.txt
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 ./out/gate/test-auth.log
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 ./out/gate/test-conductor.log
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 ./out/gate/test-config.log
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe ./out/gate/test-discord.log
b9cdb6e354bd14d0fa795fc0409d1683e1d8ce380b0417053fee662e38e91773 ./out/gate/test-extension-package.log
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f ./out/gate/test-foundation.log
1d0336ab459e089658ccff8c84ac53b0ffbab1a2bba2066750ece3909c876f41 ./out/gate/test-queue.log
fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 ./out/gate/test-release.log
ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a ./out/gate/test-task.log
7bf6bff528b53468e1341ca374159616593640f2a825e15e0bc1e22ec8ad5520 ./out/mutants-base/ad-no-f.diff
d055ceef2310a6c9f12ad995e490b789b255f6b92d70c3f43db27828d50ec4e9 ./out/mutants-base/ad-no-f.log
634196d7d864fd749ba8d5ebc1509e3f95085826613fed42ef830232218b1aa8 ./out/mutants-base/ad-unquoted-run.diff
8ef4cecdf963642d82e5a3975293a8accea2cfb0b6f6e3471ce67220b98d43ed ./out/mutants-base/ad-unquoted-run.log
340cbba44bd8a7858a07bf558c38439144073c1f8b7c8f7fdda2f8faa8e313ed ./out/mutants-base/ad-x-only.diff
dd99890494e316b4265805ace7e1676f1d9585f6e7bef0bee337a4c55bdfb1c2 ./out/mutants-base/ad-x-only.log
330147faa56b457ccc0af2e3bc94bb82b339c89fb761d3332697eedc631caa10 ./out/mutants-base/baseline.log
0dd50a28ce2991879c83d85099e88ebb050be226a303fbbe8df7a6f1ca086646 ./out/mutants-base/bs-no-f.diff
badfd557f34625bb7fd59273012c05b878507bfaa50bfbf3d24373dee2eaae8c ./out/mutants-base/bs-no-f.log
0dcd53ab5db4652c0e4c0c7ffd84b91fd2570d9694deea3bb463294d9463d7bd ./out/mutants-base/bs-resolve-default.diff
77548036af5235f493d957eccb31920d14becd4023abffaaae5842dcc0a38729 ./out/mutants-base/bs-resolve-default.log
2d20403ddada87d232bdec3e1c834e2d181094f10bdc76d59b450c4d1cd8da70 ./out/mutants-base/bs-x-only.diff
fdb4801844f159eac9c23176a4df97fe14dd04af39bd0bf8a2f5fbeb46596c6e ./out/mutants-base/bs-x-only.log
bf2d9d276c7bcb6be4eff8eb8c7a6202e0da97df465895cf02a77e1bbd28dae4 ./out/mutants-base.txt
7bf6bff528b53468e1341ca374159616593640f2a825e15e0bc1e22ec8ad5520 ./out/mutants-candidate/ad-no-f.diff
6edd0a941faf86fa14e0d4a2c673f3c3ce42cef2dad60bc36181c5e83867d92e ./out/mutants-candidate/ad-no-f.log
634196d7d864fd749ba8d5ebc1509e3f95085826613fed42ef830232218b1aa8 ./out/mutants-candidate/ad-unquoted-run.diff
db9c14962b7feda199eed4d47f9c951b55fadf30fbd9f2a3cc585d2d21c2f962 ./out/mutants-candidate/ad-unquoted-run.log
340cbba44bd8a7858a07bf558c38439144073c1f8b7c8f7fdda2f8faa8e313ed ./out/mutants-candidate/ad-x-only.diff
fabdd03dafe4dc80a279902ec87b1d6e53cb1c327f762d82d40809f08a712026 ./out/mutants-candidate/ad-x-only.log
5a6abe15c51ee48130de09f9032ca12fc42abac7bc96cc77acf6a3ff96219cc8 ./out/mutants-candidate/baseline.log
0dd50a28ce2991879c83d85099e88ebb050be226a303fbbe8df7a6f1ca086646 ./out/mutants-candidate/bs-no-f.diff
35ffa313386bbee6487abc2a6939010544ecab3e6d6a8ff86ef827b79c284759 ./out/mutants-candidate/bs-no-f.log
0dcd53ab5db4652c0e4c0c7ffd84b91fd2570d9694deea3bb463294d9463d7bd ./out/mutants-candidate/bs-resolve-default.diff
688b0a4eb377bc7b481202f8eccbfa6f70465ba552f7a63970ab861b0788a0ca ./out/mutants-candidate/bs-resolve-default.log
2d20403ddada87d232bdec3e1c834e2d181094f10bdc76d59b450c4d1cd8da70 ./out/mutants-candidate/bs-x-only.diff
5351195eae6351bad35226fb230cf49aef7d867de7a93db69f07595d0cee28c3 ./out/mutants-candidate/bs-x-only.log
a4d82296596fac6986a3fb717a9da164f8c345412d8aa5f1d3ddb1dd33f03660 ./out/mutants-candidate.txt