Files
stack/agents/researcher/work/queue-99/r1/BUILD.md
T
jason.woltjeandClaude Opus 5.5 8ac8055c4d researcher: row 99 round 1 packet, text-mode inbox fallback, phase B green (#1593)
Built by a Haiku subagent (decision 129). cli 133/0, nine scripts green,
7 of 7 mutants killed. Manifest 5c979e49.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 09:02:24 -05:00

9.0 KiB

Row 99 (#1593) build, round 1

Phase A: built and packeted. No test has been run. The only check was node --check (a parse, no test code runs) on both edited files, which passed.

  • BASE: 4233534da82ac15119358722eb6182c73cfd75b1 (base.txt)
  • Owned paths changed: packages/cli/src/cli.mjs, packages/cli/tests/cli.test.mjs (files.txt). Nothing else in the worktree changed.
  • candidate-manifest.sha256 sha256 (of the file itself): 3c3f5629a792b4a052d6c2c6e1bb2d5184109dca7537c3c75aff5089e278f0ad

What changed and why

packages/cli/src/cli.mjs, the inbox branch of human(). The messages paging loop now runs inside a try. In text mode, when the error's code is unknown-verb, the branch prints formatInbox(decisions), a blank line, and messages: not available until the bus restarts, then returns, so the exit is 0. Any other error is rethrown unchanged. The --json branch returns before the loop, so it is unchanged.

The code check is e.code, the same pattern resolveDecision uses for outcome-unknown. humanTransport sets error.code from the bus error on stderr (transport.mjs:62-63). The stub in helpers.mjs sets error.code the same way. unknown-verb is the broker's code for an unrecognized verb (broker.mjs:574, :1019).

packages/cli/tests/cli.test.mjs: one import (formatInbox), four helpers (inboxStub, coded, unread, NO_MESSAGES_VERB), and four tests after the existing inbox test. All use a stub transport; none touch the real bus.

Choices I made

  1. Blank line before the unavailable line. The normal path puts a blank line between the decisions and the messages: line, because formatInbox ends with a newline and the normal path adds \nmessages:. The fallback does the same, so the two outputs match. The brief says "then the line", and this is the one place I read the layout. Removing the blank line is a one-character change if Sage wants it.
  2. A later page answers unknown-verb. This means the bus restarted between pages. The partial list from earlier pages is dropped, and only the fallback prints. A partial count printed as the full count would be wrong. Test T_D covers it; mutant M6 breaks it.

Acceptance points and their tests

Acceptance point Test (name as it appears in TAP)
unknown-verb from messages: decisions as formatInbox prints them, the line, exit 0 T_A: "text-mode inbox on a bus without the messages verb prints the decisions and the unavailable line, exit 0"
any other error from messages still fails, and prints nothing (codes unauthenticated, invalid-response, outcome-unknown) T_B: "text-mode inbox still fails on any other messages error, and prints nothing"
normal path: unread count and each unread message, paged with after, read messages left out T_C: "text-mode inbox prints the unread count and every unread message across pages"
choice 2: later page unknown-verb drops the partial list T_D: "a later page answered unknown-verb drops the partial list and prints only the unavailable line"
--json unchanged Existing test "inbox lists only decisions routed to the human..." (cli.test.mjs, first test), which asserts the --json output and the call order inbox, messages, inbox. Not changed.

Mutants (planned, not run)

All edits are to packages/cli/src/cli.mjs. Each old string occurs exactly once in the candidate (checked with grep -F -c). The runner in mutants.py applies each one, runs cli.test.mjs, and requires the named test to show not ok.

Mutant Before After Named test that must die
M1 remove the fallback if (e.code !== "unknown-verb") throw e; throw e; T_A
M2 widen to every error if (e.code !== "unknown-verb") throw e; if (false) throw e; T_B
M3 change the printed line messages: not available until the bus restarts messages: unavailable T_A
M4 drop the decisions in the fallback ${formatInbox(decisions)}\nmessages: not available \nmessages: not available T_A
M5 fallback exits nonzero return io.stdout.write(...fallback...); { io.stdout.write(...fallback...); throw new CliError("messages not available", 1); } T_A
M6 partial list printed as the count the fallback return io.stdout.write(...) return print({ decisions, messages }, ...), the normal text output T_D
M7 read messages counted as unread messages.push(...page.messages.filter(m => !m.read)); messages.push(...page.messages); T_C

A mutant counts as killed only when the run exits nonzero, does not time out, and the named test is not ok. A timeout counts as a failure, not a kill, as the brief requires. The runner gives each run its own process group, kills that group afterwards, and restores cli.mjs in a finally block, then checks the restored hash.

Planned phase B test commands

Run sequentially, each with DOCKER_HOST=unix:///nonexistent.sock, output tee'd into out/, from the worktree root:

  1. node --test 'packages/cli/tests/*.test.mjs' (the cli node suite, as packages/cli/README.md gives it)
  2. Every scripts/test-*.sh: test-auth.sh, test-conductor.sh, test-config.sh, test-discord.sh, test-extension-package.sh, test-foundation.sh, test-queue.sh, test-release.sh, test-task.sh
  3. python3 -I mutants.py <worktree> <packet>/out. It runs the unmutated baseline first and stops if the baseline is not green.

Open questions for Sage

  1. Is the blank line before the messages: fallback line right, or should the line follow the decisions directly? (Choice 1.)
  2. Is dropping the partial list on a later-page unknown-verb right? The other option is to print the partial messages with a note that the list is incomplete. (Choice 2.)
  3. Scripts: the brief says scripts/test-*.sh. I listed the nine .sh files only. The .mjs and .py files named test-* are not in the gate list. Say if they are wanted.

Round 2: T_A correction (test-only)

Step 1 of phase B (out/step1-cli-suite.log, kept) ran T_A and it failed with task-required, not from the CLI code. T_A raised git.push.protected with blocking: true and no task_ref. The broker refuses a blocking decision without task_ref (packages/bus/src/broker.mjs:657), so the test never reached main().

Fix: packages/cli/tests/cli.test.mjs line 65 now passes task_ref: "vikunja:1/7", the same as the other blocking raises in that file. Nothing else changed. packages/cli/src/cli.mjs is unchanged, so the mutant guard hash still holds.

Hashes after the fix:

  • packages/cli/src/cli.mjs afcb9d9276c96e543cdc4dac1856b2dd6e973dfa2f1c68d179ddb5fc0e25b1ad (unchanged)
  • packages/cli/tests/cli.test.mjs c018997949b425e3274aaf9cc0877e5cd1419a1901e070ac84d114d788a53f7f (was 9b84d533374809c1ed1e3e748fb56a3351fdd4cf5d2b35f2f8ab5700631edac2)
  • candidate-manifest.sha256 (the file itself) 5c979e49d0b8433b7479fb8faf0c254012b4e76e7f39cb0ae0d4013995e4df60 (was 3c3f5629a792b4a052d6c2c6e1bb2d5184109dca7537c3c75aff5089e278f0ad)

Regenerated: build.patch (git diff against base.txt), candidate-manifest.sha256. The round-1 copies are not kept. The round-1 log is kept at out/step1-cli-suite.log. sha256sum -c passes.

Environment: node_modules is a symlink to the canonical checkout, per Sage. It is untracked and not part of the candidate. Remove the link before the worktree is removed.

Round 2: phase B results (candidate unchanged from the T_A fix)

  • Step 1, cli node suite (out/step1-cli-suite-r2.log): 133 pass, 0 fail, 0 cancelled, exit 0. The round-1 log is kept as out/step1-cli-suite.log.
  • Step 2, the nine suites: test-auth 15/0, test-conductor 17/0, test-config 24/0, test-discord 66/0, test-extension-package 18/0, test-foundation 44/0, test-queue 27/0 (1 skip: render check, this is a worktree, not the canonical root), test-release 14/0, test-task 26/0 (1 skip: live recall, docker daemon unavailable under DOCKER_HOST). All exit 0. Logs: out/test-*-r2.log.
  • Docker: test-release created agent-a37ace32df0dd168f_default, removed after it with docker network rm.
  • Step 3, mutants: baseline green. M1 to M7 killed (named killers in out/results.json).

Runner defect found and fixed (mutants.py only, no candidate change)

First mutant run (kept: out/mutants-run1/, mutants.py.run1 beside mutants.py): M6 reported NOT killed. Cause: its replacement text used join(\"\") inside a raw string, which produces invalid JavaScript (SyntaxError at cli.mjs:189). The module never loaded, so T_D never ran. The file-level not ok 1 - packages/cli/tests/cli.test.mjs was the only failure, and the runner correctly refused to count it. Fix: line 82 now uses join(''). The rest of the runner is unchanged.

Second run, after the fix: M1 to M7 all killed, baseline green, cli.mjs restored to afcb9d9276c96e543cdc4dac1856b2dd6e973dfa2f1c68d179ddb5fc0e25b1ad.

  • M1 killed by T_A, T_D
  • M2 killed by T_B
  • M3 killed by T_A, T_D
  • M4 killed by T_A, T_D
  • M5 killed by T_A, T_D
  • M6 killed by T_A, T_D (T_A fails because the fallback text changed)
  • M7 killed by T_C