diff --git a/agents/sage/work/vikunja-setup/comment-mos0845-rulings.mjs b/agents/sage/work/vikunja-setup/comment-mos0845-rulings.mjs
new file mode 100644
index 000000000..d51a264c9
--- /dev/null
+++ b/agents/sage/work/vikunja-setup/comment-mos0845-rulings.mjs
@@ -0,0 +1,46 @@
+// Sage, 2026-10-19: mos-0845 rulings as task comments on P19 379-382, with Sage's own key (bot user 13). The sync key
+// reads each comment back. Comments only: no title, description, done, label or bucket change. Prints no secret.
+// Usage: node comment-mos0845-rulings.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
+import { readFileSync, writeFileSync, existsSync } from "node:fs";
+const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
+if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
+if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
+const SIG = "Added by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
+const COMMENTS = {
+ 379: [
+ "mos-0845 D1: go on the one-registry design as a new queue row. Until it lands, the window plans read `threads --business mosaic-stack --json` and expect [] (agents/sage/work/window-plan/repin-5fa9cc8f2.md, 6dbc67302).",
+ ],
+ 380: [
+ "mos-0845 D2: a tester message to a role with nobody running is refused, not queued. That is this task's fix. The fix belongs in talk's tester path, before the message is stored, with a test that a tester talk to a role with no running instance exits non-zero and writes no message.",
+ "Message 02f0e5d6 from the 8e7ea89a1 window stays queued for pm as it is. Any store edit to it waits for Jason.",
+ ],
+ 381: [
+ "Source for the usage-unknown line, corrected. It is in the bus journal at 2026-10-19T17:29:00Z, not 17:23Z: event usage-unknown, run r8fba7e598156, instance chief, turn 21e204db-8f55-4348-8f13-c10f85e764a2, source @none, code baseline-unknown. Sage read it at 17:32Z with journalctl --user -u 'mosaic-bus@mosaic-stack' --since \"2026-10-19T17:20:00Z\". A bare --since 17:23:30 reads as CDT on dragon-lin (22:23:30Z) and finds nothing; --since \"2026-10-19 17:23:30 UTC\" counts 1. Sage's sage-1003 gave the smoke time, 17:23Z, as the line's time.",
+ "mos-0845 D3: go. In Window A, Jason adds \"usage-source\": [\"transcript\"] with the live 8e7ea89a1 setter right before activate, and the activate restart loads it. Condition: Darkwing's scratch --plan run for A uses a business file that already has usage-source on and proves both 5fa9cc8f2 and a rollback to 8e7ea89a1 load it.",
+ ],
+ 382: [
+ "mos-0845 D4: go on the gate change. stack-release.sh accepts one named harness-off refusal for launch.by in place of the launched line. Then Jason runs plugins set harness [\"claude-code\"], then a plain unit restart with --pm, then W6. Writing harness from the target copy before activate can't work, because plugins set goes through the live bus (c216f47db cli.mjs:572). The silent default is refused.",
+ "The tool change gets an independent review and its own scratch rehearsal, since the blob changes from 4b16e848. Window B stays at ca15d73e9. No window past c216f47db goes to Jason until this task has landed.",
+ ],
+};
+const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
+const esc = (s) => s.replace(/&/g, "&").replace(//g, ">");
+async function api(method, path, body, auth) {
+ const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
+ const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
+ return { status: r.status, json };
+}
+writeFileSync(OUT, "", { flag: "wx" });
+const out = { at: new Date().toISOString(), comments: [] };
+for (const [task, lines] of Object.entries(COMMENTS)) {
+ const text = [...lines, SIG].map((l) => `
${esc(l)}
`).join("");
+ const r = await api("PUT", `/api/v1/tasks/${task}/comments`, { comment: text }, key);
+ const rec = { task: Number(task), status: r.status, id: r.json?.id, code: r.json?.code, message: r.json?.message };
+ const back = await api("GET", `/api/v1/tasks/${task}/comments`, undefined, sync);
+ rec.readBack = back.status;
+ rec.found = Array.isArray(back.json) && back.json.some((c) => c.id === r.json?.id);
+ out.comments.push(rec);
+ if (!rec.found) break;
+}
+writeFileSync(OUT, JSON.stringify(out, null, 1) + "\n");
+console.log(JSON.stringify(out));
diff --git a/agents/sage/work/vikunja-setup/mos0845-rulings-2026-10-19.json b/agents/sage/work/vikunja-setup/mos0845-rulings-2026-10-19.json
new file mode 100644
index 000000000..843bef89b
--- /dev/null
+++ b/agents/sage/work/vikunja-setup/mos0845-rulings-2026-10-19.json
@@ -0,0 +1,33 @@
+{
+ "at": "2026-10-19T17:44:59.064Z",
+ "comments": [
+ {
+ "task": 379,
+ "status": 201,
+ "id": 273,
+ "readBack": 200,
+ "found": true
+ },
+ {
+ "task": 380,
+ "status": 201,
+ "id": 274,
+ "readBack": 200,
+ "found": true
+ },
+ {
+ "task": 381,
+ "status": 201,
+ "id": 275,
+ "readBack": 200,
+ "found": true
+ },
+ {
+ "task": 382,
+ "status": 201,
+ "id": 276,
+ "readBack": 200,
+ "found": true
+ }
+ ]
+}
diff --git a/agents/sage/work/window-plan/repin-5fa9cc8f2.md b/agents/sage/work/window-plan/repin-5fa9cc8f2.md
index 387d60a48..349925fd7 100644
--- a/agents/sage/work/window-plan/repin-5fa9cc8f2.md
+++ b/agents/sage/work/window-plan/repin-5fa9cc8f2.md
@@ -91,7 +91,7 @@ takes the `revision` from a read made just before it.
| # | Who | Step |
|---|-----|------|
| W0 | Mos | `cd $REPO && scripts/stack-copy.sh copy 5fa9cc8f2c66b52408b652b59c3070f7210b44d0`. Note the full id it prints. Then P1 for NEW. |
-| W1 | Jason, then Mos | PM-idle check. Jason: `mosaic agents --business mosaic-stack`; chief not `working`, pm not running. Mos: `mosaic launches list`. Nobody talks to the chief until W6 passes. Record `date -u +%FT%TZ` as the start stamp. Baseline reads, kept for W6: `$C_OLD names --business mosaic-stack --json` and `$C_OLD threads --business mosaic-stack --json` (both `[]` on 10-19), and the record count from `mosaic usage --business mosaic-stack --json`. |
+| W1 | Jason, then Mos | PM-idle check. Jason: `mosaic agents --business mosaic-stack`; chief not `working`, pm not running. Mos: `mosaic launches list`. Nobody talks to the chief until W6 passes. Record `date -u +%FT%TZ` as the start stamp. Baseline reads, kept for W6: `$C_OLD names --business mosaic-stack --json` and `$C_OLD threads --business mosaic-stack --json` (both `[]` on 10-19), and `mosaic usage --business mosaic-stack --json` saved whole, for its `total` and `byRun` (U1, mos-0846). |
| W2 | Jason | Usage source, OLD's setter. `$C_OLD plugins --business mosaic-stack --json`; note `revision`. Write PATCH as `{"revision": , "plugins": {"usage-source": ["transcript"]}}`, then `$C_OLD plugins set --file PATCH --business mosaic-stack`. Read again: `usage-source` is `["transcript"]` and no other kind is set. A refusal ends the window with nothing stopped and nothing to undo. |
| W2b | Mos | `node $P/business-check.mjs 8e7ea89a1e86447d26ec335e4b138acfd5130cbc` and the same for NEW, on the patched file. Both LOAD_OK. A REFUSED stops the window; undo W2 as below. |
| W3 | Mos | `scripts/stack-release.sh activate 5fa9cc8f2c66b52408b652b59c3070f7210b44d0 mosaic-stack --plan`. Writes nothing. Its from, to, store schema, target schema, migration and needs lines must match P3's scratch output: from 8e7ea89a1, to 5fa9cc8f2, store schema 3h read from the store the running bus holds, target 3h, no migration step, no `--migrate` need, 78 commits. The bus, Console and CLI lines carry the pointer's flags; beyond those, only copy paths differ. Any other difference stops the window before W5 and goes to Mos. |
@@ -118,7 +118,7 @@ $C_NEW threads --business mosaic-stack --json # rows 128/129: []
mosaic project list --json # exit 0
mosaic limits --business mosaic-stack --json # row 125: exit 0
mosaic business instances mosaic-stack --json # row 165: the file's six instances, nothing added
-mosaic usage --business mosaic-stack --json # exit 0, same record count as W1
+mosaic usage --business mosaic-stack --json # total and byRun equal W1's, field for field
mosaic launches list # one running chief from this start
mosaic agents --business mosaic-stack # pm not running
```
@@ -134,11 +134,17 @@ wrote a name during the window; stop and report.
```
mosaic talk chief "[mos test] re-pin 5fa9cc8f2: reply with one line, no staffing request" --tester
-mosaic usage --business mosaic-stack --json # one new record for that turn, source transcript, status known
+mosaic usage --business mosaic-stack --json # one new byRun entry, this start's chief run: input > 0, output > 0, unknownTurns 0
```
-The `usage` read is F4's acceptance. A new record with `status` `unknown` goes to
-Mos with its code; it doesn't roll the window back by itself.
+The `usage` read is F4's acceptance. `mosaic usage --json` prints no record list,
+only `total` and the groups `byRun`, `byInstance`, `byModel`, `byProvider` and
+`byAccount` (5fa9cc8f2 `packages/cli/src/usage.mjs`), so the check reads the new
+`byRun` entry. A turn counts as known only when the transcript added usage
+(5fa9cc8f2 `packages/bus/src/usage.mjs:34` returns `no-new-usage` otherwise), so
+nonzero input and output with `unknownTurns` 0 prove the transcript source works.
+Any other result goes to Mos with the journal's `usage-unknown` code; it doesn't
+roll the window back by itself.
There is no pm smoke. pm isn't running, and a tester message to it waits for a
claim nobody makes (F3). The 8e7ea89a1 window's message `02f0e5d6` is still
diff --git a/agents/sage/work/window-plan/repin-ca15d73e9.md b/agents/sage/work/window-plan/repin-ca15d73e9.md
index 6a35488bf..bf82bc684 100644
--- a/agents/sage/work/window-plan/repin-ca15d73e9.md
+++ b/agents/sage/work/window-plan/repin-ca15d73e9.md
@@ -49,7 +49,7 @@ after the window, with a `launch.limits.tokens.daily` entry, on Mos's go.
| # | Who | Step |
|---|-----|------|
| W0 | Mos | `cd $REPO && scripts/stack-copy.sh copy ca15d73e9fdcdcf66a10f86cdedeb2d6a08f2b00`. Note the full id. Then P1 for NEW. |
-| W1 | Jason, then Mos | PM-idle check as Window A. Record the start stamp. Baseline reads: `$C_OLD names --business mosaic-stack --json`, `$C_OLD threads --business mosaic-stack --json` and the record count from `mosaic usage --business mosaic-stack --json`. Nobody talks to the chief until W6 passes, so no turn writes a usage record (D3). |
+| W1 | Jason, then Mos | PM-idle check as Window A. Record the start stamp. Baseline reads: `$C_OLD names --business mosaic-stack --json`, `$C_OLD threads --business mosaic-stack --json` and `mosaic usage --business mosaic-stack --json` saved whole, for its `total` and `byRun` (U1, mos-0846). Nobody talks to the chief until W6 passes, so no turn writes a usage record (D3). |
| W2 | Mos | `scripts/stack-release.sh status mosaic-stack`: pointer on 5fa9cc8f2, schema 3h, last line ok. |
| W3 | Mos | `scripts/stack-release.sh activate ca15d73e9fdcdcf66a10f86cdedeb2d6a08f2b00 mosaic-stack --plan`. It must match P3 on from 5fa9cc8f2, to ca15d73e9, store schema 3h read from the store the running bus holds, target 3i, one migration step 3h to 3i, needs `--migrate`, needs Jason present, 8 commits. Any other difference stops the window before W5. |
| W4 | Mos | Drop-ins, as Window A. |
@@ -66,21 +66,29 @@ $C_NEW plugins --business mosaic-stack --json # unchanged from Wi
$C_NEW names --business mosaic-stack --json # same as W1
$C_NEW threads --business mosaic-stack --json # same as W1 ([] unless Chat was used after A)
mosaic limits --business mosaic-stack --json # exit 0
-mosaic usage --business mosaic-stack --json # exit 0, every record from before still listed
+mosaic usage --business mosaic-stack --json # total and byRun equal W1's, field for field
mosaic launches list # one running chief from this start
mosaic agents --business mosaic-stack # pm not running
```
-The `usage` read shows that the 3i migration kept every usage record. The count
-must equal W1's.
+The `usage` read shows that the 3i migration kept the usage records. `mosaic usage
+--json` prints no record list, only `total` and the groups `byRun`, `byInstance`,
+`byModel`, `byProvider` and `byAccount` (ca15d73e9 `packages/cli/src/usage.mjs`,
+the same blob as 5fa9cc8f2's), so `total` and `byRun` must equal W1's field for
+field.
### W7
```
mosaic talk chief "[mos test] re-pin ca15d73e9: reply with one line, no staffing request" --tester
-mosaic usage --business mosaic-stack --json # one new known record, source transcript
+mosaic usage --business mosaic-stack --json # one new byRun entry, this start's chief run: input > 0, output > 0, unknownTurns 0
```
+A turn counts as known only when the transcript added usage (ca15d73e9
+`packages/bus/src/usage.mjs`, the `no-new-usage` return), so nonzero input and
+output with `unknownTurns` 0 prove the transcript source still works under 3i. ca15d73e9
+also writes an account on new rows, so W7 doesn't compare `byAccount` with W1.
+
No pm smoke (F3, P19 380). Jason's browser check: Settings shows Token limits
with no daily quota entry. Nobody presses Save.
@@ -111,7 +119,9 @@ stamp leave the live store.
post-attempt copy the run keeps still holds them, and nobody opens or changes
it without Jason.
-W1's idle rule and W7's single turn keep the usage count at one record or none.
+W1's idle rule and W7's single turn keep the `usage_records` rows after the stamp
+to one turn's begin and end rows, or none. This count is read from the store, not
+from `mosaic usage`.
`mosaic name` stays out of the window (mos-0641), so `instance_names` should
have none.
@@ -135,29 +145,16 @@ target copy before activate doesn't help either: it goes through the live bus
(c216f47db `cli.mjs:572`), which runs the old code. A silent default for a
missing `harness` kind is refused (D4).
-The fix, 382 (D4). `stack-release.sh` accepts one named `harness-off` refusal for
-`launch.by` in place of the `launched` line, for a target that has the harness
-kind. It's a tool change, so it gets an independent review and its own scratch
-rehearsal; the blob changes from 4b16e848.
-
-The steps, once 382 has landed:
-
-1. `activate mosaic-stack --pm`. The target's schema decides
- `--migrate`. If Window B is live at 3i, c216f47db doesn't change the schema
- (`SCHEMA_VERSION = '3i'` on 894ce3f37), so there's no migration; Jason is
- present anyway for step 2. If B isn't live, the target carries 3h to 3i and
- needs `--migrate` with Jason (P19 371).
-2. Jason: `$C_NEW plugins --business mosaic-stack --json` for the revision, then
- PATCH `{"revision": , "plugins": {"harness": ["claude-code"]}}`
- with `$C_NEW plugins set --file PATCH --business mosaic-stack`. Read again:
- `harness` is `["claude-code"]` and the other kinds are unchanged.
-3. Mos: a plain restart of `mosaic-bus@mosaic-stack.service`, still `--pm`. The
- chief launches. Check exactly one `launched` line for that start.
-4. The W6 reads, then the chief tester smoke.
-5. One managed coder session with its manifest attached (row 65's landing
- session: module, route, counted model, thinking).
-
-Mos puts step 2 to Jason when that window opens.
+The fix, 382. mos-0845 D4 ruled that the gate accepts one named `harness-off`
+refusal for `launch.by` in place of the `launched` line, then Jason's plugins
+set, then a plain restart. That can't work as ruled, and Sage reported it in
+sage-1007. A refused PM launch takes the bus down: c216f47db
+`packages/cli/src/launcher.mjs:657` throws `PM launch refused: ...` with exit 3,
+`packages/cli/src/cli.mjs:507-509` closes the host and rethrows, and the unit's
+`RestartPreventExitStatus=2 3 4` (`packages/cli/systemd/mosaic-bus.service.in:33`)
+keeps it down. The gate's unit and status checks fail before `pm_launched`, and
+there is no live bus for Jason's plugins set. The steps wait for Mos's
+ruling on sage-1007. The earlier step list is withdrawn until then.
## What Sage ran for this plan