# Row 130 (#1625) round 1: human and tester commands name the tree the bus runs from Brief: docs/plans/2026-10-14_clone-cli-copy-path.md (blob 8120438a6, re-pinned at 8089b98c4). Sage assigned the row in sage-0242 and granted the slot in sage-0246. BASE: c6570f142. The candidate is build.patch against BASE, and candidate-manifest.sha256 lists its 4 files. `repro.sh` exports BASE with `git archive`, applies build.patch and checks every manifest digest: DIGESTS_OK (repro-run.log). | File | Change | |---|---| | packages/cli/src/cli.mjs | the entry block becomes an exported `run(argv, io, deps)`, plus the `OTHER_TREE` text and a set of the two codes. Nothing above the entry changes | | packages/cli/tests/other-tree.test.mjs | new, 4 tests | | packages/cli/README.md | one sentence in the `--tester` bullet, one paragraph at the end of "One pinned copy" | | docs/TOOLS.md | one sentence at the end of "Pinned stack update", linking the README section | ## The code `run()` calls `main()` and catches a CliError, as the entry did. It writes `mosaic: ` as before. When the error's `code` is `human-required` or `tester-required`, it writes one more line: ``` mosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH ``` It returns the error's exit code, which `busExit` sets to 3 for both codes. The entry sets `process.exitCode` from it only when it isn't 0, so a success leaves the exit code unset, as before. `run()`'s default io is the same as `main()`'s. The hint goes at the print site, not into the error. app.mjs (lines 347-379) compares `e.message` to `e.code`, so the message has to stay the bare code. The code comes from the transport's own stderr parse (`error.code`), so the agent-check refusal from `refuseInsideAgent`, which has no code, never gets the line. The line names no path. `run()` is exported so the tests drive the same print path the command line uses. ## Tests other-tree.test.mjs, 4 of 4 passing, and 191 of 191 in the cli suite (gate/pkg-cli.log). 1. "a human command refused with human-required ...": `inbox --business demo` through the real `humanTransport`, whose spawn is a stand-in that exits 1 with `human-required` on stderr, so the real code parse runs. It asserts exit 3, stderr exactly `mosaic: human-required` and then the hint line, and empty stdout. 2. "a --tester command refused with tester-required ...": `agents --tester --business demo` from an agent environment, through `deps.testerSpawn`. It asserts exit 3 and stderr exactly the code line plus the hint. 3. "other exit-3 refusals print no other-tree line": `tester-off` the same way gives exit 3 and stderr exactly `mosaic: tester-off`. The plain human `inbox` from an agent environment gives the agent-check refusal, exit 3, with no hint. 4. "the line names no path": `OTHER_TREE` has no `/`. No existing test changes. stack-copy.test.mjs:144 reads `human-required` from human-cli.mjs run on its own, not through cli.mjs, so it is unaffected. ## Mutant mutants.py drops the `if (OTHER_TREE_CODES.has(...))` line from `run()`, runs the cli suite, restores the file from `.orig` with a sha256 check and then kills any process whose cwd is under this row's TMPDIR. mutants-run.log has the run. | Mutant | Change | Result | |---|---|---| | m1-drop-other-tree-line | drop the extra stderr line | killed, 2 of 191 fail: tests 1 and 2 | The run ended `restored: True`, and the sweep killed nothing. ## Gate A targeted builder gate under #271 and decision 136 (gate.sh, logs in gate/). Sage's landing gate stays full. | Step | Result | |---|---| | packages/cli `node --test tests/*.test.mjs` | 191 pass, 0 fail | | packages/webui `node --test tests/reads.test.mjs tests/views.test.mjs tests/pinned-copy.test.mjs` | 23 pass, 0 fail | GATE_PASSED 00:27:55Z (gate-run.log). webui/src/reads.mjs imports `readNotifyConfig` from cli.mjs, and those three webui tests import reads.mjs directly. serve.mjs and webui's cli.mjs also import reads.mjs, and Sage's full gate covers the rest of webui. No scripts/test-*.sh reads a changed path. packages/queue/tests/dispatch.test.mjs names `/packages/cli/src/cli.mjs` only as a string. host.mjs:375 matches a process argv ending in that path, and the path doesn't change. No step failed, so the flake rule had nothing to count. ## Leftovers (sweep.log) No process was left under the TMPDIR. The cli suite left 6 `mosaic-launch-*` directories, 3 from the mutant run and 3 from the gate. They come from the cli tests' own launches (#1624) and are unrelated to this change. sweep.log lists them with their mtimes and records that I removed them. ## Going live This row has no live step. The refusal the line explains happens on the checkout's `scripts/mosaic`, which runs the checkout's own cli.mjs, so the line shows there once Sage lands the row. `mosaic` on PATH runs the pinned copy and gets the change only at the next re-pin, on Mos's go (decision 109). Per the brief, that re-pin is when the row counts as live.