From 779e97805ae8ab271071ea8767ae5a9a3a4516ea Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 9 Oct 2026 20:53:06 -0500 Subject: [PATCH] docs(build): row 41 slice 1 S6 round 2 candidate packet (filbert) R1 request ids on host-broker IPC, R2 launch.sock close bound, R3 dangling symlinks refused, notes 1-5. 42 files, +4679/-63, base 915e00e5, gate at 8dd5ff00. Candidate manifest digest fd21bdc2...eb02. Source uncommitted. Co-Authored-By: Claude Opus 5.5 --- agents/filbert/work/s6/BUILD.md | 181 +++++- agents/filbert/work/s6/build.patch | 585 +++++++++++++++--- .../filbert/work/s6/candidate-manifest.sha256 | 35 +- agents/filbert/work/s6/files.txt | 5 +- agents/filbert/work/s6/out/node-bus.txt | 150 ++--- agents/filbert/work/s6/out/node-business.txt | 122 ++-- agents/filbert/work/s6/out/node-cli.txt | 165 ++--- .../work/s6/out/node-control-board.txt | 250 ++++---- .../filbert/work/s6/out/node-conversation.txt | 306 ++++----- agents/filbert/work/s6/out/node-discord.txt | 358 +++++------ agents/filbert/work/s6/out/node-harness.txt | 101 +-- agents/filbert/work/s6/out/node-ledger.txt | 158 ++--- agents/filbert/work/s6/out/node-mosaic.txt | 140 ++--- agents/filbert/work/s6/out/node-queue.txt | 298 ++++----- agents/filbert/work/s6/out/node-seat.txt | 56 +- agents/filbert/work/s6/out/node-tasks.txt | 104 ++-- agents/filbert/work/s6/out/node-webui.txt | 30 +- agents/filbert/work/s6/out/summary.txt | 4 +- agents/filbert/work/s6/out/test-conductor.txt | 36 +- .../work/s6/out/test-extension-package.txt | 2 +- agents/filbert/work/s6/out/test-queue.txt | 2 +- agents/filbert/work/s6/packet-manifest.sha256 | 42 +- 22 files changed, 1867 insertions(+), 1263 deletions(-) diff --git a/agents/filbert/work/s6/BUILD.md b/agents/filbert/work/s6/BUILD.md index c0201af8..276a765a 100644 --- a/agents/filbert/work/s6/BUILD.md +++ b/agents/filbert/work/s6/BUILD.md @@ -1,17 +1,164 @@ -# Row 41 (#1523): slice 1 S6, candidate packet, round 1 +# Row 41 (#1523): slice 1 S6, candidate packet, round 2 Author: Filbert. Reviewer: Darkwing. Brief: `docs/plans/2026-10-04_slice-1.md`, section "Slice 1 S6", blob `72d11de2`. Plan: `PLAN.md` here (b6d2fe2b). -Rulings: lead decisions 77 and 78. Base: `915e00e5` (`base.txt`). None of -the 41 files changed between the base and `2855e628`, where the gate ran. -The candidate source is uncommitted. There are no pushes. No token, private +Rulings: lead decisions 77 and 78, and Sage's round 2 ruling (fix R1, R2 +and R3). Base: `915e00e5` (`base.txt`). None of the 42 files changed between +the base and `8dd5ff00`, where the round 2 gate ran, or `54cb7ab8`. The +candidate source is uncommitted. There are no pushes. No token, private binding or tracker host was read or written, and nothing touched the live `mosaic-bus@mosaic-stack` unit or `~/.mosaic-dev/bus/`. -## Files (`files.txt`, 41) +## Round 2 + +Darkwing's round 1 asked for changes (#1523 comment 26995, rev 247, +packet `agents/darkwing/work/s6-review/` at `f19787ee`). Sage ruled that all three +findings are fixed in this round. R1 sits inside decision 77: it's trusted +IPC, with no socket verb, event kind or schema change. This round adds one +file, `packages/cli/tests/host.test.mjs`, which round 1 didn't touch. + +### R1: host↔broker replies carry the request's id + +- `host.mjs`: every request gets `id = ++seq`, and the broker process echoes + it on every launch-op, bind and startup reply (`tag()` in + `packages/bus/src/process.mjs`). A reply is taken only if its id is the + waiting request's. +- Any of these breaks the channel (`breakChannel`): no reply within + `REQUEST_TIMEOUT_MS` (10 s), a reply with another id, a reply with no id, + or a reply with no request waiting. +- Once the channel is broken: + - the waiting request refuses with `broker-channel-broken`, and so does + every later one, including those already queued; + - the host logs it and closes with exit 1, for the unit to restart; + - a launch waiting on the channel is refused, never allowed. +- Tests: + - `host.test.mjs` "a broker reply that misses the wait breaks the + channel …". The broker is SIGSTOPped with one request waiting and a + bind queued behind it. Both refuse, the broker is resumed and its late + reply answers nothing, the host exits 1, and a later request refuses. + - "a broker reply with another request's id, or none, breaks the + channel …" changes the id on the way out, so the broker echoes the + wrong one, or drops it. + - `end-launch.test.mjs` checks the echo, refusals included. +- The cli and bus READMEs say this. + +Darkwing's `probe/desync.mjs` against this round: after the stall, p1 to p4 +all refuse with `broker-channel-broken`, and the host logs "broker channel +broken (no reply within 10 s); stopping the host". The probe's second half +(two binds during a stall) then fails with `kill ESRCH`, because the host +has already stopped the broker. Round 1 cross-wired the replies there +(`b2 bind run-y (reviewer) got: {"run":"run-x"}`). + +### R3: the gate refuses dangling symlinks + +`gate.mjs` `real()` walks up with `lstat`, not `exists`, so a dangling +symlink counts as an existing name. It then takes the realpath of the +nearest existing ancestor, and an `ENOENT` from `realpathSync` means the +path reaches a dangling link. `decide()` refuses that as "path goes through +a dangling symlink", at any depth, even when the link points inside the +workspace. A write through the link would create its target wherever the +link names, and Pi's `write` creates missing parent directories first. A +link whose target exists is checked as before, so one pointing outside the +workspace is refused as outside it. + +- `gate.test.mjs` "a dangling symlink is refused at any depth, in both + harnesses" covers Pi `write` and Claude Code `Write`, by relative and + absolute path. `notes.md` points outside, `dangling` points nowhere and + `inner` points to a missing name inside the workspace. All of + `notes.md`, `dangling/x`, `dangling/deep/x` and `inner` are refused. Once + the targets exist, `notes.md` is refused as outside and `inner` is + allowed. A path under a regular file is refused as `ENOTDIR`. +- `pi-session.test.mjs` "pi: a write through a dangling symlink is blocked, + and nothing appears outside" runs a real Pi session's `write` through the + extension. `notes.md` and `gone/x.md` come back as tool errors, a plain + `fine.md` lands in the workspace, and the outside directory stays empty. +- Claude Code: `--restricted` stays defence in depth. The gate refuses the + path whether or not the flag is set. + +Darkwing's probes against this round: +- `pi-dangling.sh`: the dangling link gives "mosaic gate: write path goes + through a dangling symlink: notes.md", and the outside file stays absent. + With the target present, the write is refused as outside the workspace + and the file keeps its content. +- `claude-dangling.sh`: the hook refuses with the same reason, and the + outside file stays absent. +- `gate-edges.mjs`: the output matches round 1 except one line, `write + dangling/x`, which went from ALLOW to the dangling-symlink refusal. + +The README's "Limits" gains a TOCTOU line: the gate checks a path when the +call is made, so a link that `bash` or another process of the same user +changes between the check and the open isn't seen. That reach is the same +as `bash` itself. + +### R2: a half-open `launch.sock` client can't hold `close()` + +The launcher keeps its `launch.sock` connections in a set. `close()` calls +`server.close` and then destroys each connection, before it waits for the +server to close. `launcher.test.mjs` "a launch client that never closes its +side doesn't hold the host's close" opens one client that was answered and +keeps its side open, and one that never sends. `close()` must finish within +5 s. Darkwing's `close-hang.mjs`, in both modes (`silent`, `line`), now +gives `closed 0` about 15 ms after `host.close(0)`. In round 1, the close +waited until the probe destroyed its client at 25 s. The cli README says +so. + +### Notes 1 to 5 + +1. **`--restricted`.** The harness README and the `adapters/claude/adapter.sh` + comment now say that the flag keeps `CLAUDE.md` files (user, parent, + workspace) and auto-memory out of the prompt, and that the gate doesn't + rely on it for paths. Two tests cover it: + - "claude adapter: --restricted is always passed" runs the adapter + against a stand-in `claude` and checks argv. It runs without Claude + Code installed. + - "claude: CLAUDE.md files and auto-memory don't reach the model; + without --restricted they do" plants the four markers and runs the + real adapter against the mock API: none reaches the request. A copy of + the adapter without the flag sends all four. + + Darkwing's `claude-memory.sh` gives the same result for this round: 0 of + 4 with the candidate, 4 of 4 without the flag. +2. **`launchPm` skips `authorizeLaunch`.** The cli README now says so: + `launches off` doesn't stop `bus start --pm`, because the human launches + the PM, and the other launcher checks still apply. +3. **Policy parse.** `runner.mjs` reads and parses the policy inside the + `try`, so a missing or malformed policy exits 2 (usage) before the claim. + Test: "a missing or malformed policy exits 2 before the claim". +4. **System prompt in argv.** This is now a harness README "Limits" line. + No code change. +5. **Mutants.** Each new test was run against its mutant and fails. Each + source file was restored from a copy and checked with `cmp`. + +| Mutant | Change | Round 1 | Round 2 | +|---|---|---|---| +| R1a | the host takes any reply while a request waits | (finding) | "another request's id, or none" fails | +| R1b | no reply timer | (finding) | the stall test fails at its 30 s test timeout instead of hanging | +| R2 | `close()` doesn't destroy connections | (finding) | the half-open test fails within its 5 s bound | +| R3 | round 1's `existsSync` walk in `real()` | (finding) | the gate and pi-session dangling tests fail | +| N1 | the adapter without `--restricted` | (note) | both claude-session tests fail | +| Mg | `founderCheck` returns null | runner test hangs | the unit test fails; the runner test fails at its 60 s clock | +| Mh | no SIGKILL after the stop timeout in `close()` | survives | "a runner that ignores SIGTERM is killed when the host closes" fails | +| Mi | no SIGKILL in `recover()` | survives | the died-hard "kills them" case fails | +| Mj | no 4096-byte stdin cap | survives | "no capability, or a malformed one, on stdin" fails | +| Mk | no `LINE_MAX` on `launch.sock` | survives | "an over-long launch request is refused at once …" fails | + +How the new tests separate the mutants: +- **Mh and Mi** rely on a SIGSTOPped process, which ignores SIGTERM and + dies only on SIGKILL. +- **Mi:** in the died-hard test, the leftover session and its runner are + stopped before the next host starts. Without the stop, the runner exits + on its own once the broker is gone, which is why Mi survived round 1. +- **Mh** uses a test-only `stopTimeoutMs` (default 30000), set to 1 s. +- **Mg and Mj:** `startRunner` in `runner.test.mjs` now SIGKILLs a runner + still alive after 60 s. A regression fails there instead of hanging the + file. +- **The stall test** registers its SIGCONT hook before `host.close`. Hooks + run in order, so a stopped broker can't hold the cleanup. + +## Files (`files.txt`, 42) `build.patch` is `git diff --cached --binary 915e00e5` over those files, -+4302/−55. It applies cleanly to `2855e628` with `git apply --index`, and ++4679/−63. It applies cleanly to `8dd5ff00` with `git apply --index`, and `sha256sum -c candidate-manifest.sha256` passes in that tree. | Area | Files | What | @@ -34,7 +181,8 @@ binding or tracker host was read or written, and nothing touched the live blocks, a missing `-e` refuses to start, and a hang is bounded by the runner's wall clock plus the `agent_end` turn marker. Claude Code uses a command hook run as `timeout -k 2 10 || exit 2` with hook timeout - 20; `--bare` is never passed. `--restricted` is defence in depth only. + 20; `--bare` is never passed. `--restricted` is defence in depth for the + gate, and it keeps `CLAUDE.md` files and auto-memory out of the prompt. Line 5 (`bash`) is the tool limit and is written as a limit. - **The PM launches through the broker, within `launch`**: the host's launch socket checks the instance list, that the instance isn't already @@ -125,30 +273,31 @@ project extensions) and is untouched. ## Gate -Run at `2855e628` with `build.patch` applied, in a detached worktree, +Run at `8dd5ff00` with this round's patch applied, in a detached worktree, sequentially, each output in `out/` (`out/summary.txt`). `TMPDIR` on the scratch disk; `DOCKER_HOST=unix:///nonexistent.sock`, so nothing reaches -Docker. +Docker. Round 1's outputs stay in this directory at `9b670e27`. | Suite | Pass | Fail | |---|---|---| -| node: bus, business, cli, control-board | 74, 60, 77, 124 | 0 | -| node: conversation, discord, harness, ledger | 152, 178, 45, 78 | 0 | +| node: bus, business, cli, control-board | 74, 60, 82, 124 | 0 | +| node: conversation, discord, harness, ledger | 152, 178, 50, 78 | 0 | | node: mosaic, queue, seat, tasks, webui | 69, 148, 27, 51, 14 | 0 | | test-auth, conductor, config, discord | 15, 17, 24, 66 | 0 | | test-extension-package, foundation, queue, release | 18, 44, 27, 4 | 0 | | test-task | 26 | 2 | +Against round 1, cli gains 5 tests and harness 5. + The two `test-task` failures are "user recall run succeeds (exit 1)" and "recalled user name". That check runs a live worker and needs Docker. The unpatched base fails the same two (`out/base-test-task.txt`, identical PASS/FAIL lines), so they are the environment, not this candidate. -An earlier gate over this candidate without the `--no-approve` edits had -`test-queue` fail F1 once ("a plain `commit -e` whose guard ran before -update-ref fails at its own HEAD update": only the stderr match). It passed -in two patched reruns, in the base run, and in this gate. See the -follow-ups below. +One edit came after the gate started: the R2 sentence in +`packages/cli/README.md` ("drops every open `launch.sock` connection …"). +No suite reads that file, and it is the only file where the manifest and +the gated tree differ. ## Acceptance run: waiting diff --git a/agents/filbert/work/s6/build.patch b/agents/filbert/work/s6/build.patch index 48402e9a..67ee4789 100644 --- a/agents/filbert/work/s6/build.patch +++ b/agents/filbert/work/s6/build.patch @@ -46,10 +46,10 @@ index 33ed6ded..a9460c3f 100644 +`packages/harness/README.md`. diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh new file mode 100644 -index 00000000..2e29fe25 +index 00000000..8a259540 --- /dev/null +++ b/adapters/claude/adapter.sh -@@ -0,0 +1,77 @@ +@@ -0,0 +1,79 @@ +#!/bin/sh +# Claude Code adapter: implements the Mosaic adapter contract for the host's +# `claude` CLI, for managed sessions (slice 1 S6). Headless only. @@ -101,8 +101,10 @@ index 00000000..2e29fe25 +# --restricted no user/project/local settings files; --settings +# (the gate hook) still applies; no code-running +# tool unless --tools names it; file tools confined -+# to the working directory. Defence in depth: the -+# S0 lines above don't depend on it. ++# to the working directory; no CLAUDE.md file or ++# auto-memory in the prompt. The S0 lines above ++# don't depend on it, but it is what keeps founder ++# and repository memory out; a test fails without it. +# --tools the built-in tool limit (S0 line 5); empty = none +# --allowedTools the same tools plus the mosaic MCP server, so +# --permission-mode dontAsk nothing waits on a prompt and anything else is denied @@ -208,16 +210,20 @@ index e07eabfa..b96e9012 100644 `3` refused or config problem · `4` usage. Details: `packages/cli/README.md`. diff --git a/packages/bus/README.md b/packages/bus/README.md -index 20d3f9a0..29b4add7 100644 +index 20d3f9a0..fff96910 100644 --- a/packages/bus/README.md +++ b/packages/bus/README.md -@@ -60,6 +60,27 @@ The embedded runtime offers the same `bindLaunch(record)` wrapper, which also +@@ -60,6 +60,31 @@ The embedded runtime offers the same `bindLaunch(record)` wrapper, which also updates the human ancestry registry. S6 must use that wrapper. Do not use the underlying Broker's test-level binding API to bypass runtime process checks. +A rebind of a run that already has `session.ended` refuses with `run-ended`, +also after a restart, and a refused bind leaves the run unbound. + ++A request that carries a safe-integer `id` gets the same `id` on its reply, ++refusals included, so the host can match each reply to its request ++(`packages/cli/README.md`). A request without one gets a reply without one. ++ +S6 adds launch ops on the same IPC channel, one reply each +(`{ok:true,result}` or `{ok:false,error}`), none of them socket verbs: + @@ -306,10 +312,10 @@ index a22b5950..0ebc8ff8 100644 bindHuman(record) { keys(record, ['business', 'human', 'via', 'outsideAgent'], ['business', 'human', 'via', 'outsideAgent']); diff --git a/packages/bus/src/process.mjs b/packages/bus/src/process.mjs -index 562e6a9e..bf0ce4f1 100644 +index 562e6a9e..68276a03 100644 --- a/packages/bus/src/process.mjs +++ b/packages/bus/src/process.mjs -@@ -2,6 +2,14 @@ +@@ -2,6 +2,17 @@ // never command arguments, stdout or service-token-bearing environment variables. import { startBroker } from './runtime.mjs'; import { BusError } from './broker.mjs'; @@ -321,25 +327,42 @@ index 562e6a9e..bf0ce4f1 100644 + if (m.op === 'endLaunch') return runtime.endLaunch(m.record); + return runtime.credentialStatus(m.business, m.role); +} ++// The host's request id comes back on the reply, so a late reply can't ++// answer a later request (host.mjs). A message without one gets none. ++const tag = (message, reply) => (Number.isSafeInteger(message?.id) ? { ...reply, id: message.id } : reply); let runtime, booted = false, closing = false; -@@ -31,6 +39,15 @@ if (!process.send) { - } - return; - } -+ // S6 launcher ops, one reply each; the host sends them one at a time like bindLaunch. -+ if (LAUNCH_OPS.has(message?.op) && runtime) { -+ try { -+ process.send({ ok: true, result: launchOp(message) }); +@@ -25,9 +36,18 @@ if (!process.send) { + try { + if (message?.op === 'bindLaunch' && runtime) { + try { +- process.send({ ok: true, launch: runtime.bindLaunch(message.record) }); ++ process.send(tag(message, { ok: true, launch: runtime.bindLaunch(message.record) })); + } catch (e) { -+ process.send({ ok: false, error: e instanceof BusError ? e.code : 'launch-op-refused' }); ++ process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'bind-refused' })); + } + return; + } - if (message?.op === 'close') { - clearTimeout(timer); - await close(0); ++ // S6 launcher ops, one reply each, like bindLaunch. ++ if (LAUNCH_OPS.has(message?.op) && runtime) { ++ try { ++ process.send(tag(message, { ok: true, result: launchOp(message) })); + } catch (e) { +- process.send({ ok: false, error: e instanceof BusError ? e.code : 'bind-refused' }); ++ process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'launch-op-refused' })); + } + return; + } +@@ -52,7 +72,7 @@ if (!process.send) { + } + process.send({ ok: true, path: runtime.path, launches: runtime.launches, readers: runtime.readers }); + } catch (e) { +- process.send?.({ ok: false, error: e instanceof BusError ? e.code : 'startup-refused' }, () => ++ process.send?.(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'startup-refused' }), () => + close(2), + ); + } diff --git a/packages/bus/src/runtime.mjs b/packages/bus/src/runtime.mjs index be39c0f9..ebc07cf3 100644 --- a/packages/bus/src/runtime.mjs @@ -390,10 +413,10 @@ index be39c0f9..ebc07cf3 100644 async close() { diff --git a/packages/bus/tests/end-launch.test.mjs b/packages/bus/tests/end-launch.test.mjs new file mode 100644 -index 00000000..574129b7 +index 00000000..68720d71 --- /dev/null +++ b/packages/bus/tests/end-launch.test.mjs -@@ -0,0 +1,187 @@ +@@ -0,0 +1,192 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { fork } from 'node:child_process'; @@ -571,6 +594,11 @@ index 00000000..574129b7 + assert.deepEqual((await ask(child, { op: 'credentialStatus', business: 'demo', role: 'pm' })).result, []); + const end = await ask(child, { op: 'endLaunch', record: { business: 'demo', run: 'pm-1', reason: 'stopped', exitCode: 0 } }); + assert.deepEqual(end, { ok: true, result: { released: true } }); ++ // The host's request id comes back on every launch-op and bind reply, refusals too. ++ assert.equal((await ask(child, { op: 'identity', cap: cto, id: 7 })).id, 7); ++ assert.deepEqual(await ask(child, { op: 'identity', cap: 'f'.repeat(64), id: 8 }), { ok: false, error: 'unauthenticated', id: 8 }); ++ assert.equal((await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 9 })).id, 9); ++ assert.deepEqual(await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 10 }), { ok: false, error: 'duplicate-run', id: 10 }); + await assert.rejects(new Client({ path: ready.path, cap: pm }).call('agents'), /unauthenticated/); + const trail = await reader.call('trail', { subject: 'pm-1' }); + assert.deepEqual( @@ -582,7 +610,7 @@ index 00000000..574129b7 + assert.equal((await exit)[0], 0); +}); diff --git a/packages/cli/README.md b/packages/cli/README.md -index 0eb1423c..be9e6531 100644 +index 0eb1423c..32977afa 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2,9 +2,10 @@ @@ -610,7 +638,7 @@ index 0eb1423c..be9e6531 100644 ``` - The business is `--business`, or else the business of the bus host running -@@ -49,11 +54,25 @@ mosaic trail [--business ] [--json] +@@ -49,11 +54,28 @@ mosaic trail [--business ] [--json] - `trail` prints rows in the broker's order (at, table, seq). A decision's trail ends with its `task_ref` and `follow with: mosaic trail `. It does not pull in the task's rows. @@ -622,6 +650,9 @@ index 0eb1423c..be9e6531 100644 +- `launches off` and `on` are the broker's `launch.revoke` and + `launch.restore`. While off, every `role.launch` refuses with + `launch-revoked`; running sessions keep running. Stop them with `stop`. ++ `bus start --pm` doesn't ask the broker (`launchPm` skips ++ `authorizeLaunch`): the human launches the PM, so `launches off` doesn't ++ stop it. The other checks in "Sessions" below still apply. +- `stop` and `launches list` read `/bus-host/sessions.json`, not + the bus (see "Sessions" below). `stop` refuses inside an agent run; + `launches list` does not, because the file is readable to the same user @@ -637,7 +668,7 @@ index 0eb1423c..be9e6531 100644 mosaic bus stop SIGTERM to the recorded host, then wait mosaic bus status [--json] host state file, socket, writer.lock ``` -@@ -76,12 +95,19 @@ mosaic bus status [--json] host state file, socket, writer.lock +@@ -76,12 +98,23 @@ mosaic bus status [--json] host state file, socket, writer.lock 5. It writes `/bus-host/host.json` (0600): the pid, the process start time, the business, the start time as text and the notifier binding. The file holds no capability. @@ -656,12 +687,16 @@ index 0eb1423c..be9e6531 100644 +one of the broker process's launch ops (`identity`, `authorizeLaunch`, +`refuse`, `endLaunch`, `credentialStatus`); `beforeClose(fn)` runs `fn` +before the broker closes, so the launcher stops its sessions first. -+Requests to the broker process go one at a time, because its replies carry -+no request id. ++Requests to the broker process go one at a time. Each carries an id, and ++the broker echoes it on the reply. If a reply doesn't arrive within 10 s, or ++arrives with an id no request is waiting for, the channel is broken: the ++waiting request refuses with `broker-channel-broken`, so does every later ++one, and the host stops with exit 1 for the unit to restart. A late reply ++never answers a later request, and a launch waiting on one refuses. SIGTERM or SIGINT stops the notifier after its poll in flight, then closes the broker and removes `host.json`. If either child dies on its own, the -@@ -96,6 +122,68 @@ and whose command line is `…/packages/cli/src/cli.mjs bus start`. +@@ -96,6 +129,70 @@ and whose command line is `…/packages/cli/src/cli.mjs bus start`. a broker was killed hard. Check, then remove the lock by hand (`packages/bus/README.md`). @@ -709,7 +744,9 @@ index 0eb1423c..be9e6531 100644 +refusal and end; `workspaces///` (0700) is kept across +launches; `bus-host/sessions.json` (0600) lists the running sessions. + -+When the host closes, the launcher stops taking requests, sends SIGTERM to ++When the host closes, the launcher stops taking requests and drops every ++open `launch.sock` connection, so a client that never ends its side can't ++hold the close. It sends SIGTERM to +every runner (again each second, see "Limits"), waits up to 30 s, then +SIGKILLs what is left and ends those launches as `killed`. + @@ -730,7 +767,7 @@ index 0eb1423c..be9e6531 100644 ### Trackers `config.trackers[]` is `{baseUrl, project, pollSeconds, -@@ -223,7 +311,7 @@ exit 3, so a host never starts until someone decides whether it DMs. +@@ -223,7 +320,7 @@ exit 3, so a host never starts until someone decides whether it DMs. |---|---| | 0 | ok | | 1 | failed, or outcome unknown: check before retrying | @@ -739,7 +776,7 @@ index 0eb1423c..be9e6531 100644 | 3 | refused or config problem: inside an agent run, human proof failed, unknown business, bad config, broker or notifier refused to start | | 4 | usage | -@@ -238,9 +326,22 @@ exit 3, so a host never starts until someone decides whether it DMs. +@@ -238,9 +335,22 @@ exit 3, so a host never starts until someone decides whether it DMs. - **The real human transport is untested here.** No test runs the real `human-cli.mjs`, because its proof needs a human shell. The live run covers it. @@ -932,7 +969,7 @@ index 65ae9634..0b48c7a7 100644 if (verb === "-h" || verb === "--help") return io.stdout.write(`${USAGE}\n`); throw usage(); diff --git a/packages/cli/src/host.mjs b/packages/cli/src/host.mjs -index 083f8f1d..35c44f58 100644 +index 083f8f1d..0073091b 100644 --- a/packages/cli/src/host.mjs +++ b/packages/cli/src/host.mjs @@ -5,8 +5,12 @@ @@ -950,7 +987,7 @@ index 083f8f1d..35c44f58 100644 import { fork } from "node:child_process"; import { once } from "node:events"; -@@ -14,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync +@@ -14,35 +18,19 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { CliError } from "./errors.mjs"; @@ -958,7 +995,11 @@ index 083f8f1d..35c44f58 100644 const BROKER = fileURLToPath(new URL("../../bus/src/process.mjs", import.meta.url)); const NOTIFIER = fileURLToPath(new URL("./notifier-process.mjs", import.meta.url)); -@@ -24,25 +29,7 @@ const CLOSE_TIMEOUT_MS = 20000; + export const BOOT_TIMEOUT_MS = 30000; + const START_TIMEOUT_MS = 15000; + const CLOSE_TIMEOUT_MS = 20000; ++const REQUEST_TIMEOUT_MS = 10000; + export const hostDir = (dataRoot) => join(dataRoot, "bus-host"); export const hostFile = (dataRoot) => join(hostDir(dataRoot), "host.json"); @@ -985,7 +1026,19 @@ index 083f8f1d..35c44f58 100644 // The state file, or null. `live` is true only when the pid still runs with // the recorded start time, so a recycled pid never counts as the host. -@@ -157,27 +144,45 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs +@@ -107,8 +95,9 @@ export function watchChildren(children, onDeath) { + + // boot: the {op:'boot'} config from bootConfig(). notifier: null, or + // {binding, base?, pollMs?}; base and pollMs exist for the tests, and the +-// command line never sets them. Resolves once both children are up. +-export async function startHost({ boot, business, notifier = null, bootTimeoutMs = BOOT_TIMEOUT_MS, log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`) }) { ++// command line never sets them, nor requestTimeoutMs. Resolves once both ++// children are up. ++export async function startHost({ boot, business, notifier = null, bootTimeoutMs = BOOT_TIMEOUT_MS, requestTimeoutMs = REQUEST_TIMEOUT_MS, log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`) }) { + const dataRoot = boot.dataRoot; + const prior = readHostState(dataRoot); + if (prior?.live) throw new CliError(`a bus host already runs for ${prior.business} (pid ${prior.pid})`, 3); +@@ -157,27 +146,83 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs rmSync(hostFile(dataRoot), { force: true }); writeHostState(dataRoot, state); @@ -997,19 +1050,60 @@ index 083f8f1d..35c44f58 100644 const done = new Promise((r) => (finish = r)); + const hooks = []; - // Replies from process.mjs carry no request id, so requests go one at a time. +- // Replies from process.mjs carry no request id, so requests go one at a time. ++ // Requests go one at a time, each with an id that process.mjs echoes. A ++ // reply that misses the wait, or carries an id no request is waiting for, ++ // breaks the channel: the waiting request and every later one refuse, and ++ // the host stops with exit 1 so the unit restarts it. A late reply can ++ // never answer a later request. let queue = Promise.resolve(); - function bindLaunch(record) { ++ let seq = 0; ++ let pending = null; ++ let broken = null; ++ const fail = (code, text) => Object.assign(new CliError(text, 1), { code }); ++ function breakChannel(why) { ++ if (broken) return; ++ broken = why; ++ if (pending) { ++ clearTimeout(pending.timer); ++ pending.reject(fail("broker-channel-broken", `broker channel broken: ${why}`)); ++ pending = null; ++ } ++ if (stopping) return; ++ log(`broker channel broken (${why}); stopping the host`); ++ close(1); ++ } ++ broker.on("message", (m) => { ++ if (pending && m?.id === pending.id) { ++ clearTimeout(pending.timer); ++ const { resolve } = pending; ++ pending = null; ++ resolve(m); ++ } else breakChannel(pending ? "reply for another request" : "reply with no request waiting"); ++ }); ++ broker.once("exit", () => { ++ if (!pending) return; ++ clearTimeout(pending.timer); ++ pending.reject(fail("broker-exited", "broker exited before it replied")); ++ pending = null; ++ }); + function request(message, { what, pick }) { const run = queue.then(async () => { - if (closing) throw new CliError("bus host is closing", 1); -+ if (closing || !broker.connected) throw Object.assign(new CliError("bus host is closing", 1), { code: "host-closing" }); - const r = firstReply(broker, 10000, "broker"); +- const r = firstReply(broker, 10000, "broker"); - broker.send({ op: "bindLaunch", record }); -+ broker.send(message); - const m = await r; +- const m = await r; - if (m?.ok !== true) throw new CliError(`launch bind refused: ${m?.error ?? "bind-refused"}`, 3); - return m.launch; ++ if (broken) throw fail("broker-channel-broken", `broker channel broken: ${broken}`); ++ if (closing || !broker.connected) throw fail("host-closing", "bus host is closing"); ++ const id = ++seq; ++ const m = await new Promise((resolve, reject) => { ++ const timer = setTimeout(() => breakChannel(`no reply within ${Math.round(requestTimeoutMs / 1000)} s`), requestTimeoutMs); ++ pending = { id, resolve, reject, timer }; ++ broker.send({ ...message, id }); ++ }); + if (m?.ok !== true) { + const code = typeof m?.error === "string" ? m.error : `${what}-refused`; + throw Object.assign(new CliError(`${what} refused: ${code}`, 3), { code }); @@ -1037,7 +1131,7 @@ index 083f8f1d..35c44f58 100644 closing = true; let result = code; if (notify) { -@@ -197,12 +202,12 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs +@@ -197,12 +242,12 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs // restarts the pair rather than run a broker without its notifier. // Runs after `queue` exists, since close() awaits it. watchChildren({ broker, notifier: notify }, (name, code, signal) => { @@ -1054,10 +1148,10 @@ index 083f8f1d..35c44f58 100644 // `mosaic bus stop`: SIGTERM to the recorded host, after checking that the diff --git a/packages/cli/src/launcher.mjs b/packages/cli/src/launcher.mjs new file mode 100644 -index 00000000..ec01a2c3 +index 00000000..b07ae333 --- /dev/null +++ b/packages/cli/src/launcher.mjs -@@ -0,0 +1,412 @@ +@@ -0,0 +1,420 @@ +// The S6 launcher, inside the trusted bus host: it starts managed role +// sessions (packages/seat/src/session.mjs) and serves the launch socket the +// PM's `launch` tool calls. One launch at a time. @@ -1143,8 +1237,8 @@ index 00000000..ec01a2c3 +} + +// host: startHost()'s handle. tracker: true when the broker has task verbs for -+// the business. adapters, namespace, sessionDefaults and versions exist for -+// the tests; the command line never sets them. ++// the business. adapters, namespace, sessionDefaults, versions and ++// stopTimeoutMs exist for the tests; the command line never sets them. +export function createLauncher({ + host, + system, @@ -1154,6 +1248,7 @@ index 00000000..ec01a2c3 + namespace = true, + sessionDefaults = {}, + versions = null, ++ stopTimeoutMs = STOP_TIMEOUT_MS, + log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`), +}) { + const dataRoot = system.dataRoot; @@ -1174,6 +1269,7 @@ index 00000000..ec01a2c3 + let chain = Promise.resolve(); + let closed = false; + let server = null; ++ const sockets = new Set(); + + const record = (entry) => { + try { @@ -1395,6 +1491,8 @@ index 00000000..ec01a2c3 + rmSync(path); + } + server = createServer((socket) => { ++ sockets.add(socket); ++ socket.once("close", () => sockets.delete(socket)); + let buf = ""; + let answered = false; + const reply = (obj) => { @@ -1437,7 +1535,11 @@ index 00000000..ec01a2c3 + async function close() { + closed = true; + if (server) { -+ await new Promise((r) => server.close(r)); ++ // server.close waits for every connection, and a client that never ++ // ends its side (or never sends) would hold it; so they go first. ++ const stopped = new Promise((r) => server.close(r)); ++ for (const socket of sockets) socket.destroy(); ++ await stopped; + rmSync(launchSocketPath(dataRoot), { force: true }); + } + await chain; @@ -1459,7 +1561,7 @@ index 00000000..ec01a2c3 + if (startTimeOf(s.pid) === s.startTime) process.kill(s.pid, "SIGKILL"); + } catch {} + } -+ }, STOP_TIMEOUT_MS); ++ }, stopTimeoutMs); + await Promise.all(pending); + // The exit handlers run on the same tick as the close events; let them finish. + while (children.size) await new Promise((r) => setTimeout(r, 20)); @@ -1499,12 +1601,82 @@ index 00000000..0acc8424 +}); +await launcher.listen(); +process.stdout.write(`${JSON.stringify(await launcher.launchPm())}\n`); +diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs +index f8f11ec9..f8914479 100644 +--- a/packages/cli/tests/host.test.mjs ++++ b/packages/cli/tests/host.test.mjs +@@ -203,6 +203,65 @@ test("a second host for the same data root refuses with exit 3 while the first r + assert.equal(await host.close(0), 0); + }); + ++test("a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1", { timeout: 30000 }, async (t) => { ++ const root = tmp(t); ++ const f = fixture(root); ++ makeDeployment(root); ++ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }); ++ const logs = []; ++ const host = await startHost({ boot, business: "acme", requestTimeoutMs: 1000, log: (l) => logs.push(l) }); ++ // Hooks run in order: the broker resumes before the close, which a ++ // stopped broker would hold. ++ t.after(() => { ++ try { ++ process.kill(host.pids.broker, "SIGCONT"); ++ } catch {} ++ }); ++ t.after(() => host.close(0)); ++ const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); ++ const coder = await host.bindLaunch(record("coder", "coder-run")); ++ assert.equal((await host.op({ op: "identity", cap: coder.cap })).role, "coder"); ++ ++ // Stall the broker with a request waiting and a bind queued behind it. ++ process.kill(host.pids.broker, "SIGSTOP"); ++ const code = (p) => p.then((v) => ({ answered: v }), (e) => e.code); ++ const first = code(host.op({ op: "identity", cap: "bogus" })); ++ const second = code(host.bindLaunch(record("reviewer", "reviewer-run"))); ++ assert.equal(await first, "broker-channel-broken"); ++ assert.equal(await second, "broker-channel-broken", "a queued request never reaches the broker"); ++ process.kill(host.pids.broker, "SIGCONT"); ++ ++ // The broker answers the stalled request late; nothing takes that reply. ++ assert.equal(await host.done, 1); ++ assert.ok(logs.some((l) => /^broker channel broken \(no reply within 1 s\); stopping the host$/.test(l)), logs.join("\n")); ++ assert.equal(await code(host.op({ op: "identity", cap: coder.cap })), "broker-channel-broken"); ++}); ++ ++test("a broker reply with another request's id, or none, breaks the channel and the host exits 1", { timeout: 30000 }, async (t) => { ++ // The broker echoes whatever id it's sent, so changing the id on the way ++ // out gives the host the reply a confused broker would send. ++ let what, tamper; ++ spySends(t, (m) => m?.cap === "tamper" && tamper(m)); ++ for ([what, tamper] of [ ++ ["another id", (m) => (m.id += 1000)], ++ ["no id", (m) => delete m.id], ++ ]) { ++ const root = tmp(t); ++ const f = fixture(root); ++ makeDeployment(root); ++ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }); ++ const logs = []; ++ const host = await startHost({ boot, business: "acme", log: (l) => logs.push(l) }); ++ t.after(() => host.close(0)); ++ const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); ++ const coder = await host.bindLaunch(record("coder", "coder-run")); ++ await assert.rejects(host.op({ op: "identity", cap: "tamper" }), (e) => e.code === "broker-channel-broken", what); ++ await assert.rejects(host.op({ op: "identity", cap: coder.cap }), (e) => e.code === "broker-channel-broken", what); ++ assert.equal(await host.done, 1, what); ++ assert.ok(logs.includes("broker channel broken (reply for another request); stopping the host"), `${what}: ${logs.join("\n")}`); ++ } ++}); ++ + test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => { + const root = tmp(t); + const f = fixture(root); diff --git a/packages/cli/tests/launcher.test.mjs b/packages/cli/tests/launcher.test.mjs new file mode 100644 -index 00000000..b0c0ba09 +index 00000000..75ee91b8 --- /dev/null +++ b/packages/cli/tests/launcher.test.mjs -@@ -0,0 +1,311 @@ +@@ -0,0 +1,394 @@ +// The S6 launcher inside a real bus host: real broker child, real runners +// under unshare, the fake adapter from packages/harness standing in for the +// harness. The PM launches a coder through its `launch` tool; every host @@ -1560,7 +1732,7 @@ index 00000000..b0c0ba09 + return { root, f, adapter }; +} + -+async function setup(t, more = (doc) => doc, { root, f, adapter } = prepare(t, more)) { ++async function setup(t, more = (doc) => doc, { root, f, adapter } = prepare(t, more), options = {}) { + const system = loadSystem({ env: f.env }); + const boot = bootConfig({ system, businessId: "acme", env: f.env }); + const logs = []; @@ -1575,6 +1747,7 @@ index 00000000..b0c0ba09 + sessionDefaults: { pollInterval: 100 }, + versions: { pi: "0.85.1", claude: null }, + log: (l) => logs.push(l), ++ ...options, + }); + await launcher.listen(); + } catch (e) { @@ -1774,6 +1947,16 @@ index 00000000..b0c0ba09 + // The broker child exits on the lost IPC channel; the session runs on. + await until(() => !existsSync(join(f.dataRoot, "bus", "writer.lock"))); + assert.equal(startTimeOf(left.runnerPid), left.runnerStartTime); ++ if (!exited) { ++ // Stopped, the leftover neither polls the dead broker nor answers ++ // SIGTERM: only the next host's SIGKILL ends it. ++ for (const pid of [left.pid, left.runnerPid]) process.kill(pid, "SIGSTOP"); ++ t.after(() => { ++ for (const [pid, start] of [[left.pid, left.startTime], [left.runnerPid, left.runnerStartTime]]) { ++ if (startTimeOf(pid) === start) process.kill(pid, "SIGKILL"); ++ } ++ }); ++ } + if (exited) { + await until(() => startTimeOf(left.pid) === null); + assert.match(readFileSync(runnerLog, "utf8"), /broker unreachable after 30 tries[\s\S]*exiting 23/); @@ -1816,6 +1999,78 @@ index 00000000..b0c0ba09 + assert.equal(readFileSync(file, "utf8"), "{"); + assert.equal(existsSync(join(given.f.dataRoot, "bus", "writer.lock")), false, "the host closed"); +}); ++ ++const within = (p, ms, what) => Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error(`${what} took over ${ms} ms`)), ms).unref())]); ++ ++test("an over-long launch request is refused at once, not at the 10 s idle timeout", async (t) => { ++ const { f } = await setup(t); ++ const s = connect(launchSocketPath(f.dataRoot)); ++ t.after(() => s.destroy()); ++ let buf = ""; ++ s.on("data", (b) => (buf += b)); ++ s.write("x".repeat(5000)); ++ await within(once(s, "end"), 3000, "the refusal"); ++ assert.deepEqual(JSON.parse(buf), { ok: false, error: "invalid-request" }); ++}); ++ ++test("a launch client that never closes its side doesn't hold the host's close", { timeout: 30000 }, async (t) => { ++ const { f, close } = await setup(t); ++ const path = launchSocketPath(f.dataRoot); ++ // One got its reply and never ends; one never sends anything. ++ const answered = connect({ path, allowHalfOpen: true }); ++ const silent = connect({ path, allowHalfOpen: true }); ++ const connected = once(silent, "connect"); ++ const gone = Promise.all([once(answered, "close"), once(silent, "close")]); ++ const drop = () => { ++ answered.destroy(); ++ silent.destroy(); ++ }; ++ t.after(drop); ++ let buf = ""; ++ answered.on("data", (b) => (buf += b)); ++ answered.write("not json\n"); ++ await once(answered, "end"); ++ assert.deepEqual(JSON.parse(buf), { ok: false, error: "invalid-request" }); ++ await within(connected, 2000, "connect"); ++ // On a failure the clients go, so the host can still close and the test ++ // fails instead of hanging. ++ const code = await within(close(), 5000, "close").catch((e) => { ++ drop(); ++ throw e; ++ }); ++ assert.equal(code, 0); ++ assert.equal(existsSync(path), false); ++ answered.end(); ++ silent.end(); ++ await within(gone, 2000, "the clients' close"); ++}); ++ ++test("a runner that ignores SIGTERM is killed when the host closes", { skip, timeout: 60000 }, async (t) => { ++ const { f, launcher, close } = await setup(t, undefined, undefined, { stopTimeoutMs: 1000 }); ++ const pm = await launcher.launchPm(); ++ const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log"); ++ await until(() => existsSync(runnerLog) && readFileSync(runnerLog, "utf8").includes("claimed by run")); ++ const [s] = readSessions(f.dataRoot); ++ // Stopped from outside its namespace, it can't act on SIGTERM. ++ process.kill(s.runnerPid, "SIGSTOP"); ++ const kill = () => { ++ if (startTimeOf(s.runnerPid) === s.runnerStartTime) process.kill(s.runnerPid, "SIGKILL"); ++ }; ++ t.after(kill); ++ const started = Date.now(); ++ const code = await within(close(), 15000, "close").catch((e) => { ++ kill(); ++ throw e; ++ }); ++ assert.equal(code, 0); ++ assert.ok(Date.now() - started >= 1000, "it waited for the stop timeout"); ++ assert.equal(startTimeOf(s.pid), null); ++ assert.equal(startTimeOf(s.runnerPid), null); ++ assert.deepEqual(readSessions(f.dataRoot), []); ++ const ends = log(f).filter((e) => e.event === "end" && e.run === pm.run); ++ assert.equal(ends.length, 1); ++ assert.equal(ends[0].signal, "SIGKILL"); ++}); diff --git a/packages/cli/tests/verbs.test.mjs b/packages/cli/tests/verbs.test.mjs new file mode 100644 index 00000000..41432793 @@ -1956,10 +2211,10 @@ index 00000000..41432793 +}); diff --git a/packages/harness/README.md b/packages/harness/README.md new file mode 100644 -index 00000000..bc2dcc85 +index 00000000..79ff829a --- /dev/null +++ b/packages/harness/README.md -@@ -0,0 +1,154 @@ +@@ -0,0 +1,167 @@ +# harness + +What a managed role session runs from and runs as (slice 1 S6, issue #1523): @@ -1988,10 +2243,13 @@ index 00000000..bc2dcc85 +| 4. gate hang | the runner's wall clock plus the `agent_end` marker | `timeout -k 2 10 \|\| exit 2`, hook timeout 20 | +| 5. bash | limited only by the tool limit | limited only by the tool limit | + -+Claude Code also runs with `--restricted` (no user, project or local -+settings; file tools confined to the working directory). That is defence -+in depth: none of the S0 lines depend on it, and no test treats it as the -+layer that holds. ++Claude Code also runs with `--restricted`: no user, project or local ++settings, file tools confined to the working directory, and no `CLAUDE.md` ++file (user, parent or workspace) or auto-memory in the prompt. None of the ++S0 lines depend on it, and the gate doesn't rely on it for paths. It is the ++layer that keeps founder and repository memory out of the session's ++prompt, though, so `claude-session.test.mjs` fails if the adapter stops ++passing it, and shows the memory reaching the model without it. + +## The bundle + @@ -2036,7 +2294,10 @@ index 00000000..bc2dcc85 +policy's built-in tools and typed tools pass, anything else is blocked, and +every path argument of a file tool (and a `find`/`Glob` pattern) must +resolve inside the workspace after symlinks and Pi's own path -+normalisation. Pi calls it from the extension, Claude Code from ++normalisation. A path that reaches a dangling symlink, at any depth, is ++refused even when the link points inside: a write through it would create ++the target wherever it names, and Pi's `write` makes the missing ++directories first. Pi calls it from the extension, Claude Code from +`claude-gate.mjs`. + +## The runner @@ -2110,8 +2371,15 @@ index 00000000..bc2dcc85 +- **Resolution** runs without the project layer, there is no skills source + (bundles list none), and the resolved `thinking` level is recorded in the + manifest but not applied to either harness. -+- **`--restricted`** (Claude Code) is an extra layer, not one the S0 lines -+ prove. ++- **`--restricted`** (Claude Code) is not one of the S0 lines. It is what ++ keeps `CLAUDE.md` files and auto-memory out of the prompt (above). ++- **The gate checks a path when the call is made.** A link created or ++ changed between the check and the tool's own open (by `bash`, or by ++ another process of the same user) isn't seen. That is the same reach as ++ `bash` itself. ++- **The system prompt is in argv** for both adapters, so the same UID can ++ read it in `/proc//cmdline`; the PID namespace hides it from other ++ sessions. It holds no secret. +- **Pi adapter paths** with spaces break its unquoted `-e` and skill + splitting; the launcher's paths don't contain spaces. diff --git a/packages/harness/package.json b/packages/harness/package.json @@ -2329,10 +2597,10 @@ index 00000000..ecada305 +} diff --git a/packages/harness/src/gate.mjs b/packages/harness/src/gate.mjs new file mode 100644 -index 00000000..322a84ce +index 00000000..47cc029e --- /dev/null +++ b/packages/harness/src/gate.mjs -@@ -0,0 +1,93 @@ +@@ -0,0 +1,102 @@ +// The tool gate both harnesses share. decide(policy, tool, input) answers +// one tool call: the policy's built-in tools and its typed tools pass, +// everything else is blocked, and every path argument of a file tool must @@ -2343,7 +2611,7 @@ index 00000000..322a84ce +// and anything bash can reach, the session can reach. See the README's +// limits. + -+import { existsSync, realpathSync } from "node:fs"; ++import { lstatSync, realpathSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; @@ -2376,17 +2644,25 @@ index 00000000..322a84ce + return s; +} + -+// Realpath of the nearest existing ancestor, with the missing tail kept. ++// Realpath of the nearest ancestor that exists as a name, with the missing ++// tail kept. lstat, not exists: a dangling symlink exists as a name, and a ++// write through it would create its target wherever that is. So a path that ++// reaches a dangling symlink, at any depth, can't be checked and is refused. +function real(p) { + let head = p; + const tail = []; -+ while (!existsSync(head)) { ++ while (!lstatSync(head, { throwIfNoEntry: false })) { + const up = dirname(head); + if (up === head) break; + tail.unshift(basename(head)); + head = up; + } -+ return join(realpathSync(head), ...tail); ++ try { ++ return join(realpathSync(head), ...tail); ++ } catch (error) { ++ if (error.code === "ENOENT") throw Object.assign(new Error("dangling symlink"), { code: "dangling-symlink" }); ++ throw error; ++ } +} + +export function insideWorkspace(workspace, p) { @@ -2422,6 +2698,7 @@ index 00000000..322a84ce + try { + if (!insideWorkspace(policy.workspace, value)) return no(`${tool} path is outside the workspace: ${value}`); + } catch (error) { ++ if (error.code === "dangling-symlink") return no(`${tool} path goes through a dangling symlink: ${value}`); + return no(`${tool} path can't be checked: ${error.code ?? error.message}`); + } + return { allow: true }; @@ -2548,7 +2825,7 @@ index 00000000..2b3b0044 +} diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs new file mode 100644 -index 00000000..e2e02d4d +index 00000000..ccf6ca43 --- /dev/null +++ b/packages/harness/src/runner.mjs @@ -0,0 +1,372 @@ @@ -2810,16 +3087,16 @@ index 00000000..e2e02d4d + process.on("SIGTERM", stop); + process.on("SIGINT", stop); + -+ let session, cap; ++ let session, cap, policy; + try { + session = { ...DEFAULTS, ...JSON.parse(readFileSync(join(runDir, "session.json"), "utf8")), runDir }; + cap = JSON.parse(await readLine(stdin)).cap; + if (typeof cap !== "string" || !/^[0-9a-f]{64}$/.test(cap)) throw new Error("no capability on stdin"); ++ policy = JSON.parse(readFileSync(session.bundle.policy, "utf8")); + } catch (e) { + log(`runner: ${e.message}`); + return EXIT.usage; + } -+ const policy = JSON.parse(readFileSync(session.bundle.policy, "utf8")); + if (stopping) { + log("runner: stopped before claim"); + return EXIT.stopped; @@ -3274,20 +3551,21 @@ index 00000000..b5d24e17 +}); diff --git a/packages/harness/tests/claude-session.test.mjs b/packages/harness/tests/claude-session.test.mjs new file mode 100644 -index 00000000..823a31ee +index 00000000..e45c5d69 --- /dev/null +++ b/packages/harness/tests/claude-session.test.mjs -@@ -0,0 +1,147 @@ +@@ -0,0 +1,209 @@ +// The host's claude CLI through adapters/claude with the bundle's hook and +// MCP server, against the scripted Messages API. Scratch CLAUDE_CONFIG_DIR +// and HOME, a dummy key, nonessential traffic off: nothing reaches a real -+// model or the user's Claude configuration. Skipped when claude isn't on PATH. ++// model or the user's Claude configuration. Skipped when claude isn't on PATH, ++// except the argv check, which uses a stand-in claude. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawn, spawnSync } from "node:child_process"; -+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -+import { dirname, join } from "node:path"; ++import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; ++import { basename, dirname, join } from "node:path"; +import { buildBundle } from "../src/bundle.mjs"; +import { adapterEnv } from "../src/runner.mjs"; +import { fakeToolSocket, mockAnthropic, REPO, resolvedFor, scratch } from "./helpers.mjs"; @@ -3337,9 +3615,9 @@ index 00000000..823a31ee + return { dir, workspace, api, sock, s, env, manifest }; +} + -+function turn(env, request, cwd) { ++function turn(env, request, cwd, adapter = ADAPTER) { + return new Promise((resolve) => { -+ const child = spawn("/bin/sh", [ADAPTER], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true }); ++ const child = spawn("/bin/sh", [adapter], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (b) => (stdout += b)); @@ -3416,6 +3694,67 @@ index 00000000..823a31ee + assert.equal(readFileSync(join(s.sessionDir, "claude-session-id"), "utf8"), id); +}); + ++// --restricted is what keeps CLAUDE.md files and auto-memory out of the ++// session's prompt (README "Limits"). This one runs without claude: a ++// stand-in records the adapter's argv. ++test("claude adapter: --restricted is always passed", (t) => { ++ const dir = scratch(t); ++ mkdirSync(join(dir, "bin")); ++ writeFileSync(join(dir, "bin", "claude"), '#!/bin/sh\nprintf "%s\\n" "$@" > "$ARGV_OUT"\necho ok\n'); ++ chmodSync(join(dir, "bin", "claude"), 0o755); ++ for (const f of ["prompt.md", "settings.json", "mcp.json"]) writeFileSync(join(dir, f), "{}"); ++ const r = spawnSync("/bin/sh", [ADAPTER], { ++ encoding: "utf8", ++ stdio: ["ignore", "pipe", "pipe"], ++ env: { ++ PATH: `${join(dir, "bin")}:/usr/bin:/bin`, ++ ARGV_OUT: join(dir, "argv"), ++ MOSAIC_SYSTEM_PROMPT_FILE: join(dir, "prompt.md"), ++ MOSAIC_REQUEST: "x", ++ MOSAIC_WORKSPACE: join(dir, "ws"), ++ MOSAIC_SESSION_DIR: join(dir, "session"), ++ MOSAIC_MODEL: "claude-sonnet-5-5", ++ MOSAIC_CLAUDE_SETTINGS: join(dir, "settings.json"), ++ MOSAIC_CLAUDE_MCP_CONFIG: join(dir, "mcp.json"), ++ }, ++ }); ++ assert.equal(r.status, 0, r.stderr); ++ const argv = readFileSync(join(dir, "argv"), "utf8").split("\n"); ++ assert.ok(argv.includes("--restricted"), argv.join(" ")); ++ assert.ok(!argv.includes("--bare"), argv.join(" ")); ++}); ++ ++test("claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do", { skip }, async (t) => { ++ const { dir, workspace, env } = await session(t, []); ++ const slug = realpathSync(workspace).replace(/[/.]/g, "-"); ++ const plant = { ++ "MARKER-USER": [join(env.HOME, ".claude", "CLAUDE.md"), join(env.CLAUDE_CONFIG_DIR, "CLAUDE.md")], ++ "MARKER-PARENT": [join(dir, "CLAUDE.md")], ++ "MARKER-WS": [join(workspace, "CLAUDE.md")], ++ "MARKER-MEMORY": [join(env.HOME, ".claude", "projects", slug, "memory", "MEMORY.md"), join(env.CLAUDE_CONFIG_DIR, "projects", slug, "memory", "MEMORY.md")], ++ }; ++ for (const [marker, files] of Object.entries(plant)) { ++ for (const f of files) { ++ mkdirSync(dirname(f), { recursive: true }); ++ writeFileSync(f, `${marker}\n`); ++ } ++ } ++ const seen = async (adapter) => { ++ const api = await mockAnthropic(t, {}); ++ const r = await turn({ ...env, ANTHROPIC_BASE_URL: api.url, MOSAIC_SESSION_DIR: join(dir, `session-${basename(adapter)}`) }, "x", workspace, adapter); ++ assert.equal(r.code, 0, r.stderr); ++ const all = api.requests.map((x) => x.raw).join("\n"); ++ assert.match(all, /## This session/); ++ return Object.keys(plant).filter((m) => all.includes(m)); ++ }; ++ assert.deepEqual(await seen(ADAPTER), []); ++ // The control: the same adapter without --restricted lets all four in. ++ const loose = join(dir, "adapter-loose.sh"); ++ writeFileSync(loose, readFileSync(ADAPTER, "utf8").replace(" --restricted \\\n", "")); ++ assert.notEqual(readFileSync(loose, "utf8"), readFileSync(ADAPTER, "utf8")); ++ assert.deepEqual(await seen(loose), Object.keys(plant)); ++}); ++ +test("claude: a missing hook or MCP file refuses before claude starts", { skip }, async (t) => { + const { dir, api, workspace, env } = await session(t, []); + for (const k of ["MOSAIC_CLAUDE_SETTINGS", "MOSAIC_CLAUDE_MCP_CONFIG", "MOSAIC_SYSTEM_PROMPT_FILE"]) { @@ -3469,10 +3808,10 @@ index 00000000..4024172d +} diff --git a/packages/harness/tests/gate.test.mjs b/packages/harness/tests/gate.test.mjs new file mode 100644 -index 00000000..66e00bb3 +index 00000000..c32394c2 --- /dev/null +++ b/packages/harness/tests/gate.test.mjs -@@ -0,0 +1,107 @@ +@@ -0,0 +1,130 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdirSync, symlinkSync, writeFileSync } from "node:fs"; @@ -3554,6 +3893,29 @@ index 00000000..66e00bb3 + blocked(decide(policy, "write", { path: "link/new.txt" }), /outside the workspace/); +}); + ++test("a dangling symlink is refused at any depth, in both harnesses", (t) => { ++ for (const [harness, tool, field] of [["pi", "write", "path"], ["claude-code", "Write", "file_path"]]) { ++ const { dir, workspace, policy } = setup(t, harness, ["read", "write"]); ++ mkdirSync(join(dir, "outside")); ++ // notes.md names a file that doesn't exist yet, outside; dangling names a ++ // directory that doesn't; inner points inside but at nothing. ++ symlinkSync(join(dir, "outside", "planted.txt"), join(workspace, "notes.md")); ++ symlinkSync(join(dir, "nowhere"), join(workspace, "dangling")); ++ symlinkSync(join(workspace, "later.txt"), join(workspace, "inner")); ++ for (const rel of ["notes.md", "dangling/x", "dangling/deep/x", "inner"]) { ++ blocked(decide(policy, tool, { [field]: rel }), /goes through a dangling symlink/); ++ blocked(decide(policy, tool, { [field]: join(workspace, rel) }), /goes through a dangling symlink/); ++ } ++ // Once the target exists, the usual realpath rule decides. ++ writeFileSync(join(dir, "outside", "planted.txt"), "p"); ++ blocked(decide(policy, tool, { [field]: "notes.md" }), /outside the workspace/); ++ writeFileSync(join(workspace, "later.txt"), "l"); ++ allowed(decide(policy, tool, { [field]: "inner" })); ++ // A file used as a directory can't be checked. ++ blocked(decide(policy, tool, { [field]: "a.txt/x" }), /can't be checked: ENOTDIR/); ++ } ++}); ++ +test("claude path fields per tool", (t) => { + const { workspace, policy } = setup(t, "claude-code", ["read", "write", "edit", "grep", "find"]); + allowed(decide(policy, "Read", { file_path: join(workspace, "a.txt") })); @@ -3582,7 +3944,7 @@ index 00000000..66e00bb3 +}); diff --git a/packages/harness/tests/helpers.mjs b/packages/harness/tests/helpers.mjs new file mode 100644 -index 00000000..19f13a64 +index 00000000..96c1923f --- /dev/null +++ b/packages/harness/tests/helpers.mjs @@ -0,0 +1,105 @@ @@ -3667,7 +4029,7 @@ index 00000000..19f13a64 + const results = toolResults(body.messages); + const step = tools.length ? script[results.length] : null; + const r = step ? { kind: "tool", id: `toolu_${results.length + 1}`, name: step.name, input: step.input } : { kind: "text", text: tools.length ? done(results) : "mock" }; -+ requests.push({ tools, results, system: body.system, answer: r }); ++ requests.push({ tools, results, system: body.system, answer: r, raw }); + const content = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: {} } : { type: "text", text: "" }; + const delta = r.kind === "tool" ? { type: "input_json_delta", partial_json: JSON.stringify(r.input) } : { type: "text_delta", text: r.text }; + const stop = r.kind === "tool" ? "tool_use" : "end_turn"; @@ -3789,17 +4151,17 @@ index 00000000..72992dde +}); diff --git a/packages/harness/tests/pi-session.test.mjs b/packages/harness/tests/pi-session.test.mjs new file mode 100644 -index 00000000..8fc8b4e4 +index 00000000..0486857f --- /dev/null +++ b/packages/harness/tests/pi-session.test.mjs -@@ -0,0 +1,115 @@ +@@ -0,0 +1,139 @@ +// Real pi (the pinned CLI) through adapters/pi with the extension, against +// the scripted Messages API. No real model, no network beyond 127.0.0.1. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; -+import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"; ++import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { buildBundle } from "../src/bundle.mjs"; +import { adapterEnv } from "../src/runner.mjs"; @@ -3891,6 +4253,30 @@ index 00000000..8fc8b4e4 + assert.ok(readdirSync(s.sessionDir).length > 0); +}); + ++test("pi: a write through a dangling symlink is blocked, and nothing appears outside", async (t) => { ++ const { dir, workspace, s, env } = await session(t, [ ++ { name: "write", input: { path: "notes.md", content: "planted\n" } }, ++ { name: "write", input: { path: "gone/x.md", content: "planted\n" } }, ++ { name: "write", input: { path: "fine.md", content: "inside\n" } }, ++ ]); ++ mkdirSync(join(dir, "outside")); ++ symlinkSync(join(dir, "outside", "planted.txt"), join(workspace, "notes.md")); ++ symlinkSync(join(dir, "outside", "made"), join(workspace, "gone")); ++ const r = await turn(env, "Message 1 from jason, class REQUEST:\n\nwrite your notes", workspace); ++ assert.equal(r.code, 0, r.stderr); ++ const results = JSON.parse(r.stdout.trim().slice("ANSWER ".length)); ++ assert.equal(results.length, 3); ++ assert.equal(results[0][0], true); ++ assert.match(results[0][1], /dangling symlink: notes\.md/); ++ assert.equal(results[1][0], true); ++ assert.match(results[1][1], /dangling symlink: gone\/x\.md/); ++ assert.equal(results[2][0], false); ++ assert.ok(existsSync(join(workspace, "fine.md")), "the write inside landed"); ++ assert.deepEqual(readdirSync(join(dir, "outside")), []); ++ assert.ok(!existsSync(join(dir, "outside", "made"))); ++ assert.ok(existsSync(s.turnMarker)); ++}); ++ +test("pi: a missing extension refuses before any model call", async (t) => { + const { dir, api, s, env } = await session(t, []); + const r = await turn({ ...env, MOSAIC_EXTENSIONS: join(dir, "missing.mjs") }, "x", s.workspace); @@ -3910,10 +4296,10 @@ index 00000000..8fc8b4e4 +}); diff --git a/packages/harness/tests/runner.test.mjs b/packages/harness/tests/runner.test.mjs new file mode 100644 -index 00000000..77027f36 +index 00000000..25b4d8a9 --- /dev/null +++ b/packages/harness/tests/runner.test.mjs -@@ -0,0 +1,312 @@ +@@ -0,0 +1,337 @@ +// The runner against a real broker on a real socket, with a fake adapter. +// The broker runs in this process; the runner is a child, as the host +// starts it (minus the PID namespace, which packages/seat tests). @@ -4026,15 +4412,22 @@ index 00000000..77027f36 + return { dir, runDir, store, broker, server, cap, pm, call, launches, pids: join(dir, "pids") }; +} + -+function startRunner(runDir, cap, env = {}) { ++// A runner that hasn't exited after a minute is killed, so a test that ++// expected an exit fails on the code instead of hanging. ++function startRunner(runDir, cap, env = {}, input = cap === null ? "" : JSON.stringify({ cap }) + "\n") { + const child = spawn(process.execPath, [RUNNER, runDir], { + stdio: ["pipe", "ignore", "pipe"], + env: { PATH: process.env.PATH, ...env }, + }); + let stderr = ""; + child.stderr.on("data", (b) => (stderr += b)); -+ child.stdin.end(cap === null ? "" : JSON.stringify({ cap }) + "\n"); -+ const exited = once(child, "exit").then(([code, signal]) => ({ code, signal, stderr })); ++ child.stdin.on("error", () => {}); ++ child.stdin.end(input); ++ const clock = setTimeout(() => child.kill("SIGKILL"), 60_000); ++ const exited = once(child, "exit").then(([code, signal]) => { ++ clearTimeout(clock); ++ return { code, signal, stderr }; ++ }); + return { child, exited, stderr: () => stderr }; +} + @@ -4224,6 +4617,24 @@ index 00000000..77027f36 + assert.equal((await startRunner(ctx.runDir, null).exited).code, EXIT.usage); + assert.equal((await startRunner(ctx.runDir, "not-hex").exited).code, EXIT.usage); + assert.equal((await startRunner(join(ctx.dir, "nope"), ctx.cap).exited).code, EXIT.usage); ++ // A valid capability inside an over-long line: the 4096-byte cap refuses it. ++ const long = await startRunner(ctx.runDir, ctx.cap, {}, JSON.stringify({ cap: ctx.cap, pad: "x".repeat(5000) }) + "\n").exited; ++ assert.equal(long.code, EXIT.usage, long.stderr); ++ assert.match(long.stderr, /stdin too large/); ++ assert.equal(ctx.store.get("SELECT 1 FROM role_claims WHERE role='coder'"), undefined); ++}); ++ ++test("a missing or malformed policy exits 2 before the claim", async (t) => { ++ const ctx = await setup(t); ++ const policy = join(ctx.runDir, "bundle", "policy.json"); ++ writeFileSync(policy, "{"); ++ const bad = await startRunner(ctx.runDir, ctx.cap).exited; ++ assert.equal(bad.code, EXIT.usage, bad.stderr); ++ assert.match(bad.stderr, /^runner: /m); ++ rmSync(policy); ++ const missing = await startRunner(ctx.runDir, ctx.cap).exited; ++ assert.equal(missing.code, EXIT.usage, missing.stderr); ++ assert.match(missing.stderr, /runner: ENOENT/); + assert.equal(ctx.store.get("SELECT 1 FROM role_claims WHERE role='coder'"), undefined); +}); diff --git a/packages/harness/tests/tools.test.mjs b/packages/harness/tests/tools.test.mjs diff --git a/agents/filbert/work/s6/candidate-manifest.sha256 b/agents/filbert/work/s6/candidate-manifest.sha256 index 0a602d8d..fbef2ba7 100644 --- a/agents/filbert/work/s6/candidate-manifest.sha256 +++ b/agents/filbert/work/s6/candidate-manifest.sha256 @@ -1,37 +1,38 @@ -2a223392268c2ff798a3718ba0386335877ac998c124e48c74c264be71397c53 adapters/claude/adapter.sh -962255271e95cb30788e97cd9e86e17f384dd5f74cead435692e8b30e47af9cf adapters/pi/adapter.sh 863640ee44598a5bffd6d37d328dcddfd4bdc671f792d029ae2ee18b1055b28b adapters/README.md +8121e4e05b0559471e0d44fc0325fb08c8a883ab3db1bca451a956753cccdfe3 adapters/claude/adapter.sh +962255271e95cb30788e97cd9e86e17f384dd5f74cead435692e8b30e47af9cf adapters/pi/adapter.sh 009059ea86e1d14c5b12ed0fdad64e8cd501e19021ef6f4148a1463d55860125 docs/TOOLS.md -8cbbe58045188489932c63d4361c4b0679ae7fbf8a4a7341fc8cad43ec2b26ac packages/bus/README.md +49dc3cf603358fdbce768faaa9ebe8b5e4359a1aa813c3ffeef0d96a01d37035 packages/bus/README.md aa71088d656455de83d9c1a42745852041ee61e2d5eb6f4c1e48e8ae29623433 packages/bus/src/broker.mjs -eb7a281746bacc65da303ff90f7bef709793c82606f177c5f21ff2005a966a3c packages/bus/src/process.mjs +0b51592777d2b035e79e2864d061615e81591bf99d43820ea9b3e52f8cf56559 packages/bus/src/process.mjs 12634ff6b6ef5b5a282bb306b30c9f02929d1fb6597e149d47d21069201399b5 packages/bus/src/runtime.mjs -1e301c58a562c7d9c1669dc657be708b17d37f26816af755894cc727653f7d6b packages/bus/tests/end-launch.test.mjs -667998fd1af37818e65b16bb515f22db73ab6077c5449367e731d16590598783 packages/cli/README.md +5b06f799e18deba2ee2eb737322f0dcfdd4a8eeae8c92e465f5acdbb894483dd packages/bus/tests/end-launch.test.mjs +e5e41c8bef670daac0507d860f7104c446c736a3897d247cbacb5ab36b440d41 packages/cli/README.md bd207b81a2b9965b9e46da66ab31ed9a587b87e8669e8293184d4b842e45bff5 packages/cli/src/cli.mjs -9e5e08e78be214e2cfe8a1a9a0d310231d53f503e2efc1814ee00efab38fd280 packages/cli/src/host.mjs -bf7115a9404a7d3b7406d078c6be409d0d6f726d5b88496a6995ddb1c17a6fa2 packages/cli/src/launcher.mjs +e9eeec4bef3384b5b15d1e7a2c9d913d2f3e329228c3e3e2cacb8f741aa21379 packages/cli/src/host.mjs +2d0b075982f295a88161607d2795aadc86f286994ab6cc31873b83b2daebf7fa packages/cli/src/launcher.mjs 9b30a3bfe96a5d7c6956b6c75196c08bc35ceec302859337915ca1cfe2a76b76 packages/cli/tests/fixtures/launch-host.mjs -aa705a5bc9de9fa50471b5aba6217fba6c36bc2479590d88a9197c99c8c94cec packages/cli/tests/launcher.test.mjs +436c15af25c48a8135d6b4bbb7df26d318b87da32fc6432455f23043a07e791c packages/cli/tests/host.test.mjs +670e3a8dd3e57b96b96f6932693853e1149e53da577299931cbe3d428ddb89da packages/cli/tests/launcher.test.mjs 709bd331bb0d76f28b464e518f4e1fc3bb0b303614e79d7e82479dcd679043cc packages/cli/tests/verbs.test.mjs +07f8033da769b18c194520356f900dd10a12d146d4d10f1a80e3c40a2a60655e packages/harness/README.md 34ef8212e27f052223443c66830839517f7a54ecb6ef7d85795b3e89c65b4d4c packages/harness/package.json -8b3bab21b0725cb19690cde0036a6899bc608911e14446cb32139dfbbcb10c6b packages/harness/README.md 22efd0bed7991561920ad29f6bf9a1ab2d7384185fab4267684a037e026d8e85 packages/harness/src/bundle.mjs 32b5090760c95eea0e1232ff36ec13a1d9b58335b2eade621196dec1ebdbe784 packages/harness/src/claude-gate.mjs -0144ed5591d941689e8cf783daaed445ec728507d43b246cf3fffa317b0f6599 packages/harness/src/gate.mjs +570a1e64db8624b57eb972fd7599c7543e3c41324d0e9666cb5701d5491ec39b packages/harness/src/gate.mjs 71e00113b8e5b55b410c7aae6232f76e972fc77aa68afa893da45c6b78d297e2 packages/harness/src/mcp-server.mjs d19753fa72f0b57e751749359644093713bcb650bfac566f55d2bc05cb817121 packages/harness/src/pi-extension.mjs -4065d94c84bc08f570a43071bce1955fed46503352242ebdca806e011ec624d7 packages/harness/src/runner.mjs +1047aa092080e92efde2044dddaf82bacf428ed4b143c3846693471481f3612e packages/harness/src/runner.mjs 92153d9e2161ceef4ee76f2ff992014760a8c62b0ea709b51e2ea0ce965d3edb packages/harness/src/tools.mjs 96796238b7effab78cc6356ed8ea163f02aa39999d8dfc97fef83d45309d6574 packages/harness/tests/bundle.test.mjs 96524da43b212e84d78108cbbf05eef324c934f3ad9e0d4cb2edcb0f3df256d4 packages/harness/tests/claude-gate.test.mjs -3846c68c0fda682952fe7d76e8e2006ea2d1b355516897795b4a61f55ba47ea2 packages/harness/tests/claude-session.test.mjs +197ec0f6c842552bea65fb2ab4c8cb8615fd6e691a2d0a592e1465d18a45d75f packages/harness/tests/claude-session.test.mjs 8392172b243356932c974bdca9b4c449a312ae7a042ee7a22e0f22fbb98dbec9 packages/harness/tests/fixtures/fake-adapter.mjs -df3225783a1fa35f1084a3c89b5496185901d0bb8e65f13165a5c07ea5047ee6 packages/harness/tests/gate.test.mjs -e9b0dfc28f187d58714bfdf57ec7a5f3eb19dbaa8ff6d267e19f2ad10404d073 packages/harness/tests/helpers.mjs +8576749388b3f962ed6cbd694d9af814665f3f491f278a3cdf9b9c5435388460 packages/harness/tests/gate.test.mjs +6e7509cfe6ad909440c25b750528360c0ba617c6c68b05d400bcd94b481c1f76 packages/harness/tests/helpers.mjs 793eb11f970e4a8eecddec4dc48c24331e4de795bc04bfadf806a2fc3a15a8b4 packages/harness/tests/mcp-server.test.mjs -92529a27ebac228fe2a03a12a0a2ee3b29203af4a26182a0e380a299dc327449 packages/harness/tests/pi-session.test.mjs -2fc5a3522297c16f31ced5f31f707277e18e1f860f0a6372faef3e86ea5d2ecd packages/harness/tests/runner.test.mjs +ba907e3bee4547c97c8700b4b19febc46c19074a259dd113b727522d68115273 packages/harness/tests/pi-session.test.mjs +c8f23144316501c3e163cf5a5566ddee552b3c93dee13594ae8c1eac66aedc64 packages/harness/tests/runner.test.mjs ef9130a3cb1ca3e278a8ed370060afd1145d1ceb211a915e83d75c8346b04931 packages/harness/tests/tools.test.mjs 4e34456187387b02fa6d996c270dea4076b5d57952cddaa01f7a04843b351a02 packages/seat/README.md 382cbf7e0ff336911e288ce858bdbfbec693bc2b69879720d1f0b4c7d8455198 packages/seat/src/proc.mjs diff --git a/agents/filbert/work/s6/files.txt b/agents/filbert/work/s6/files.txt index 57fbc3e5..67685134 100644 --- a/agents/filbert/work/s6/files.txt +++ b/agents/filbert/work/s6/files.txt @@ -1,6 +1,6 @@ +adapters/README.md adapters/claude/adapter.sh adapters/pi/adapter.sh -adapters/README.md docs/TOOLS.md packages/bus/README.md packages/bus/src/broker.mjs @@ -12,10 +12,11 @@ packages/cli/src/cli.mjs packages/cli/src/host.mjs packages/cli/src/launcher.mjs packages/cli/tests/fixtures/launch-host.mjs +packages/cli/tests/host.test.mjs packages/cli/tests/launcher.test.mjs packages/cli/tests/verbs.test.mjs -packages/harness/package.json packages/harness/README.md +packages/harness/package.json packages/harness/src/bundle.mjs packages/harness/src/claude-gate.mjs packages/harness/src/gate.mjs diff --git a/agents/filbert/work/s6/out/node-bus.txt b/agents/filbert/work/s6/out/node-bus.txt index aea221d5..3b6f6ca7 100644 --- a/agents/filbert/work/s6/out/node-bus.txt +++ b/agents/filbert/work/s6/out/node-bus.txt @@ -1,77 +1,77 @@ -✔ launch identity is stamped, payload identity is refused and stale holder cannot send (158.150768ms) -✔ decision classes route from policy; gated resolution is human-only, choice and target must match (258.805434ms) -✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (247.340125ms) -✔ launch events require a human CLI capability; generic emit cannot forge authority events (154.366458ms) -✔ within-role decisions close atomically and invalid options or blocking omissions refuse (141.971144ms) -✔ observer capabilities read human inbox but cannot mutate or forge launch identity (131.526953ms) -✔ task action subjects and linked decision trail are complete and ordered (132.519219ms) -✔ launch binding is durable and reconnecting requires the identical trusted record (94.302493ms) -✔ business isolation includes inherited object names and cross-business message references (143.859485ms) -✔ authority never transfers between action, run, target, unresolved or replaced role holder (215.909845ms) -✔ task projection uses schema current view, skipping earlier and equal-start polls (108.67248ms) -✔ revocation permanently bars the old run from reclaiming first, including after broker restart (165.948325ms) -✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (103.673551ms) -✔ both arbiters require human resolution when their cross-role route is themselves (179.340595ms) -✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (3.248662ms) -✔ only validated broker references load; returned data and exceptions cannot expose a known token (7.094661ms) -✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (5.019312ms) -✔ expiry refuses use and env references never become client data (3.278188ms) -✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.965278ms) -✔ opaque tokens shorter than 16 characters refuse before use (0.609186ms) -✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (128.865172ms) -✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (145.44321ms) -✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (142.744227ms) -✔ endLaunch leaves a claim another run took alone (153.860535ms) -✔ refuse records action.refused against the caller with the code only (109.560376ms) -✔ launches off refuses role.launch with launch-revoked until launches on (158.905054ms) -✔ broker process: launch ops authorize role.launch, record refusals and end runs (203.667266ms) -✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (6.466732ms) -✔ process reader gets own kernel identity without exposing environment values (1.515607ms) -✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.392126ms) -✔ real pid 1 remains inspectable when its environment is protected (0.473384ms) -✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (182.153792ms) -✔ missing and foreign-business citations refuse and roll back message and grant (183.462033ms) -✔ cross-role sends still need a matching resolved decision and consume it once (225.561481ms) -✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (192.194069ms) -✔ startup token refusal returns safe code without value or partial listening broker (38.623009ms) -✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (167.701961ms) -✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (169.750172ms) -✔ trusted host registers later launches; socket clients never have a registration verb (166.888863ms) -✔ runtime excludes declared project roots even when host supplies no repoRoots (39.861791ms) -✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (149.249527ms) -✔ v3b prototype refusals, views and append-only mutations (942.242629ms) -✔ gated approval authorizes once, survives store reopen, and fresh approval works (222.807767ms) -✔ another run cannot consume an approval; a failed check leaves it usable (216.443103ms) -✔ two scheduled callers have exactly one grant and one consumed refusal (149.72494ms) -✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (276.865404ms) -✔ class drift gated to cross-role refuses before consumption (170.894519ms) -✔ class drift cross-role to gated refuses before consumption (158.250182ms) -✔ class drift gated to within-role refuses before consumption (177.520217ms) -✔ class drift cross-role to within-role refuses before consumption (185.093462ms) -✔ class drift within-role to gated refuses before consumption (144.027639ms) -✔ class drift within-role to cross-role refuses before consumption (152.049889ms) -✔ message.send consumes approval and prevents a later send or authorize (168.498462ms) -✔ role.revoke consumes approval and prevents a later revoke or authorize (198.374954ms) -✔ creates private WAL store and excludes a second writer until explicit close (116.738279ms) -✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (175.406652ms) -✔ existing empty database and symlink runtime directory refuse, never initialize over damage (182.907272ms) -✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (154.638962ms) -✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (94.948081ms) -✔ async transactions refuse before invoking their function (78.29418ms) -✔ recordTask keeps sync reads and a role write apart (155.00518ms) -✔ read_at must be one canonical UTC format, so the projection compares strings safely (97.553598ms) -✔ a bad entry refuses the whole record (103.483603ms) -✔ taskView reads the projection for one business (126.462963ms) -✔ requestTask hands only a holder and a task verb to the handler, and records refusals (216.111452ms) -✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (389.096711ms) -✔ without an adapter the server refuses every task verb (159.18258ms) -✔ the runtime refuses an invalid adapter and closes a valid one (154.938435ms) -✔ the process loads the S3 adapter from plain-data trackers (236.701945ms) -✔ socket capability stamps launch identity; shared views use wire, no SQL client (138.617926ms) -✔ two wire claims serialize; a lost reply never automatically retries (171.818149ms) -✔ malformed, oversized and identity-forging envelopes refuse without echoing input (110.874989ms) -✔ client preserves UTF-8 when a response divides a multibyte character (11.810021ms) -✔ committed mutation followed by dropped reply reports unknown and is never retried (120.128497ms) +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (158.294055ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (259.614212ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (242.506362ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (161.976041ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (148.695884ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (137.583297ms) +✔ task action subjects and linked decision trail are complete and ordered (186.436959ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (104.399977ms) +✔ business isolation includes inherited object names and cross-business message references (188.02737ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (350.15037ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (159.818248ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (189.67006ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (124.230049ms) +✔ both arbiters require human resolution when their cross-role route is themselves (199.180591ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.264919ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.481319ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (4.18746ms) +✔ expiry refuses use and env references never become client data (1.105304ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (7.468986ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.513997ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (135.481002ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (134.248144ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (153.895653ms) +✔ endLaunch leaves a claim another run took alone (161.970249ms) +✔ refuse records action.refused against the caller with the code only (111.340973ms) +✔ launches off refuses role.launch with launch-revoked until launches on (160.281981ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (221.362748ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.096307ms) +✔ process reader gets own kernel identity without exposing environment values (2.657909ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.273874ms) +✔ real pid 1 remains inspectable when its environment is protected (0.505849ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (188.712257ms) +✔ missing and foreign-business citations refuse and roll back message and grant (177.133146ms) +✔ cross-role sends still need a matching resolved decision and consume it once (230.758224ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (193.74033ms) +✔ startup token refusal returns safe code without value or partial listening broker (39.198239ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (184.106087ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (157.880698ms) +✔ trusted host registers later launches; socket clients never have a registration verb (173.751442ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (40.167143ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (154.175551ms) +✔ v3b prototype refusals, views and append-only mutations (974.316595ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (249.273742ms) +✔ another run cannot consume an approval; a failed check leaves it usable (219.736933ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (156.511323ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (273.588058ms) +✔ class drift gated to cross-role refuses before consumption (196.834803ms) +✔ class drift cross-role to gated refuses before consumption (220.857243ms) +✔ class drift gated to within-role refuses before consumption (200.437464ms) +✔ class drift cross-role to within-role refuses before consumption (298.862655ms) +✔ class drift within-role to gated refuses before consumption (232.176051ms) +✔ class drift within-role to cross-role refuses before consumption (171.660401ms) +✔ message.send consumes approval and prevents a later send or authorize (191.809221ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (196.68699ms) +✔ creates private WAL store and excludes a second writer until explicit close (116.781089ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.437705ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (169.581936ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (158.747085ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (99.516826ms) +✔ async transactions refuse before invoking their function (86.659243ms) +✔ recordTask keeps sync reads and a role write apart (149.917777ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (117.042328ms) +✔ a bad entry refuses the whole record (99.401189ms) +✔ taskView reads the projection for one business (121.318568ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (221.737809ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (392.357601ms) +✔ without an adapter the server refuses every task verb (200.843487ms) +✔ the runtime refuses an invalid adapter and closes a valid one (213.365501ms) +✔ the process loads the S3 adapter from plain-data trackers (327.129025ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (140.998311ms) +✔ two wire claims serialize; a lost reply never automatically retries (179.486351ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (118.361562ms) +✔ client preserves UTF-8 when a response divides a multibyte character (12.083191ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (127.825313ms) ℹ tests 74 ℹ suites 0 ℹ pass 74 @@ -79,4 +79,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 2336.232793 +ℹ duration_ms 2747.495478 diff --git a/agents/filbert/work/s6/out/node-business.txt b/agents/filbert/work/s6/out/node-business.txt index 1050b02e..1411b835 100644 --- a/agents/filbert/work/s6/out/node-business.txt +++ b/agents/filbert/work/s6/out/node-business.txt @@ -1,63 +1,63 @@ -✔ config directory and file path follow MOSAIC_CONFIG (1.341178ms) -✔ the fixture business validates and comes back frozen (4.26895ms) -✔ two instances may share a definition (1.580704ms) -✔ top-level refusals (4.501169ms) -✔ arbiters and projects (5.4681ms) -✔ role instances (3.487746ms) -✔ Vikunja bots (8.02749ms) -✔ a role without Vikunja takes no tracker block (10.757229ms) -✔ credential references match the definition's services (3.823637ms) -✔ launch (9.590847ms) -✔ loadBusiness: file checks (1.975951ms) -✔ loadBusiness: not a regular file (41.201527ms) -✔ loading writes nothing (1.314092ms) -✔ names that are Object.prototype properties don't count as declared (2.899494ms) -✔ the shipped example refuses as written and validates once filled in (0.575228ms) -✔ usage errors exit 4 (318.00225ms) -✔ validate: a good business exits 0 and prints instance digests (68.995958ms) -✔ validate: project files (349.686854ms) -✔ validate: missing files and a broken system config (326.343919ms) -✔ validate: credential reference problems exit 2 and name each one (99.678089ms) -✔ validate: a token file inside the repository is refused (78.247664ms) -✔ validate: role definitions come from MOSAIC_ROLES_DIR (216.675669ms) -✔ resolve: prints one instance's record (248.880265ms) -✔ resolve: refusals (412.874057ms) -✔ parse: exactly one of file or env, plus the service's date (2.325764ms) -✔ check: a good file has no problems (0.75954ms) -✔ check never opens the file: a write-only token passes (0.311868ms) -✔ check: file problems (0.763239ms) -✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.798484ms) -✔ check: dates and environment references (0.361131ms) -✔ path and load (2.885491ms) -✔ refusals (1.515385ms) -✔ systemVars flattens the validated config (1.790585ms) -✔ precedence: system, business, project, project role, agent (4.547817ms) -✔ limits narrow the definition and never widen it (2.16752ms) -✔ role.launch stays within-role only for the instance the launch block names (4.55638ms) -✔ limits.authority without role.launch leaves the launcher with no launch block (1.912786ms) -✔ limits.authority narrows cross-role actions too (1.129226ms) -✔ classify (1.062031ms) -✔ the record carries what the broker and launcher need (9.084401ms) -✔ digest: key order doesn't matter, any value change does (7.179908ms) -✔ refusals (2.455766ms) -✔ the four shipped version 2 roles load (3.959226ms) -✔ shipped role scopes match addendum B section 2 and the SR runbook (1.430244ms) -✔ shipped authority follows the note's table (0.81314ms) -✔ version 1 files keep loading with no authority (1.244032ms) -✔ the conductor policy isn't a role (0.262325ms) -✔ a missing role file is exit 4, a symbolic link too (0.547733ms) -✔ version 2 refusals (1.428674ms) -✔ authority: closed vocabulary, no gated-only action, no overlap (2.035327ms) -✔ credentials: Gitea scopes (0.825672ms) -✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.075392ms) -✔ credentials: services (8.42904ms) -✔ contract: a non-empty regular Markdown file beside the role file (0.830731ms) -✔ every key names known layers and a merge rule (0.926442ms) -✔ unknown keys and wrong layers refuse (0.796639ms) -✔ types (1.829487ms) -✔ merge: defaults, then the most specific layer wins (0.289155ms) -✔ merge: limits only narrow, and provenance lists each source (0.406369ms) -✔ merge doesn't change its inputs (0.149857ms) +✔ config directory and file path follow MOSAIC_CONFIG (1.91162ms) +✔ the fixture business validates and comes back frozen (5.712812ms) +✔ two instances may share a definition (1.68694ms) +✔ top-level refusals (4.925076ms) +✔ arbiters and projects (6.586166ms) +✔ role instances (3.076648ms) +✔ Vikunja bots (6.665056ms) +✔ a role without Vikunja takes no tracker block (2.871312ms) +✔ credential references match the definition's services (2.642532ms) +✔ launch (7.613634ms) +✔ loadBusiness: file checks (2.052319ms) +✔ loadBusiness: not a regular file (47.666198ms) +✔ loading writes nothing (1.520534ms) +✔ names that are Object.prototype properties don't count as declared (2.954491ms) +✔ the shipped example refuses as written and validates once filled in (0.63865ms) +✔ usage errors exit 4 (322.723133ms) +✔ validate: a good business exits 0 and prints instance digests (71.239074ms) +✔ validate: project files (341.689159ms) +✔ validate: missing files and a broken system config (291.858814ms) +✔ validate: credential reference problems exit 2 and name each one (75.189953ms) +✔ validate: a token file inside the repository is refused (74.77167ms) +✔ validate: role definitions come from MOSAIC_ROLES_DIR (222.784057ms) +✔ resolve: prints one instance's record (227.709604ms) +✔ resolve: refusals (443.985563ms) +✔ parse: exactly one of file or env, plus the service's date (2.456548ms) +✔ check: a good file has no problems (0.775399ms) +✔ check never opens the file: a write-only token passes (0.407183ms) +✔ check: file problems (0.843938ms) +✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.861362ms) +✔ check: dates and environment references (0.465771ms) +✔ path and load (2.010503ms) +✔ refusals (1.064579ms) +✔ systemVars flattens the validated config (2.461642ms) +✔ precedence: system, business, project, project role, agent (8.241305ms) +✔ limits narrow the definition and never widen it (2.836179ms) +✔ role.launch stays within-role only for the instance the launch block names (5.636504ms) +✔ limits.authority without role.launch leaves the launcher with no launch block (2.024129ms) +✔ limits.authority narrows cross-role actions too (1.025935ms) +✔ classify (1.912616ms) +✔ the record carries what the broker and launcher need (0.981644ms) +✔ digest: key order doesn't matter, any value change does (6.433678ms) +✔ refusals (2.701053ms) +✔ the four shipped version 2 roles load (3.348744ms) +✔ shipped role scopes match addendum B section 2 and the SR runbook (1.219132ms) +✔ shipped authority follows the note's table (0.64651ms) +✔ version 1 files keep loading with no authority (1.080549ms) +✔ the conductor policy isn't a role (0.255061ms) +✔ a missing role file is exit 4, a symbolic link too (0.438644ms) +✔ version 2 refusals (1.487183ms) +✔ authority: closed vocabulary, no gated-only action, no overlap (2.444188ms) +✔ credentials: Gitea scopes (1.019814ms) +✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.419399ms) +✔ credentials: services (0.596546ms) +✔ contract: a non-empty regular Markdown file beside the role file (0.836011ms) +✔ every key names known layers and a merge rule (0.986663ms) +✔ unknown keys and wrong layers refuse (0.879891ms) +✔ types (2.321956ms) +✔ merge: defaults, then the most specific layer wins (0.418729ms) +✔ merge: limits only narrow, and provenance lists each source (0.426782ms) +✔ merge doesn't change its inputs (0.189432ms) ℹ tests 60 ℹ suites 0 ℹ pass 60 @@ -65,4 +65,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 2187.474485 +ℹ duration_ms 2141.835239 diff --git a/agents/filbert/work/s6/out/node-cli.txt b/agents/filbert/work/s6/out/node-cli.txt index fcc7ba42..65eb59c4 100644 --- a/agents/filbert/work/s6/out/node-cli.txt +++ b/agents/filbert/work/s6/out/node-cli.txt @@ -1,87 +1,92 @@ -✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (118.155092ms) -✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (135.340975ms) -✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (110.707482ms) -✔ decide prints a declining choice as declining (92.798251ms) -✔ an unknown outcome is reported once and never resent (87.28131ms) -✔ a decision closed before the answer arrives exits 2 and points at its trail (95.98171ms) -✔ a prefix that matches two open decisions exits 2 and resolves neither (88.223671ms) -✔ without --business a command uses the live host's business, and a stale host.json is not a host (62.710471ms) -✔ every human command refuses inside an agent run before it touches the bus (61.875025ms) -✔ usage errors exit 4; no business and no host is a usage error (65.175578ms) -✔ agents and tasks print through the broker (68.785667ms) -✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.467569ms) -✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (59.037059ms) -✔ trackers come from the tracker.* variables of the one project that names a tracker project (43.777779ms) -✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (50.148848ms) -✔ two projects that each name a tracker project refuse, since the boot shape holds one (53.581893ms) -✔ a business without tracker.baseUrl gets no trackers entry (39.300001ms) -✔ an unknown business and a broken system config refuse with exit 3 (83.383681ms) -✔ empty views say so (1.181626ms) -✔ the trail keeps the broker's order and names a decision's task without its rows (1.366794ms) -✔ tasks print the tracker fields the snapshot carries (0.251098ms) -✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (974.34103ms) -✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (212.356371ms) -✔ a second host for the same data root refuses with exit 3 while the first runs (128.8785ms) -✔ a notifier that refuses stops the broker and the host refuses with exit 3 (222.476083ms) -✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (176.961283ms) -✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (135.640371ms) -✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (198.923159ms) -✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (117.161338ms) -✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (220.743628ms) -✔ watchChildren reports a child that died before it was called, and one that dies later (22.783177ms) -✔ bus stop refuses to signal a live pid that is not a bus host (202.440829ms) -✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (259.625833ms) -✔ bus start refuses with exit 3 without a notifier config (99.337871ms) -✔ bus start runs until bus stop; status reports it while it runs (723.184797ms) -✔ bus-service.sh renders the unit and installs it into a given directory (40.188497ms) -✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1298.529746ms) -✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (648.981918ms) -✔ a runner that stops at once ends its launch with the runner's reason (218.756086ms) -✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (718.902837ms) -✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3649.636207ms) -✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (133.892391ms) -✔ zoned uses the IANA zone across DST (20.415121ms) -✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (116.186459ms) -✔ two blocking decisions get two DMs with different nonces (120.670182ms) -✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.445474ms) -✔ a failed DM is journaled, backs off, and is retried until it lands (120.593616ms) -✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (101.787488ms) -✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (97.104103ms) -✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (92.253269ms) -✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (163.676718ms) -✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.941061ms) -✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (87.725819ms) -✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (63.461429ms) -✔ an inbox read failure is logged and the next poll retries (0.580971ms) -✔ no Discord id reaches the journal or the log (72.972195ms) -✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (11.00011ms) -✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (16.377097ms) -✔ the journal: a whole file that is one torn line truncates to empty (21.36201ms) -✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.604157ms) -✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.56872ms) -✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.811585ms) -✔ the journal: a symlinked directory refuses and says it is a link (0.322853ms) -✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.464501ms) -✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.375347ms) -✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.438934ms) -✔ digest content stays within Discord's 2000 characters (0.353809ms) -✔ runLoop never overlaps ticks and stops after the one in flight (111.16002ms) +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (146.776928ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (136.801344ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (103.086176ms) +✔ decide prints a declining choice as declining (134.723723ms) +✔ an unknown outcome is reported once and never resent (118.430821ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (96.582082ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (93.910913ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (100.191586ms) +✔ every human command refuses inside an agent run before it touches the bus (89.223907ms) +✔ usage errors exit 4; no business and no host is a usage error (83.466371ms) +✔ agents and tasks print through the broker (99.638395ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.56895ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (53.987196ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (50.54766ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (52.575457ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (41.939509ms) +✔ a business without tracker.baseUrl gets no trackers entry (44.220059ms) +✔ an unknown business and a broken system config refuse with exit 3 (77.545035ms) +✔ empty views say so (1.213263ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.465349ms) +✔ tasks print the tracker fields the snapshot carries (0.259502ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (968.120835ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (244.370802ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (195.501934ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1168.798507ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (252.111853ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (192.327382ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (149.865957ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (109.163695ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (152.634134ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (116.928022ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (185.169842ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (26.156512ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.06222ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (218.54117ms) +✔ bus start refuses with exit 3 without a notifier config (87.417832ms) +✔ bus start runs until bus stop; status reports it while it runs (665.256061ms) +✔ bus-service.sh renders the unit and installs it into a given directory (27.206795ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1344.798577ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (619.298083ms) +✔ a runner that stops at once ends its launch with the runner's reason (199.689244ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (651.083095ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3599.486558ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (100.745764ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (115.983641ms) +✔ a launch client that never closes its side doesn't hold the host's close (138.95718ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1228.324814ms) +✔ zoned uses the IANA zone across DST (19.841055ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (152.572961ms) +✔ two blocking decisions get two DMs with different nonces (127.82059ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.211354ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (112.394683ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (138.735105ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (129.023835ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (102.098533ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (172.839344ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (125.552318ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (112.654074ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.674926ms) +✔ an inbox read failure is logged and the next poll retries (0.621548ms) +✔ no Discord id reaches the journal or the log (86.296393ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (41.163659ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (55.929482ms) +✔ the journal: a whole file that is one torn line truncates to empty (24.260055ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.363193ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.952902ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.077859ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.589086ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.754225ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.464759ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.527358ms) +✔ digest content stays within Discord's 2000 characters (0.352942ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.100812ms) task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 -✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (444.699888ms) -✔ the transport writes {business, verb, args} to the child and reads its JSON (47.864309ms) -✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2212.180406ms) -✔ busExit and refuseInsideAgent (0.532377ms) -✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (210.726131ms) -✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1134.898685ms) -✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (98.253828ms) -✔ launches off and on go to the broker and change the business's launch state (87.660028ms) -✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (73.549533ms) -ℹ tests 77 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (450.705988ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (46.868792ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2232.458816ms) +✔ busExit and refuseInsideAgent (0.506966ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (213.349559ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1113.591923ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (116.819293ms) +✔ launches off and on go to the broker and change the business's launch state (109.402112ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (64.2915ms) +ℹ tests 82 ℹ suites 0 -ℹ pass 77 +ℹ pass 82 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 6786.010947 +ℹ duration_ms 8105.490386 diff --git a/agents/filbert/work/s6/out/node-control-board.txt b/agents/filbert/work/s6/out/node-control-board.txt index 8dba721c..4a7b425d 100644 --- a/agents/filbert/work/s6/out/node-control-board.txt +++ b/agents/filbert/work/s6/out/node-control-board.txt @@ -1,127 +1,127 @@ -✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (4.641886ms) -✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.268969ms) -✔ completed smoke replies and ordinary questions are idle, not human blockers (1.020682ms) -✔ only an explicit first-line input request makes a finished reply waiting (0.289824ms) -✔ tool activity, user text, errors and unfinished turns override attention text (0.167445ms) -✔ STOP access failure is unknown, not absence, under a non-root identity (54.374685ms) -✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.166721ms) -✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.667541ms) -✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (6.805422ms) -✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.669073ms) -✔ server rescans connector discovery and refuses HTTP reply without transport (43.145184ms) -✔ connector session links and linked directories are not read (1.27055ms) -✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (3.270946ms) -✔ CLI print uses the relaunch notice instead of old current preview (68.257724ms) -✔ connector owner and fixed task never inherit a native relaunch notice (3.060282ms) -✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.70917ms) -✔ loadConfig: missing file throws ConfigError (1.787365ms) -✔ loadConfig: invalid JSON throws ConfigError (0.347612ms) -✔ loadConfig: missing dataRoot throws ConfigError (0.259123ms) -✔ loadConfig: relative dataRoot throws ConfigError (0.243686ms) -✔ loadConfig: valid config returns dataRoot (0.798268ms) -✔ findNewestSession: picks the newest by mtime among two files (0.526496ms) -✔ findNewestSession: finds files in nested subdirectories (0.406065ms) -✔ findNewestSession: returns null for a missing dir (0.137243ms) -✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.770616ms) -✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.401954ms) -✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.536856ms) -✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (1.449147ms) -✔ deriveState: full state table (0.200445ms) -✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.407812ms) -✔ rule: newest entry is a tool result with no assistant text after it is working (0.324887ms) -✔ rule: a finished ordinary turn is idle, even if it says your move (0.34233ms) -✔ task: the first user message of the session, from text blocks (0.364454ms) -✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.306066ms) -✔ task: no user message in the log means null (shown as unknown), never a guess (0.341252ms) -✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.442806ms) -✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.797736ms) -✔ scan: the written record carries task, workspace and activeProject (0.634047ms) -✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.683352ms) -✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.458167ms) -✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (1.138285ms) -✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.509043ms) -✔ loadRegistrations: a missing seatsDir gives empty lists (0.193861ms) -✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (1.188153ms) -✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (0.37717ms) -✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (1.460457ms) -✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.322685ms) -✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.671017ms) -✔ scanAgent: ageSeconds is computed from the injected now (0.324056ms) -✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.192104ms) -✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.429286ms) -✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.42239ms) -✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (1.202797ms) -✔ scan: relative boardDir throws ConfigError (0.116583ms) -✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (75.904446ms) -✔ CLI: missing config exits 2 with a refused: message (68.3446ms) -✔ CLI: unknown command exits 2 (60.818611ms) -✔ CLI: unknown --liveness value exits 2 (62.922692ms) -✔ panesRunPi: true when any trimmed line equals 'pi' (0.277749ms) -✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.110431ms) -✔ tmuxIsAlive: a pane running pi is alive (0.261019ms) -✔ tmuxIsAlive: session exists but pi has exited is not alive (0.088258ms) -✔ tmuxIsAlive: no such tmux session is not alive (0.054366ms) -✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.074409ms) -✔ tmuxIsAlive: passes -L only when a socket is given (0.100801ms) -✔ parsePanes: one pane per line, command and optional tab-separated path (0.101551ms) -✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.123154ms) -✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.170557ms) -✔ loadSeen: missing file returns {} (0.209322ms) -✔ loadSeen: invalid JSON throws ConfigError (0.415243ms) -✔ loadSeen: a JSON array throws ConfigError (0.211211ms) -✔ loadSeen: a non-string value throws ConfigError (0.231069ms) -✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.40028ms) -✔ markSeen: seen false deletes the key (0.283061ms) -✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.238733ms) -✔ markSeen: project containing '/' throws ConfigError (0.134177ms) -✔ markSeen: non-boolean seen throws ConfigError (0.123902ms) -✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.320389ms) -✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.270898ms) -✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.244607ms) -✔ scanAgent: an error-state session with a matching mark is seen (0.257963ms) -✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.568729ms) -✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.20305ms) -✔ taskSetBy: a registered task carries the record's setter; a record without the field (pre-#1511) reads unknown; the value is not copied into registered (0.726555ms) -✔ taskSetBy: null whenever the task shown is not the registered one: no registration, an empty registered task, a stale registration; the field is always present (0.645998ms) -✔ taskSetBy: scan() reads the field from disk through the seat package (bounded there), writes it to the per-agent record and index, and an invalid on-disk value is a registrationError, never a row value (1.045846ms) -✔ isLoopbackHost: recognizes loopback hosts (1.386893ms) -✔ isLoopbackHost: rejects non-loopback hosts (4.71065ms) -✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (3.447209ms) -✔ startServer: serves page, healthz, and a rescanning /api/board (40.64407ms) -✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (7.221166ms) -✔ startServer: /api/board returns 500 JSON with an error field when scan throws (3.361978ms) -✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (72.261995ms) -✔ CLI: serve rejects a non-numeric --port with exit 2 (59.137725ms) -✔ CLI: scan still works after the async cli refactor (64.194896ms) -✔ CLI: live serve prints its URL and answers /healthz (70.924896ms) -✔ page.html: esc() escapes every HTML-significant character (0.796687ms) -✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (9.921107ms) -✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.365807ms) -✔ POST /api/seen with invalid JSON returns 400 (3.525478ms) -✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (2.617466ms) -✔ POST /api/seen with a missing agent returns 400 (1.613299ms) -✔ POST /api/board returns 405; PUT /api/seen returns 405 (2.065397ms) -✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (54.790079ms) -✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.36576ms) -✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.343455ms) -✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.206124ms) -✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.160804ms) -✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.345793ms) -✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (0.628983ms) -✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S :control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (34.931804ms) -✔ POST /api/reply: a registration with a tmux socket adds -L (35.233392ms) -✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (45.23328ms) -✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (22.970179ms) -✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (6.70926ms) -✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.244726ms) -✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.703156ms) -✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (4.279429ms) -✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.433195ms) -✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (11.556202ms) -✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (33.618842ms) -✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (6.280378ms) -✔ every refusal code the reader can raise has an HTTP status (1.042543ms) -✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (54.030753ms) +✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (3.691168ms) +✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.238072ms) +✔ completed smoke replies and ordinary questions are idle, not human blockers (0.714916ms) +✔ only an explicit first-line input request makes a finished reply waiting (0.167776ms) +✔ tool activity, user text, errors and unfinished turns override attention text (0.129623ms) +✔ STOP access failure is unknown, not absence, under a non-root identity (33.013191ms) +✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.180599ms) +✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.38538ms) +✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (6.11372ms) +✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.622748ms) +✔ server rescans connector discovery and refuses HTTP reply without transport (33.252474ms) +✔ connector session links and linked directories are not read (0.975739ms) +✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (2.6993ms) +✔ CLI print uses the relaunch notice instead of old current preview (62.64595ms) +✔ connector owner and fixed task never inherit a native relaunch notice (2.453627ms) +✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.69238ms) +✔ loadConfig: missing file throws ConfigError (1.813512ms) +✔ loadConfig: invalid JSON throws ConfigError (0.320534ms) +✔ loadConfig: missing dataRoot throws ConfigError (0.241769ms) +✔ loadConfig: relative dataRoot throws ConfigError (0.243223ms) +✔ loadConfig: valid config returns dataRoot (0.694324ms) +✔ findNewestSession: picks the newest by mtime among two files (0.606778ms) +✔ findNewestSession: finds files in nested subdirectories (0.449554ms) +✔ findNewestSession: returns null for a missing dir (0.190029ms) +✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.953037ms) +✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.571058ms) +✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.503874ms) +✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (1.243797ms) +✔ deriveState: full state table (0.285701ms) +✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.375156ms) +✔ rule: newest entry is a tool result with no assistant text after it is working (0.333717ms) +✔ rule: a finished ordinary turn is idle, even if it says your move (0.278815ms) +✔ task: the first user message of the session, from text blocks (0.299581ms) +✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.272129ms) +✔ task: no user message in the log means null (shown as unknown), never a guess (0.302093ms) +✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.433808ms) +✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.574491ms) +✔ scan: the written record carries task, workspace and activeProject (0.567433ms) +✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.655101ms) +✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.43647ms) +✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (1.087099ms) +✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.468892ms) +✔ loadRegistrations: a missing seatsDir gives empty lists (0.171266ms) +✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (0.848083ms) +✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (0.378216ms) +✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (1.248013ms) +✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.252124ms) +✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.603413ms) +✔ scanAgent: ageSeconds is computed from the injected now (0.2699ms) +✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.170176ms) +✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.37218ms) +✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.378987ms) +✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (0.967412ms) +✔ scan: relative boardDir throws ConfigError (0.093777ms) +✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (58.802027ms) +✔ CLI: missing config exits 2 with a refused: message (51.021156ms) +✔ CLI: unknown command exits 2 (50.285259ms) +✔ CLI: unknown --liveness value exits 2 (55.257373ms) +✔ panesRunPi: true when any trimmed line equals 'pi' (0.247684ms) +✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.078529ms) +✔ tmuxIsAlive: a pane running pi is alive (0.211692ms) +✔ tmuxIsAlive: session exists but pi has exited is not alive (0.076486ms) +✔ tmuxIsAlive: no such tmux session is not alive (0.061456ms) +✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.062541ms) +✔ tmuxIsAlive: passes -L only when a socket is given (0.096803ms) +✔ parsePanes: one pane per line, command and optional tab-separated path (0.07962ms) +✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.074285ms) +✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.097511ms) +✔ loadSeen: missing file returns {} (0.155968ms) +✔ loadSeen: invalid JSON throws ConfigError (0.219738ms) +✔ loadSeen: a JSON array throws ConfigError (0.168137ms) +✔ loadSeen: a non-string value throws ConfigError (0.168774ms) +✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.309188ms) +✔ markSeen: seen false deletes the key (0.24856ms) +✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.218582ms) +✔ markSeen: project containing '/' throws ConfigError (0.111987ms) +✔ markSeen: non-boolean seen throws ConfigError (0.108609ms) +✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.282144ms) +✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.247828ms) +✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.227325ms) +✔ scanAgent: an error-state session with a matching mark is seen (0.227839ms) +✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.496047ms) +✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.186552ms) +✔ taskSetBy: a registered task carries the record's setter; a record without the field (pre-#1511) reads unknown; the value is not copied into registered (0.619757ms) +✔ taskSetBy: null whenever the task shown is not the registered one: no registration, an empty registered task, a stale registration; the field is always present (0.563664ms) +✔ taskSetBy: scan() reads the field from disk through the seat package (bounded there), writes it to the per-agent record and index, and an invalid on-disk value is a registrationError, never a row value (0.939906ms) +✔ isLoopbackHost: recognizes loopback hosts (1.277363ms) +✔ isLoopbackHost: rejects non-loopback hosts (4.523177ms) +✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (3.59353ms) +✔ startServer: serves page, healthz, and a rescanning /api/board (35.659466ms) +✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (6.146856ms) +✔ startServer: /api/board returns 500 JSON with an error field when scan throws (2.400245ms) +✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (53.880169ms) +✔ CLI: serve rejects a non-numeric --port with exit 2 (47.763257ms) +✔ CLI: scan still works after the async cli refactor (53.502565ms) +✔ CLI: live serve prints its URL and answers /healthz (59.412096ms) +✔ page.html: esc() escapes every HTML-significant character (0.648508ms) +✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (7.799646ms) +✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.508134ms) +✔ POST /api/seen with invalid JSON returns 400 (1.903236ms) +✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (1.975783ms) +✔ POST /api/seen with a missing agent returns 400 (1.259383ms) +✔ POST /api/board returns 405; PUT /api/seen returns 405 (1.573435ms) +✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (50.607954ms) +✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.317323ms) +✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.260037ms) +✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.154397ms) +✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.16282ms) +✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.283516ms) +✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (0.548275ms) +✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S :control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (28.520776ms) +✔ POST /api/reply: a registration with a tmux socket adds -L (27.006224ms) +✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (27.012188ms) +✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (13.925126ms) +✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (4.242121ms) +✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.14168ms) +✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.571395ms) +✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (2.507482ms) +✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.311159ms) +✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (7.759398ms) +✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (31.693895ms) +✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (5.606208ms) +✔ every refusal code the reader can raise has an HTTP status (0.955939ms) +✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (45.78585ms) ℹ tests 124 ℹ suites 0 ℹ pass 124 @@ -129,4 +129,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 774.560773 +ℹ duration_ms 625.036405 diff --git a/agents/filbert/work/s6/out/node-conversation.txt b/agents/filbert/work/s6/out/node-conversation.txt index 5a527eca..fa737fc6 100644 --- a/agents/filbert/work/s6/out/node-conversation.txt +++ b/agents/filbert/work/s6/out/node-conversation.txt @@ -1,155 +1,155 @@ -✔ W1: two processes acquire the same pair at once; exactly one claim (366.928224ms) -✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (39.093165ms) -✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (22.903774ms) -✔ W2: acquire while a claim is reserved or active refuses already-active (542.049518ms) -✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (504.802304ms) -✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (322.66265ms) -✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (265.106214ms) -✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (240.449712ms) -✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.957749ms) -✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (12666.098171ms) -✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (30953.87984ms) -✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (266.87467ms) -✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (145.296641ms) -✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (410.27827ms) -✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (282.414513ms) -✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (352.278489ms) -✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (119.390385ms) -✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (185.414736ms) -✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (368.009297ms) -✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (813.857743ms) -✔ W11: the controller writes no session file; only the fake engine's own appends appear (185.527387ms) -✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (89.437752ms) -✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (72.840588ms) -✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.634554ms) -✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.002053ms) -✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.819507ms) -✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (3075.872051ms) -✔ K2: K1 on the process-group fallback ends uncertain, never stopped (550.816208ms) -✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2938.878855ms) -✔ K4: two engines; force stop one; the other survives by independent observation (5462.103605ms) -✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2612.388068ms) -✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2982.619477ms) -✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2432.457076ms) -✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (5115.442166ms) -✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (761.540049ms) -✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (672.791692ms) -✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (615.943344ms) -✔ K6: recover without proof, without confirmation, or with changed pins is refused (879.881573ms) -✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (417.30095ms) -✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (585.898732ms) -✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3559.770078ms) -✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (28.327037ms) -✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (451.142569ms) -✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (461.907538ms) -✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (352.559273ms) -✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (260.467726ms) -✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (246.161026ms) -✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (338.624679ms) -✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (240.108119ms) -✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.597623ms) -✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (224.445395ms) -✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (365.969368ms) -✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (254.094418ms) -✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (226.861015ms) -✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (223.090316ms) -✔ E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once (280.697687ms) -✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.533788ms) -✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (257.59532ms) -✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (295.516735ms) -✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (312.122245ms) -✔ terminal: engine control characters are made visible; a lost connection refuses submit (255.96624ms) -✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.511256ms) -✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.298392ms) -✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.366575ms) -✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.137018ms) -✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (313.570237ms) -✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (334.013214ms) -✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (339.086815ms) -✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (963.27244ms) -✔ H4: self-takeover is refused (238.984643ms) -✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (489.013009ms) -✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (1350.016488ms) -✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (277.044896ms) -✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (903.141931ms) -✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (381.724817ms) -✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (216.486779ms) -✔ H13: a retry with the same request ID and different text is refused (245.581951ms) -✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (729.986176ms) -✔ H15: a revoked connection's command is refused; the revocation fence holds (650.617325ms) -✔ H16: a second controller for the same session refuses already-active; the first is untouched (277.608467ms) -✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (1252.558084ms) -✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (1188.412704ms) -✔ H18: two prompts before any native output: the second refuses busy; one engine write (271.516375ms) -✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (396.880958ms) -✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.859705ms) -✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (724.938928ms) -✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (798.741876ms) -✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (925.243776ms) -✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2958.527643ms) -✔ H23: requests pending at a restart are not resent; each shows outcome unknown (700.652019ms) -✔ a plain conversation: catalogue row, one page, CHAT-01 records (6.875614ms) -✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.974279ms) -✔ F1: a malformed line is an unavailable part at its position, and reading continues (2.412892ms) -✔ F1: a missing parent stops the history with a notice that names the unreadable lines (3.648476ms) -✔ F1: an unreadable fork is never merged into another branch's history (1.996645ms) -✔ F1: a follow stays on its branch when the next entry's parent is unreadable (2.144196ms) -✔ F1: a file whose entries are all unreadable shows a notice per line (0.973217ms) -✔ F2: a truncated trailing line marks the view incomplete, not an error (1.342077ms) -✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (4.217095ms) -✔ F3: a replaced file (new inode) refuses old cursors with reconcile (4.361039ms) -✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (5.856576ms) -✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.198373ms) -✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (8.623027ms) -✔ F7: a symlinked file and a symlinked directory component are refused, never opened (9.408628ms) -✔ F8: a file swapped for a symlink after the catalogue is refused (2.534355ms) -✔ F9: registrations never add or redirect a root (1.900641ms) -✔ F10: a header cwd naming another project is refused (5.34781ms) -✔ F11: parentSession renders with a marker and the parent is never opened (1.297027ms) -✔ F12: two leaves: the default leaf is shown and the other branch reads alone (5.378605ms) -✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.400711ms) -✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.357901ms) -✔ F13: compaction is a marker in place, then the retained content (0.791328ms) -✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (869.46465ms) -✔ fragments never cut a surrogate pair and keep an empty string (5.322608ms) -✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.694545ms) -✔ unknown conversations, empty files and non-Pi files refuse (2.993734ms) -✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.835641ms) -✔ a seat directory without search permission refuses that root, not the catalogue (3.409315ms) -✔ every page and cursor is a valid CHAT-01 record (1171.522693ms) -✔ the engine pin holds for the installed package (2.969351ms) -✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (339.716856ms) -✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (465.727325ms) -✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (689.187817ms) -✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (514.977427ms) -✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (292.410254ms) -✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (23.468859ms) -✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (443.221347ms) -✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (247.308821ms) -✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (322.736492ms) -✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (551.870499ms) -✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1961.565503ms) -✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (244.212591ms) -✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (290.90596ms) -✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (246.059572ms) -✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (289.589211ms) -✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (501.30469ms) -✔ N10: fake conformance (34.023867ms) -✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (831.090384ms) -✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (361.055636ms) -✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (358.002753ms) -✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (589.637419ms) -✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (581.06703ms) -✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (562.073768ms) -✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (264.022573ms) -✔ N17: the run fails on its own during the exchange: failed, Failed on its own (462.86689ms) -✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (860.27459ms) -✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (580.591412ms) -✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (480.465854ms) -✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (339.574789ms) -✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (338.952874ms) -✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (233.893576ms) -✔ N24: the seal is an allow-list: --extension, a missing --no-* flag, a second --mode or --session, a session or output flag, or a stray word refuses unsealed-engine; no engine starts (240.739111ms) +✔ W1: two processes acquire the same pair at once; exactly one claim (500.172302ms) +✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (34.447301ms) +✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (21.245207ms) +✔ W2: acquire while a claim is reserved or active refuses already-active (380.782492ms) +✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (415.536314ms) +✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (258.443009ms) +✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (208.321924ms) +✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (209.096493ms) +✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.343681ms) +✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (10639.643125ms) +✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (33722.598575ms) +✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (297.831509ms) +✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (167.189405ms) +✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (484.422997ms) +✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (317.977402ms) +✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (434.213183ms) +✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (138.169018ms) +✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (202.378983ms) +✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (397.75689ms) +✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (946.3831ms) +✔ W11: the controller writes no session file; only the fake engine's own appends appear (225.315133ms) +✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (93.363585ms) +✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (72.203607ms) +✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.569722ms) +✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.325889ms) +✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.738078ms) +✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2794.484922ms) +✔ K2: K1 on the process-group fallback ends uncertain, never stopped (469.89623ms) +✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2644.635443ms) +✔ K4: two engines; force stop one; the other survives by independent observation (4852.089312ms) +✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2563.661152ms) +✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2494.802302ms) +✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2746.623948ms) +✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (5278.616647ms) +✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (920.216881ms) +✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (750.409927ms) +✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (590.614603ms) +✔ K6: recover without proof, without confirmation, or with changed pins is refused (984.081218ms) +✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (486.910482ms) +✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (679.003936ms) +✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3412.938836ms) +✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (36.405575ms) +✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (486.630344ms) +✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (531.704758ms) +✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (504.380237ms) +✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (217.876841ms) +✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (212.438677ms) +✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (308.695737ms) +✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (205.567788ms) +✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.474109ms) +✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (213.418843ms) +✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (304.548788ms) +✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (230.67903ms) +✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (232.279238ms) +✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (225.911236ms) +✔ E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once (226.928276ms) +✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.157214ms) +✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (240.351129ms) +✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (230.11528ms) +✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (317.639113ms) +✔ terminal: engine control characters are made visible; a lost connection refuses submit (374.398052ms) +✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.539984ms) +✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.282556ms) +✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.30821ms) +✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.123271ms) +✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (245.322895ms) +✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (559.299151ms) +✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (306.639949ms) +✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (864.367817ms) +✔ H4: self-takeover is refused (207.977962ms) +✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (489.967376ms) +✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (1162.284778ms) +✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (400.286574ms) +✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (854.487718ms) +✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (474.571479ms) +✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (235.94039ms) +✔ H13: a retry with the same request ID and different text is refused (212.137643ms) +✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (722.250079ms) +✔ H15: a revoked connection's command is refused; the revocation fence holds (301.681102ms) +✔ H16: a second controller for the same session refuses already-active; the first is untouched (230.452513ms) +✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (939.093746ms) +✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (959.856778ms) +✔ H18: two prompts before any native output: the second refuses busy; one engine write (363.959262ms) +✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (409.106014ms) +✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.655539ms) +✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (703.309217ms) +✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (603.731272ms) +✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (790.922524ms) +✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2967.111484ms) +✔ H23: requests pending at a restart are not resent; each shows outcome unknown (1706.924017ms) +✔ a plain conversation: catalogue row, one page, CHAT-01 records (7.943027ms) +✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.772943ms) +✔ F1: a malformed line is an unavailable part at its position, and reading continues (1.752425ms) +✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.556892ms) +✔ F1: an unreadable fork is never merged into another branch's history (1.541133ms) +✔ F1: a follow stays on its branch when the next entry's parent is unreadable (1.990715ms) +✔ F1: a file whose entries are all unreadable shows a notice per line (1.130976ms) +✔ F2: a truncated trailing line marks the view incomplete, not an error (1.328783ms) +✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (4.148949ms) +✔ F3: a replaced file (new inode) refuses old cursors with reconcile (7.152193ms) +✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (6.687027ms) +✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.6262ms) +✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (7.512257ms) +✔ F7: a symlinked file and a symlinked directory component are refused, never opened (8.779647ms) +✔ F8: a file swapped for a symlink after the catalogue is refused (2.496262ms) +✔ F9: registrations never add or redirect a root (1.588804ms) +✔ F10: a header cwd naming another project is refused (3.361116ms) +✔ F11: parentSession renders with a marker and the parent is never opened (0.776124ms) +✔ F12: two leaves: the default leaf is shown and the other branch reads alone (5.470116ms) +✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.272352ms) +✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.350252ms) +✔ F13: compaction is a marker in place, then the retained content (0.727293ms) +✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (523.566064ms) +✔ fragments never cut a surrogate pair and keep an empty string (5.179993ms) +✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.677094ms) +✔ unknown conversations, empty files and non-Pi files refuse (2.369772ms) +✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.093831ms) +✔ a seat directory without search permission refuses that root, not the catalogue (2.100106ms) +✔ every page and cursor is a valid CHAT-01 record (804.092641ms) +✔ the engine pin holds for the installed package (1.919918ms) +✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (331.021752ms) +✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (275.832025ms) +✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (806.068343ms) +✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (469.189878ms) +✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (254.60826ms) +✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (22.156872ms) +✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (368.884282ms) +✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (213.556408ms) +✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (331.026384ms) +✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (537.207506ms) +✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1925.400277ms) +✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (258.018644ms) +✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (378.641567ms) +✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (210.25368ms) +✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (309.741586ms) +✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (446.67773ms) +✔ N10: fake conformance (31.971148ms) +✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (474.454081ms) +✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (300.784606ms) +✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (324.956671ms) +✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (414.997271ms) +✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (450.246043ms) +✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (562.546304ms) +✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (274.511468ms) +✔ N17: the run fails on its own during the exchange: failed, Failed on its own (398.931267ms) +✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (919.910026ms) +✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (662.641784ms) +✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (582.515503ms) +✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (203.649783ms) +✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (225.882597ms) +✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (213.819475ms) +✔ N24: the seal is an allow-list: --extension, a missing --no-* flag, a second --mode or --session, a session or output flag, or a stray word refuses unsealed-engine; no engine starts (229.585286ms) ℹ tests 152 ℹ suites 0 ℹ pass 152 @@ -157,4 +157,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 51185.439363 +ℹ duration_ms 52102.666518 diff --git a/agents/filbert/work/s6/out/node-discord.txt b/agents/filbert/work/s6/out/node-discord.txt index 30828ee5..a0cdbaf9 100644 --- a/agents/filbert/work/s6/out/node-discord.txt +++ b/agents/filbert/work/s6/out/node-discord.txt @@ -1,181 +1,181 @@ -✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.30556ms) -✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.570005ms) -✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.647254ms) -✔ approvals: a button approves only on its own request message with the matching custom id (0.487432ms) -✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.371847ms) -✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (9.276715ms) -✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.081808ms) -✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.09649ms) -✔ authorize: open channel, listed user (3.676715ms) -✔ authorize: wrong guild (0.208021ms) -✔ authorize: no guild (DM) (0.188467ms) -✔ authorize: unlisted channel (0.18996ms) -✔ authorize: unknown channel, no info (0.40522ms) -✔ authorize: thread of listed parent (0.205975ms) -✔ authorize: thread of unlisted parent (0.173754ms) -✔ authorize: text channel that is not a thread and not listed (0.145406ms) -✔ authorize: unlisted user (0.174784ms) -✔ authorize: no author (0.408525ms) -✔ authorize: bot author (listed id, bot flag) (0.129557ms) -✔ authorize: system author (0.168624ms) -✔ authorize: the bot itself (0.099418ms) -✔ authorize: webhook (0.09274ms) -✔ authorize: mention channel without mention (0.117129ms) -✔ authorize: mention channel with bot mention (0.133164ms) -✔ authorize: mention channel with @everyone only (0.094295ms) -✔ authorize: mention channel mentioning someone else (0.074797ms) -✔ authorize: mention channel, content says @bot but mentions empty (0.094055ms) -✔ authorize: private thread under mention channel, mentioned (0.0797ms) -✔ authorize: private thread under mention channel, not mentioned (0.068534ms) -✔ authorize: thread in another guild per channel info (0.069749ms) -✔ authorize: not an object (0.063066ms) -✔ authorize: no id (2.317016ms) -✔ authorize: oversize content is accepted and flagged (0.100512ms) -✔ authorize: exactly the limit is not oversize (0.06151ms) -✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.514873ms) -✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.162041ms) -✔ binding: a complete binding validates and is frozen (2.678019ms) -✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.292634ms) -✔ binding: empty allowlists refuse (0.395772ms) -✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.460534ms) -✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.072326ms) -✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.829546ms) -✔ binding: file must be 0600, regular, not a symlink (3.695384ms) -✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (3.125574ms) -✔ cli: check refuses a non-0600 token file with exit 2 before any network use (89.43539ms) -✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.535478ms) -✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (501.87896ms) -✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (351.757699ms) -✔ cli: run refuses when STOP is present, before any network use (290.165911ms) -✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (3.754926ms) -✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.998397ms) -✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.999258ms) -✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.07216ms) -✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.420386ms) -✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.100597ms) -✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.177542ms) -✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.756928ms) -✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.594306ms) -✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.043242ms) -✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.119759ms) -✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.915495ms) -✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.686556ms) -✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.574885ms) -✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.382548ms) -✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.44967ms) -✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.296315ms) -✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.264071ms) -✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.449145ms) -✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.790787ms) -✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (3.599111ms) -✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.515266ms) -✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.616309ms) -✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.647835ms) -✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.331026ms) -✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.548305ms) -✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.806956ms) -✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.861674ms) -✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.359756ms) -✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.52254ms) -✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.414285ms) -✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.647646ms) -✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.406205ms) -✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.407002ms) -✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (62.210504ms) -✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (41.029075ms) -✔ engine: one prompt, one turn, text and usage come back (53.820075ms) -✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (354.233703ms) -✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (264.785114ms) -✔ engine: timeout sends abort and fails only that turn; the process stays (121.168917ms) -✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (277.171717ms) -✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (158.997315ms) -✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (215.616932ms) -✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.286838ms) -✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.417736ms) -✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.455254ms) -✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.744815ms) -✔ engine: a malformed JSONL line fails the turn, not the process (25.344053ms) -✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.663653ms) -✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.462075ms) -✔ gateway: missed ack closes the socket and resumes with the last sequence (1.806422ms) -✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.633551ms) -✔ gateway: op 9 resumable resumes (0.539501ms) -✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.784361ms) -✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.708919ms) -✔ gateway: close() is final and unparseable frames are ignored (0.627142ms) -✔ git: config validation is strict, needs write: true, a work tree and a private token file (58.774185ms) -✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (41.09462ms) -✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (128.590736ms) -✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.476101ms) -✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (127.537922ms) -✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (221.213798ms) -✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (236.019056ms) -✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (487.643115ms) -✔ git: push pushes the named branch only and reports up to date (91.790307ms) -✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (93.185619ms) -✔ git: the credential helper answers get over https from a private file and nothing else (216.017392ms) -✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (737.247015ms) -✔ lock: the claim is exclusive; a second start against a live owner refuses (5.952379ms) -✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (75.442042ms) -✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.726436ms) -✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.710958ms) -✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (62.258522ms) -✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (476.231867ms) -✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.810492ms) -✔ lock: a process whose start marker or boot id cannot be read refuses to claim (49.81495ms) -✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (4.254546ms) -✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (98.186867ms) -✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (261.39297ms) -✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (189.602729ms) -✔ notices: a kind is recorded per UTC day and found again (0.872726ms) -✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (4.954714ms) -✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (1.924999ms) -✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.597964ms) -✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (38.82067ms) -✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (36.200206ms) -✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (53.157519ms) -✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (100.977523ms) -✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (1154.269076ms) -✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.410433ms) -✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.954227ms) -✔ rest: content and nonce limits are enforced locally; typing never throws (1.166406ms) -✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.771431ms) -✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.876233ms) -✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.974428ms) -✔ setspark config: reaches the tools config and the binding as a fixed key (2.950589ms) -✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.409577ms) -✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.61629ms) -✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (19.615454ms) -✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (7.461312ms) -✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (5.264184ms) -✔ setspark keys: read per call, one printable token per file, rotation without a restart (2.464963ms) -✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.491046ms) -✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.075832ms) -✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1016.278029ms) -✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.852283ms) -✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (31.796702ms) -✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (6.636421ms) -✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.474284ms) -✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (9.083242ms) -✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.662943ms) -✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.042075ms) -✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.050779ms) -✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (2.896622ms) -✔ tools: listing and search caps hold (10.266894ms) -✔ tools: credential shapes are caught; ordinary prose and ids are not (0.853382ms) -✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (4.582953ms) -✔ tools: an unreadable file under the root is skipped by search and refused by read (0.987122ms) -✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.046852ms) -✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.759934ms) -✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.765593ms) -✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.666132ms) -✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.483424ms) -✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (1.90685ms) -✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1024.220555ms) -✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (15.168569ms) -✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.539613ms) -✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (13.220746ms) -✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (6.399067ms) +✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.513067ms) +✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.622039ms) +✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.657851ms) +✔ approvals: a button approves only on its own request message with the matching custom id (0.506829ms) +✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.372481ms) +✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.942082ms) +✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.686265ms) +✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.451802ms) +✔ authorize: open channel, listed user (2.089988ms) +✔ authorize: wrong guild (0.204754ms) +✔ authorize: no guild (DM) (0.17918ms) +✔ authorize: unlisted channel (2.484597ms) +✔ authorize: unknown channel, no info (0.252987ms) +✔ authorize: thread of listed parent (0.213716ms) +✔ authorize: thread of unlisted parent (0.155577ms) +✔ authorize: text channel that is not a thread and not listed (0.331012ms) +✔ authorize: unlisted user (0.195206ms) +✔ authorize: no author (0.352216ms) +✔ authorize: bot author (listed id, bot flag) (0.171847ms) +✔ authorize: system author (0.11868ms) +✔ authorize: the bot itself (0.100113ms) +✔ authorize: webhook (0.091549ms) +✔ authorize: mention channel without mention (0.139131ms) +✔ authorize: mention channel with bot mention (0.269928ms) +✔ authorize: mention channel with @everyone only (0.110887ms) +✔ authorize: mention channel mentioning someone else (0.074523ms) +✔ authorize: mention channel, content says @bot but mentions empty (0.089763ms) +✔ authorize: private thread under mention channel, mentioned (0.092374ms) +✔ authorize: private thread under mention channel, not mentioned (0.064862ms) +✔ authorize: thread in another guild per channel info (0.076909ms) +✔ authorize: not an object (0.063716ms) +✔ authorize: no id (0.066689ms) +✔ authorize: oversize content is accepted and flagged (0.077499ms) +✔ authorize: exactly the limit is not oversize (0.058051ms) +✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.495786ms) +✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.552985ms) +✔ binding: a complete binding validates and is frozen (2.984441ms) +✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.82498ms) +✔ binding: empty allowlists refuse (0.367254ms) +✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.571886ms) +✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.058133ms) +✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.492391ms) +✔ binding: file must be 0600, regular, not a symlink (3.49713ms) +✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.149593ms) +✔ cli: check refuses a non-0600 token file with exit 2 before any network use (110.567998ms) +✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.40013ms) +✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (349.708865ms) +✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (216.498377ms) +✔ cli: run refuses when STOP is present, before any network use (137.183231ms) +✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.049555ms) +✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.221218ms) +✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (17.53611ms) +✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.196582ms) +✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.487369ms) +✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.174302ms) +✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.223402ms) +✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.155626ms) +✔ turn: a second message during a turn is held by the engine, both get their own reply and record (31.706556ms) +✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.981659ms) +✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.826213ms) +✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.655156ms) +✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.645133ms) +✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.315913ms) +✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.881895ms) +✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.712336ms) +✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (1.233407ms) +✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (3.998497ms) +✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.241541ms) +✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.496718ms) +✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (5.570856ms) +✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.101271ms) +✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.133321ms) +✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.142423ms) +✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.321246ms) +✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.391083ms) +✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.782881ms) +✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.023176ms) +✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.253659ms) +✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.584064ms) +✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.262211ms) +✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.94795ms) +✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (4.050497ms) +✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.467297ms) +✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (65.861693ms) +✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (42.322062ms) +✔ engine: one prompt, one turn, text and usage come back (35.874086ms) +✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (350.609136ms) +✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (240.28076ms) +✔ engine: timeout sends abort and fails only that turn; the process stays (106.320108ms) +✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.398024ms) +✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.558786ms) +✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.642182ms) +✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.22918ms) +✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (2.158478ms) +✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.938839ms) +✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (431.707736ms) +✔ engine: a malformed JSONL line fails the turn, not the process (27.92475ms) +✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.568294ms) +✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.962931ms) +✔ gateway: missed ack closes the socket and resumes with the last sequence (1.820945ms) +✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.505877ms) +✔ gateway: op 9 resumable resumes (0.388086ms) +✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.880231ms) +✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.577018ms) +✔ gateway: close() is final and unparseable frames are ignored (0.433901ms) +✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.813399ms) +✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (47.008635ms) +✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (75.188574ms) +✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.289055ms) +✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (101.080883ms) +✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (105.441377ms) +✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (94.002264ms) +✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (234.268712ms) +✔ git: push pushes the named branch only and reports up to date (74.775987ms) +✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (98.386955ms) +✔ git: the credential helper answers get over https from a private file and nothing else (211.796826ms) +✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (832.162327ms) +✔ lock: the claim is exclusive; a second start against a live owner refuses (4.805541ms) +✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (91.35218ms) +✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.026631ms) +✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.010642ms) +✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (39.081716ms) +✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (335.097304ms) +✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.478148ms) +✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.306592ms) +✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.148782ms) +✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (44.742055ms) +✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (151.909677ms) +✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (91.508358ms) +✔ notices: a kind is recorded per UTC day and found again (0.663793ms) +✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.350295ms) +✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.184953ms) +✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.79695ms) +✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (47.744741ms) +✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (48.327362ms) +✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (35.166632ms) +✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.466581ms) +✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (736.615945ms) +✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (4.774151ms) +✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.44002ms) +✔ rest: content and nonce limits are enforced locally; typing never throws (1.936608ms) +✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.290834ms) +✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.845933ms) +✔ setspark config: a bare https or loopback origin, a private key file, a principal (4.723533ms) +✔ setspark config: reaches the tools config and the binding as a fixed key (3.019049ms) +✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.727043ms) +✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.039368ms) +✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (23.379469ms) +✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.751887ms) +✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.042864ms) +✔ setspark keys: read per call, one printable token per file, rotation without a restart (2.676422ms) +✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.46837ms) +✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.002481ms) +✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.153004ms) +✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.47554ms) +✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (7.378494ms) +✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.8349ms) +✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.306603ms) +✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.854819ms) +✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.013354ms) +✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.014583ms) +✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.436845ms) +✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.615438ms) +✔ tools: listing and search caps hold (9.65926ms) +✔ tools: credential shapes are caught; ordinary prose and ids are not (0.903078ms) +✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (6.27101ms) +✔ tools: an unreadable file under the root is skipped by search and refused by read (1.332638ms) +✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.474367ms) +✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.680721ms) +✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.018418ms) +✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.491755ms) +✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.852318ms) +✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.335601ms) +✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1027.404183ms) +✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.231804ms) +✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.2477ms) +✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.610441ms) +✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.220307ms) ℹ tests 178 ℹ suites 0 ℹ pass 178 @@ -183,4 +183,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 2785.874422 +ℹ duration_ms 2687.487226 diff --git a/agents/filbert/work/s6/out/node-harness.txt b/agents/filbert/work/s6/out/node-harness.txt index 6678c6e2..48a0928e 100644 --- a/agents/filbert/work/s6/out/node-harness.txt +++ b/agents/filbert/work/s6/out/node-harness.txt @@ -1,53 +1,58 @@ -✔ sessionModel: agent vars win, then the system's execution settings (9.863443ms) -✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.904843ms) -✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.507558ms) -✔ a bundle is written once: an existing file refuses (2.342637ms) -✔ a path with a single quote can't go into the hook command (2.404071ms) -✔ allow exits 0, a deny exits 2 with the reason on stderr (119.133617ms) -✔ a missing or wrong policy, or a bad event, exits 2 (89.62829ms) -✔ the bundle's wrapped command: a missing gate or node still blocks (1090.433581ms) -✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (764.522718ms) -✔ claude: the hook alone blocks a path outside the workspace (429.718725ms) -✔ claude: a second turn resumes the first turn's session (697.342346ms) -✔ claude: a missing hook or MCP file refuses before claude starts (7.462303ms) -✔ pi: policy tools and typed tools pass, anything else is blocked (3.652974ms) -✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.053779ms) -✔ file tool paths must resolve inside the workspace (1.386549ms) -✔ pi's own path normalisation can't be used to step out (1.267113ms) -✔ a symlink inside the workspace that points out is outside (1.233345ms) -✔ claude path fields per tool (1.085399ms) -✔ glob patterns stay inside the workspace (1.257359ms) -✔ a path that can't be checked is blocked (0.780884ms) -✔ initialize, ping and tools/list (45.612153ms) -✔ tools/call goes through the tool socket; a refusal is an isError result (35.942819ms) -✔ unknown tools and methods are JSON-RPC errors and never reach the socket (36.869003ms) -✔ a missing argument is a usage error (31.167661ms) -✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (362.276951ms) -✔ pi: a missing extension refuses before any model call (7.449552ms) -✔ pi: an extension without its configuration fails pi's start (260.274424ms) -✔ founderCheck: founder variables, then a needed service without a usable token (1.668322ms) -✔ turnRequest names the sender, class, reply and decision (0.273454ms) -✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (245.265643ms) -✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (105.218887ms) -✔ typed tools carry the runner's capability; launch goes to the host's launch socket (395.154351ms) -✔ a RESULT gets no automatic reply; failed turns reply with the reason (1288.799719ms) -✔ SIGTERM during a turn kills the turn's process group and still exits 0 (150.812622ms) -✔ founder credentials stop before the claim (20) (163.622892ms) -✔ a refused claim exits 21; an ended run's capability exits 22 (193.590834ms) -✔ the launch ending under a running session exits 22 (142.09675ms) -✔ a broker that stays unreachable exits 23 after brokerRetries polls (280.860796ms) -✔ a broker that is down at the claim exits 23, not 21 (88.821778ms) -✔ no capability, or a malformed one, on stdin exits 2 (155.374009ms) -✔ the PM gets launch, its task verbs and the reads (9.166295ms) -✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.866852ms) -✔ launch only when the business's launch block names the instance as launcher (2.365004ms) -✔ an action outside the instance's authority has no tool (2.33749ms) -✔ callTool: one JSON line out, the result back, a refusal rejects (8.138127ms) -ℹ tests 45 +✔ sessionModel: agent vars win, then the system's execution settings (13.13889ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.754143ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.793955ms) +✔ a bundle is written once: an existing file refuses (2.706625ms) +✔ a path with a single quote can't go into the hook command (1.81122ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (124.816491ms) +✔ a missing or wrong policy, or a bad event, exits 2 (95.807838ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1096.785958ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (851.292573ms) +✔ claude: the hook alone blocks a path outside the workspace (519.27064ms) +✔ claude: a second turn resumes the first turn's session (776.321586ms) +✔ claude adapter: --restricted is always passed (5.499034ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (867.702037ms) +✔ claude: a missing hook or MCP file refuses before claude starts (10.298703ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.220059ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.062326ms) +✔ file tool paths must resolve inside the workspace (1.522023ms) +✔ pi's own path normalisation can't be used to step out (1.483331ms) +✔ a symlink inside the workspace that points out is outside (1.269628ms) +✔ a dangling symlink is refused at any depth, in both harnesses (4.263112ms) +✔ claude path fields per tool (1.118926ms) +✔ glob patterns stay inside the workspace (1.022964ms) +✔ a path that can't be checked is blocked (0.454162ms) +✔ initialize, ping and tools/list (48.504759ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (38.124036ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.297546ms) +✔ a missing argument is a usage error (30.763266ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (405.108517ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (381.475986ms) +✔ pi: a missing extension refuses before any model call (7.159081ms) +✔ pi: an extension without its configuration fails pi's start (340.423518ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.358533ms) +✔ turnRequest names the sender, class, reply and decision (0.199896ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (249.9341ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (116.889992ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (423.548366ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1349.631237ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (177.09152ms) +✔ founder credentials stop before the claim (20) (194.175258ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (236.143025ms) +✔ the launch ending under a running session exits 22 (118.270606ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (250.227078ms) +✔ a broker that is down at the claim exits 23, not 21 (93.191317ms) +✔ no capability, or a malformed one, on stdin exits 2 (189.733487ms) +✔ a missing or malformed policy exits 2 before the claim (128.184915ms) +✔ the PM gets launch, its task verbs and the reads (6.94313ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.622772ms) +✔ launch only when the business's launch block names the instance as launcher (2.472817ms) +✔ an action outside the instance's authority has no tool (2.525087ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (10.34312ms) +ℹ tests 50 ℹ suites 0 -ℹ pass 45 +ℹ pass 50 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 10300.527757 +ℹ duration_ms 10459.373798 diff --git a/agents/filbert/work/s6/out/node-ledger.txt b/agents/filbert/work/s6/out/node-ledger.txt index 352f240c..aadae7ce 100644 --- a/agents/filbert/work/s6/out/node-ledger.txt +++ b/agents/filbert/work/s6/out/node-ledger.txt @@ -1,81 +1,81 @@ -✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (114.436722ms) -✔ the raw path accepts nothing else, and refuses before curl runs (377.041391ms) -✔ the file checks still apply on the raw path: mode, symlink, missing, directory (304.201805ms) -✔ the raw path base URL has no override (66.337482ms) -✔ the JSON path is unchanged, and JSON never falls through to the raw path (253.125376ms) -✔ a file that changes between the two reads refuses before curl runs, with or without a body (761.646883ms) -✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (687.660023ms) -✔ real helper GET HTTP 200 preserves exit 0 without credentials (14.229594ms) -✔ real helper POST HTTP 201 preserves exit 0 without credentials (10.603237ms) -✔ real helper GET HTTP 403 preserves exit 1 without credentials (8.102968ms) -✔ fixture git subjects only, follow-ups and three session kinds (167.878949ms) -✔ text and JSON carry same numbers, open and truncated title (229.73911ms) -✔ missing credentials exit 2, no-issues never calls API and shows unknown (196.523617ms) -✔ empty range gives no rows and zero totals (162.150955ms) -✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (162.339817ms) -✔ close-only issue included, even median, missing metadata stays unknown (136.236958ms) -✔ unique commits but per-issue links count multiple tags once each (153.608813ms) -✔ page cap refuses rather than silently undercounting (190.21276ms) -✔ bad API payload not JSON refuses (138.853987ms) -✔ bad API payload {} refuses (136.396639ms) -✔ bad API payload [{"number":1}] refuses (128.488652ms) -✔ partial or malformed session log refuses with location, not content (148.218826ms) -✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (152.576029ms) -✔ no sessions is an empty table; symlink source refuses (213.349624ms) -✔ reads only refactor even when another branch is checked out (158.481962ms) -✔ invalid dates, reverse dates and duplicate options refuse (113.994757ms) -✔ T3 agent assignments do not count as human in Table 2 (144.192731ms) -✔ preamble parsing and issue number boundaries (0.467113ms) -✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.151781ms) -✔ no closed issues with human messages means undefined ratio, not invented zero (0.195161ms) -✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (488.437473ms) -✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (762.579521ms) -✔ T3: a HOME with no database exits 1 and names --no-t3 (138.909365ms) -✔ T3: a file that is not a database exits 1 and names --no-t3 (168.711684ms) -✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (450.231489ms) -✔ T3: an unmapped thread addressed as a seat exits 1 (443.080389ms) -✔ T3: a header to another thread id is not cross-checked (443.796967ms) -✔ T3: no project, or two, for this root exits 1 (980.381993ms) -✔ T3: a removed column exits 1 and names it (258.690726ms) -✔ T3: a missing table exits 1 and names it (259.346622ms) -✔ T3: a counted row with an unknown role exits 1 without its text (404.02286ms) -✔ T3: a counted row with non-text content exits 1 without its text (474.680664ms) -✔ T3: a counted row with an unparseable created_at exits 1 without its text (705.621194ms) -✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (776.458741ms) -✔ T3: a human message with no event counts in humanWithoutEvent (400.28293ms) -✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (712.325593ms) -✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (720.348958ms) -✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (349.418685ms) -✔ T3: a symlink at ~/.t3 exits 1 (146.908862ms) -✔ T3: a symlink at ~/.t3/userdata exits 1 (135.470026ms) -✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (138.959173ms) -✔ T3: with --t3-db, a symlinked file or directory exits 1 (459.255718ms) -✔ T3 WAL: the newest message only in -wal, writer attached, is counted (397.757105ms) -✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (394.598399ms) -✔ T3 WAL: -wal without -shm in a writable directory is read (421.291307ms) -✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (414.513572ms) -✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (407.146353ms) -✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5413.589411ms) -✔ a done row whose closing issue is open is a violation; a row that is not done is not (1.794885ms) -✔ an issue several rows close is expected closed only once all of them are done (0.499267ms) -✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.358303ms) -✔ each owner of an in-progress or in-review row gets one liveness class (7.492986ms) -✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.357012ms) -✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.22952ms) -✔ the text section always ends in a count and a result, and never prints a full pass (0.318585ms) -✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (30.752549ms) -✔ issue states: open list first, then the metric page, then at most 10 lookups (282.692701ms) -✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (402.628517ms) -✔ the open list refuses on a failed call or a bad record, and never echoes the helper (198.012007ms) -✔ a helper call past the deadline is killed with its child, and the call reports it (2010.177468ms) -✔ readQueue loads queue.json through the queue validator and refuses anything else (151.117396ms) -✔ protected changes list every in-range entry that changes a required or parked row (448.554587ms) -✔ the CLI prints the queue section above the weekly table and under a queue key in --json (551.263507ms) -✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (271.834681ms) -✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (340.015635ms) -✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (302.404168ms) -✔ --unsupported-runtime repeats once per seat and takes a seat name (368.468703ms) -✔ an unreadable config makes every owner invalid instead of passing them (194.237873ms) +✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (115.944145ms) +✔ the raw path accepts nothing else, and refuses before curl runs (435.681779ms) +✔ the file checks still apply on the raw path: mode, symlink, missing, directory (292.596759ms) +✔ the raw path base URL has no override (57.417954ms) +✔ the JSON path is unchanged, and JSON never falls through to the raw path (241.09635ms) +✔ a file that changes between the two reads refuses before curl runs, with or without a body (716.736353ms) +✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (785.126238ms) +✔ real helper GET HTTP 200 preserves exit 0 without credentials (13.102534ms) +✔ real helper POST HTTP 201 preserves exit 0 without credentials (9.478963ms) +✔ real helper GET HTTP 403 preserves exit 1 without credentials (8.609799ms) +✔ fixture git subjects only, follow-ups and three session kinds (161.732019ms) +✔ text and JSON carry same numbers, open and truncated title (283.864137ms) +✔ missing credentials exit 2, no-issues never calls API and shows unknown (203.728119ms) +✔ empty range gives no rows and zero totals (173.519667ms) +✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (160.627786ms) +✔ close-only issue included, even median, missing metadata stays unknown (146.587403ms) +✔ unique commits but per-issue links count multiple tags once each (149.63893ms) +✔ page cap refuses rather than silently undercounting (155.481838ms) +✔ bad API payload not JSON refuses (133.857006ms) +✔ bad API payload {} refuses (135.827145ms) +✔ bad API payload [{"number":1}] refuses (138.744231ms) +✔ partial or malformed session log refuses with location, not content (160.62317ms) +✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (151.198757ms) +✔ no sessions is an empty table; symlink source refuses (242.064585ms) +✔ reads only refactor even when another branch is checked out (174.678374ms) +✔ invalid dates, reverse dates and duplicate options refuse (115.749728ms) +✔ T3 agent assignments do not count as human in Table 2 (151.662023ms) +✔ preamble parsing and issue number boundaries (0.467437ms) +✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.152354ms) +✔ no closed issues with human messages means undefined ratio, not invented zero (0.198078ms) +✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (557.126543ms) +✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (718.686387ms) +✔ T3: a HOME with no database exits 1 and names --no-t3 (163.535756ms) +✔ T3: a file that is not a database exits 1 and names --no-t3 (142.692882ms) +✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (418.551026ms) +✔ T3: an unmapped thread addressed as a seat exits 1 (460.458843ms) +✔ T3: a header to another thread id is not cross-checked (446.050289ms) +✔ T3: no project, or two, for this root exits 1 (1228.151017ms) +✔ T3: a removed column exits 1 and names it (263.155213ms) +✔ T3: a missing table exits 1 and names it (250.866899ms) +✔ T3: a counted row with an unknown role exits 1 without its text (378.791672ms) +✔ T3: a counted row with non-text content exits 1 without its text (487.567915ms) +✔ T3: a counted row with an unparseable created_at exits 1 without its text (402.431871ms) +✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (698.410145ms) +✔ T3: a human message with no event counts in humanWithoutEvent (434.282761ms) +✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (725.856377ms) +✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (709.744542ms) +✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (349.43309ms) +✔ T3: a symlink at ~/.t3 exits 1 (148.50403ms) +✔ T3: a symlink at ~/.t3/userdata exits 1 (172.909616ms) +✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (178.900103ms) +✔ T3: with --t3-db, a symlinked file or directory exits 1 (586.925199ms) +✔ T3 WAL: the newest message only in -wal, writer attached, is counted (473.441354ms) +✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (481.222531ms) +✔ T3 WAL: -wal without -shm in a writable directory is read (522.716681ms) +✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (489.764562ms) +✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (389.568508ms) +✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5415.405848ms) +✔ a done row whose closing issue is open is a violation; a row that is not done is not (1.763922ms) +✔ an issue several rows close is expected closed only once all of them are done (0.477384ms) +✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.353983ms) +✔ each owner of an in-progress or in-review row gets one liveness class (7.516547ms) +✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.386933ms) +✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.229823ms) +✔ the text section always ends in a count and a result, and never prints a full pass (0.335678ms) +✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (22.228761ms) +✔ issue states: open list first, then the metric page, then at most 10 lookups (314.974468ms) +✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (413.703487ms) +✔ the open list refuses on a failed call or a bad record, and never echoes the helper (171.870124ms) +✔ a helper call past the deadline is killed with its child, and the call reports it (2008.175883ms) +✔ readQueue loads queue.json through the queue validator and refuses anything else (153.594153ms) +✔ protected changes list every in-range entry that changes a required or parked row (407.661757ms) +✔ the CLI prints the queue section above the weekly table and under a queue key in --json (555.885026ms) +✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (261.24945ms) +✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (308.534593ms) +✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (294.675823ms) +✔ --unsupported-runtime repeats once per seat and takes a seat name (395.674304ms) +✔ an unreadable config makes every owner invalid instead of passing them (195.182174ms) ℹ tests 78 ℹ suites 0 ℹ pass 78 @@ -83,4 +83,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 20056.827353 +ℹ duration_ms 20587.431008 diff --git a/agents/filbert/work/s6/out/node-mosaic.txt b/agents/filbert/work/s6/out/node-mosaic.txt index e17b2bb2..67688477 100644 --- a/agents/filbert/work/s6/out/node-mosaic.txt +++ b/agents/filbert/work/s6/out/node-mosaic.txt @@ -1,72 +1,72 @@ -✔ pure resolution selects current default or explicit enrolled account (1.985324ms) -✔ scope is explicit, bounded and never inferred (1.73224ms) -✔ fork pin is preserved against default change, override, missing account and revocation (0.773137ms) -✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (0.816764ms) -✔ only explicit synthetic credential forms and internal fixture stores admitted (6.000185ms) -✔ two concurrent workspaces of the same agent publish distinct complete private generations (66.448108ms) -✔ same execution ID is exclusively claimed and cannot overwrite a published generation (40.55271ms) -✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (49.434978ms) -✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (59.888007ms) -✔ credential lock contention refuses without duplicate side effects (12.175935ms) -✔ symlinked pre-existing final target is refused and never followed (28.367262ms) -✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.309415ms) -✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (28.285567ms) -✔ expired credentials refresh under transaction and subsequent generation reuses rotation (90.362553ms) -✔ refresh failure retains prior generation and store state (65.024119ms) -✔ refresh timeout retains prior generation and store state (147.617882ms) -✔ refresh malformed retains prior generation and store state (72.635364ms) -✔ concurrent refresh on same account refuses contention while unrelated account proceeds (224.359729ms) -✔ invalid refresh options refuse before burning claim (32.821353ms) -✔ fixed fake process rotates both OAuth fields without mutating caller input (35.083253ms) -✔ concurrent isolated processes preserve separate provider credentials (35.340301ms) -✔ fake failure is refused with fixed diagnostics (28.741764ms) -✔ fake malformed is refused with fixed diagnostics (25.676151ms) -✔ fake timeout is refused with fixed diagnostics (104.470792ms) -✔ fake unchanged is refused with fixed diagnostics (29.116894ms) -✔ caller executable/environment injection is rejected before spawning (0.338944ms) -✔ valid fixture tree validates and lists without secrets (91.216095ms) -✔ unknown-field refuses (0.431311ms) -✔ invalid-id refuses uppercase and traversal shapes (0.296856ms) -✔ plain-http baseUrl requires allowInsecureTransport (0.309826ms) -✔ native provider rejects allowInsecureTransport (0.13606ms) -✔ unsupported credential type and kind refuse (0.154604ms) -✔ account provider-path mismatch refuses (0.112673ms) -✔ profile account refs must be provider/account shaped (0.273262ms) -✔ seat selection accepts fork pin field, validates account refs (0.323896ms) -✔ harness manifest id must equal executable (gate 1) (0.257206ms) -✔ CLI validate: duplicate provider id across files refuses (35.850199ms) -✔ CLI validate: missing referenced provider/account refuse (39.95611ms) -✔ CLI validate: broken JSON refuses without secret echo (33.756201ms) -✔ CLI usage errors exit 2 (60.031617ms) -✔ credential.json sibling presence does not break validation and is never read (70.398482ms) -✔ D1 missing, empty and structurally empty roots refuse, no list projection (210.576539ms) -✔ D1 required directory auth cannot be absent (55.85294ms) -✔ D1 required directory auth/providers cannot be absent (60.124101ms) -✔ D1 required directory auth/accounts cannot be absent (75.645966ms) -✔ D1 required directory auth/settings cannot be absent (63.682272ms) -✔ D1 required directory harnesses cannot be absent (63.842306ms) -✔ D1 root file and unreadable metadata refuse (109.803932ms) -✔ D2 no symlink traversal at auth/providers/openai-codex.json (72.57022ms) -✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (64.503262ms) -✔ D2 no symlink traversal at auth/providers (55.265618ms) -✔ D2 no symlink traversal at auth (58.05731ms) -✔ D2 root and ancestor symlinks and lexical traversal refuse (167.655041ms) -✔ private filesystem modes enforced for root (56.250121ms) -✔ private filesystem modes enforced for auth (56.308938ms) -✔ private filesystem modes enforced for auth/providers/openai-codex.json (66.924543ms) -✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (63.934499ms) -✔ D3 numeric version 1 only across all record kinds (1.260244ms) -✔ D4 nested unknown keys and missing per-kind required fields refuse (69.021863ms) -✔ D5 unenrolled default refuses even when account exists (64.812827ms) -✔ D6 provider/account credential type must match (62.987986ms) -✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.472439ms) -✔ D8 URLs reject embedded credentials and unsupported protocols without echo (173.260503ms) -✔ D9 malformed JSON diagnostics contain no content excerpt (63.240916ms) -✔ D10 missing metadata is missing-path, not invalid-json (61.83338ms) -✔ D10 library returns no partial entries on any invalid record (76.193682ms) -✔ null/scalar/array metadata refuses without stack or echo (226.566502ms) -✔ credential sibling is never opened, even when an unreadable symlink (31.456806ms) -✔ oversized metadata refuses before parsing (62.953789ms) +✔ pure resolution selects current default or explicit enrolled account (1.878586ms) +✔ scope is explicit, bounded and never inferred (1.581008ms) +✔ fork pin is preserved against default change, override, missing account and revocation (0.716452ms) +✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (0.776018ms) +✔ only explicit synthetic credential forms and internal fixture stores admitted (5.536742ms) +✔ two concurrent workspaces of the same agent publish distinct complete private generations (39.166081ms) +✔ same execution ID is exclusively claimed and cannot overwrite a published generation (32.875243ms) +✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (45.958981ms) +✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (56.130026ms) +✔ credential lock contention refuses without duplicate side effects (11.675666ms) +✔ symlinked pre-existing final target is refused and never followed (30.62088ms) +✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.276916ms) +✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (26.435512ms) +✔ expired credentials refresh under transaction and subsequent generation reuses rotation (84.153218ms) +✔ refresh failure retains prior generation and store state (64.937175ms) +✔ refresh timeout retains prior generation and store state (144.305597ms) +✔ refresh malformed retains prior generation and store state (65.747822ms) +✔ concurrent refresh on same account refuses contention while unrelated account proceeds (215.395134ms) +✔ invalid refresh options refuse before burning claim (37.333871ms) +✔ fixed fake process rotates both OAuth fields without mutating caller input (30.930125ms) +✔ concurrent isolated processes preserve separate provider credentials (31.4959ms) +✔ fake failure is refused with fixed diagnostics (24.940459ms) +✔ fake malformed is refused with fixed diagnostics (25.340709ms) +✔ fake timeout is refused with fixed diagnostics (104.107324ms) +✔ fake unchanged is refused with fixed diagnostics (23.534776ms) +✔ caller executable/environment injection is rejected before spawning (0.284482ms) +✔ valid fixture tree validates and lists without secrets (66.88632ms) +✔ unknown-field refuses (0.382128ms) +✔ invalid-id refuses uppercase and traversal shapes (0.275639ms) +✔ plain-http baseUrl requires allowInsecureTransport (0.296326ms) +✔ native provider rejects allowInsecureTransport (0.125085ms) +✔ unsupported credential type and kind refuse (0.153165ms) +✔ account provider-path mismatch refuses (0.100346ms) +✔ profile account refs must be provider/account shaped (0.214059ms) +✔ seat selection accepts fork pin field, validates account refs (0.645005ms) +✔ harness manifest id must equal executable (gate 1) (0.201294ms) +✔ CLI validate: duplicate provider id across files refuses (29.663789ms) +✔ CLI validate: missing referenced provider/account refuse (32.571833ms) +✔ CLI validate: broken JSON refuses without secret echo (30.087738ms) +✔ CLI usage errors exit 2 (50.630519ms) +✔ credential.json sibling presence does not break validation and is never read (67.345203ms) +✔ D1 missing, empty and structurally empty roots refuse, no list projection (178.078068ms) +✔ D1 required directory auth cannot be absent (55.124406ms) +✔ D1 required directory auth/providers cannot be absent (55.174051ms) +✔ D1 required directory auth/accounts cannot be absent (65.607705ms) +✔ D1 required directory auth/settings cannot be absent (68.194942ms) +✔ D1 required directory harnesses cannot be absent (68.430456ms) +✔ D1 root file and unreadable metadata refuse (123.016733ms) +✔ D2 no symlink traversal at auth/providers/openai-codex.json (61.956083ms) +✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (70.11802ms) +✔ D2 no symlink traversal at auth/providers (75.574364ms) +✔ D2 no symlink traversal at auth (59.487548ms) +✔ D2 root and ancestor symlinks and lexical traversal refuse (170.024215ms) +✔ private filesystem modes enforced for root (63.592633ms) +✔ private filesystem modes enforced for auth (53.667674ms) +✔ private filesystem modes enforced for auth/providers/openai-codex.json (56.37671ms) +✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (58.259061ms) +✔ D3 numeric version 1 only across all record kinds (1.367263ms) +✔ D4 nested unknown keys and missing per-kind required fields refuse (63.141006ms) +✔ D5 unenrolled default refuses even when account exists (63.538674ms) +✔ D6 provider/account credential type must match (65.440288ms) +✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.464132ms) +✔ D8 URLs reject embedded credentials and unsupported protocols without echo (174.497296ms) +✔ D9 malformed JSON diagnostics contain no content excerpt (75.363136ms) +✔ D10 missing metadata is missing-path, not invalid-json (71.448136ms) +✔ D10 library returns no partial entries on any invalid record (77.188608ms) +✔ null/scalar/array metadata refuses without stack or echo (238.266046ms) +✔ credential sibling is never opened, even when an unreadable symlink (36.426891ms) +✔ oversized metadata refuses before parsing (56.665304ms) ℹ tests 69 ℹ suites 0 ℹ pass 69 @@ -74,4 +74,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 2245.097655 +ℹ duration_ms 2258.430424 diff --git a/agents/filbert/work/s6/out/node-queue.txt b/agents/filbert/work/s6/out/node-queue.txt index f21eab86..5d71889d 100644 --- a/agents/filbert/work/s6/out/node-queue.txt +++ b/agents/filbert/work/s6/out/node-queue.txt @@ -1,153 +1,153 @@ -✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1315.813425ms) -✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1081.115352ms) +✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1324.965524ms) +✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1069.545926ms) ℹ git commit -e: index.lock free during the editor -✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (1017.68701ms) +✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (1091.836977ms) ℹ git commit -e -- src.txt: index.lock held during the editor -✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (1051.31222ms) -✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1040.687515ms) -✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1041.875297ms) -✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1357.055046ms) -✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (967.860816ms) -✔ F1: a shared-index change during the procedure is not committed (1109.614ms) -✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (1087.82765ms) -✔ F1: a missing or a different hook refuses (744.467811ms) -✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1257.445186ms) -✔ F1: the canary refuses a hook that git would not run (681.891727ms) -✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (980.199611ms) -✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (881.638875ms) -✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (911.897555ms) -✔ bootstrap: the archived tests and validator run outside any repository (943.738754ms) -✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (903.425594ms) -✔ general: a queue write after the snapshot is not committed (1305.011012ms) -✔ general: a snapshot whose log does not extend the base refuses (1021.887028ms) -✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (154.209801ms) -✔ general: environment overrides, a linked worktree and usage (770.148726ms) -✔ general: a queue path staged before the run refuses at step 1 (672.104169ms) -✔ general: HEAD's queue tests failing in the archive refuse (910.522991ms) -✔ genesis document serializes deterministically and replays (4.541225ms) -✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (2.647633ms) -✔ a tampered result, receipt or viewSha fails replay (2.377146ms) -✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.154114ms) -✔ add: defaults for an ordinary seat, privileged extras, refusals (3.687345ms) -✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (7.218915ms) -✔ matrix: release, review round, changes requested and waiting-on-jason (12.362106ms) -✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (9.377367ms) -✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (19.474585ms) -✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.520257ms) -✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1418.675427ms) -✔ matrix: blocked keeps the claim and returns only to previousState (3.604666ms) -✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.595486ms) -✔ field edits: who may change what (4.724985ms) -✔ set issues keeps a logged narrowing of closes (N10) (2.533927ms) -✔ text the table shows refuses \ and <, everywhere it enters (N8) (1.673054ms) -✔ every accepted text renders to nine cells on every row (N8) (22.975ms) -✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.519538ms) -✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.719428ms) -✔ replay holds every op id to the caller's rule (N11) (5.497412ms) -✔ note: owner, listed reviewer or privileged; empty clears (1.299237ms) -✔ assign moves the claim with the owner; done clears it (2.632329ms) -✔ render is byte-stable and escapes pipes (0.526935ms) -✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.628848ms) -✔ next: resume, then review, then start, then wait, then nothing; lowest id first (21.047921ms) -✔ canonical args make a retry's identity independent of list order (0.313693ms) -✔ manifests, headings and blob ids (0.451512ms) -✔ the migration map: one queue-map block, exact keys (0.685253ms) -✔ every call but `queue` reaches the seat CLI exactly as before A2 (625.694927ms) -✔ `queue` reaches the queue CLI with the rest of the arguments (230.825945ms) -✔ the pre-A2 fixture is the script A2 changed (0.371132ms) -✔ acquire publishes the record by link; release removes only its own lock (10.852743ms) -✔ a kill between the temp write and the link leaves no lock (53.801935ms) -✔ a short or failed temp write refuses and leaves no lock and no temp (9.614997ms) -✔ a link error other than EEXIST refuses (10.524222ms) -✔ an error after the link releases the lock: unreadable gate, failing temp stat (22.084768ms) -✔ a release that fails on a gate path is reported, never a stack trace (P1) (38.708582ms) -✔ a paused holder: another writer waits 10 s, then refuses naming it live (10100.133413ms) -✔ two concurrent unlockers: the second refuses on the gate (39.149916ms) -✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (14.804068ms) -✔ a writer publishing during an unlock, gate first: the writer releases and refuses (19.392826ms) -✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (21.070219ms) -✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (18.357174ms) -✔ the same pid and start on a different boot is mismatch (0.782158ms) -✔ a foreign host is unknown whatever the local pid says; unlock refuses (68.810381ms) -✔ unreadable /proc: classification is unknown and acquire refuses (0.764468ms) -✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.193095ms) -✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (20.318818ms) -✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (28.304317ms) -✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (13.662347ms) -✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (29.026503ms) -✔ the migration map validates and renders the golden genesis table (3.535676ms) -✔ the marked QUEUE.md holds every row and parked item between its markers (1.323144ms) -✔ map-check reports each kind of drift (4.081592ms) -✔ a request posts once as the requester; a retry sends nothing (855.868743ms) -✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (4503.092654ms) -✔ the pre-send checks: GET user must name the requester, under the deadline (1589.810662ms) -✔ the lead's request refuses a token for login sage (920.746201ms) -✔ the credential file: the seat's own, 0600, no symlink, never the shared default (964.828249ms) -✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (2386.843801ms) -✔ a same-op retry after a kill sends nothing, even with a stale view (3202.595418ms) -✔ a held lock at the outcome exits 3 and names what the transport said (691.782854ms) -✔ late outcomes: after an abandon, and after a resolve with the same or another id (2338.703576ms) -✔ resolve checks the comment: issue, markers, round, candidate and author (1687.173607ms) -✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (725.637294ms) -✔ validateRow checks a request round's shape, which every replayed entry must keep (524.098243ms) -✔ request, changes, a new candidate, approval: every round pinned; no review files (1820.157622ms) -✔ a row with no reviewers opens a round that posts nothing (1223.172232ms) -✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1331.172512ms) -✔ semantics: v1 entries replay as before; review entries need v2 (490.341261ms) -✔ set reviewers refuses the row's owner (2026-09-28) (344.928618ms) -✔ the owner records no verdict, even as a listed reviewer (502.322231ms) -✔ a request comment over the length limit is not sent (593.527411ms) -✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (759.992641ms) -✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (2475.472287ms) -✔ genesis: refusals before anything is written (417.116561ms) -✔ genesis: the map must be committed, well formed, with committed briefs and seats (679.059586ms) -✔ genesis: markers, a stray witness, once only; a retry returns the receipt (558.113068ms) -✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (406.475892ms) -✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (682.226948ms) -✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (748.523026ms) -✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (743.149367ms) -✔ a retried add returns the id it first allocated, after reassignment and after done (903.80109ms) -✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (728.329374ms) -✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (1297.824741ms) -✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (850.318323ms) -✔ add, set reviewers and assign refuse the row's owner as a reviewer (600.731271ms) -✔ the working-brief check: a changed working copy refuses the start and flags next (713.071623ms) -✔ next: resume first, then nothing for an idle seat; needs a seat (320.325818ms) -✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (703.004529ms) -✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1199.175189ms) -✔ a hand edit to queue.json refuses every verb, reads included (601.872275ms) -✔ verify and render --check leave bytes and mtimes unchanged (432.353168ms) -✔ render is byte-stable across runs and repositories (300.566987ms) -✔ snapshot and verify --snapshot (817.800722ms) -✔ usage errors exit 4 (555.243725ms) -✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (284.484768ms) -✔ a rename failure: nothing visible, temp removed (169.193867ms) -✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (267.647309ms) -✔ a directory fsync failure, then sync names the op (386.599681ms) -✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (230.007017ms) -✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (202.930537ms) -✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (235.406465ms) -✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (183.252633ms) -✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (222.500035ms) -✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (195.858452ms) -✔ unlock prints a swapped gate's warning on stderr, the result on stdout (175.972928ms) -✔ a view write that fails keeps the op and reports a stale view (207.541223ms) -✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (689.655744ms) -✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (696.045038ms) -✔ SIGKILL after the witness, before the view: the stale refusal names the op (627.384ms) -✔ SIGKILL after the view, before the receipt: the retry returns the receipt (669.367102ms) -✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (254.179057ms) -✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (1053.58748ms) -✔ a deleted witness: refused after the locked recheck; accept-history records it absent (344.005ms) -✔ a header edit during a write: the op stands, the view write is skipped with a warning (202.885594ms) -✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (201.370104ms) -✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (236.401892ms) -✔ a writer paused before and after the witness rename: readers see a tail, then a match (239.793838ms) -✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (687.791547ms) -✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (645.274879ms) -✔ the platform check refuses other filesystems (158.550198ms) -✔ tmpfs passes only a test layer that allows it (N5) (226.220123ms) -✔ unlock keeps a multi-line lock record on stdout (P3) (205.315769ms) +✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (1178.432914ms) +✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1226.51663ms) +✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1214.38844ms) +✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1436.845813ms) +✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (995.087339ms) +✔ F1: a shared-index change during the procedure is not committed (1152.559542ms) +✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (1066.061784ms) +✔ F1: a missing or a different hook refuses (752.89826ms) +✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1254.926539ms) +✔ F1: the canary refuses a hook that git would not run (711.558543ms) +✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (996.255958ms) +✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (940.929157ms) +✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (1127.235024ms) +✔ bootstrap: the archived tests and validator run outside any repository (933.119951ms) +✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (892.477365ms) +✔ general: a queue write after the snapshot is not committed (1503.673101ms) +✔ general: a snapshot whose log does not extend the base refuses (1079.409414ms) +✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (159.738017ms) +✔ general: environment overrides, a linked worktree and usage (629.320722ms) +✔ general: a queue path staged before the run refuses at step 1 (672.626209ms) +✔ general: HEAD's queue tests failing in the archive refuse (856.104785ms) +✔ genesis document serializes deterministically and replays (6.025791ms) +✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (3.307413ms) +✔ a tampered result, receipt or viewSha fails replay (3.307094ms) +✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.180195ms) +✔ add: defaults for an ordinary seat, privileged extras, refusals (4.430774ms) +✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (5.558526ms) +✔ matrix: release, review round, changes requested and waiting-on-jason (12.150619ms) +✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (8.739873ms) +✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (19.356517ms) +✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.343705ms) +✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1434.478155ms) +✔ matrix: blocked keeps the claim and returns only to previousState (3.724545ms) +✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.737006ms) +✔ field edits: who may change what (5.257737ms) +✔ set issues keeps a logged narrowing of closes (N10) (3.005939ms) +✔ text the table shows refuses \ and <, everywhere it enters (N8) (1.967734ms) +✔ every accepted text renders to nine cells on every row (N8) (27.287644ms) +✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.513896ms) +✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.900355ms) +✔ replay holds every op id to the caller's rule (N11) (5.131008ms) +✔ note: owner, listed reviewer or privileged; empty clears (1.192835ms) +✔ assign moves the claim with the owner; done clears it (2.62756ms) +✔ render is byte-stable and escapes pipes (0.498811ms) +✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.542756ms) +✔ next: resume, then review, then start, then wait, then nothing; lowest id first (17.804243ms) +✔ canonical args make a retry's identity independent of list order (0.283935ms) +✔ manifests, headings and blob ids (0.411391ms) +✔ the migration map: one queue-map block, exact keys (0.52762ms) +✔ every call but `queue` reaches the seat CLI exactly as before A2 (674.544225ms) +✔ `queue` reaches the queue CLI with the rest of the arguments (195.352557ms) +✔ the pre-A2 fixture is the script A2 changed (0.290873ms) +✔ acquire publishes the record by link; release removes only its own lock (9.563753ms) +✔ a kill between the temp write and the link leaves no lock (51.394846ms) +✔ a short or failed temp write refuses and leaves no lock and no temp (6.803209ms) +✔ a link error other than EEXIST refuses (6.005551ms) +✔ an error after the link releases the lock: unreadable gate, failing temp stat (19.514826ms) +✔ a release that fails on a gate path is reported, never a stack trace (P1) (32.128036ms) +✔ a paused holder: another writer waits 10 s, then refuses naming it live (10096.79375ms) +✔ two concurrent unlockers: the second refuses on the gate (40.573245ms) +✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (24.490257ms) +✔ a writer publishing during an unlock, gate first: the writer releases and refuses (21.506575ms) +✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (20.288019ms) +✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (21.734301ms) +✔ the same pid and start on a different boot is mismatch (0.911222ms) +✔ a foreign host is unknown whatever the local pid says; unlock refuses (84.192907ms) +✔ unreadable /proc: classification is unknown and acquire refuses (0.761046ms) +✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.231473ms) +✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (21.32465ms) +✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (26.912867ms) +✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (5.802826ms) +✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (41.223634ms) +✔ the migration map validates and renders the golden genesis table (4.79073ms) +✔ the marked QUEUE.md holds every row and parked item between its markers (1.258254ms) +✔ map-check reports each kind of drift (4.415784ms) +✔ a request posts once as the requester; a retry sends nothing (905.088098ms) +✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (4705.943451ms) +✔ the pre-send checks: GET user must name the requester, under the deadline (1827.694497ms) +✔ the lead's request refuses a token for login sage (941.467669ms) +✔ the credential file: the seat's own, 0600, no symlink, never the shared default (866.334216ms) +✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (2369.942983ms) +✔ a same-op retry after a kill sends nothing, even with a stale view (3210.314414ms) +✔ a held lock at the outcome exits 3 and names what the transport said (695.500575ms) +✔ late outcomes: after an abandon, and after a resolve with the same or another id (2548.722514ms) +✔ resolve checks the comment: issue, markers, round, candidate and author (1716.454565ms) +✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (847.067684ms) +✔ validateRow checks a request round's shape, which every replayed entry must keep (544.335506ms) +✔ request, changes, a new candidate, approval: every round pinned; no review files (1799.442121ms) +✔ a row with no reviewers opens a round that posts nothing (1183.071596ms) +✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1371.550499ms) +✔ semantics: v1 entries replay as before; review entries need v2 (561.678604ms) +✔ set reviewers refuses the row's owner (2026-09-28) (416.725441ms) +✔ the owner records no verdict, even as a listed reviewer (669.107016ms) +✔ a request comment over the length limit is not sent (551.594405ms) +✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (664.607ms) +✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (2517.080568ms) +✔ genesis: refusals before anything is written (427.885336ms) +✔ genesis: the map must be committed, well formed, with committed briefs and seats (680.25551ms) +✔ genesis: markers, a stray witness, once only; a retry returns the receipt (554.26491ms) +✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (433.591615ms) +✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (668.163054ms) +✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (940.316189ms) +✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (779.602848ms) +✔ a retried add returns the id it first allocated, after reassignment and after done (992.408974ms) +✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (895.763614ms) +✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (1324.679043ms) +✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (838.423346ms) +✔ add, set reviewers and assign refuse the row's owner as a reviewer (520.931597ms) +✔ the working-brief check: a changed working copy refuses the start and flags next (694.243984ms) +✔ next: resume first, then nothing for an idle seat; needs a seat (323.350984ms) +✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (749.885267ms) +✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1166.173374ms) +✔ a hand edit to queue.json refuses every verb, reads included (579.327776ms) +✔ verify and render --check leave bytes and mtimes unchanged (470.413342ms) +✔ render is byte-stable across runs and repositories (345.591299ms) +✔ snapshot and verify --snapshot (844.369863ms) +✔ usage errors exit 4 (590.929916ms) +✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (294.991623ms) +✔ a rename failure: nothing visible, temp removed (214.12502ms) +✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (285.208379ms) +✔ a directory fsync failure, then sync names the op (395.573749ms) +✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (216.389577ms) +✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (199.305752ms) +✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (220.389696ms) +✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (213.091354ms) +✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (254.333892ms) +✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (255.382903ms) +✔ unlock prints a swapped gate's warning on stderr, the result on stdout (180.863444ms) +✔ a view write that fails keeps the op and reports a stale view (198.772798ms) +✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (773.517989ms) +✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (711.603704ms) +✔ SIGKILL after the witness, before the view: the stale refusal names the op (643.250736ms) +✔ SIGKILL after the view, before the receipt: the retry returns the receipt (710.760378ms) +✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (322.711236ms) +✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (1134.208445ms) +✔ a deleted witness: refused after the locked recheck; accept-history records it absent (408.04457ms) +✔ a header edit during a write: the op stands, the view write is skipped with a warning (230.851588ms) +✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (220.343628ms) +✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (224.916508ms) +✔ a writer paused before and after the witness rename: readers see a tail, then a match (228.249859ms) +✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (601.437386ms) +✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (603.09745ms) +✔ the platform check refuses other filesystems (172.544266ms) +✔ tmpfs passes only a test layer that allows it (N5) (225.79821ms) +✔ unlock keeps a multi-line lock record on stdout (P3) (232.916337ms) ℹ tests 148 ℹ suites 0 ℹ pass 148 @@ -155,4 +155,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 30007.76345 +ℹ duration_ms 30990.382954 diff --git a/agents/filbert/work/s6/out/node-seat.txt b/agents/filbert/work/s6/out/node-seat.txt index f18e0674..7f812134 100644 --- a/agents/filbert/work/s6/out/node-seat.txt +++ b/agents/filbert/work/s6/out/node-seat.txt @@ -1,30 +1,30 @@ -✔ resolveSeat: by name under --repo resolves the repo layout (1.245323ms) -✔ resolveSeat: by path resolves the fleet layout (0.310504ms) -✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.697631ms) -✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.632989ms) -✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.786135ms) -✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.257165ms) -✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.105947ms) -✔ CLI launch: registers, execs the fake launch script, and passes args through (31.799286ms) -✔ CLI launch: --harness lands in the record (29.529466ms) -✔ CLI launch: the launch script's own exit code passes through (29.242989ms) -✔ CLI launch: relaunching a seat rewrites the one registration record (55.726663ms) -✔ CLI launch: omitting --task records an empty string, not null (30.591302ms) -✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (80.218911ms) -✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (134.378932ms) -✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.403602ms) -✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.532978ms) -✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.718033ms) -✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.848881ms) -✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (268.708629ms) -✔ family: exactly one launch.max key in the model name, else null (0.713929ms) -✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.664675ms) -✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.485059ms) -✔ session file and launch log: 0600, the session file written once (0.953821ms) -✔ endReason maps the runner's exit codes; a signal is killed (0.111125ms) -✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.285128ms) -✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.017973ms) -✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (246.632034ms) +✔ resolveSeat: by name under --repo resolves the repo layout (1.319343ms) +✔ resolveSeat: by path resolves the fleet layout (0.336467ms) +✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.765941ms) +✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.678008ms) +✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.83333ms) +✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.31461ms) +✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.206706ms) +✔ CLI launch: registers, execs the fake launch script, and passes args through (31.07694ms) +✔ CLI launch: --harness lands in the record (34.419142ms) +✔ CLI launch: the launch script's own exit code passes through (31.343789ms) +✔ CLI launch: relaunching a seat rewrites the one registration record (61.170784ms) +✔ CLI launch: omitting --task records an empty string, not null (34.172856ms) +✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (83.178003ms) +✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (140.712262ms) +✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.469104ms) +✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (1.461526ms) +✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.754913ms) +✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.88094ms) +✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (305.051589ms) +✔ family: exactly one launch.max key in the model name, else null (0.882429ms) +✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.899512ms) +✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.695646ms) +✔ session file and launch log: 0600, the session file written once (1.399717ms) +✔ endReason maps the runner's exit codes; a signal is killed (0.136655ms) +✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.737279ms) +✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.649146ms) +✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (252.111361ms) ℹ tests 27 ℹ suites 0 ℹ pass 27 @@ -32,4 +32,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 719.297907 +ℹ duration_ms 786.592101 diff --git a/agents/filbert/work/s6/out/node-tasks.txt b/agents/filbert/work/s6/out/node-tasks.txt index 7a443f48..591d6b29 100644 --- a/agents/filbert/work/s6/out/node-tasks.txt +++ b/agents/filbert/work/s6/out/node-tasks.txt @@ -1,54 +1,54 @@ -✔ the boot config is checked before anything starts (119.97767ms) -✔ a business with no tracker entry refuses task verbs (137.857625ms) -✔ credential.expiring and .expired are recorded once per instance (223.935762ms) -✔ a token file that changes on disk records credential.changed (120.172495ms) -✔ autostart polls, reconciles and retries a startup the tracker was down for (145.800704ms) -✔ a refusal a restart must clear is not retried by the poll (113.632769ms) -✔ a poll that fires while two are queued is dropped (105.358072ms) -✔ close waits for a running verb and refuses one that has not started (249.063575ms) -✔ the bundled Vikunja is the pinned upstream image the runbook names (0.71601ms) -✔ every published port is on 127.0.0.1, and no secret is in the file (0.256866ms) -✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (418.468613ms) -✔ the recorded task bodies pass the checks S3 applies to every read (0.562714ms) -✔ the client works against the fake over real HTTP with the platform fetch (269.350239ms) -✔ a correct install starts, and the first reconcile records tasks that already exist (130.356365ms) -✔ verbs refuse while a business is starting and after startup refused it (145.385943ms) -✔ startup refuses a token that can do more than its role needs (406.541994ms) -✔ startup refuses an unsupported version and flags an untested one (319.754907ms) -✔ startup refuses a board that the runbook did not install (370.242996ms) -✔ startup refuses a project the sync bot cannot read (83.884438ms) -✔ startup refuses a configured label the pm bot cannot see (103.105883ms) -✔ startup refuses an expired credential and a missing sync credential (183.15347ms) -✔ an unreachable tracker refuses with tracker-unavailable (87.4792ms) -✔ an edit in the UI is recorded once, with the fields that changed (234.03919ms) -✔ a move between open buckets is seen on the board, though updated does not change (184.384822ms) -✔ a person's comment is counted and a bot's is not (288.788654ms) -✔ the hourly reconcile catches a comment through comment_count (237.351272ms) -✔ a task closed in the UI leaves the open view with its done bucket (442.960893ms) -✔ a task that leaves the board is recorded as deleted, moved or out of reach (271.049202ms) -✔ a poll that read before a verb wrote does not overwrite the verb (178.64957ms) -✔ a tracker fault during a tick is reported and the next tick catches up (173.029944ms) -✔ a malformed answer refuses the tick with tracker-shape (142.07532ms) -✔ no token value reaches the database, the log or a refusal (284.205053ms) -✔ the first look at a task counts only comments inside the window (182.927221ms) -✔ task.create needs a recorded human request and a requirement id (234.003886ms) -✔ only labels named in the business file can be written (229.763681ms) -✔ task.schedule sets and clears a due date and relations (319.86945ms) -✔ assign and reassign move the role bots and record task.assigned (336.838827ms) -✔ task.update.assigned is for the assignee and records task.state (338.823149ms) -✔ a wrong expected digest records task.conflict and writes nothing (215.122804ms) -✔ a cross-role verb needs a resolved decision, used once (278.383423ms) -✔ task.close needs a verdict; after it every verb refuses with task-done (255.889778ms) -✔ a lost answer is settled by a re-read and never retried (273.373847ms) -✔ a create whose answer is lost is reported uncertain, and the poll finds the task (182.244162ms) -✔ a task the sync bot cannot read refuses and records nothing (104.189372ms) -✔ verbs and polls for one business run one at a time (172.998567ms) -✔ a due date with milliseconds is written to the second (171.413102ms) -✔ every write landed and the final read failed: the verb succeeds and records what it wrote (117.387915ms) -✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (84.895896ms) -✔ a create whose final read fails succeeds and records task.created (93.866128ms) -✔ an edit between the last write and the final read shows as external on the next poll (117.570787ms) -✔ task.created is recorded when a later label write fails (84.597114ms) +✔ the boot config is checked before anything starts (86.53616ms) +✔ a business with no tracker entry refuses task verbs (121.033728ms) +✔ credential.expiring and .expired are recorded once per instance (238.662195ms) +✔ a token file that changes on disk records credential.changed (110.167027ms) +✔ autostart polls, reconciles and retries a startup the tracker was down for (137.516851ms) +✔ a refusal a restart must clear is not retried by the poll (83.605374ms) +✔ a poll that fires while two are queued is dropped (102.558062ms) +✔ close waits for a running verb and refuses one that has not started (218.733326ms) +✔ the bundled Vikunja is the pinned upstream image the runbook names (0.943209ms) +✔ every published port is on 127.0.0.1, and no secret is in the file (0.356824ms) +✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (375.792226ms) +✔ the recorded task bodies pass the checks S3 applies to every read (0.594935ms) +✔ the client works against the fake over real HTTP with the platform fetch (247.020492ms) +✔ a correct install starts, and the first reconcile records tasks that already exist (107.080549ms) +✔ verbs refuse while a business is starting and after startup refused it (156.327278ms) +✔ startup refuses a token that can do more than its role needs (390.24299ms) +✔ startup refuses an unsupported version and flags an untested one (257.301253ms) +✔ startup refuses a board that the runbook did not install (377.532789ms) +✔ startup refuses a project the sync bot cannot read (87.091341ms) +✔ startup refuses a configured label the pm bot cannot see (98.739148ms) +✔ startup refuses an expired credential and a missing sync credential (176.990677ms) +✔ an unreachable tracker refuses with tracker-unavailable (72.813225ms) +✔ an edit in the UI is recorded once, with the fields that changed (198.316094ms) +✔ a move between open buckets is seen on the board, though updated does not change (195.702643ms) +✔ a person's comment is counted and a bot's is not (282.562191ms) +✔ the hourly reconcile catches a comment through comment_count (230.197458ms) +✔ a task closed in the UI leaves the open view with its done bucket (392.842554ms) +✔ a task that leaves the board is recorded as deleted, moved or out of reach (272.858491ms) +✔ a poll that read before a verb wrote does not overwrite the verb (166.093849ms) +✔ a tracker fault during a tick is reported and the next tick catches up (163.758114ms) +✔ a malformed answer refuses the tick with tracker-shape (127.398439ms) +✔ no token value reaches the database, the log or a refusal (270.094556ms) +✔ the first look at a task counts only comments inside the window (137.891107ms) +✔ task.create needs a recorded human request and a requirement id (188.444054ms) +✔ only labels named in the business file can be written (244.533409ms) +✔ task.schedule sets and clears a due date and relations (289.26533ms) +✔ assign and reassign move the role bots and record task.assigned (294.57936ms) +✔ task.update.assigned is for the assignee and records task.state (299.706629ms) +✔ a wrong expected digest records task.conflict and writes nothing (196.565364ms) +✔ a cross-role verb needs a resolved decision, used once (254.681696ms) +✔ task.close needs a verdict; after it every verb refuses with task-done (217.486778ms) +✔ a lost answer is settled by a re-read and never retried (252.812073ms) +✔ a create whose answer is lost is reported uncertain, and the poll finds the task (174.684319ms) +✔ a task the sync bot cannot read refuses and records nothing (105.904241ms) +✔ verbs and polls for one business run one at a time (171.006531ms) +✔ a due date with milliseconds is written to the second (168.703594ms) +✔ every write landed and the final read failed: the verb succeeds and records what it wrote (107.939507ms) +✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (88.213901ms) +✔ a create whose final read fails succeeds and records task.created (94.169486ms) +✔ an edit between the last write and the final read shows as external on the next poll (115.615544ms) +✔ task.created is recorded when a later label write fails (79.373231ms) ℹ tests 51 ℹ suites 0 ℹ pass 51 @@ -56,4 +56,4 @@ ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 3697.915463 +ℹ duration_ms 3446.444304 diff --git a/agents/filbert/work/s6/out/node-webui.txt b/agents/filbert/work/s6/out/node-webui.txt index b51c95b2..8b5401ed 100644 --- a/agents/filbert/work/s6/out/node-webui.txt +++ b/agents/filbert/work/s6/out/node-webui.txt @@ -1,18 +1,18 @@ -✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3036.471142ms) +✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3469.489458ms) Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286} -✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3034.366279ms) -✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3015.132576ms) -✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1653.090341ms) -✔ conversation view: a newer session with no readable history keeps the marker (1205.30957ms) -✔ conversation view: seats without history say so and offer no reply (746.156155ms) -✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (2896.507237ms) -✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (52750.187787ms) -✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3001.025458ms) -✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21708.6077ms) -✔ loopback host and board origin fail closed (6.090954ms) -✔ real board fixture passes through WebUI; assets and isolated seen/reply work (77.465842ms) -✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (55.459641ms) -✔ unreachable board reports URL; CLI rejects unsupported options (965.757487ms) +✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3364.830245ms) +✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3544.973289ms) +✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1654.619907ms) +✔ conversation view: a newer session with no readable history keeps the marker (1085.988414ms) +✔ conversation view: seats without history say so and offer no reply (793.672153ms) +✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (3334.88261ms) +✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54055.861502ms) +✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3478.193516ms) +✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21953.046074ms) +✔ loopback host and board origin fail closed (6.453466ms) +✔ real board fixture passes through WebUI; assets and isolated seen/reply work (90.944633ms) +✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (125.009085ms) +✔ unreachable board reports URL; CLI rejects unsupported options (954.243888ms) ℹ tests 14 ℹ suites 0 ℹ pass 14 @@ -20,4 +20,4 @@ Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286} ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 -ℹ duration_ms 53029.312669 +ℹ duration_ms 54438.577036 diff --git a/agents/filbert/work/s6/out/summary.txt b/agents/filbert/work/s6/out/summary.txt index 3cccaab1..d7be879c 100644 --- a/agents/filbert/work/s6/out/summary.txt +++ b/agents/filbert/work/s6/out/summary.txt @@ -1,10 +1,10 @@ node-bus exit=0 ℹ pass 74 ℹ fail 0 node-business exit=0 ℹ pass 60 ℹ fail 0 -node-cli exit=0 ℹ pass 77 ℹ fail 0 +node-cli exit=0 ℹ pass 82 ℹ fail 0 node-control-board exit=0 ℹ pass 124 ℹ fail 0 node-conversation exit=0 ℹ pass 152 ℹ fail 0 node-discord exit=0 ℹ pass 178 ℹ fail 0 -node-harness exit=0 ℹ pass 45 ℹ fail 0 +node-harness exit=0 ℹ pass 50 ℹ fail 0 node-ledger exit=0 ℹ pass 78 ℹ fail 0 node-mosaic exit=0 ℹ pass 69 ℹ fail 0 node-queue exit=0 ℹ pass 148 ℹ fail 0 diff --git a/agents/filbert/work/s6/out/test-conductor.txt b/agents/filbert/work/s6/out/test-conductor.txt index 62afc513..1ff4f3c0 100644 --- a/agents/filbert/work/s6/out/test-conductor.txt +++ b/agents/filbert/work/s6/out/test-conductor.txt @@ -1,7 +1,39 @@ -On branch refactor -Your branch is up to date with 'origin/refactor'. +Note: switching to '8dd5ff004b0ce6157446ef78523a1036ed86f42c'. +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +Not currently on any branch. nothing to commit, working tree clean +Note: switching to '8dd5ff004b0ce6157446ef78523a1036ed86f42c'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + OK dry-run: allowed change, exit 0, nothing committed (exit 0) OK dry-run committed nothing OK apply: allowed change exits 0 (exit 0) diff --git a/agents/filbert/work/s6/out/test-extension-package.txt b/agents/filbert/work/s6/out/test-extension-package.txt index 9688d28c..52057434 100644 --- a/agents/filbert/work/s6/out/test-extension-package.txt +++ b/agents/filbert/work/s6/out/test-extension-package.txt @@ -8,7 +8,7 @@ OK check detects an extra destination directory OK check rejects a destination symlink OK sync accepts a canonical source update OK updated installation matches canonical source -scripts/test-extension-package.sh: line 14: 4103097 Killed "$@" > /dev/null 2>&1 +scripts/test-extension-package.sh: line 14: 945540 Killed "$@" > /dev/null 2>&1 OK forced interruption kills the replacing process OK next invocation recovers old consistent installation OK interrupted replacement rolled back diff --git a/agents/filbert/work/s6/out/test-queue.txt b/agents/filbert/work/s6/out/test-queue.txt index 0ff5a25e..67579c87 100644 --- a/agents/filbert/work/s6/out/test-queue.txt +++ b/agents/filbert/work/s6/out/test-queue.txt @@ -30,6 +30,6 @@ OK packages/queue declares no dependencies ℹ fail 0 OK node --test packages/queue/tests/ OK scripts/mosaic queue help -skip queue verify and render --check: this checkout (/home/jwoltje/filbert-scratch/s6-gate2) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) +skip queue verify and render --check: this checkout (/home/jwoltje/filbert-scratch/s6-gate-r2) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) queue suite: 27 passed, 0 failed diff --git a/agents/filbert/work/s6/packet-manifest.sha256 b/agents/filbert/work/s6/packet-manifest.sha256 index 8f9ad1f8..2f050045 100644 --- a/agents/filbert/work/s6/packet-manifest.sha256 +++ b/agents/filbert/work/s6/packet-manifest.sha256 @@ -1,29 +1,29 @@ 5dc9e67c9ce42b86c40d83c8b1aa8d5ba93d6e8b51bb1f4d2964f4b270ee52aa base.txt -ff57f58ab3fea8bd08d44046c66afa3c9dbdbe315af1884fe5f66e1914b870cf BUILD.md -dc346027db9b650c70b57ff390b57a8d76d3e2ccd434538cb083fe9a97ea4454 build.patch -5b3a934d01eb518e23b842623aeb3cbfc287f1b88110edc1e044d4131b2927f3 candidate-manifest.sha256 -ec2844698bd087200858279bd145060855374f88bad98a94e2a31d94a34cff61 files.txt +610db30b90e3ce17614bf8d806f35102947deb8396f0045eec0c58b22aca549d BUILD.md +f8cbf7bde355312253265d2071e5756f4b04d116bac06cfcf0a4a2c0474da942 build.patch +fd21bdc27573f080d965da3121a390b148a3370acd8d9dfa5d34fcd59460eb02 candidate-manifest.sha256 +c6309079b471a4b304ba55679bfa2ce7f1e145137ec784f2e35ee7330df844ea files.txt 8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/base-test-task.txt -c6d438dd3ffa913d2e05aeb6fb65052285d06f29fc9acd4c10f58c23515614a6 out/node-business.txt -9787d63649bbe350b9f5ce5f4f14fc95e5a2def3269db0124dd992a55c91d789 out/node-bus.txt -f8b612d08369d22f4bedc7d026a63a7a9592f652ab7602bab27e98daea36eb90 out/node-cli.txt -3d85276fa1675f46c8b26e2600cb8650d50231f8b39b5f142dae6615b73e6ce2 out/node-control-board.txt -927a609029f89118e2ce57fe77e2435669c7a6b49d1b69c783f9ff0dcd0fa636 out/node-conversation.txt -0d136f1b18657e38974378e579efc79080dc90c7ab94adb56a00d9861b45917c out/node-discord.txt -bab5785db84cdd60bccbe263b0c5c692471b8eed539729a9a9596697f2cd110f out/node-harness.txt -a43ef2dbaba0f0f8f17ae84296e08e6067b97c0467cbe89337a73f3bce01b8d5 out/node-ledger.txt -04ed00245b6c46f554f6d4453ac9455f79e9571f5eaf05021c03f6d03f1276cb out/node-mosaic.txt -39bb0c4e1030d5e94f1efdb0de1bc77c5c4baef65b1b39c1fceaa70d6bf9a4e6 out/node-queue.txt -97d94f5e9d1a715fab380ca7f1afa5ce1806a700923186bf290036b83656ba1c out/node-seat.txt -cd90ab3dc11bc8c457fc7d33ab9aac570d1bc2854025a089e3630bea01bb008f out/node-tasks.txt -c0310ed920b5b5b1f1eaa8a51cd5ae2721c0eb404889d024127939faa5c1c497 out/node-webui.txt -5a1bb131fdc9f3bb09829fabd9cd3f58cd649db694cd4d69d3e391b5aa1faf9a out/summary.txt +d72ae9083340920f68e4f549d110810d8790450cb09920745e969af2b2568145 out/node-business.txt +7eaf5878f8e31e7eb01c51c8daf52a8be2e983b3fd9c4d3b98fb2243476ccbb4 out/node-bus.txt +a83e51052bdb03b412b1451002bafad57ad89ceffc73baf4be3caa61fb8f4200 out/node-cli.txt +fe7f83107c5a25fe617f8e58413a9ff704e140c1884dc6f241c58d55bd9912d7 out/node-control-board.txt +b60466aba670a9b30a8537e24b9f291d5cbe49befe13c31c6781e3c46b8889e0 out/node-conversation.txt +e58a0e4907739b6c79ca291b428bbd5160f2444fca78656bde0283f92032891f out/node-discord.txt +1c6c21518e709a43acbf3ca4a4a7eb89cda52cf9ecc3a2a55d3c9160c0129ba8 out/node-harness.txt +7e47533785ff363fe2f7e9a3ff6fdd308f6f161578c15b836fce96a0730e4466 out/node-ledger.txt +493b230afa7be6bdbc8baad8430fb32f8e0450340c609d91648d2b2c2344db14 out/node-mosaic.txt +634086ba61d96f9bd421fdda0d1c20e08760980fbce6f70056a31586f2e0fb21 out/node-queue.txt +75f62e174199cae604672b2cc883dc0d8588816db63048090d03290f2b67bc93 out/node-seat.txt +70d50a65bf6e55adf11a9ed594f3ad3f37a75ba54ca9febf4746df82f42c6a35 out/node-tasks.txt +43f03acf96397b1658e7b0eae7136feae19f052ae323a69aef28a77c6c035a46 out/node-webui.txt +5790ec180458b55445a450625a583e09b6a5de5d820f062be4ff500074c8a9e4 out/summary.txt f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 out/test-auth.txt -e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 out/test-conductor.txt +b13547b025c824fae58dcb2d15bdac9d331a7965218e1c7491814020572984da out/test-conductor.txt 52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 out/test-config.txt 7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe out/test-discord.txt -89e5e21216ce3dfbdd4d9ebaaeb2cb1a3f8a03e34e458ffa948c6d478f375dff out/test-extension-package.txt +836c9cbc92e8c384b80a17e8e31a7ece39aca33efcad61b86898293e8d2f2cc3 out/test-extension-package.txt 83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f out/test-foundation.txt -75549ab0587c1bb5e06f9acd0e49ca21627fd936e5e2d94121ed8fe1d6e8898b out/test-queue.txt +45f31e21b7685482530fc5bae64e910462e4c6f2c1554e56465d098d0faf3da0 out/test-queue.txt 6183e6b9b05edb497e92ffd09a19bba49c6f656c750ecb2acc283021113d1a98 out/test-release.txt 8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/test-task.txt