# Row 184 (#1682): tester thread verbs in the CLI Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-20_tester-thread-follow-ups.md, section "Brief: tester thread verbs in the CLI", blob 3b4313cb9ffa (commit 841c56d05, rev 2161). Assignment sage-1202. Design and changed tests accepted in sage-1208. Base: 7fc98cf19 (base.txt), origin/refactor after row 178 landed as b2b33aee5 (sage-1220). The candidate is build.patch on that base. candidate-manifest.sha256 lists the 7 changed paths (changed-paths.txt) with their sha256. The manifest digest is e7e2c76308e1b00391bb758d66fb8b427aad3e2985f30c651f5ef84c02ee2630, and the stable patch-id is ad846db177b35b7c64b333900554bbca75852bce. The patch applied to a fresh index at the base rebuilds every manifest line. ### Head moved after run 2 (sage-1225) origin/refactor is 3da362982. Past 7fc98cf19 it adds row 182 (33ab5eeb9), Sage's SESSIONS and BUILD-LOG sweep (ef09f8b3f) and queue commit 3da362982. None of them touches a 184 path. Sage ruled no rebase or rerun before review; the landing gate reruns everything on the head current then. Row 182 changes packages/bus/src/broker.mjs, and tests/tester-threads.test.mjs runs a real Broker in-process, so I read the change. `#testerOwns` refuses `thread-not-owned` when the tester's reply chain reaches a thread the tester doesn't own, and it leaves a role's reply into a tester thread unchanged. In this row's test, the tester's message.send calls (:136, :179, :210) all go to chief, and none has `in_reply_to`. chief's definition is `chief-of-staff` (:25), and THREAD_DEFINITION is `assistant` (business vocabulary.mjs:131), so `#threaded` is false for chief and `#threadFor` returns null. The tester reaches threads only through thread.send, which `#thread()` already checks. The replies into the tester's threads are role sends from `a`. So I expect no change. This is a read of the code, not a run on 3da362982. ### Rebase from 841c56d05 The first build was on 841c56d05. Its patch, manifest (aa15e024...), base and patch-id are in r1-841c56d05/. Of the 7 paths, only docs/TOOLS.md changed between the bases. Row 178 added its Console proxy proof section at :856, and this row's hunks are at :264-281, so all 7 paths apply cleanly with no conflict. The TOOLS.md lines this patch adds and removes are the same before and after, the patch-id is unchanged, and only the TOOLS.md blob in the manifest differs. Row 184 doesn't change packages/cli/tests/stack-release.test.mjs, which row 178 also touched. 178's two T178 tests there run stack-release activate with the proof pair and --tester-port and read nothing this row changes. They run in the cli suite in run 2. ## No bus change Every verb the CLI now sends as the tester is already in the broker's TESTER_VERBS: project.list, thread.list, thread.create, thread.open, thread.send, thread.messages and message.receive. The cli channel module's verb set covers the same names. The broker's `#thread()` refuses `thread-not-owned` when the thread's owner isn't the caller, and the tester's owner is `tester` (row 177), so the CLI adds no check. A thread run's reply to the tester lands in the run's thread, so follow's thread filter (threadMembers over thread.messages) works as it does for the human. sage-1208: the ask-first clause doesn't apply. ## What ships - `packages/cli/src/cli.mjs`: talk drops the row 177 refusal and its comment: ```js // Row 177 (#1675): the tester has threads of its own now, but the CLI's thread verbs stay the // human's; a tester talk --thread is a follow-up row. if (threadRef !== undefined && parsed.flags.has("--tester")) throw usage("--thread takes no --tester"); ``` talk's header comment gains three lines for row 184. No other line changes: `callFor` and `channelCallFor` already route `--tester` to tester-cli.mjs, and the follow loop already receives with message.receive. - `packages/cli/src/thread.mjs`: `thread create|open|read` accept `--tester` (`flags: sub === "read" ? ["--json", "--tester"] : ["--tester"]`), so `callFor(parsed)` runs them as the tester. The usage lines show `[--tester]`. The header comment names row 184. threadsCommand already took `--tester` (row 175), so only its comment changes. - `docs/TOOLS.md` and `packages/cli/README.md` list `[--tester]` on threads, thread create/open/read, talk --thread and follow --thread. The README's tester paragraph names row 184, and its refusal list no longer names the thread verbs. ## Changed existing tests (sage-1208) Both tests pinned the refusal the brief removes. - `packages/cli/tests/thread-cli.test.mjs:311`: the exit-4 loop drops one argv, `["talk", "--thread", "stack/alpha", "hi", "--tester"]`. The other three argvs and the unchanged-writes assert stay. - `packages/cli/tests/tester-reads.test.mjs`, test "row 175: --tester on limits set, reset and migrate and every write form outside W1-W7 exits 4": the list drops five entries, and a two-line comment points to tests/tester-threads.test.mjs: ```js ["thread", "create", "alpha", "one", "--business", "acme"], ["thread", "open", "t-1", "--business", "acme"], ["thread", "read", "t-1", "--business", "acme"], ["talk", "--thread", "t-1", "hello", "--business", "acme"], ["follow", "--thread", "t-1", "--business", "acme"], ``` ## New tests: `packages/cli/tests/tester-threads.test.mjs` The test runs an in-process Broker on a Store with business demo (`bus.tester` true, roles chief and assistant). The human and tester caps sit behind stand-in transports that record each call and turn a bus refusal into a CliError carrying the code. Tester forms run with the CLAUDECODE markers set, as an agent would. 1. With `--tester`, thread create, threads, open, talk --thread and read run on the tester's own thread. Checks: owner `tester`, the open recorded for the tester, from_role `tester` and from_run `tester-cli` on the sent message, the exact printed lines, no human-transport call, no inbox call, and the human's `threads` still printing "no threads". 2. talk --thread --follow and follow --thread with `--tester` print and read only the probe thread's messages. A message outside any thread and a reply in another thread stay unread. Each receive is message.receive; inbox is never called (brief item 3). 3. On a human thread, `--tester` open, read, talk, follow and talk --follow refuse with `thread-not-owned`, and the CLI prints it. A label or prefix reference gives "no thread of yours matches". Creating the same name gives `thread-exists`. Writes are unchanged and the waiting tester message stays unread. In reverse, the human gets `thread-not-owned` on the tester's thread. 4. Without `--tester`, every human form runs and never calls the tester transport. The existing thread-cli tests pin the human output unchanged. 5. In a real process, scripts/mosaic with a started broker and tester-cli.mjs runs thread create, threads --json, talk --thread --wait 0 and thread read --json as the tester. ## Deviations 1. The usage text shows `[--tester]` on the thread lines, so a usage error prints the new text. Without `--tester`, each verb's result is unchanged; thread-cli.test.mjs pins it. 2. Brief item 3 (follow receives through message.receive) needed no code change. The loop already did, and test 2 proves it for the tester. 3. thread.send returns no `waiting` field, so the tester-target-not-running line doesn't apply to `--thread`. ## Runs ### Run 1, on 841c56d05 Slot sage-1211, after my row 178 r2 review. Busy check CLEAR at 05:19:37Z; the run went 2026-10-20T05:19:37.901Z to 05:24:06.168Z. The summary is in run-841c56d05.txt. After each mutant, cli.mjs and thread.mjs were restored and their sha256 checked. No process was left in the run directory. | Run | Expect | Result | |---|---|---| | red-base: base cli.mjs and thread.mjs with the new test file | fail | 1/5, the four tester tests fail | | tester-threads.test.mjs | pass | 5/5 | | thread-cli.test.mjs | pass | 11/11 | | tester-reads.test.mjs | pass | 6/6 | | tester.test.mjs | pass | 8/8 | | follow.test.mjs | pass | 6/6 | | names.test.mjs | pass | 2/2 | | channel-cli.test.mjs | pass | 2/2 | | app.test.mjs | pass | 30/30 | | mark-read.test.mjs | pass | 6/6 | | cli.test.mjs | pass | 12/12 | | awk sum of the singles | pass | 88/88 | | cli package suite (`node --test tests/*.test.mjs` in packages/cli) | pass | 418/418 | | M1a: talk --thread drops `--tester` (sent as the human; brief mutant 1) | killed | 1/5 | | M1b: thread create/open/read drop `--tester` before callFor | killed | 2/5 | | M2: no thread filter on follow with `--tester` (brief mutant 2) | killed | 3/5 | | M3: the refusal put back | killed | 1/5 | | M4: thread flags without `--tester` | killed | 2/5 | The base passes test 4 (without `--tester`), as it should. M2 fails test 2 at :162, where follow prints "outside any thread" and "other answer". It also fails test 3 at :216: follow by full id on a human thread exits 0 and prints the waiting message. Without the filter no thread.messages call is made, so the broker never says `thread-not-owned`. ### Run 2, on 7fc98cf19 Slot sage-1223. Busy check CLEAR at 05:47:40Z; the run went 2026-10-20T05:47:40.864Z to 05:51:48.408Z. The summary is in run-7fc98cf19.txt. No process was left in the run directory, and the worktree matched the manifest after the run. | Run | Result | |---|---| | tester-threads.test.mjs | 5/5 | | thread-cli.test.mjs | 11/11 | | tester-reads.test.mjs | 6/6 | | names.test.mjs (reads USAGE) | 2/2 | | awk sum of the singles | 24/24 | | cli package suite | 420/420 | The suite count is run 1's 418 plus row 178's two T178 tests in stack-release.test.mjs. The mutants weren't rerun. The source and test files are byte-identical to run 1, and only docs/TOOLS.md changed, which no test reads. stack-release.test.mjs:436 names "TOOLS.md step 4" only inside an expected refusal string. Readers and pins, checked on 7fc98cf19. host.test.mjs:826 reads packages/cli/README.md to find the bus unit's EnvironmentFile line, which this row doesn't change, and it passes in both suite runs. names.test.mjs reads USAGE for the name and names lines only. No test or scripts/test-*.sh pins the base sha256 of any of the 7 paths.