diff --git a/agents/researcher/work/queue-109/build.md b/agents/researcher/work/queue-109/build.md new file mode 100644 index 000000000..0f762619c --- /dev/null +++ b/agents/researcher/work/queue-109/build.md @@ -0,0 +1,74 @@ +# Row 109 (#1603) build notes + +Researcher, 2026-10-13. Brief: `docs/plans/2026-10-13_pm-tester-and-fresh-state.md`, "A new PM run reads current launch state and task status before it plans". BASE: `02fe2d9d5` (origin/refactor after row 106 landed as `8b58cb7e4`). Reviewer: Filbert. Gate owner: Sage. + +Candidate change: `row109.patch` (8 files: 7 modified, 1 new). Built and run in `/mnt/storage/scratch/researcher/q109`, detached at `02fe2d9d5`, `node_modules` linked to the canonical one. The patch applies to BASE; the packet is not in it. + +## What ships + +- `packages/bus/src/broker.mjs`: read verb `state`, no arguments, open to role, human and reader sessions (reader allowlist gains `state`). It returns `{launch:{state,at,event}, tasks:{open:[{task_ref,title}], total, cap}}` for the capability's business. + - `launch` is the latest `launch.revoked` / `launch.restored` event of the business: `state` `revoked`, else `allowed`; `at` and `event` are that event's time and id, null when no event exists (`allowed`). + - `tasks.open` is `#tasks(business,{done:false})`, so it is the tracker snapshot's open tasks, tombstones left out, in `task_ref` order, refs and titles only. The cap is the `tasks` page size (`TASK_PAGE`, 50), stated as `cap`; `total` counts every open task. + - Nothing is stored. No schema change. +- `packages/harness/src/runner.mjs`: before each turn the runner calls `state` and prefixes the turn request with `stateBlock(state, readAt)`: `Current state, read at :`, `Launches: ...`, `Open tasks: N[, the first K of cap C].`, one line per task. The read time is the runner's clock at the call. A failed read (anything but a dead session) fails that turn without running the adapter: the sender gets the usual RESULT `The turn failed: could not read the current state: `, the failure is logged, and `message.read` still runs. A dead session (`SESSION_GONE`) exits as it does on `message.receive`. +- `turnRequest()`: the header gains `sent ` from the message row's `at`, after the sender and before the class. +- `roles/pm.md`: one rule in Protocol: the block is the truth for launch state and task status; a message or a note that disagrees is out of date; before planning to resume or launch work on a task, check it with `get_task`. +- READMEs: one table row in `packages/bus/README.md`, one paragraph in `packages/harness/README.md`. +- `packages/harness/src/tools.mjs` is untouched: `state` is not exposed as a tool; only the runner calls it. + +## Fixture edit (accepted by Sage, rows 109 scope) + +`packages/harness/tests/fixtures/fake-adapter.mjs` took the message body as everything after the first blank line of the request. The state block adds a blank line above the `Message ...:` header, which broke every TOOL/SLEEP/FAIL/NOMARKER test. It now takes the body after the `Message ...:` header line (regex), with the old split as fallback. This is the only reason for the edit; no other harness test needed a change. Every other test using the fixture passes in the full harness suite (79/79). + +## Tests + +- `packages/bus/tests/state.test.mjs` (new, 6): never switched reads `allowed` with null time and event; revoked then restored reads `allowed` with the restore event's time and id (and `revoked` with the revoke id in between); a task closed on the tracker leaves the open list and `total` drops; a tombstone is not open; the list stops at the cap and says so while `total` counts all; role and reader sessions read the same state, an argument refuses `invalid-request`, an unbound cap refuses `unauthenticated`. +- `packages/harness/tests/runner.test.mjs` (6 new tests, 20 in the file; harness suite 73 -> 79). By name: + 1. `turnRequest shows the message's own sent time, a day old as well as new` + 2. `stateBlock names the launch state with its time and event, and the open tasks with the cap` + 3. `launches revoked and then restored: the turn's block reads allowed and gives the restore time` + 4. `a task closed on the tracker is missing from the next turn's open list` + 5. `a message read a day after it was sent shows the time it was sent` (the broker's clock jumps a day after the send) + 6. `a failed state read fails the turn: no adapter runs, the sender hears why, the message is read` (no adapter turn file, RESULT with the reason, message read, log line) + + My message to Sage before the gate run said five new runner tests. That was a miscount: it left out test 2, the `stateBlock` unit test, which I wrote as a unit test beside test 1. The suite count (+6) is the evidence. The existing first end-to-end test, `a message becomes a turn, the answer goes back as a RESULT, ...`, is changed, not new: it now checks the exact block and the header with `sent `. +- The state block and the time are checked by the runner tests through the fake adapter, which echoes the request. + +## Mutants (the kills) + +`mutants.sh ` runs one test file for the control and one per mutant, each mutant one exact-once text edit, and restores every source on exit. Outputs in `mutants/`; run summary in `mutants-run.txt`. `mutants/.patch` is `git diff` of that file against BASE, so it also shows the candidate's own edits; the mutation is the one changed expression. The candidate was byte-identical to `row109.patch` after the run. + +| mutant | edit | result | failing tests | +|---|---|---|---| +| control | none | runner 20/20, bus 6/6 | none | +| m1 | request is `turnRequest(m)`, no block | runner 17/20 | the first end-to-end test; "launches revoked and then restored"; "a task closed on the tracker is missing" | +| m2 | header drops `sent ` | runner 17/20 | the first end-to-end test; "a message read a day after it was sent"; "turnRequest shows the message's own sent time" | +| m3 | a failed read sets `block = ""` instead of failing the turn | runner 19/20 | "a failed state read fails the turn" | +| m4 | launch event read oldest-first | bus 5/6 | "launches revoked and then restored read allowed, with the restore time and event id" | +| m5 | open list built without the `done:false` filter | bus 5/6 | "a task closed on the tracker leaves the open list" | +| m6 | `cap` dropped from the output | bus 4/6 | the closed-task test and the cap test | + +The `roles/pm.md` rule has no test; it is prose. Out of scope as the brief says: expiring old messages, tracker poller or `list_tasks` defaults, and who may switch launches. + +## Suites (all in q109, output teed to `suites/`) + +BASE `02fe2d9d5`; slot held 23:04:51Z to 23:08:36Z, "clear" sent; no process of mine left in q109. + +| suite | result | Sage's gate count | +|---|---|---| +| `node --test packages/bus/tests/*.test.mjs` | 134 pass, 0 fail | 128 (+6 new) | +| `node --test packages/harness/tests/*.test.mjs` | 79 pass, 0 fail | 73 (+6 new) | +| `node --test packages/queue/tests/*.test.mjs` | 156 pass, 0 fail | 156 | +| `scripts/test-release.sh` | 14 passed, 0 failed | 14 | +| `test-auth` 15, `test-conductor` 17, `test-config` 24, `test-discord` 66, `test-extension-package` 18, `test-foundation` 44, `test-task` 98 | 0 failed each | n/a | +| `scripts/test-queue.sh` | 28 passed, 0 failed | n/a | + +The `queue verify and render --check` case of `test-queue.sh` is skipped in a worktree (q109 is not the queue's canonical root `/mnt/storage/src/mosaic-stack`; Filbert's N3 from row 101). Sage's canonical-root gate rerun is the evidence of record. I did not apply the patch to or run suites in the canonical checkout. + +## PM workspace (read-only) + +`pm-workspace-names.txt`: the PM's persistent workspace holds one planning-notes file, `findings-2026-10-12.md` (7737 bytes, 2026-10-12T14:53Z), and `stack/`, a git checkout of the repository. Names only; no contents read. The note is a likely second source of the stale picture alongside old inbox messages; the pm.md rule covers it. The file is left alone. + +## Scope and live + +Owned files only, plus the accepted fixture. Live use needs a new copy and re-pin under Mos's go, with decision 109 notices before and after (runner.mjs reaches new role sessions at their next start; the broker verb needs a bus restart). diff --git a/agents/researcher/work/queue-109/candidate-manifest.sha256 b/agents/researcher/work/queue-109/candidate-manifest.sha256 new file mode 100644 index 000000000..15e7337e2 --- /dev/null +++ b/agents/researcher/work/queue-109/candidate-manifest.sha256 @@ -0,0 +1,40 @@ +62bce061032623636835100f60b3ab29d2d6399603c0c89100cfec4eaf98ee28 agents/researcher/work/queue-109/build.md +1a92b106886608a836d2b4683d5d0f4cfa7c6ea7a6c0eff0e0d2527ef60b6413 agents/researcher/work/queue-109/mutants/control-bus.out +9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa agents/researcher/work/queue-109/mutants/control-bus.rc +23883a50bb236ccbfdeb123097f29c4db25e8322c3b68549891fa825fc5149ab agents/researcher/work/queue-109/mutants/control-runner.out +9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa agents/researcher/work/queue-109/mutants/control-runner.rc +f48ac9ad7b681fbfa417604338d6cd320259e746d097b98bf7cf64c85544ac17 agents/researcher/work/queue-109/mutants/m1-drop-block.out +55662e82c005a5042cb09bed61ab28d088cf424871d0ec6200bf0d56aaa3d11b agents/researcher/work/queue-109/mutants/m1-drop-block.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m1-drop-block.rc +fea2ebac6cedf9a2018a018454ae3ac16f744b5bec3c4ee2e031d03ffc7912aa agents/researcher/work/queue-109/mutants/m2-drop-time.out +f4598e1f4f1182dfdc5de727acb53f84632d81c16f0f52adff4262a4c73f5a08 agents/researcher/work/queue-109/mutants/m2-drop-time.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m2-drop-time.rc +9a1ef422b9c1163cb95f58e481090d700bc077589f9d651d1ef822189c4f7cb6 agents/researcher/work/queue-109/mutants/m3-drop-failure-check.out +b30ee17aa4144cb6f4f2c64ea09a0eff00a2ba7a09f64ebb9a6c59a6ca06523a agents/researcher/work/queue-109/mutants/m3-drop-failure-check.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m3-drop-failure-check.rc +d65461b462514360d80d7e1677b0f6dc1223041d1bb19f867f4d253e4a1ab603 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.out +ced0a5f192f1302bffb5d73608dbc3dddae4ee0c117ad92a8513d92c37c09ce7 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.rc +b7aeeda30f679b5bcb2d0132cdf32f90e15a092ca5cc618090b07a62e8ff3d6a agents/researcher/work/queue-109/mutants/m5-closed-in-open.out +758218267fd42cba65d4cb39a03dbd45b0cc54dbb8191499a29f1ec3b067dcdf agents/researcher/work/queue-109/mutants/m5-closed-in-open.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m5-closed-in-open.rc +b4b2bec5b1ca6acf5f830c6eb3aeba6a74a1964168749c04d45ad673b15d9503 agents/researcher/work/queue-109/mutants/m6-cap-unstated.out +f047e709535a10ba427eb2a6585c7dde3c7ea16b9934a295450a6d8eea5db815 agents/researcher/work/queue-109/mutants/m6-cap-unstated.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m6-cap-unstated.rc +c3ff3575420cebbb57aa665592c17c2e7cc49da90f9800f0d49283f58c129863 agents/researcher/work/queue-109/mutants-run.txt +34ba363fba2e5b9384a8662e2402bc1abcb6a796ad718b01571b218c2018f797 agents/researcher/work/queue-109/mutants.sh +b0ef977027ad0a39b69347dc2d3f20765d4d8e1d57d783b03c1cc653b96e5fdb agents/researcher/work/queue-109/pm-workspace-names.txt +55fcdf13135818abde7157f78f4f9afbfcedc292d95fe02109908392f69fc4c7 agents/researcher/work/queue-109/row109.patch +bc33dae0ca339fb80f8ed5a0de7d84ea73df1f8f684f5001eed10dcb1abad9ec agents/researcher/work/queue-109/suites/bus-node-suite.out +d5731e9121956e7629a2e8040b4ee329f74996a680707ed3a9ae8193568df775 agents/researcher/work/queue-109/suites/harness-node-suite.out +b996f93d6f321014cd4e9ef327b6004bf8c525accf4e7c74cf3ca6ff3b145dd7 agents/researcher/work/queue-109/suites/queue-node-suite.out +c7ed4247eb9916e67bf5a2dd86687110fbf3e0eaaaa234b31b551d26497c5856 agents/researcher/work/queue-109/suites.sh +f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 agents/researcher/work/queue-109/suites/test-auth.out +e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 agents/researcher/work/queue-109/suites/test-conductor.out +52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 agents/researcher/work/queue-109/suites/test-config.out +7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe agents/researcher/work/queue-109/suites/test-discord.out +bd12416272ae0a9d9d78e3a340663c68a8c7423a2c7ed0ad2c2558ec97d8be90 agents/researcher/work/queue-109/suites/test-extension-package.out +83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f agents/researcher/work/queue-109/suites/test-foundation.out +4636814ca4819fa843fe9abf6285bb6e32a295997221252269cc4bf112f1aabf agents/researcher/work/queue-109/suites/test-queue.out +fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 agents/researcher/work/queue-109/suites/test-release.out +ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a agents/researcher/work/queue-109/suites/test-task.out diff --git a/agents/researcher/work/queue-109/mutants-run.txt b/agents/researcher/work/queue-109/mutants-run.txt new file mode 100644 index 000000000..d91a7caa3 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants-run.txt @@ -0,0 +1,25 @@ +control-runner: exit 0; ℹ tests 20 ℹ pass 20 ℹ fail 0 +control-bus: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0 +m1-drop-block: exit 1; ℹ tests 20 ℹ pass 17 ℹ fail 3 + failed: ✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 + failed: ✖ a task closed on the tracker is missing from the next turn's open list + failed: ✖ failing tests: + failed: ✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time +m2-drop-time: exit 1; ℹ tests 20 ℹ pass 17 ℹ fail 3 + failed: ✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 + failed: ✖ a message read a day after it was sent shows the time it was sent + failed: ✖ failing tests: + failed: ✖ turnRequest shows the message's own sent time, a day old as well as new +m3-drop-failure-check: exit 1; ℹ tests 20 ℹ pass 19 ℹ fail 1 + failed: ✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read + failed: ✖ failing tests: +m4-oldest-launch-event: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1 + failed: ✖ failing tests: + failed: ✖ launches revoked and then restored read allowed, with the restore time and event id +m5-closed-in-open: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1 + failed: ✖ a task closed on the tracker leaves the open list, and total counts the open ones + failed: ✖ failing tests: +m6-cap-unstated: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2 + failed: ✖ a task closed on the tracker leaves the open list, and total counts the open ones + failed: ✖ failing tests: + failed: ✖ the open list stops at the cap and says so; total still counts every open task diff --git a/agents/researcher/work/queue-109/mutants.sh b/agents/researcher/work/queue-109/mutants.sh new file mode 100755 index 000000000..6b962659f --- /dev/null +++ b/agents/researcher/work/queue-109/mutants.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Row 109 (#1603): each new check must fail on a mutant. For each mutant this runs the named test +# file against the real source (control) and against the source with one exact-once text edit. +# Runs in the given worktree, one test file at a time, and restores every source file on exit. +# Usage: mutants.sh +set -u +WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd) +cd "$WT" || exit 2 +BAK=$(mktemp -d "$OUT/.bak.XXXXXX") +declare -A SAVED +save() { [ -n "${SAVED[$1]:-}" ] || { mkdir -p "$BAK/$(dirname "$1")"; cp "$1" "$BAK/$1"; SAVED[$1]=1; }; } +restore() { for f in "${!SAVED[@]}"; do cp "$BAK/$f" "$f"; done; rm -rf "$BAK"; } +trap restore EXIT +edit() { # file old new: exact-once replace + save "$1" + node -e ' +const fs = require("fs"); const [f, a, b] = process.argv.slice(1); +const s = fs.readFileSync(f, "utf8"); const n = s.split(a).length - 1; +if (n !== 1) { console.error("expected one match, found " + n); process.exit(3); } +fs.writeFileSync(f, s.replace(a, () => b)); +' "$1" "$2" "$3" +} +run() { # name testfile + setsid -w timeout -k 10 240 node --test "$2" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc" + echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')" + grep -E '^✖' "$OUT/$1.out" | sort -u | sed 's/ ([0-9.]*ms)$//' | sed "s/^/ failed: /" +} +mutant() { # name testfile file old new + edit "$3" "$4" "$5" || { echo "$1: edit failed"; return; } + git diff -- "$3" > "$OUT/$1.patch" + run "$1" "$2" + cp "$BAK/$3" "$3" +} +R=packages/harness/src/runner.mjs; RT=packages/harness/tests/runner.test.mjs +B=packages/bus/src/broker.mjs; BT=packages/bus/tests/state.test.mjs +run control-runner "$RT" +run control-bus "$BT" +# m1: the turn request no longer carries the state block +mutant m1-drop-block "$RT" "$R" 'request: `${block}\n${turnRequest(m)}`' 'request: turnRequest(m)' +# m2: the header no longer carries the message time +mutant m2-drop-time "$RT" "$R" ' if (m.at) head.push(`sent ${m.at}`); +' '' +# m3: a failed state read no longer fails the turn; the turn runs with no block +mutant m3-drop-failure-check "$RT" "$R" 'unread = `could not read the current state: ${e.code ?? e.message}`;' 'block = "";' +# m4: the block's launch line shows the older event instead of the latest +mutant m4-oldest-launch-event "$BT" "$B" "kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1" "kind IN ('launch.revoked','launch.restored') ORDER BY seq ASC LIMIT 1" +# m5: the open list includes closed tasks +mutant m5-closed-in-open "$BT" "$B" 'const open = this.#tasks(s.business, { done: false });' 'const open = this.#tasks(s.business, { limit: TASK_PAGE });' +# m6: the cap isn't stated in the output +mutant m6-cap-unstated "$BT" "$B" 'total: open.total, cap: TASK_PAGE }' 'total: open.total }' diff --git a/agents/researcher/work/queue-109/mutants/control-bus.out b/agents/researcher/work/queue-109/mutants/control-bus.out new file mode 100644 index 000000000..f9c5aebae --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/control-bus.out @@ -0,0 +1,14 @@ +✔ a business that never switched launches reads allowed with no time and no event (12.740463ms) +✔ launches revoked and then restored read allowed, with the restore time and event id (10.115322ms) +✔ a task closed on the tracker leaves the open list, and total counts the open ones (10.612196ms) +✔ a tombstone is not open (7.867781ms) +✔ the open list stops at the cap and says so; total still counts every open task (12.014832ms) +✔ a role and a reader session read state; arguments, and an unbound cap, refuse (9.286701ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 6 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 117.997122 diff --git a/agents/researcher/work/queue-109/mutants/control-bus.rc b/agents/researcher/work/queue-109/mutants/control-bus.rc new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/control-bus.rc @@ -0,0 +1 @@ +0 diff --git a/agents/researcher/work/queue-109/mutants/control-runner.out b/agents/researcher/work/queue-109/mutants/control-runner.out new file mode 100644 index 000000000..dafefe99a --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/control-runner.out @@ -0,0 +1,28 @@ +✔ founderCheck: founder variables, then a needed service without a usable token (1.105369ms) +✔ turnRequest names the sender, class, reply and decision (0.21491ms) +✔ turnRequest shows the message's own sent time, a day old as well as new (0.126433ms) +✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.195396ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (152.361989ms) +✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (169.022648ms) +✔ a task closed on the tracker is missing from the next turn's open list (167.718931ms) +✔ a message read a day after it was sent shows the time it was sent (87.662171ms) +✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.771972ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (48.50038ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (271.762253ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1192.335593ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (93.322815ms) +✔ founder credentials stop before the claim (20) (96.496233ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (100.411332ms) +✔ the launch ending under a running session exits 22 (102.903092ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (204.000168ms) +✔ a broker that is down at the claim exits 23, not 21 (47.277428ms) +✔ no capability, or a malformed one, on stdin exits 2 (151.489917ms) +✔ a missing or malformed policy exits 2 before the claim (87.115027ms) +ℹ tests 20 +ℹ suites 0 +ℹ pass 20 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10345.427283 diff --git a/agents/researcher/work/queue-109/mutants/control-runner.rc b/agents/researcher/work/queue-109/mutants/control-runner.rc new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/control-runner.rc @@ -0,0 +1 @@ +0 diff --git a/agents/researcher/work/queue-109/mutants/m1-drop-block.out b/agents/researcher/work/queue-109/mutants/m1-drop-block.out new file mode 100644 index 000000000..67c2b7e50 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m1-drop-block.out @@ -0,0 +1,87 @@ +✔ founderCheck: founder variables, then a needed service without a usable token (0.927996ms) +✔ turnRequest names the sender, class, reply and decision (0.187137ms) +✔ turnRequest shows the message's own sent time, a day old as well as new (0.121749ms) +✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.176786ms) +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (147.584047ms) +✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time (91.206024ms) +✖ a task closed on the tracker is missing from the next turn's open list (90.236176ms) +✔ a message read a day after it was sent shows the time it was sent (89.018287ms) +✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.256302ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (48.492078ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (247.488656ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1191.072755ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (93.718876ms) +✔ founder credentials stop before the claim (20) (94.802529ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (98.777833ms) +✔ the launch ending under a running session exits 22 (102.953767ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (206.247136ms) +✔ a broker that is down at the claim exits 23, not 21 (45.3418ms) +✔ no capability, or a malformed one, on stdin exits 2 (156.779329ms) +✔ a missing or malformed policy exits 2 before the claim (83.562291ms) +ℹ tests 20 +ℹ suites 0 +ℹ pass 17 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10156.567034 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:174:1 +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (147.584047ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello$/. Input: + + 'echo: Message 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n' + + '\n' + + 'hello' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:186:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'echo: Message 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello', + expected: /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello$/, + operator: 'match', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:217:1 +✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time (91.206024ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\nLaunches: revoked since \S+ \(event \S+\)\.\n/. Input: + + 'echo: Message 4fe85a96-a724-4368-a91f-9a7c6dfb1888 from pm, sent 2026-10-13T23:07:58.825Z, class REQUEST:\n' + + '\n' + + 'what now' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:223:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'echo: Message 4fe85a96-a724-4368-a91f-9a7c6dfb1888 from pm, sent 2026-10-13T23:07:58.825Z, class REQUEST:\n\nwhat now', + expected: /\nLaunches: revoked since \S+ \(event \S+\)\.\n/, + operator: 'match', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:229:1 +✖ a task closed on the tracker is missing from the next turn's open list (90.236176ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\nOpen tasks: 2\.\n- vikunja:3\/1 Task 1\n- vikunja:3\/2 Task 2\n/. Input: + + 'echo: Message a189d6e4-745d-49d2-9ab6-60e13bf1b5e6 from pm, sent 2026-10-13T23:07:58.914Z, class REQUEST:\n' + + '\n' + + 'what now' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:236:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'echo: Message a189d6e4-745d-49d2-9ab6-60e13bf1b5e6 from pm, sent 2026-10-13T23:07:58.914Z, class REQUEST:\n\nwhat now', + expected: /\nOpen tasks: 2\.\n- vikunja:3\/1 Task 1\n- vikunja:3\/2 Task 2\n/, + operator: 'match', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m1-drop-block.patch b/agents/researcher/work/queue-109/mutants/m1-drop-block.patch new file mode 100644 index 000000000..8efee812a --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m1-drop-block.patch @@ -0,0 +1,64 @@ +diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs +index 00595542b..87d66dd36 100644 +--- a/packages/harness/src/runner.mjs ++++ b/packages/harness/src/runner.mjs +@@ -62,7 +62,10 @@ export function founderCheck(policy, env) { + + // The request text one message becomes. + export function turnRequest(m) { +- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`]; ++ const head = [`Message ${m.id} from ${m.from_role}`]; ++ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old. ++ if (m.at) head.push(`sent ${m.at}`); ++ head.push(`class ${m.class}`); + if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`); + if (m.decision) head.push(`citing decision ${m.decision}`); + // Row 90 (#1584): the human input id task_create cites as its request. +@@ -70,6 +73,22 @@ export function turnRequest(m) { + return `${head.join(", ")}:\n\n${m.body}`; + } + ++// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`, ++// the truth for launch state and task status over any older message. ++export function stateBlock(state, readAt) { ++ const { launch, tasks } = state; ++ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)"; ++ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : ""; ++ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd()); ++ return [ ++ `Current state, read at ${readAt}:`, ++ `Launches: ${launch.state}${since}.`, ++ `Open tasks: ${tasks.total}${shown}.`, ++ ...open, ++ "", ++ ].join("\n"); ++} ++ + function clip(text) { + return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text; + } +@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l + } + for (const m of messages) { + if (stopping) break loop; +- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) }); ++ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn. ++ let block = null; ++ let unread = null; ++ try { ++ block = stateBlock(await client.call("state"), new Date().toISOString()); ++ } catch (e) { ++ if (SESSION_GONE.has(e.code)) { ++ log(`runner: session stopped working: ${e.code}`); ++ code = EXIT.session; ++ break loop; ++ } ++ unread = `could not read the current state: ${e.code ?? e.message}`; ++ } ++ ++n; ++ const turn = unread ++ ? { ok: false, reason: unread } ++ : await runTurn({ session, request: turnRequest(m), n, env: turnEnv, onChild: (c) => (child = c) }); + if (stopping) break loop; + if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`); + try { diff --git a/agents/researcher/work/queue-109/mutants/m1-drop-block.rc b/agents/researcher/work/queue-109/mutants/m1-drop-block.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m1-drop-block.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/mutants/m2-drop-time.out b/agents/researcher/work/queue-109/mutants/m2-drop-time.out new file mode 100644 index 000000000..c7c442904 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m2-drop-time.out @@ -0,0 +1,93 @@ +✔ founderCheck: founder variables, then a needed service without a usable token (0.987417ms) +✔ turnRequest names the sender, class, reply and decision (0.198796ms) +✖ turnRequest shows the message's own sent time, a day old as well as new (0.813441ms) +✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.188045ms) +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (145.435504ms) +✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (169.165152ms) +✔ a task closed on the tracker is missing from the next turn's open list (168.933118ms) +✖ a message read a day after it was sent shows the time it was sent (90.269575ms) +✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.164189ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (50.491583ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (272.210562ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1192.639985ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (94.793583ms) +✔ founder credentials stop before the claim (20) (93.850655ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (99.075117ms) +✔ the launch ending under a running session exits 22 (101.888229ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (204.216765ms) +✔ a broker that is down at the claim exits 23, not 21 (49.593739ms) +✔ no capability, or a malformed one, on stdin exits 2 (156.481136ms) +✔ a missing or malformed policy exits 2 before the claim (91.206713ms) +ℹ tests 20 +ℹ suites 0 +ℹ pass 17 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10339.878414 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:60:1 +✖ turnRequest shows the message's own sent time, a day old as well as new (0.813441ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + 'Message m5 from jason, class REQUEST:\n\nresume' + - 'Message m5 from jason, sent 2026-10-12T09:30:00.000Z, class REQUEST:\n\nresume' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:62: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'Message m5 from jason, class REQUEST:\n\nresume', + expected: 'Message m5 from jason, sent 2026-10-12T09:30:00.000Z, class REQUEST:\n\nresume', + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:174:1 +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (145.435504ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, sent 2026-10-13T23:08:08.949Z, class REQUEST:\n\nhello$/. Input: + + 'echo: Current state, read at 2026-10-13T23:08:08.986Z:\n' + + 'Launches: allowed (never switched).\n' + + 'Open tasks: 0.\n' + + '\n' + + 'Message 2b684cd9-8412-4498-9700-651ca8840067 from pm, class REQUEST:\n' + + '\n' + + 'hello' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:186:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'echo: Current state, read at 2026-10-13T23:08:08.986Z:\nLaunches: allowed (never switched).\nOpen tasks: 0.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, class REQUEST:\n\nhello', + expected: /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, sent 2026-10-13T23:08:08.949Z, class REQUEST:\n\nhello$/, + operator: 'match', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:243:1 +✖ a message read a day after it was sent shows the time it was sent (90.269575ms) + AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: + + assert.ok(reply.body.includes(`Message ${sent.id} from pm, sent ${sentAt}, class REQUEST:`)) + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:255:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m2-drop-time.patch b/agents/researcher/work/queue-109/mutants/m2-drop-time.patch new file mode 100644 index 000000000..3276c081f --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m2-drop-time.patch @@ -0,0 +1,63 @@ +diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs +index 00595542b..57f4b141a 100644 +--- a/packages/harness/src/runner.mjs ++++ b/packages/harness/src/runner.mjs +@@ -62,7 +62,9 @@ export function founderCheck(policy, env) { + + // The request text one message becomes. + export function turnRequest(m) { +- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`]; ++ const head = [`Message ${m.id} from ${m.from_role}`]; ++ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old. ++ head.push(`class ${m.class}`); + if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`); + if (m.decision) head.push(`citing decision ${m.decision}`); + // Row 90 (#1584): the human input id task_create cites as its request. +@@ -70,6 +72,22 @@ export function turnRequest(m) { + return `${head.join(", ")}:\n\n${m.body}`; + } + ++// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`, ++// the truth for launch state and task status over any older message. ++export function stateBlock(state, readAt) { ++ const { launch, tasks } = state; ++ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)"; ++ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : ""; ++ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd()); ++ return [ ++ `Current state, read at ${readAt}:`, ++ `Launches: ${launch.state}${since}.`, ++ `Open tasks: ${tasks.total}${shown}.`, ++ ...open, ++ "", ++ ].join("\n"); ++} ++ + function clip(text) { + return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text; + } +@@ -339,7 +357,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l + } + for (const m of messages) { + if (stopping) break loop; +- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) }); ++ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn. ++ let block = null; ++ let unread = null; ++ try { ++ block = stateBlock(await client.call("state"), new Date().toISOString()); ++ } catch (e) { ++ if (SESSION_GONE.has(e.code)) { ++ log(`runner: session stopped working: ${e.code}`); ++ code = EXIT.session; ++ break loop; ++ } ++ unread = `could not read the current state: ${e.code ?? e.message}`; ++ } ++ ++n; ++ const turn = unread ++ ? { ok: false, reason: unread } ++ : await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) }); + if (stopping) break loop; + if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`); + try { diff --git a/agents/researcher/work/queue-109/mutants/m2-drop-time.rc b/agents/researcher/work/queue-109/mutants/m2-drop-time.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m2-drop-time.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.out b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.out new file mode 100644 index 000000000..411aa7bf1 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.out @@ -0,0 +1,52 @@ +✔ founderCheck: founder variables, then a needed service without a usable token (1.027678ms) +✔ turnRequest names the sender, class, reply and decision (0.175568ms) +✔ turnRequest shows the message's own sent time, a day old as well as new (0.108091ms) +✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.164473ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (151.370525ms) +✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (168.631533ms) +✔ a task closed on the tracker is missing from the next turn's open list (193.462068ms) +✔ a message read a day after it was sent shows the time it was sent (88.274542ms) +✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (89.644081ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (50.931607ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (273.817356ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1189.440166ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (95.175856ms) +✔ founder credentials stop before the claim (20) (89.610808ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (96.280534ms) +✔ the launch ending under a running session exits 22 (100.621249ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (202.570602ms) +✔ a broker that is down at the claim exits 23, not 21 (49.247418ms) +✔ no capability, or a malformed one, on stdin exits 2 (161.38161ms) +✔ a missing or malformed policy exits 2 before the claim (86.000718ms) +ℹ tests 20 +ℹ suites 0 +ℹ pass 19 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10397.655899 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:259:1 +✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (89.644081ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + 'echo: \n' + + + 'Message 586075ac-c0cc-4abf-9f4f-2d1289199157 from pm, sent 2026-10-13T23:08:19.896Z, class REQUEST:\n' + + + '\n' + + + 'plan the week' + - 'The turn failed: could not read the current state: state-unavailable' + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:273:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'echo: \nMessage 586075ac-c0cc-4abf-9f4f-2d1289199157 from pm, sent 2026-10-13T23:08:19.896Z, class REQUEST:\n\nplan the week', + expected: 'The turn failed: could not read the current state: state-unavailable', + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.patch b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.patch new file mode 100644 index 000000000..79398c41f --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.patch @@ -0,0 +1,64 @@ +diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs +index 00595542b..c56e9fc38 100644 +--- a/packages/harness/src/runner.mjs ++++ b/packages/harness/src/runner.mjs +@@ -62,7 +62,10 @@ export function founderCheck(policy, env) { + + // The request text one message becomes. + export function turnRequest(m) { +- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`]; ++ const head = [`Message ${m.id} from ${m.from_role}`]; ++ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old. ++ if (m.at) head.push(`sent ${m.at}`); ++ head.push(`class ${m.class}`); + if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`); + if (m.decision) head.push(`citing decision ${m.decision}`); + // Row 90 (#1584): the human input id task_create cites as its request. +@@ -70,6 +73,22 @@ export function turnRequest(m) { + return `${head.join(", ")}:\n\n${m.body}`; + } + ++// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`, ++// the truth for launch state and task status over any older message. ++export function stateBlock(state, readAt) { ++ const { launch, tasks } = state; ++ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)"; ++ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : ""; ++ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd()); ++ return [ ++ `Current state, read at ${readAt}:`, ++ `Launches: ${launch.state}${since}.`, ++ `Open tasks: ${tasks.total}${shown}.`, ++ ...open, ++ "", ++ ].join("\n"); ++} ++ + function clip(text) { + return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text; + } +@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l + } + for (const m of messages) { + if (stopping) break loop; +- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) }); ++ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn. ++ let block = null; ++ let unread = null; ++ try { ++ block = stateBlock(await client.call("state"), new Date().toISOString()); ++ } catch (e) { ++ if (SESSION_GONE.has(e.code)) { ++ log(`runner: session stopped working: ${e.code}`); ++ code = EXIT.session; ++ break loop; ++ } ++ block = ""; ++ } ++ ++n; ++ const turn = unread ++ ? { ok: false, reason: unread } ++ : await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) }); + if (stopping) break loop; + if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`); + try { diff --git a/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.rc b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m3-drop-failure-check.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.out b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.out new file mode 100644 index 000000000..544e498d0 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.out @@ -0,0 +1,45 @@ +✔ a business that never switched launches reads allowed with no time and no event (11.927647ms) +✖ launches revoked and then restored read allowed, with the restore time and event id (10.621051ms) +✔ a task closed on the tracker leaves the open list, and total counts the open ones (9.618177ms) +✔ a tombstone is not open (7.741209ms) +✔ the open list stops at the cap and says so; total still counts every open task (11.030111ms) +✔ a role and a reader session read state; arguments, and an unbound cap, refuse (8.605084ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 5 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 112.662835 + +✖ failing tests: + +test at packages/bus/tests/state.test.mjs:42:1 +✖ launches revoked and then restored read allowed, with the restore time and event id (10.621051ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + { + + at: '2026-10-13T23:08:29.735Z', + + event: '03630f89-6d18-4f43-8ced-8f36a81f8fbc', + + state: 'revoked' + - at: '2026-10-13T23:08:29.737Z', + - event: 'f2c5df5c-6b5b-4f25-91ca-2137aa67912d', + - state: 'allowed' + } + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:51: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: { state: 'revoked', at: '2026-10-13T23:08:29.735Z', event: '03630f89-6d18-4f43-8ced-8f36a81f8fbc' }, + expected: { state: 'allowed', at: '2026-10-13T23:08:29.737Z', event: 'f2c5df5c-6b5b-4f25-91ca-2137aa67912d' }, + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.patch b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.patch new file mode 100644 index 000000000..a7708ff58 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.patch @@ -0,0 +1,37 @@ +diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs +index 732c7310e..054b7acff 100644 +--- a/packages/bus/src/broker.mjs ++++ b/packages/bus/src/broker.mjs +@@ -654,7 +654,7 @@ export class Broker { + if (!object(args)) fail('invalid-request'); + this.#secretCheck(args); + return this.#store.transaction(() => { +- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb)) ++ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb)) + fail('read-only'); + const result = this.#dispatch(s, request.verb, args); + this.#secretCheck(result); +@@ -1086,6 +1086,23 @@ export class Broker { + this.#agent(s); + return this.#requirements; + } ++ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with ++ // the time and id of the event that set it (`allowed`, no time and no id when none has), and ++ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them, ++ // `total` counting every open task. A read for role and reader sessions; nothing is stored. ++ case 'state': { ++ keys(a, []); ++ if (!s.human && !s.reader) this.#agent(s); ++ const set = this.#store.get( ++ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq ASC LIMIT 1", ++ s.business, ++ ); ++ const open = this.#tasks(s.business, { done: false }); ++ return { ++ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null }, ++ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE }, ++ }; ++ } + case 'inbox': + case 'agents': + case 'tasks': diff --git a/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.rc b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/mutants/m5-closed-in-open.out b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.out new file mode 100644 index 000000000..15520f11a --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.out @@ -0,0 +1,42 @@ +✔ a business that never switched launches reads allowed with no time and no event (11.379419ms) +✔ launches revoked and then restored read allowed, with the restore time and event id (9.235032ms) +✖ a task closed on the tracker leaves the open list, and total counts the open ones (10.543996ms) +✔ a tombstone is not open (8.500886ms) +✔ the open list stops at the cap and says so; total still counts every open task (11.900535ms) +✔ a role and a reader session read state; arguments, and an unbound cap, refuse (8.938047ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 5 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 117.78508 + +✖ failing tests: + +test at packages/bus/tests/state.test.mjs:55:1 +✖ a task closed on the tracker leaves the open list, and total counts the open ones (10.543996ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + [ + 'vikunja:3/1', + + 'vikunja:3/2', + 'vikunja:3/3' + ] + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:65: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: [ 'vikunja:3/1', 'vikunja:3/2', 'vikunja:3/3' ], + expected: [ 'vikunja:3/1', 'vikunja:3/3' ], + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m5-closed-in-open.patch b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.patch new file mode 100644 index 000000000..44d62e851 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.patch @@ -0,0 +1,37 @@ +diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs +index 732c7310e..138991706 100644 +--- a/packages/bus/src/broker.mjs ++++ b/packages/bus/src/broker.mjs +@@ -654,7 +654,7 @@ export class Broker { + if (!object(args)) fail('invalid-request'); + this.#secretCheck(args); + return this.#store.transaction(() => { +- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb)) ++ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb)) + fail('read-only'); + const result = this.#dispatch(s, request.verb, args); + this.#secretCheck(result); +@@ -1086,6 +1086,23 @@ export class Broker { + this.#agent(s); + return this.#requirements; + } ++ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with ++ // the time and id of the event that set it (`allowed`, no time and no id when none has), and ++ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them, ++ // `total` counting every open task. A read for role and reader sessions; nothing is stored. ++ case 'state': { ++ keys(a, []); ++ if (!s.human && !s.reader) this.#agent(s); ++ const set = this.#store.get( ++ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1", ++ s.business, ++ ); ++ const open = this.#tasks(s.business, { limit: TASK_PAGE }); ++ return { ++ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null }, ++ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE }, ++ }; ++ } + case 'inbox': + case 'agents': + case 'tasks': diff --git a/agents/researcher/work/queue-109/mutants/m5-closed-in-open.rc b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m5-closed-in-open.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/mutants/m6-cap-unstated.out b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.out new file mode 100644 index 000000000..d88ddeeb0 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.out @@ -0,0 +1,65 @@ +✔ a business that never switched launches reads allowed with no time and no event (11.422277ms) +✔ launches revoked and then restored read allowed, with the restore time and event id (8.798469ms) +✖ a task closed on the tracker leaves the open list, and total counts the open ones (9.437594ms) +✔ a tombstone is not open (7.259551ms) +✖ the open list stops at the cap and says so; total still counts every open task (10.864472ms) +✔ a role and a reader session read state; arguments, and an unbound cap, refuse (7.816773ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 4 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 110.771289 + +✖ failing tests: + +test at packages/bus/tests/state.test.mjs:55:1 +✖ a task closed on the tracker leaves the open list, and total counts the open ones (9.437594ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + { + - cap: 50, + open: [ + { + task_ref: 'vikunja:3/1', + title: 'Task 1' + }, + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:58: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: { open: [ [Object], [Object], [Object] ], total: 3 }, + expected: { open: [ [Object], [Object], [Object] ], total: 3, cap: 50 }, + operator: 'deepStrictEqual', + diff: 'simple' + } + +test at packages/bus/tests/state.test.mjs:75:1 +✖ the open list stops at the cap and says so; total still counts every open task (10.864472ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + undefined !== 50 + + at TestContext. (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:81: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: undefined, + expected: 50, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-109/mutants/m6-cap-unstated.patch b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.patch new file mode 100644 index 000000000..754e2d442 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.patch @@ -0,0 +1,37 @@ +diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs +index 732c7310e..6df80db83 100644 +--- a/packages/bus/src/broker.mjs ++++ b/packages/bus/src/broker.mjs +@@ -654,7 +654,7 @@ export class Broker { + if (!object(args)) fail('invalid-request'); + this.#secretCheck(args); + return this.#store.transaction(() => { +- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb)) ++ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb)) + fail('read-only'); + const result = this.#dispatch(s, request.verb, args); + this.#secretCheck(result); +@@ -1086,6 +1086,23 @@ export class Broker { + this.#agent(s); + return this.#requirements; + } ++ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with ++ // the time and id of the event that set it (`allowed`, no time and no id when none has), and ++ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them, ++ // `total` counting every open task. A read for role and reader sessions; nothing is stored. ++ case 'state': { ++ keys(a, []); ++ if (!s.human && !s.reader) this.#agent(s); ++ const set = this.#store.get( ++ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1", ++ s.business, ++ ); ++ const open = this.#tasks(s.business, { done: false }); ++ return { ++ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null }, ++ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total }, ++ }; ++ } + case 'inbox': + case 'agents': + case 'tasks': diff --git a/agents/researcher/work/queue-109/mutants/m6-cap-unstated.rc b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-109/mutants/m6-cap-unstated.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-109/pm-workspace-names.txt b/agents/researcher/work/queue-109/pm-workspace-names.txt new file mode 100644 index 000000000..dd85c33be --- /dev/null +++ b/agents/researcher/work/queue-109/pm-workspace-names.txt @@ -0,0 +1,12 @@ +Row 109 (#1603): the PM's persistent workspace, read-only, names/sizes/mtimes only (no contents read). +Checked 2026-10-13, before 22:54Z with ls and find -printf; nothing opened, nothing changed. +Path: /workspaces/mosaic-stack/pm (dataRoot /home/jwoltje/.mosaic-dev) + +Top level (2 entries): + f 7737 bytes 2026-10-12T14:53Z findings-2026-10-12.md <- planning notes from an earlier run + d 2026-10-13T16:48Z stack/ <- a git checkout of this repository (10356 entries in the workspace in all), not notes + +Finding: findings-2026-10-12.md is the only planning-notes file. It predates the current launch state and task +status, so it is a likely second source of the stale picture a PM run planned from, next to old inbox +messages. The roles/pm.md rule covers it ("a message or a note of yours that disagrees with the block is out +of date"). The file is left alone; this row doesn't expire, move or edit workspace files. diff --git a/agents/researcher/work/queue-109/row109.patch b/agents/researcher/work/queue-109/row109.patch new file mode 100644 index 000000000..17f5219ec --- /dev/null +++ b/agents/researcher/work/queue-109/row109.patch @@ -0,0 +1,405 @@ +diff --git a/packages/bus/README.md b/packages/bus/README.md +index 0cbbc8192..b0c469263 100644 +--- a/packages/bus/README.md ++++ b/packages/bus/README.md +@@ -157,6 +157,7 @@ an operator inspect content and delivery evidence after an uncertain result. + | `decisions` | `{list}`: `roles` or `recent`; human and reader sessions only. Open decisions routed to a role, or the last 50 closed. | + | `credentials` | No args; human and reader sessions only. `[{service,account,role,dateKind,date,state}]` for the capability's business, never a token, file path or variable name. | + | `requirements` | No args; role sessions only, as `task.create` is (row 90, #1584). The approved PRD's requirement ids, `{prd,version,requirements:[{id,title}]}`, from `packages/tasks/requirements.json`, loaded when the broker starts; a bad file refuses the start. A human session refuses `agent-required`, a reader `read-only`. | ++| `state` | No args; role, human and reader sessions (row 109, #1603). `{launch:{state,at,event},tasks:{open:[{task_ref,title}],total,cap}}` for the capability's business. `launch` is the latest `launch.revoked` or `launch.restored` event: `allowed` with `at` and `event` null when none exists. `open` is the tracker snapshot's open tasks in `task_ref` order, at most `cap` (the `tasks` page size, 50); `total` counts every open task. The harness runner reads it before each turn. Writes nothing. | + | `replies` | `{id}`: a message UUID; human and reader sessions only. The messages whose `in_reply_to` is that id, oldest first, at most 50. Writes nothing. | + + ### Agent state (row 59, #1548) +diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs +index 732c7310e..7565e82da 100644 +--- a/packages/bus/src/broker.mjs ++++ b/packages/bus/src/broker.mjs +@@ -654,7 +654,7 @@ export class Broker { + if (!object(args)) fail('invalid-request'); + this.#secretCheck(args); + return this.#store.transaction(() => { +- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb)) ++ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb)) + fail('read-only'); + const result = this.#dispatch(s, request.verb, args); + this.#secretCheck(result); +@@ -1086,6 +1086,23 @@ export class Broker { + this.#agent(s); + return this.#requirements; + } ++ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with ++ // the time and id of the event that set it (`allowed`, no time and no id when none has), and ++ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them, ++ // `total` counting every open task. A read for role and reader sessions; nothing is stored. ++ case 'state': { ++ keys(a, []); ++ if (!s.human && !s.reader) this.#agent(s); ++ const set = this.#store.get( ++ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1", ++ s.business, ++ ); ++ const open = this.#tasks(s.business, { done: false }); ++ return { ++ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null }, ++ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE }, ++ }; ++ } + case 'inbox': + case 'agents': + case 'tasks': +diff --git a/packages/bus/tests/state.test.mjs b/packages/bus/tests/state.test.mjs +new file mode 100644 +index 000000000..3a14c7ae9 +--- /dev/null ++++ b/packages/bus/tests/state.test.mjs +@@ -0,0 +1,89 @@ ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtempSync, rmSync } from 'node:fs'; ++import { tmpdir } from 'node:os'; ++import { join } from 'node:path'; ++import { Store } from '../src/store.mjs'; ++import { Broker, TASK_PAGE } from '../src/broker.mjs'; ++// Row 109 (#1603): `state` is what a run reads before it plans: the launch state with the time and ++// id of the event that set it, and the open tasks from the tracker snapshot, capped. ++const authority = { withinRole: ['message.send', 'role.launch'], crossRole: [] }; ++const roles = { pm: { authority }, coder: { authority } }; ++const businesses = { ++ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, launch: { by: 'pm', instances: ['coder'], max: { opus: 2, sonnet: 2 } }, roles }, ++}; ++const at = '2026-10-12T12:00:00.000Z'; ++const digest = 'd'.repeat(64); ++const ref = (i) => `vikunja:3/${i}`; ++const fields = (i, done = false) => ({ project_id: 3, title: `Task ${i}`, description: `

${i}

`, done, bucket: 11 }); ++ ++function setup(t) { ++ const root = mkdtempSync(join(tmpdir(), 'bus-state-')); ++ const store = new Store(root); ++ const b = new Broker({ store, businesses }); ++ t.after(() => { ++ store.close(); ++ rmSync(root, { recursive: true, force: true }); ++ }); ++ const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); ++ const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm', harness: 'claude-code' }); ++ b.request(pm, { verb: 'role.claim' }); ++ const snap = (i, over = {}, read_at = at) => ({ task_ref: ref(i), updated: read_at, digest, fields: { ...fields(i), ...over }, via: 'board', read_at }); ++ const record = (...snapshots) => b.recordTask({ business: 'demo', snapshots }); ++ const state = (cap = pm, args = {}) => b.request(cap, { verb: 'state', args }); ++ return { b, store, human, pm, snap, record, state, reader: b.bindReader({ business: 'demo' }) }; ++} ++ ++test('a business that never switched launches reads allowed with no time and no event', (t) => { ++ const { state } = setup(t); ++ assert.deepEqual(state().launch, { state: 'allowed', at: null, event: null }); ++}); ++ ++test('launches revoked and then restored read allowed, with the restore time and event id', (t) => { ++ const { b, store, human, pm, state } = setup(t); ++ const revoked = b.request(human, { verb: 'launch.revoke' }).id; ++ assert.deepEqual( ++ { state: state().launch.state, event: state().launch.event }, ++ { state: 'revoked', event: revoked }, ++ ); ++ const restored = b.request(human, { verb: 'launch.restore' }).id; ++ const row = store.get('SELECT at FROM events WHERE id=?', restored); ++ assert.deepEqual(state(pm).launch, { state: 'allowed', at: row.at, event: restored }); ++ assert.notEqual(restored, revoked); ++}); ++ ++test('a task closed on the tracker leaves the open list, and total counts the open ones', (t) => { ++ const { snap, record, state } = setup(t); ++ record(snap(1), snap(2), snap(3)); ++ assert.deepEqual(state().tasks, { ++ open: [1, 2, 3].map((i) => ({ task_ref: ref(i), title: `Task ${i}` })), ++ total: 3, ++ cap: TASK_PAGE, ++ }); ++ record(snap(2, { done: true }, '2026-10-12T13:00:00.000Z')); ++ const { open, total } = state().tasks; ++ assert.deepEqual(open.map((x) => x.task_ref), [ref(1), ref(3)]); ++ assert.equal(total, 2); ++}); ++ ++test('a tombstone is not open', (t) => { ++ const { snap, record, state } = setup(t); ++ record(snap(1), { ...snap(2), fields: { gone: 'no-access' }, via: 'task' }); ++ assert.deepEqual(state().tasks.open.map((x) => x.task_ref), [ref(1)]); ++}); ++ ++test('the open list stops at the cap and says so; total still counts every open task', (t) => { ++ const { snap, record, state } = setup(t); ++ record(...Array.from({ length: TASK_PAGE + 5 }, (_, k) => snap(k + 1))); ++ const { open, total, cap } = state().tasks; ++ assert.equal(open.length, TASK_PAGE); ++ assert.equal(total, TASK_PAGE + 5); ++ assert.equal(cap, TASK_PAGE); ++}); ++ ++test('a role and a reader session read state; arguments, and an unbound cap, refuse', (t) => { ++ const { reader, pm, state } = setup(t); ++ assert.deepEqual(state(reader), state(pm)); ++ assert.throws(() => state(pm, { business: 'other' }), { code: 'invalid-request' }); ++ assert.throws(() => state('f'.repeat(64)), /unauthenticated/); ++}); +diff --git a/packages/harness/README.md b/packages/harness/README.md +index 6f87ac731..4d2d6fb96 100644 +--- a/packages/harness/README.md ++++ b/packages/harness/README.md +@@ -182,6 +182,15 @@ it never touches the disk and isn't in the environment or argv. + Its stdout goes back to the sender as a RESULT; a failed turn replies + `The turn failed: `; an incoming RESULT never gets an automatic + reply. Then `message.read`. ++ ++ Row 109 (#1603): before each turn the runner calls the broker's `state` ++ verb and puts a block at the top of the request, headed `Current state, ++ read at