Row 178 gave the Console --proof-header NAME --proof-file PATH, but on the pinned stack only
activate --tester-port could write a new Console flag into the pointer. A hand install of the pair
would make every later activate, pm-on and rollback refuse unit-flags-drift. activate now takes
the pair, which replaces any pair in the pointer, and --no-proof, which drops it. The pair goes
after the preview flags and before --send and --tester-port, the order webui-service.sh render
writes and flags_drift compares. With neither flag, a target Console that takes --proof-header
carries the pair; a rollback to one that doesn't drops it and says so, and an activate to one
refuses proof-target unless --no-proof is given.
An activate may name the pointer's copy only when it sets a pair or drops one with --no-proof, so
Part B's B3 and its corrected rollback line B-R1 (activate <live copy> --pm --no-proof) can run;
every other activate of the live copy still refuses already-active. Before anything stops, the run
refuses proof-needs-preview, proof-header-invalid (proofOptions' own rules) and proof-file-invalid:
not absolute, a character the unit can't carry, a symlink, missing, not a regular file, inside the
repository or the copies, mode other than 600 or 400, or another owner. The file gets lstat, stat
and realpath only; the script never opens it and no line it writes holds the value. --plan prints
one proof: line. docs/TOOLS.md "Pinned stack update" lists the flags and describes the pair.
stack-release.test.mjs adds T53, eleven subtests. Each ends with a scan of every written file and
output for the proof file's sentinel and an inotify watch that counts no open or read of it. The
five brief mutants and Darkwing's two are killed. Filbert's M8 (a carried pair skips the checks)
and M9 (the repository case dropped) survive T53; both are follow-ups on P19, with a stale comment
in proof_for_target and the stale row 178 paragraph in docs/TOOLS.md.
This is step A0 of mos-0928 Part B and builds the tool only; nothing turns the proof on for the
live Console. Filbert approved round 1 on #1685.
activate --plan printed the backup line whenever the store file existed. But run_steps rolls back
with store-newer or migration-needs-flag before backup_store, so in those two cases the plan said
both that the run rolls back and that it backs up the store. The plan now prints
"backup: none; the run rolls back before the backup" there. The no-store line comes first, since
with no store file the run skips the whole store block. When S_CHANGED is set the plan keeps the
backup line: a wrong backup line costs nothing, a wrong none line hides the directory a --restore
needs. run_steps, backup_store, refusals, exit codes and flags are unchanged; the diff to
stack-release.sh is inside plan only. docs/TOOLS.md "Pinned stack update" step 3 names the
backup line. These are Filbert's N1 and N2 on row 185 (P19 404).
stack-release.test.mjs updates T26's not-given case and adds T52, six activate cases that plan,
check the one backup line and that nothing was written, then run and check the backup exists
exactly when the plan said so. The three brief mutants are killed, and so is Filbert's M7 (an
S_CHANGED read counted as early, by T29). Filbert's M6 (the early test ahead of the no-store test)
survives, since no test plans a missing store with an out-of-chain pointer schema; that is a
bounded follow-up with the store-unclean reading, on P19 404.
Filbert approved round 1 in #1684 comment 29419, with two non-blocking notes.
102 files under agents/sage/work/vikunja-setup/: the comment, close and file scripts Sage ran on P19 and their
output records, including P19 386 comment 350 for the mos-0928 Part A window (rows 177 and 180-185). Each script
refuses to write a record that holds a key; a scan for both key values found none.
NEW 490e2771d live for mosaic-stack, activation 20261020T075800Z-490e2771d3d0, store 3i. W0-W10 pass;
stops at W9 (assistant idle stop, mos-0948) and S2b/B (old chief end rows in the backup, mos-0950).
The rollback's own bus start launches a real chief on OLD, since the pointer keeps pm true, so the
store after the rollback can't equal S2 plus tester refusals. R1 (snapshot.mjs --held) now runs right
after the rollback passes: S2's rows unchanged, new rows only the PM launch's, listed by table. R1post
compares against R1 and expects only the tester action.refused rows for the refused reads.
P3's limits rerun and two proofs (lim-20261020T074644Z, 074854Z, 074916Z) show OLD's tester limits
400 came from the emulated chief session with no provider (launch.mjs usageCounts), on both copies.
The rollback section now says what R1post expects: no limits refusal on live, one action.refused
(actor tester) per refused read such as replies with an unknown id, none per 200. W11 lists the three
P3 scratch deviations.
NEW moves from the 9f5153deb candidate to 490e2771d, which lands row 185 (#1683), 52 commits past
89af38a83. The tool blob at NEW is f8051d81cc. P5 now checks that REPO's four service scripts
match NEW instead of HEAD equal to NEW, since record commits after NEW move HEAD. Sage's SESSIONS
line for the row 185 landing.
stack-release.sh activate now backs up the stopped store under its activation id on every run
with a store file, before any migration. Before this, backup_store ran only when the schemas
differed, so a same-schema activate took no backup and rollback --restore had nothing to name.
store-newer and migration-needs-flag still roll back before the backup, since those runs never
touch the store, and a missing store takes no backup.
BACKUP and TAKEN now mean two things. BACKUP is set by every backup, so the log line names it and
from_log backup finds it. TAKEN is set only right before a migration, so a gate failure after a
same-schema activate puts back the units and the wrapper and leaves the store as the failed run
left it. activate --plan prints a backup line, or "backup: none" with no store file. Rollback and
pm-on are unchanged; a same-schema rollback --restore already works through the row 181 guard.
docs/TOOLS.md "Pinned stack update" steps 5-7 say so.
stack-release.test.mjs updates T5, T11, T26 and T29 and adds T47-T51, with one fixture fault
(start-writes-row-<copy>). The three brief mutants are killed, and Filbert's two more (the backup
ahead of the schema checks, the plan line on a rollback) are killed by T6, T9 and T50.
This is the precondition for mos-0928 Part A, the re-pin from 89af38a83.
Filbert approved round 1 in #1683 comment 29404, with two non-blocking notes.
A same-schema activate takes no store backup: backup_store runs only when
the schemas differ, and the two live backups (3f, 3h) don't match 3i, so
rollback --restore refuses both. Row 185 makes every activate back up the
stopped store before any migration and keeps the in-run restore
migration-only. Part A of the mos-0928 plan (re-pin 89af38a83 to the
refactor head) needs it first. Part B, the row 178 proxy proof
activation, is drafted for Jason and runs nothing.
Sweeps Dewey's uncommitted SESSIONS line for the row 184 rebase and in-review, and adds
Sage's line for the mos-0870 assignment (rows 177, 178, 180 and 181 and follow-ups 182-184).
Additions only. No existing line changes.
mosaic thread create|open|read and threads take --tester and run as the tester through
tester-cli.mjs. talk --thread and follow --thread take --tester too, and the row 177 refusal of
--thread with --tester is gone. The bus already checks thread ownership (thread-not-owned), so
the CLI adds no rule of its own, and every verb it sends as the tester is in TESTER_VERBS. follow
already receives with message.receive, so a tester reply arrives without polling inbox.
docs/TOOLS.md and packages/cli/README.md list the new flags.
The new tester-threads.test.mjs runs an in-process Broker. It covers create, list, open, send,
read and follow on a tester thread; thread-not-owned on a human thread both ways; and the human
forms unchanged with no tester transport call. A real-process test runs scripts/mosaic as the
tester. thread-cli.test.mjs and tester-reads.test.mjs drop the argvs that pinned the old
refusal. Brief mutants (talk sent as the human, follow without the thread filter) are killed.
Filbert approved round 1 in #1682 comment 29396 with no findings or notes. Their run on 3da362982,
with row 182 in the broker, added one more mutant (no thread read before the first receive in
follow), and it was killed.
Sweeps lines the seats appended but didn't commit: Darkwing's SESSIONS lines for rows 180, 181
and 182, and Rocko's SESSIONS and BUILD-LOG lines for rows 138, 164, 171, 176 and 178. Additions
only, as the seats wrote them. No existing line changes.
#threadFor puts a message to a threaded instance in the thread found up its in_reply_to chain. It
checked the thread's instance but not its owner, so a tester reply to a message in a human's thread
landed in the human's thread. A new #testerOwns check refuses thread-not-owned when the sender is the
tester and the thread's owner isn't. It runs on both the thread branch and the row 143 spec-notice
branch. A role's reply into a tester's thread is unchanged, since that's how the answer comes back.
The bus README says so in its row 177 paragraph.
tester-threads.test.mjs adds four T9 tests: a tester reply into the human's thread refuses and
stores nothing, one into its own thread lands, the chief's reply into a tester thread lands, and a
tester reply to a spec notice refuses while the chief's relay still lands. messaging.test.mjs:203
used to send as the tester into a human thread. It now sends from a tester-owned thread.
Filbert approved round 1 in #1680 comment 29388. Their notes N1 (thread-instance-mismatch is
checked first, as before) and N2 (BUILD.md missed project-runtime.test.mjs:13 as a text reader of
broker.mjs, now corrected) need no change.
webui serve takes --proof-header NAME --proof-file PATH, only with the --public-host preview flags.
Before listen it reads the file once through O_NOFOLLOW and fstat: a regular file owned by the
Console uid, mode 0400 or 0600, at least 32 base64url bytes with one optional final LF. Any file
problem gives one fixed refusal that names neither the path nor the contents. The header name can't
collide with Host, Origin, auth or tester names.
On a public Host the guard runs before any auth header is read. It needs exactly one proof header
and compares equal-length buffers with timingSafeEqual. Missing, wrong and repeated proofs get the
same fixed 401. A proof header on another Host or on the tester listener gets 403. The existing
auth, Origin, Fetch Metadata and human write checks are unchanged.
webui-service.sh renders both flags and checks the file's metadata and canonical location without
opening it, and the send-status regex accepts the proof pair. stack-release.sh only gains a comment:
it already carries Console flags through re-pin and rollback, and new tests pin that.
This is a partial fix. Same-uid seats can still read the proof file. Activation (middleware, unit
flags, restart, smoke) is a separate Mos-checked window and isn't part of this commit.
Dewey approved round 2 in #1676 comment 29386, candidate 9300a670. Round 1 changes were in 29374.
The chat.mjs header comment now says the seam takes the bus transport it is given: the human one
from the signed-in listener, the tester one from the row 176 listener (row 177). The TypeError for a
missing or non-function call says 'chat needs a bus transport function' and no longer says human.
No behavior changes. The webui README doesn't repeat either phrase and is unchanged.
A new chat.test.mjs test passes undefined, null, a string, a number, an object, an array and no call,
and reads the new TypeError text. Putting the old text back fails it.
Dewey approved round 1 in #1681 comment 29376. Their notes (the tester line names no file, and the
test title says both transports pass when only a stand-in function does) need no change.
COUNTED, next to STORE_JS, lists the 11 tables a bus writer fills without an event in the same
transaction (role_claims through usage_records, in schema order). STORE_JS count and heldcount
return events after STAMP plus each listed table after STAMP from the same open as before. A
listed table the store lacks counts 0, and one that exists but can't be read refuses
restore-mismatch.
rollback --restore refuses post-backup-events unless the total is 0 or --events equals it, and
the message names each table with rows. The in-run recount compares the same total. An
events-only store prints the old text. T46 reads every table in schema.sql and fails on a table
in no list or in two.
Filbert approved round 1 in #1679 comment 29372. Follow-ups F1 (task_comment_cursors) and F2
(the refusal text at stack-release.sh:909) are recorded in agents/darkwing/work/queue-181/BUILD.md.
next_activation() takes the UTC second once and tries the base id, then -1 through -9, against
stack-rollback/, backups/stack/ and the post-attempt store, all under the lock. ACT and POST share
the suffix, and with all ten taken it refuses capture-failed with nothing stopped. Adopt uses the
same helper. --restore takes the old and the suffixed form.
Under --plan, a bad --tester-port reports tester-port-invalid through check_refuse and
tester-port-target goes through check_refuse in both modes, so the plan lists every refusal.
Without --plan the output is the usage block, byte for byte as before. webui-service.sh counts the
tester port's digits as written before stripping zeros, so 007333 refuses as serve does.
Round 2 adds subtests C1 (adopt takes the next free id) and C2 (a bad port is usage before any
pointer read) for Dewey's round 1 (#1678 comment 29352). Dewey approved round 2 in comment 29361.
The tester gets threads of its own: thread.create, thread.open, thread.send, thread.messages and
assistant.stop join TESTER_VERBS, with owner `tester`, and neither the tester nor the human reaches
or lists the other's threads. A tester thread.send is from tester-cli with no human.input; in a
tester thread the lists read the tester as the human, the run answers at `tester`, and its send to
`human` refuses tester-thread-human. The tester stop revokes only tester threads, by `tester`. A
business whose human is `tester` refuses invalid-business at start.
The tester listener (7333) serves chat through the TESTER_CLI caller, its fence admitting only the
four chat write routes, with the listener's own Origin required; 7332's chat is unchanged. The
Console composer is enabled for the tester. READMEs state calls (a)-(c), N1, N2 and where a tester
reads replies (sage-1148).
Design 99ce5780 approved in sage-1146 (#1675 comment 29333).
Evidence for the STATE report Sage checked and filed in 88f3253d9 (sage-1151). The skills-enabled
tar stays out of git; it is the W0b restore point under $WIN only. No credential or Environment
values: unit reads record ExecStart only, and the P3 credential files were fake placeholders.
serve --tester-port N starts a second listener on 127.0.0.1:N in the same
process, with a bus caller through TESTER_CLI. N is 1024..65535, not
--port and never 7332, and the flag needs the bus configured. The tester
listener takes only instances.set, routes.set and name.set; settings
save, project-file save, assistant stop, bus message and chat writes
refuse 403 with no bus call. It refuses the main listener's auth header,
and every other listener refuses any request carrying x-mosaic-tester.
/api/config answers tester: true and the page shows a tester banner with
the routes, instances and seat name editors only. tester-off and
tester-required map to 403 in a serve.mjs coreBusStatus wrapper, so
bus.mjs stays pinned (row 164).
The broker lets a tester read credentials (with #secretCheck), routes,
project.files and project.file; project.file.save stays human-only.
webui-service.sh takes --tester-port with serve's checks. stack-release
activate --tester-port N writes the flag to the pointer and the unit,
refuses tester-port-target when the target copy's Console lacks it, and
activate or rollback to such a copy drops the flag and says so in --plan.
docs/TOOLS.md rewords the stale row 175 two-phase step (#1673 F1).
Built by Rocko on 473258a51 (manifest a4c728fc...52c9), approved by Dewey
in round 2 (#1674 comment 29322) after round 1 changes (29309), Sage's
gate green on 473258a51 plus the patch in run 2. Run 1 failed T34's
failed-pm_launched subtest with capture-failed: activate and pm-on name
the activation by the second, so the two ran in one second and collided
on the rollback set. That race is row 169's (the base fails the same way
with the id pinned), and a follow-up row fixes it (Vikunja P19 391).
TESTER_VERBS gains usage, names, limits, plugins, instances, project.list,
thread.list, name.set and project.create. A tester reads limits, plugins
and instances and sends plugins.set, instances.set and routes.set, each
logged with actor tester. The project file verbs refuse a tester
tester-refused. The broker and CLI drop the [mos test]
prefix rule (mos-0872 Q1). The CLI takes --tester on the seven reads and
the W1-W7 write forms and refuses it (exit 4) elsewhere, bus start and
stop included.
project init --tester checks the project area, refuses the area root
itself, binds the tester with one agents read, then commits the layout as
tester (mos-0872 Q2). scripts/mosaic sends plugins, name, names, follow,
threads and thread to packages/cli (sage-1081). The row 164 pins are
untouched.
Built by Dewey on 9e74925c2 (three commits to 989699f7c, manifest
2e30f2a1...29d5f), approved by Filbert in round 3 (#1673 comment 29279),
Sage's gate green on 716ddcf1e plus the patch.
F1: pm-on-down stops the Console, so the paths section now says what each
path does to it. F2: the working `node <copy>/packages/cli/src/cli.mjs
plugins set` form and the PATCH shape, until row 175's fixed `mosaic
plugins set` lands. F3: the business-unreadable refusal on activate and
rollback.
Darkwing's candidate on ce5807989, manifest 2b161c51, approved by Dewey
(#1677 comment 29271). Doc-only: no test reads docs/TOOLS.md
(stack-release.test.mjs:428 names it in an expected message string), so
the approval is the gate (decision 137).
activate ID BUSINESS --pm-after-harness installs the bus without --pm
and writes a pointer with pm false and pmPending true. It refuses
pm-after-harness-target when the target has no harness kind, and
pm-after-harness-set when the business file already sets plugins.harness.
pm-on BUSINESS refuses not-pending and harness-unset, then reinstalls the
bus with --pm on the pointer's copy and runs the full gate. A failure puts
the phase-1 unit back (pm-on-failed, exit 1), or stops both units
(pm-on-down, exit 2).
A plain activate refuses pm-pending. status prints the pending state and
the next step. activate and rollback refuse business-unreadable when the
target's code can't read the business file's plug-ins.
Built by Darkwing on 4b738e2da (patch-id 17a90c4ad795, manifest
c6aaac1f...54af), approved by Filbert (#1666 comment 29263), Sage's gate
green on e7d47c346 plus the patch.
drop_in_override refuses drop-in-override when a unit path, a drop-in
directory or a .conf exists but can't be read, as install_guard in
bus-service.sh would after the stop. A unit path that isn't absolute
refuses too.
plan_store and the rollback --restore event count check is-active again
after reading the store the running bus holds. If the bus stopped
meanwhile, --plan prints "store schema: unknown" with no migration lines,
and rollback refuses restore-mismatch before anything stops.
The gate's copy-check step compares the check's bus line with the copy
this run installed and names both when they differ.
read_units returns 1 when the bus Environment= line or either unit's
ExecStart argv holds a quote or a backslash, so activate and rollback
refuse unit-flags-drift, adopt refuses check-failed and status prints
that it cannot read the units. Nothing parses systemd quoting.
Tests T28 to T31 in packages/cli/tests/stack-release.test.mjs.
Author and gate owner: Sage (mos-0855).
Role files take an optional skills list. The bundle reads each named skill
from <dataRoot>/skills-enabled, copies it into the bundle and renders every
file into prompt.md between skill-file markers, so the prompt text matches
the bundle copy byte for byte. The role's skills together are capped at
SKILL_BYTES (64 KiB): the claude adapter passes prompt.md as one argument,
and Linux caps one argument at 128 KiB. Symlinks are refused, not followed.
The launcher checks the list before a run exists: skill-not-enabled names
the activate command, anything else refuses skill-refused.
Pi keeps --no-skills; Claude Code keeps --disable-slash-commands and
--restricted. The two harness modules and MOSAIC_SKILLS are unchanged.
First lists: coder, reviewer, cto and pm, ms-unslop on each.
The click handler returned on busy, which refresh() holds for the whole /api/board read, so a click during a
poll did nothing and the sign-out test waited 5 s for a sign-out that was never asked for. The row 165 test's
location.reload() from an evaluate let the next wait start on the old document, and the reload killed it.
Researcher's candidate 8977d907 (patch 843fdee9), approved by Filbert in #1669 comment 29193. Built on d58c9c5cd and applied on 04155deff without fuzz; the patch's +/- lines hash the same on both bases. Row 171's one broker.mjs line (project-name-is-repo, at :2256) is the only change on a manifest path between them.
Darkwing's queue-162 packet for sage-1004/1006/1008: the two scratch
--plan runs on blob 4b16e848 and the load checks of the usage-source
file under 5fa9cc8f2 and 8e7ea89a1 (mos-0845 D3, mos-0847). Secrets
scan clean.
Both plans cite the files. W3 notes that the scratch store line reads
the pointer, since the scratch root has no store.
U1: usage checks read total and byRun, since `mosaic usage --json` prints
no record list. W7 wants one new byRun entry for the chief run with
input > 0, output > 0 and unknownTurns 0.
Window B's last section: the D4 design can't work as ruled. A refused
PM launch exits 3 and the unit won't restart it (sage-1007).
The comment script and its record put the mos-0845 rulings on P19
379-382 (comments 273-276).
Window A (repin-5fa9cc8f2.md): 3h, no --migrate, Jason's usage-source
patch with the 8e7ea89a1 setter before activate and the channel patch
after, copy-CLI calls for the verbs scripts/mosaic misroutes (F1),
threads --business expecting [] (F2), no pm smoke (F3), a usage record
check after the chief smoke (F4). Window B (repin-ca15d73e9.md): row 126
only, 3h to 3i with --migrate, the D3 restore report, and a section for
the first window past c216f47db that waits on P19 382.
Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi
and claude-code hold what core branched on: the adapter, thinking levels,
preflight, version probe, sandbox mounts, builtins line, typed tool names,
gate timeout, bundle files, adapter env, turn end and session-files path.
Core reaches a module only through the loader.
A session's route is the instance's harness, model and thinking vars, or
the three fallback. vars; a missing one refuses no-route, and the system
file's execution keys are never read for a session. The launcher checks
the primary route, then the fallback: harness-off, harness-refused,
thinking-unsupported, then preflight. A failed preflight falls back when
a fallback is set, and any refusal of the fallback names the primary's
failure too. Account checks run
only when the route's module counts an account. The manifest records
route, routeReason and harnessVersion, replacing piVersion and
claudeVersion. pi gets --thinking, Claude Code --effort, both from
MOSAIC_THINKING.
The business file refuses a partial fallback. routes and routes.set are
human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by
routes.set, instances.set and limits.set and checks only changed routes.
The chief of staff changes an instance's model, and the assistant's whole
vars (Q169), through limits.set, so its route write runs the same checks.
A model outside launch.models refuses unknown-model, primary or fallback,
and routes.set passes that code through.
A new run binds only on a switched-on harness module name and records
its transcript path, which the usage reader uses. Settings gains a
Routing table with six selects per instance and loses the Role models
fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is
unchanged.
Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new
launch refuses harness-off.
The business file can name the account claude-code launches use,
`launch.account` = {name, credential}. `name` is an identifier and
`credential` names one login set's environment variables, never a value.
`launch.limits.tokens.daily.<account>` = {limit, since, reset, timeZone}
sets a daily quota for that account. A daily key that isn't the account's
name refuses the business read.
The new plug-in limit-check/daily-quota (interface 2, pure) starts a day at
the first instant at or after `reset` on that local date in `timeZone`. Across
DST that makes a 23- or 25-hour day, and a reset in a skipped hour starts at
the gap's end. It counts the account's known records of all four kinds since
the later of the day start and `since`, and counts unknown turns apart.
The launcher refuses account-credential-missing, account-credential-ambiguous
and account-is-credential (an account name equal to a session credential's
value). Messages name variables, never values. The account rides mayLaunch,
bindLaunch, the usage records, mustStop and the launch record. A pi launch
has no account, records NULL, and no daily quota covers it. `mosaic limits`
and the Console's Settings show one fixed line when a daily row shows:
"Daily quotas count only this stack's own use of the account, not its use
anywhere else."
Bus schema 3h to 3i: 3h reserved usage_records.account as
CHECK (account IS NULL). The new step 3h-3i.sql rebuilds the table in one
transaction, keeping every row's seq and the sqlite_sequence value, and
recreates the three append-only triggers. All of sqlite_master equals 3h's
apart from the account line. A 3h engine refuses a 3i store.
packages/bus/tests/account-migration.test.mjs covers all three, with a
step that fails partway and leaves 3h intact. 3i goes live only through
`stack-release.sh activate ... --migrate` with Jason present (P19 371); the
activate path takes the pre-migrate backup, and the way back is restoring
it (agents/dewey/work/queue-126/MIGRATION.md).
Mos's go mos-0835, six conditions: (1) one transaction, account-migration
test 2 and mutant D29; (2) sqlite_master against 3h, test 1 and mutant D28;
(3) account is never a credential, the launcher's account-is-credential
refusal (launch-account.test.mjs) and the business name pattern
(token-limit-settings.test.mjs); (4) MIGRATION.md, accepted by Mos in
mos-0837; (5) the packet's DESIGN.md "no schema change" line corrected;
(6) the public/ digest below.
The public/ change in bus.js (the daily line under the token-limit table)
carries Mos's decision 123 go mos-0833 on the +/- digest 03e6d9da...ba78,
unchanged at landing.
Owner Dewey, brief docs/plans/2026-10-14_prd-1.2-build.md, "Daily quota for
the credential the stack uses today". Review round 2 rebases the round 4
build onto 1df04f52f (row 167). Reviewer: Filbert approve 29068 on round 1
(manifest d18314f7...8e65); Filbert approve 29080 on
round 2 (manifest f332db76...6fa6).
Seat run on round 2: touched 158/0, singles 1388/0 with 1 esbuild skip,
test-land-work.sh 86/0, 30/30 mutants killed.
Sage gate on 54319f552: GATE_PASSED, 2474+422/0, all 15 packages, the queue
suite and 10 script selftests.
Nothing live.
Filbert's r1 note (a): the scope line didn't name the 3i rebuild or the
account-is-credential guard, which sage-0970 and mos-0835 authorize.
Item 6 describes both. The acceptance list gains the mos-0835 tests, and
the scope bullet now covers the migration step, the version bump and the
guard.
The Settings page held the launch settings twice, one copy for the launch form and the
token-limit reset and one for the Assistant section. A save in one form left the other with
the old revision, so its next save was refused with revision-conflict. Now one snapshot
serves both. Every successful save or reset sets it from the answer, a read replaces it only
while no form has a draft, and reload clears it.
The names read now rethrows a 403 or signed-out like the usage, instances and limits reads,
so a refusal gives the refused page instead of an empty name field.
Tests: the names refusal, one per snapshot path (launch save, assistant save, reset), and a
null body on /api/name and /api/assistant-stop. limits-browser.test.mjs:54 now expects the
reset's revision on the following save.
Owner Darkwing, brief docs/plans/2026-10-18_settings-reads-after-q169.md.
Mos's decision 123 go mos-0836 on the public/ +/- digest
82a8a810...e984. Mos's F1 and Filbert's notes 1-2 are P19 370.
Reviewer on candidate manifest dba38330...59cc: Filbert approve 29062.
Seat run: 231/0 on all 38 webui files singly, 8/8 mutants killed.
Sage gate on ee3f1fb3f: 2443 package tests and 422 script selftests, 0 failed,
GATE_PASSED 01:06:56Z.
Nothing live.
`mosaic business instances BUSINESS` reads the role instances, and
`mosaic business instance add|set` writes one add or change through the
broker's new human-only `instances` and `instances.set` verbs. The writer is
packages/business/src/instance-settings.mjs. It takes the caller's revision,
validates the next document, refuses a definition that declares credentials
(organization-credentials-out-of-scope), keeps protected instances as they
are, logs the human as the actor, and says when the running bus needs a
restart. Neither command restarts the bus.
The writer refuses the instance ids human and tester with invalid-request,
because the broker refuses a business whose roles use either name when the
bus starts.
The Console's Settings view shows the instances and, under --send only,
posts to POST /api/instances with the exact Origin and same-origin Fetch
Metadata. A refusal keeps the draft. An answer without the view's shape is
outcome-unknown (503). The page's public/ diff carries the decision 123 go
in mos-0832.
Neither verb is in ORGANIZATION_KEYS, so no agent tool reaches them.
Owner Darkwing, brief in row 165 (#1662). Round 3 rebases round 2 onto
4f45cadc0 (row 156). The public/ +/- digest is 939ecd28...9444, unchanged
at landing. Reviewers on candidate manifest e30b54da...f580: Dewey
approve 29052, Filbert approve 29053. Seat run on 4f45cadc0: 1436/0
singles, 37/37 mutants killed. Sage gate on e8b09735a: GATE_PASSED 25/25,
packages 2282/0, queue node 156/0, script selftests 422/0, 23:59:13Z to
00:05:35Z. Rebased onto 50a57e454 over two SESSIONS-only commits.
Nothing live.
Until now a landing ran only when the chief of staff or merge-gate
followed row 149's manual steps. The host now does it.
Broker. landHolders(business) lists the approved merge decisions each
instance holds, sharing the #held generator with landHeld.
landRelease(record) releases a run's claim with by='land-relaunch'.
Delivered and unread messages keep the run, and a run that hasn't had
its first message.receive answers {released:false, landing:true}
(#received, round 2), so a landing turn in progress is never cut. A
run that never reaches its first receive, a hung landing turn or a
broker restart, keeps the release closed, and the decision falls back
to row 149's path.
usage.begin accepts a decision id that landHolders lists for the run.
The bus process answers both ops from their own launchOp lines and
refuses any op it doesn't name.
Launcher. start() reads landHolders once for a non-thread instance and
takes land_work and the session's landing list from that one answer.
landPass() runs after threadPass() on each tick.
Runner. One turn per landing entry before the message loop, with a
usage bracket on the decision id.
Docs. A "Landing runs" section in the cli README and docs/TOOLS.md.
roles/merge-gate.md, cto.md and chief-of-staff.md say the host starts
the run, and row 149's steps are the fallback.
Owner Dewey, brief docs/plans/2026-10-18_landing-run-launch.md, design
DESIGN.md in the packet (sage-0808). Round 2 answers Filbert's F1-F3
(29033, rulings sage-0942). Reviewer on candidate manifest
72a5c1b6...b700: Filbert approve 29039. Seat run 4: 290/290 singles,
36/36 mutants killed. Sage gate on a618528b2 (candidate 524bfcc80): GATE_PASSED 25/25. Package tests 2258 plus queue 156, script selftests 422, 23:30:07Z to 23:38:04Z. Run 1 stopped at pkg-webui on board-held.test.mjs:163, the P19 366 timeout, 220/1. Run 2 resumed at pkg-webui on the same tree: webui 221/0 and every scripts/test-*.sh green.
Nothing live.
Jason's Q168: a visual editor for the Console's project documents,
held to row 148's rendering rules.
Library. ProseMirror with prosemirror-markdown and markdown-it, chosen
in the phase 1 library report (LIBRARY.md, REPORT.md in the packet).
packages/webui/third_party/editor/ holds 19 upstream packages as source,
byte for byte, each with its license, plus NOTICE and PROVENANCE.json.
Build. packages/webui/editor/ holds the model, view, entry and
build.mjs. esbuild 0.28.2 is an exact devDependency of packages/webui
with its own lock; editor/toolchain.mjs checks the version and the
binary's sha256 before any build. MOSAIC_REQUIRE_EDITOR_BUILD=1 turns a
missing or different esbuild into a failure, and the rebuild test then
checks the committed src/public/editor.js and editor.css match a fresh
build. The built bundle is committed under Mos's conditions
(mos-0755, mos-0765).
Console. The docs view gets Visual and Source modes. A save still
writes Markdown through the existing project write path, with the same
stale-base and read-only handling. A mode switch reuses the draft and
reads nothing from the project. Links and images keep row 148's
target rules, and no raw HTML runs. serve.mjs serves /editor.js and
/editor.css; index.html loads them.
Owner Researcher, brief docs/plans/2026-10-16_q168-wysiwyg-editor.md.
Round 5 is a rebase onto d88613bac (row 155); only docs/TOOLS.md,
webui/README.md, public/bus.js and serve.mjs differ from the round
before. Reviewers on candidate manifest 3a4bc21c...3b05: Dewey
(security) approve 29027, Filbert approve 29026. Mos's decision 123 go
for public.diff 902f0244 (handwritten part ea59079e): mos-0824. Sage gate
on 72c3fa598 with MOSAIC_REQUIRE_EDITOR_BUILD=1: GATE_PASSED 25/25
(node 2397, selftests 422), 22:07:29Z to 22:13:50Z; the rebuild test
ran and passed.
Nothing live.
reviewer, coder, pm and cto declare gitea and vikunja credentials, and the
row keeps credentials out. The writer refuses an added instance or a
definition change whose definition declares credentials, with a named
refusal that says why, and never writes an instance without its binding.
One added test line. Acceptance 314 is covered: security-reviewer and
merge-gate declare none. Jason rules on credential binding in daylight.
Jason's Q169: only the chief of staff removes the assistant; the chief
and the human configure it and its seat name.
Configure. A new organization key, assistants, replaces the vars,
tracker or definition of an instance whose definition is already the
assistant. holder and credentials are refused, and the whole document
still goes through validateBusinessDocument. set() refuses
organization-definition-refused if any other instance's definition
changed. THREAD_DEFINITION and END_PROTECTED move to
packages/business/src/vocabulary.mjs, so the broker and
launch-settings.mjs read one list. The human writes it with
mosaic limits set --file or the Console Settings view's Assistant
section; the chief writes it with change_organization. Both carry the
file's revision.
Seat name. The chief gets a typed set_name tool (name.set,
instance.name). The Console gets POST /api/name on the settings
boundary, through human-cli.
Remove. role.end and role.revoke of a threaded instance revoke every
thread of that instance whose latest claim is a claim, and report
threads: N. The raise and the check both record a missing holder run as
null. A new human-only verb, assistant.stop, revokes the human's own
threads of an assistant instance; the Console calls it from POST
/api/assistant-stop. The bundle, the end_run description and
roles/chief-of-staff.md now say the chief ends and configures the
assistant. No schema step.
Owner Darkwing, brief docs/plans/2026-10-16_q169-assistant-config.md.
Reviewers: Dewey (security) r1 approve 29012, r2 29014, r3 29018; Filbert r1 29010,
r2 29015, r3 29017, on candidate manifest 96cae28e...9016. Round 2 is
confirm-only after rows 162 and 125 moved the base under docs/TOOLS.md
and the cli README. Round 3 bumps row 164's webui/src/bus.mjs byte
baseline in packages/bus/tests/channel-proof.test.mjs for the eight
STATUS entries; Sage's gate on r2 failed only there. Mos's decision 123 go for the public/bus.js diff
(mos-0816) covers 9f9ce31b, unchanged here. Sage gate on 197722caa:
GATE_PASSED 25/25 (node 2370, selftests 422) 21:11:38Z, a rerun on the same tree after a first run stopped at pkg-webui on a board-held.test.mjs timeout that passes singly.
Nothing live. Follow-up brief in the packet: the names read rethrow, one
shared settings snapshot, null tests for the two new routes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
W2 adopt on Mos's go with Jason present, W2b pointer check (copy 52f35e7a0,
schema 3f) before W3, W3 plan line-by-line match against Darkwing's scratch
rerun, and the W2 undo: remove the pointer only. Landing commit 7e7f8e208.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New scripts/stack-release.sh with four verbs. adopt records the running
copy, the unit flags and the store schema in state/stack-active.json; it
stops nothing. activate ID BUSINESS [--pm] [--migrate] stops the units,
checks the stopped store, backs it up, migrates when the target's schema
differs, installs and starts the target, then gates on both units active,
a live bus host whose pid is the bus unit's MainPID with boot ok, Console
/healthz, stack-copy.sh check and one launched journal line. A failed
gate rolls back inside the same run and keeps the attempted store as
bus.post-attempt-<stamp>. rollback [--restore ACT [--events N]] goes back
to the log's previous copy and counts the events written after the
backup. status prints the pointer, the log tail and the copy check.
--plan prints what a run would do and writes nothing; it reads the store
schema from the store the running bus holds, or from a stopped store.
Exit codes: refusal 3, gate failure with rollback 1, rollback failed 2,
usage 4. Nothing is pruned.
stack-copy.sh gets a reachable function split from copy_commit and a
guard so its helpers can be sourced. TOOLS.md "Pinned stack update" and
the cli README pinned-stack paragraph describe the verbs.
Owner Darkwing, brief docs/plans/2026-10-18_stack-release.md (D1).
Reviewer Filbert: r1 approve (comment 28997) on 091693c5, r2 confirm-only
approve (29004) on candidate manifest c213a801...bd15c after row 125
moved the base under TOOLS.md and the cli README. Sage gate on 23622980f:
25 of 25 steps rc=0 on c96a95320 (15 packages + 10 scripts), node pass 2353 fail 0, selftests 422 passed 0 failed, 2026-10-18T20:32:13Z to 20:38:55Z, network sage-r162_default removed, 0 containers.
Nothing live. The 8e7ea89a1 window (repin-8e7ea89a1.md) runs these verbs
on Mos's go with Jason present.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 125's core lets a check see only launch.limits and accept hits on the
four DEPTH kinds, so the quota gets its own depth-1 kind,
tokens.daily.<account>, in the two DEPTH tables and the organization-file
schema. Display is a fixed line keyed on the daily path; no report field.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds token limits under launch.limits.tokens (instances, providers, keys,
models.P.M). Core (packages/bus/src/limits.mjs) reads the organization
file and the per-turn usage records row 124 writes, then asks every
switched-on limit-check plug-in; the first refusal wins. A limit no
switched-on check reports reads "not enforced". The limit-check kind is
interface 2 (mayLaunch, mustStop, report); a v1 check is refused at load.
The shipped token-limit check is pure and counts known end records only;
unknown turns add nothing and are shown as their own count.
The launcher refuses limit-reached through the host's mayLaunch op, or
limit-check-failed naming the check. The broker asks mustStop after a
usage end and stops the run at its next receive or state. Settings stamps
since on a new or changed limit and runs reset; launch.mjs and
business.mjs validate the shape. Readers: mosaic limits and mosaic limits
reset, the bus limits view, the Console /api/bus/limits route and the
Settings token table with Reset. Key limits stay "not enforced" until
row 126 names accounts on records.
Owner Dewey, design under lead decision 150.
Reviewer Filbert: r1 changes (comment 28965), r2 approve (28981), r3
approve (28996) on candidate manifest 8b71c405...fe56 after the rebase
onto row 164 (37 of 38 paths with identical +/- lines, TOOLS.md:621
bracket). Decision 123 go on the bus.js hunk from Mos (mos-0814). Sage
gate on b6827d19e: 25 steps, all rc 0 (15 packages, 10 script suites), 2295 node tests and 422 script selftests, 0 failed, 20:03:25Z to 20:08:08Z, network sage-r125_default removed.
Live stays as pinned. Not part of the 8e7ea89a1 window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Records Jason's "Q4: yes" (DECISIONS Q171, Q172) and lifts the hold.
The row moves to briefed once row 155 is done. No test reads this file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds plugin kind `channel` (interfaceVersion 1, dispatch(request, {call}))
with cli and webui modules. Project and thread surfaces and thread
talk/follow in the CLI and the Console chat go through the selected
channel. An empty or absent channel selection is off: the CLI exits 3 and
the Console answers 503, each naming `mosaic plugins` and `mosaic plugins
set` with the business selector. Management plugins and plugins.set stay
on the core transport. The loader gains a synchronous snapshot(kind)
getter; loaded(), modules() and last-good handling are unchanged. No lock
is held across dispatch. serve.mjs, public/ and the bus are untouched.
Owner Rocko. Reviewer Dewey: r1 changes (comment 28980, B1: the recovery
sentence was never asserted), r2 approve (comment 28984) on candidate
manifest ae2fcfda...1fc8, the B1 assert killing mutant D1. Design accepted
by Mos (mos-0768, mos-0773). Fixture rewrite at project.test.mjs:47 acked
by Mos (mos-0789) on Dewey's line-by-line check: lines 48-51 identical,
one added assert. Sage gate on e5b10d275: 25 steps green, 2081 package tests, 156 queue node tests, 422 script selftests, 0 failed.
Live stays as pinned. The re-pin that takes this live puts `mosaic
plugins set` in Jason's window right after activate (mos-0768).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos relayed Jason's answers in mos-0787. P19 332: a CLI command and a
Console view for role-instance changes, held until Jason confirms Mos's
reading. P19 333: accepted as a documented limit, so the I6 brief and the
design's limits line now say I6 writes it as a known limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the Console chat view: project and thread lists, named create forms,
per-thread drafts, explicit Resume, paging and signed-in sends. The one
HTTP-to-bus seam is packages/webui/src/chat.mjs, with fixed routes in
chat-http.mjs: exact HTTPS Origin, same-origin fetch site, JSON content
type, and 403 for each. Every label and message body renders inert,
including bidi controls. CSP unchanged, no inline script.
Owner Rocko. Reviewer Filbert: r1 changes (comment 28933), r2 approve
(comment 28950) on candidate manifest 9a665434...4034, with the four r1
survivors killed by new assertions. Mos decision 123 go on the public/
diff, sha 78574095 (mos-0780). Sage gate on 19fe85c3e: 25 steps, node
2216/0, scripts 422/0, webui 179/179.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New roles:
- roles/security-reviewer.json and .md: read tools only, review.verdict
within its role, no cross-role grant and no credentials, one verdict per
request.
- roles/merge-gate.json and .md: read tools only, cross-role
git.merge.protected only. It raises with domain technical and the task
ref, and lands with land_work once the decision resolves, the verdicts
approve and the gate passes. The chief of staff messages the relaunched
instance with the task ref and decision id (round 3 finding 28913).
Contracts:
- All 13 roles/*.md carry the proactive line in their Authority section.
- pm and chief-of-staff carry the REQ-DEC-2 routing paragraph. Its eight
hard waits are pinned to GATED_ONLY by a test.
Harness: raise_decision takes an optional domain, technical or delivery,
defaulting to delivery. The tool description says where each kind of raise
goes. end_run is unchanged (row 155).
Tests are new files only: business and bus workflow-roles tests and
harness raise-domain. Builder Dewey; Filbert approved round 4 (comment
28929) on manifest 6b4af9c2685b, base 0f715a054 plus build.patch
df0fec543dba.
Gate on 806ab89df plus the candidate, 16:34:57Z to 16:39:43Z: 2027
package tests and 422 script selftests (plus the queue suite's 156 node
tests), 0 failed. Doc-only follow-ups: business README:194-196 and the
launch.instances doc line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 126's daily quota is a second limit check with no launcher or bus
change, so it needs row 125's launcher and bus calls. Its after listed only
row 124. Row 125 moves from researcher, who is on row 154, to dewey, so the
two limit-check rows run in order with one owner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's round 2 candidate e09313aba4d155a1c5af7bfb227ec397e96c0d57444782cd0b48593102bcc684
(38 paths, base 23533625d), approved by Dewey in #1618 comment 28923 (queue rev 1757).
Packet: agents/rocko/work/queue-124/r2/.
- Schema 3g to 3h: packages/bus/src/migrations/3g-3h.sql adds an append-only
usage_records table; nothing earlier changes. usage_records is in timedTables.
- The transcript usage-source module reads token counts from Pi and Claude
Code session transcripts and follows no symlink.
- `mosaic usage` in the CLI and a read-only usage section in the Console's
Settings (one GET route; 400 on a query string).
Decision 123 go: Mos, mos-0737. Activation (3g to 3h) is a migrating run
with Jason present, in Mos's combined re-pin; landing changes nothing live.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Researcher's phase 1 report needs /editor.js and /editor.css route
entries in serve.mjs, and spec/north-star.md doesn't exist. The brief
now owns those two entries only and names
docs/plans/2026-09-27_goals-review.md for the round-trip test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decision 96 D track. D1 turns the de352dfaf window scripts and the
release.sh pointer-and-log shape into stack-release.sh adopt, activate,
rollback and status. D2 adds rehearse on a scratch root with launches
disabled and no model calls. A standing staging stack is left to Jason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A relaunched merge-gate run has land_work but no message to turn on: the
raising run's poll consumes the approval DECISION. The host writes the
instance's landHolders entries into session.json as landing, and the
runner takes one fixed-template turn per entry before its receive loop.
Row 149's chief-of-staff message stays as the fallback; a second land
gets decision-consumed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 128 round 1, Researcher's candidate (agents/researcher/work/queue-128/,
row128.patch, manifest 9eed6a78, 8 paths, base 0f3eb25ea). Filbert
approved round 1 (comment 28895).
`mosaic project create` and `project list` call project.create and
project.list; `init` and `check` keep their arguments and refusals.
`mosaic threads`, `thread create`, `thread open` and `thread read` cover
the thread verbs, and `mosaic talk --thread` sends thread.send as a
REQUEST and prints only the reply to the sent message. `mosaic follow
--thread` is the same loop without the send. A thread reference is its
id, an id prefix of 8 or more characters, or <project>/<name>; unknown,
short and ambiguous references refuse with exit 2 before any send. Every
verb refuses inside an agent run. There is no default thread, so a plain
`talk assistant` still meets thread-required-use-thread-send.
Acceptance 314 step 1 now creates the project and the assistant thread.
New: packages/cli/src/thread.mjs, packages/cli/tests/thread-cli.test.mjs.
Sage's landing gate ran on 77b1a6e8a plus the candidate: 25 steps, all
rc 0 (15 packages, 10 script suites), 14:44:02Z to 14:48:42Z, network
sage-r128_default removed. The commit sits on 7de8b7270, which adds one
docs/SESSIONS.md line that no test reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers Darkwing's round 6 review (28884) and Filbert's doc-only
follow-up (28883).
- After docker stop, the rule waits for "no such container" by id,
bounded by the core stop timeout, then rm -f and the same wait; a
container still there refuses exit 1, which the unit retries.
- A name conflict is now only a hand-made container without the store's
labels; it stays exit 3, which the unit does not retry.
- Start order: host.lock, store path, rootless check, leftover rule,
docker run.
- I6 gains two tests: run straight after a stop boots, and a leftover
never removed refuses exit 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers Darkwing's round 5 review (28880) and Filbert's doc-only
follow-ups (28881).
- connected follows the channel from the wrapper's creation, as a forked
child's does; the entry starts the broker on boot and the notifier on
start, so host.mjs:217 and :244 never wait on a pid line.
- Bus start makes <dataRoot>/bus at 0700 and takes its real path; the
bind source, mosaic.store label, leftover filter and hostStatus all use
it.
- A name conflict refuses with exit 3, names the container and the
client's exit code, gives host.lock back, and removes nothing.
- The host.pids test list names what each test does with the pid.
- I6 gains four lifecycle tests for these.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers Darkwing's round 4 review (comment 28877) and Filbert's
doc-only follow-up (28876).
- P: the core container's name is mosaic-core-<16 hex of SHA-256 of the
real path of <dataRoot>/bus>, keyed on the store like writer.lock, so
two data roots with the same business don't collide.
- Q: close() waits CLOSE_TIMEOUT_MS twice (host.mjs:30, 343, 347). The
host passes MOSAIC_CLOSE_TIMEOUT_MS to the entry; the core stop
timeout is two close timeouts plus 5 s (45 s today), used by the
leftover rule's docker stop -t.
- R: {from, pid} start line; connected turns false on the exit line,
client stdout close or exit, or a failed stdin write; host.pids is
null for both forks in containerized mode and adds container; the
pid-signalling host tests stay local, I6 uses docker exec kill.
- Nit: firstReply (129) and ended (148) cited apart from watchChildren
(159-165).
- I6 tests and design section 9 add the slow-notifier leftover, two data
roots, the missing-timeout refusal and connected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Design section 3 gains "The core container's lifecycle": the fixed name,
labels and --rm; tagged fork lines that carry each fork's replies, kills
and exit codes, so the host's per-child stop, start-failure kill and
watchChildren keep their logic; what the entry does when one fork exits;
the client ending first; SIGTERM; the host dying without closing stdin
(not spiked, I6 measures it); the leftover removal at bus start; and
bus status liveness from the engine instead of the lock's container pid.
B2 now names caps on the entry's engine-relayed stdout. Section 9 and
I6's files and tests carry the new rules. Broker cites move to
0f3eb25ea after row 143; LAUNCH_OPS is process.mjs:5-20.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 2026-10-14 brief predates rows 121, 142 and 143. This one names the
verbs on the existing broker ops, keeps plain talk asking for a thread
with no default, and targets acceptance 314 steps 1 and 3. Owner moves
to researcher.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing L (comment 28863): section 3 gains "The host channel, a
boundary change". The bus host starts the core container with
`docker run -i` as its own child; core-entry.mjs forks process.mjs and
notifier-process.mjs with IPC inside it and relays tagged JSON lines over
the client's stdin and stdout. The core container runs with
--log-driver none, since replies carry caps; I6 tests that. The channel
adds no authentication of its own, so it is boundary change B2.
Darkwing M: the registry keeps the docker run client's pid and
startTime, so bindLaunch, runtime.mjs and broker.mjs are unchanged and
I5 changes no file in packages/bus. The human proof's ancestry check
never meets the client, so the mount list carries that boundary and I5
tests that a session can't reach sock/human or sock/relay. recover()
removes the container, kills a leftover client and binds as today.
Darkwing nits: the start check's temp names are patterns from their
writers, five state files, and the git-socket.mjs split across I3, I5
and I6.
Filbert (comment 28864): section 10 states the Console's outbound
reach on the mosaic-console bridge, no wider than today, with the
--network none forwarder as the not-spiked follow-up named on P19 333.
Section 7 says four networks and lists mosaic-console.
Section 11 lists B1 (the Console bind, before I4) and B2 (the host
channel, before I6); I4's and I6's gates name their ruling.
Doc-only: no test reads either file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 143 round 2, Researcher's candidate (agents/researcher/work/queue-143/,
row143.patch aff7cff0, manifest a34136bf, 11 paths, base 8fcd0887e).
Dewey approved round 2 (comment 28862) after round 1's changes (28853).
A human save of spec/north-star.md through project.file.save stores one
notice per pm and cto instance, from _bus in no thread, in its own
transaction after the commit. If the notice can't be stored the save
answers outcome-unknown and the commit stands. pm and cto answer the
chief of staff once, a BLOCK naming each conflict or an INFO beginning
"Accept"; the broker checks a BLOCK's sender, recipient and in_reply_to
notice. The chief's relay that replies to the BLOCK is filed in Jason's
most recently opened assistant thread in the notice's project. With no
such thread the relay is refused and the commit still stands. No other
save, commit or verb sends a notice, and a notice to an unrun role waits
for a claim and launches nobody. No schema change: notices and answers
are messages rows.
Contracts: roles/pm.md and roles/cto.md say how to answer,
roles/chief-of-staff.md says to relay in_reply_to the carrying message,
roles/assistant.md says to tell Jason in its own words that the change
stands. New tests: packages/bus/tests/spec-review.test.mjs and
packages/business/tests/spec-review-contracts.test.mjs.
Sage's landing gate ran on 80a2e442b plus the candidate: 25 steps, all
rc 0 (15 packages, 10 script suites), 13:46:52Z to 13:51:25Z, network
sage-r143_default removed. The commit sits on 970e88bf2, which adds one
docs/SESSIONS.md line that no test reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Console binds 0.0.0.0 in its own namespace behind a host-loopback
publish (a decision 107 enforcement change, reach unchanged). It mounts
businesses/ and its other mounts at their host paths, checks bus-host/
against an allowlist, turns the queue panel off, and answers 404 on every
board route. The git socket's bundles and land logs move to git-host/.
recover() still ends leftover runs host-lost. RESOLVED_VIA gets a
human-socket entry, and every container kind gets mount-list and
hardening tests. Spike citations are corrected, and the root case and
two-network setup are marked as not spiked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's F5 on comment 28853: nothing tells the chief of staff to relay a
BLOCK or an accept in_reply_to the message that carried it, and the bus
refuses a relay without that link. Row 143's brief now owns one bullet in
roles/chief-of-staff.md.
Dewey's F1: the assistant is the threaded instance, so a plain talk is
refused thread-required-use-thread-send, and no CLI code creates a project
or a thread before row 128. Acceptance 314 adds row 128 to its
prerequisites and a step that creates and opens Jason's assistant thread
in the project.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 87, the I1 design. The design turns decisions 105 to 108 into
interfaces: one rootless endpoint named on every call, three images,
two broker listeners with a human-socket via value, a host relay with a
peer-pid check, sessions as hardened containers, file-held tokens,
egress only through an allowlist proxy, and the Console in a container
on the human socket. Every container claim cites spikes S0 to S9 or HP.
Mos ruled on the host-process reading (mos-0657): routes open to a
process under Jason's uid are decision 105's accepted limit; the
other-uid reach to the Console port goes to Jason as P19 333, default a
documented limit, and only I6's limits section waits on it.
The rows file holds the I2 to I6 briefs on the template, each with
owner, reviewers, files, what ships and gate. Doc-only: no test reads
either file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey 0572: organization.set's instances field only sets the model of an
instance the business file already declares (launch-settings.mjs:46-52),
and business.mjs:130 refuses a launch.instances name the roles don't
declare. The row 149 brief said chief's organization change adds the
merge-gate and security-reviewer instances, and acceptance 314's
before-the-run list repeated it. Both now say Jason adds them by editing
the business file and validates it. Doc-only; no test reads either file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's row 135 round 3 note 1: step 8 ran mosaic trail with no
argument, but the CLI takes <task|decision>. The step now lists the
tasks and reads each task's trail, which includes the decisions filed
under it and the spec commit each cites. Doc-only: no test reads
docs/acceptance/314.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The harness raise_decision tool has no domain parameter, so no agent
reaches arbiters.technical and merge-gate's technical raise can't be
made. Row 149 now owns that tool (not end_run, which stays row 155's),
plus the pm and chief Authority text that still sends every ungranted
action to the human. Mos accepted the carry on 2026-10-16.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One shared window: copy de352dfaf and the 52f35e7a0 rollback set made,
business check under both copies, rehearsal and restore scripts tested on
fixture stores, row 141 manifest checks and a row 148 Console check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 141 round 1, Darkwing's candidate (agents/darkwing/work/queue-141/,
build.patch bb036c6d, manifest d474b330, 26 paths, base 18aee8fa1),
rebased without change by Sage onto 7e40fb83b as round 2
(agents/sage/work/queue-141-r2/, build.patch 72b890d9, manifest
cae29087). Row 148 moved two of the paths, packages/bus/README.md and
packages/bus/src/broker.mjs; the interdiff touches only index and @@
lines. Filbert approved round 1 (comment 28841).
Schema 3f->3g adds instance_names, an append-only log whose latest row
per instance is its current name. message.send resolves a to that is
not an instance key or a role through the names table, and records
to_name. The new bus writer name.set and read names back the CLI verbs
mosaic name INSTANCE NAME (human-only) and mosaic names [--json].
`instance.name` is routed under REQ-MSG-2 (1.5). It joins ACTIONS and
ROUTED in the vocabulary and the broker, and roles/chief-of-staff.json
grants it within-role, as row 139's brief asks of whichever of 139 and
141 lands second. The chief has no tool for it until row 155, so the
chief's untypedActions and bundle untyped list are ["instance.name"]
(harness tools.test.mjs:43, bundle.test.mjs:94).
Follow-ups: Vikunja P19 tasks for TOOLS.md and acceptance 314 step 10
(Filbert, doc-only) and for refusing a new key or definition equal to a
current name. Row 155's brief carries the return of untyped to [].
Sage's landing gate ran on 7e40fb83b plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 22:30:48Z to 22:35:15Z,
network sage-r141_default removed. The commit sits on 2a85b41da,
which adds one docs/SESSIONS.md line that no test reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/,
row148.patch 1b63164b, manifest 95ae4736, 27 paths, base 18aee8fa1).
Dewey approved round 3 (comment 28832) and Filbert approved it with no
findings (28835). Mos gave the decision 123 go on public diff 8360122b
(mos-0636): no vendored code, no innerHTML or other HTML sinks.
The Docs page #/docs lists each project's spec/north-star.md and the
Markdown files directly under docs/. Save writes the typed text as one
local commit authored by business.human, through a new human-only bus
verb project.file.save that carries the opened commit as base and
refuses stale-base if the file moved. The preview is a textarea plus a
renderer built with createElement and textContent only; raw HTML shows
as source text and images never load. CSP is unchanged.
Round 3 replaced the backreference HR regex, which overflowed the
regexp stack, with an isRule scan. Round 4 added a textContent fallback
when paintDoc throws, and the submit handler resets saving in a finally.
Mos's rule (ceiling 1 s at 1 MiB, growth x3 per doubling over 1 to
4 MiB): 130/130 and 138/138 cells pass, worst 513 ms at 1 MiB, worst
ratio x2.50, control x2.01; the 756-cell grid has 0 timeouts. Mutants
43/44, v21 equivalent.
Follow-ups: Vikunja P19 tasks 318 (Save stays off after a non-render
paint throw), 319 (dead NUL check in bus.js) and 320 (the 1 MiB + 64 KiB
buffer before the human proof, Gitea #1650).
Sage's landing gate ran on a4779d2a5 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z,
network sage-r148g_default removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Re-pin 3190ad762 -> 52f35e7a0, no migration, per Mos's 20:58Z message.
The plan puts row 137's refs/heads/root write in the window. One working
repo, mosaic-stack/stack.git. Its seed root is the canonical checkout.
Jason's ~/.mosaic-dev/projects/mosaic-stack/stack is named and is not
touched. working-refs.sh records the refs before and after; root-rollback.sh
deletes only the seed-created refs/heads/root, matched by sha, and leaves
main alone. Copy and rollback set (sage-pin-rollback-3190ad762) are made
and checked. Nothing live ran.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SMOKE.md for the shared 139/137 re-pin window (sage-0613), from the code at
18aee8fa1. Doc only; nothing run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 137 round 4, Darkwing's candidate (agents/darkwing/work/queue-137/r4/,
build.patch 0cabe175, manifest 548ee477, 31 paths, base 63c82f1c4).
Filbert approved round 4 (comment 28809) and Dewey approved it (28816)
after round 3 changes (28801, 28805). Darkwing's round 4 ran 9/9 mutant
kills, bus 317, seat 150, land-work 86.
Round 5 is a rebase only (agents/sage/work/queue-137-r5/, build.patch
d6cbaaa8, manifest f73598fe). Row 150 changed docs/TOOLS.md, broker.mjs
and cli/README.md under it. The round 4 patch applied unchanged onto
435d1c616, and with index and @@ lines removed the two patches are
byte-identical. Filbert checked the interdiff (comment 28818).
A landing moves refs/heads/main in <dataRoot>/working/<business>/
<project>.git, never the project root or canonical refactor. The seed now
fetches the root as refs/heads/root and fast-forwards main only when it
is an ancestor, so a landed main survives the next push. Only a
cross-role merge decision lands. Its squash names the real channel:
"via the CLI" for the human, and "as its arbiter, via the broker" for an
arbiter's decision.resolve. Every other class refuses not-authorized
before the human check.
This carries row 139's two items. REQ-LAND-1: a gated merge decision
never lands. REQ-DEC-2: the host land op accepts an arbiter-resolved
merge. land-work's read.mjs stays human-only, and land-work.sh still
accepts only a human CLI resolution.
Sage's landing gate ran on 435d1c616 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 20:51:00Z to 20:55:34Z,
network sage-r137_default removed. Cherry-picked onto 5d98fb194, which
added only a docs/SESSIONS.md line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 150 round 2, Darkwing's candidate (agents/darkwing/work/queue-150/r2/,
manifest 7556dda0, 7 paths), a rebase-only round onto 63c82f1c4 after
row 139 changed broker.mjs. Filbert approved round 1 (comment 28800) and
the rebase (comment 28806): only broker.mjs's index line and hunk offset
moved. Darkwing's r2 round ran 6 files, ROUND_PASSED.
`mosaic inbox --read <id>...` and `--read-all` mark the human's unread
messages read. Each argument is tried; refusals print their code and the
command exits 2. The Console's send section now names `mosaic inbox` and
`mosaic inbox --read`. broker.mjs changes a comment only.
The public/ diff is sha256 096d922c, unchanged from round 1 and covered
by Mos's decision 123 go.
Sage's landing gate ran on 4ebef2284 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 20:41:55Z to 20:46:24Z,
network sage-r150g_default removed. Cherry-picked onto 1ad48035c, which
added only a docs/SESSIONS.md line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 139 round 2, Dewey's candidate a4c5657f (14 paths) on 5661bd233.
Filbert approved round 2 (comment 28786) after round 1 changes. Dewey's
gate: 14 runs rc 0. Mutants m1-m9 killed. Filbert ran 7 files singly:
0 fail, 0 skipped.
The broker's GATED and the business's GATED_ONLY are now the hard waits.
A cited gated decision goes to the domain arbiter unless it is a hard
wait, which always goes to the human. Under Jason's ruling Q169, a
role.revoke of the assistant is a hard wait. Its end gets past
END_PROTECTED only for the chief of staff, or when the cited decision
was resolved by the human through the CLI. PRD version 1.6 records Q169.
Mos approved the 1.6 text, including the REQ-ROLE-2 sentence on the
chief's end and revoke. scripts/test-land-work.sh adds the not-human
cases for arbiter-resolved merges. land-work's read.mjs is unchanged.
Row 137 lands second, so it carries the gated-merge refusal and the host
land op's arbiter acceptance. Making an end or revoke stop an assistant
run, and configuring the assistant, are row 155 (#1649).
Sage's landing gate ran on 26b78d483 plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 19:23:57Z to 19:28:28Z, network sage-r139_default removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PRD 1.6 REQ-ROLE-2 gains Mos's two sentences (sage-0545): the chief ends
the assistant with no decision, and any revoke of it is a hard wait.
Checked against broker.mjs; the code already does both. New candidate:
patch a5f21ead, manifest a4c5657f. Both PRD-reading tests pass; repro OK.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest 7a1fcae1 (patch 7a29d911) on 5661bd233. Only the chief of
staff, or a decision the human resolved through the CLI, ends or revokes the
assistant; a revoke of it is a hard wait. PRD 1.6 text in PRD-1.6.md for Mos.
Gate 14/14 rc=0 and mutants m1-m9 killed under slot sage-0533. SMOKE.md plan,
not run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Q169 replaces Mos's sage-0523 ruling. The chief of staff's end of the
assistant succeeds, another role's arbiter-resolved revoke of it is
refused, a human-resolved one still works, and other cited revokes route
by REQ-DEC-2. Reconfiguring the assistant is a separate row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's row 139 note 2: under 139 an arbiter could approve a cited
role.revoke of the assistant. Mos ruled it stays human-only, overridable
by Jason. Row 139 now owns the PRD note under REQ-WAIT-1 and a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason's ruling Q168 via Mos: one more library search against the row 148
bar, then an open-source WYSIWYG built from source in the repo if nothing
passes. Mos's decision 123 go on the license and candidate comes before
any code is imported.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on 5661bd233: GATED and GATED_ONLY become PRD 1.5's eight hard
waits (data.delete, network.expose and account.create are new), derived
as ACTIONS minus an explicit ROUTED list (REQ-WAIT-3). A gated raise
citing the current spec goes to the domain arbiter unless it is a hard
wait. land-work refuses an arbiter-resolved merge with not-human.
Mutants 5/5 killed; gate green (bus, business and every scripts/test-*.sh).
Row 137 has not landed, so its REQ-LAND-1 refusal and the host-op
arbiter acceptance are left out, as BUILD.md says.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Version 1.5 records Jason's Q167 ruling and added text as its source
block. It replaces REQ-DEC-2 routing: a gated decision citing the
project's current spec commit goes to the domain arbiter, and only an
uncited decision or a hard wait goes to the human. It adds the hard
waits (REQ-WAIT-1 to 3, with data.delete, network.expose and
account.create), projects and the spec file (REQ-PROJ-1 to 4), agent
landing (REQ-LAND-1), approvals that outlive the run (REQ-DEC-5),
settled decisions under a new spec (REQ-DEC-6), the four workflow roles
and proactive contracts (REQ-ROLE-5, 6), staff requests (REQ-COS-2),
assistant summaries (REQ-ASSIST-3) and name addressing (REQ-MSG-2).
docs/acceptance/314.md is the run Jason does in the Stack once the
rows are live.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The plan for Mos's A4 window: stopped-bus backup with sha256, rehearsal on
disposable copies, live migration with migrate-schema.mjs, NEW units and
wrapper, verify-new.sh and check-launch.mjs for the NEW copy (read_spec now
required), and restore steps A and B. Nine scripts committed with
SCRIPTS.sha256 so Mos can check the scratch copies before running. Not run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 138 round 2, Rocko's candidate e3f9a400 (43 paths) on 26a0b0fab.
Dewey approved (comment 28754) after round 1 changes (comment 28752):
D1-D5 mutants killed, the 17 touched test files run singly passed
124/124. Rocko's own run: 753 tests and 59/59 mutants killed.
The bus gains the project adapter through runtime.mjs, the way it
already injects the organization, so broker.mjs takes no new imports
and refuses with project-not-configured when no adapter is set. The
project socket timeout is 120s. Roles gain record.write. The schema
moves from 3e to 3f; the live migration follows MIGRATION.md and
needs Mos's window.
Sage's landing gate ran on c842686d5 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), network sage-r138_default
removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 2 candidate on 26a0b0fab, manifest digest e3f9a400...8f70, 43
paths. It adds tests for Dewey's D1-D4 (comment 28752), the N-a project
socket timeout fix, and the title and N-f text fixes (sage-0492).
Rocko's run: 753 package tests passed, 59/59 mutants killed. Packet
inventory 4a2a9c57...683f verifies 85/85; scanned for secrets and
binaries, none found.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BUILD.md gains the final suite and mutant results (747 passed, 54/54
killed); rebuild-manifest.txt gains its last entries. The packet
inventory is rebuilt to cover all 99 packet files (sha256 1a536101...b6ef,
sage-0490). Candidate digest unchanged: 75e07b65...47d1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 152 round 1, Dewey's candidate on 56639c99b. Filbert approved
(comment 28741). The bus marks a waiting message launchable or not;
when the target isn't in the launch block, launch.by is still told,
with a notice saying the launch would be refused, and talk prints that
launch.by can't launch the role. Sage's landing gate ran on 5f57d85e5
plus the candidate: 25 steps, all rc 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on 56639c99b: waiting gains launchable, and a message to a role
outside launch.instances gets a notice saying the launch is refused and a
talk line saying launch.by can't launch it. Mutants 8/8 killed; gate green
(bus, cli, business, webui and every scripts/test-*.sh).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 153 round 1, Rocko's candidate on 686fc1cb6. Filbert approved
(comment 28727). projectRepo lives in packages/project/src/paths.mjs, so
business.mjs is unchanged (sage-0469). Sage's landing gate ran on this
commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 151 round 1, Dewey's candidate on 686fc1cb6. Filbert approved
(comment 28726). Test-only. Sage's landing gate ran on this commit
plus row 153.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build packet for row 153, project init cleanup and the nested records repo
refusal. Filbert approved round 1 (comment 28727). Committed by Sage for
Rocko, who has no git authority; secrets scan clean, no binaries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
No behavior of mine changed. Round 2's patch applies to d8487b3af with no
conflict; only broker.mjs and bus/README.md blobs differ, from row 136's
edits. 66 of 66 bus, harness and cli test files pass singly; 25 of 25
mutants killed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Round 2 candidate 7a585aa9606863a0d44a26094b585160914e3d8fd5f13436b56470cd69e6d867
(Rocko, packet fafee2f22), approved by Filbert in comment 28707.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 2 candidate 7a585aa9606863a0d44a26094b585160914e3d8fd5f13436b56470cd69e6d867,
packet manifest 4292da90162c801a9c56716a0539f8fa072e67e93e4f4d9a0f1e107b84b652ea,
base 02ee7f640. Filbert approved round 2 in comment 28707. Committed by Sage
for Rocko, who has no git access; secrets grep clean, no binaries (one empty file).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Test-only round for Filbert's changes request (comment 28639): three new
staff-request tests, mutants N1, N2, N2b and n2f added (25 of 25 killed),
chief-of-staff.md conflict with row 146 resolved keeping both bullets.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Candidate db463a7b…ab89 (5 files) on 277d1fa21. Adds the note-2 test for a thread
decision raised before row 121; mutants 6 of 6 killed after fixing the test to
supersede an open decision (first run left M6 alive, recorded). Owner's gate exit 0;
gate.sh now sets COMPOSE_PROJECT_NAME=dewey-r136 and removes its network at exit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's row 146 work (agents/dewey/work/queue-146/build.patch, packet
08e327bf5), applied unchanged on 625c21f41. Candidate manifest
616f953f...9607 on dda7e31b0; no commit since touches its 10 paths.
Dewey's gate passed and 17 of 17 mutants were killed. Filbert approved
round 1 in comment 28641.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 145 added 3 lines to cli.mjs after the approved round 1 base; same change, new base, regenerated
candidate manifest, TOOLS.md line rewrapped. Every cli test file passes singly; 6/6 mutants killed on the new base.
Packet only; the candidate is row142.patch and is not applied to the tree. Nothing pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Candidate on dda7e31b0: staff.request verb (pm only, STAFF-REQUEST class no other writer can produce),
request_staff pm tool and prompt line, pm and chief-of-staff contract text, 7 bus tests, tools and bundle
tests, one launcher end-to-end test. 21/21 mutants killed; node suites and all ten scripts/test-*.sh green.
Packet only; the candidate itself is row140.patch and is not applied to the tree. Nothing pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
BUILD-LOG: the live bus window 3b to 3e and rows 144 and 145.
SESSIONS: Sage's window and landing lines, and Dewey's row 136 round 1
line, left for this sweep. Rocko's 22:26 and Researcher's 22:44 lines stay
uncommitted for their seats.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Swept revisions from other seats: Rocko 1373 and 1394, Dewey 1375, 1380,
1381 and 1393, Researcher 1376, 1398, 1399 and 1402, Filbert 1382, 1385,
1395 and 1403. The rest are Sage's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 145 BUILD.md records both review rounds, the gate on 9357baac9
(GATE_PASSED, 24 steps) and the landed commits 92938ab43..fba879f6a.
q167/ holds the issue receipts for rows 135-143 and 146-147.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, rebased: git diff f4d7b91a9..0e7d1d8b8 from
work/vikunja-19-309, stacked on 305. Business reviewer verdict dfd44af2
approve on 0e7d1d8b8; Jason resolved merge decision 12a20e47 via the CLI.
No conflict. packages/plugins/README.md, which refactor also changed,
merged with the source commit's +/- lines; the other four paths are the
source blobs. Filbert reviews the rebased diff.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, rebased: git diff 813671596..f4d7b91a9 from
work/vikunja-19-305, stacked on 295. Business reviewer verdict 57d7f0b7
approve on f4d7b91a9; Jason resolved merge decision 0eebf83f via the CLI.
Conflict resolution only. packages/bus/src/broker.mjs conflicted with
row 123 (89fd2d6fe): both sides extend the limits branch of request().
Kept row 123's organization.set role path and ORGANIZATION_KEYS, and
added 305's plugins and plugins.set to the verb list and its keys lines.
plugins and plugins.set take the human-only path as in 305: role is true
only for organization.set, the actor is the human, and the verb reaches
the organization writer unchanged. organization.set is unchanged.
packages/plugins/tests/searxng.test.mjs conflicted with 307: both add a
test at the end of the file. Kept 307's AbortSignal test, then 305's
hasModule test. Three other paths that refactor also changed merged
without conflict, with the source commit's +/- lines; the other six
paths are the source blobs. Filbert reviews the rebased diff.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, rebased: git diff 813671596..6219c5c8c from
work/vikunja-19-307, stacked on 295. Business reviewer verdict 84c9c4be
approve on 6219c5c8c; Jason resolved merge decision 17bb9efc via the CLI.
Conflict resolution only. packages/harness/src/tools.mjs conflicted with
row 132 (0e947c216): both sides add a constant after TASK_PAGE_MAX. Kept
row 132's ORGANIZATION_KEYS, then 307's WEB_QUERY_MAX. Three other paths
that refactor also changed merged without conflict. For each, the +/-
lines on the new base equal the source commit's. The other nine paths are
the source blobs. Filbert reviews the rebased diff.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, rebased: git diff 07c12b51b..813671596 from
work/vikunja-19-295, its seven commits as one (9ede6935b through
813671596, which include task 296's searxng check). Business reviewer
verdict 51c71b46 approve on 813671596; Jason resolved merge decision
29ff6589 via the CLI.
Conflict resolution only. packages/harness/src/tools.mjs conflicted with
row 132 (0e947c216): both sides add a block after review_verdict. Kept
row 132's end_run and change_organization block, then 295's web_search and
web_fetch block, each with its own closing brace. Seven other paths that
refactor also changed merged without conflict. For each, the +/- lines on
the new base equal the source commit's. The other 20 paths are the
source blobs. Filbert reviews the rebased diff.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, landed unchanged: git diff bbf61d6dd..3c80fea74
from work/vikunja-19-306. Business reviewer verdict 8ee5be8b approve on that
commit; Jason resolved merge decision 0eebf83f via the CLI. Applied cleanly
on the parent landing; no conflict. Filbert reviews the rebased diff.
Integration with row 121, found in Filbert's round 1 (#1639, comment 28623):
packages/bus/tests/csuite-messaging.test.mjs setup() binds the assistant to a
thread, since row 121 made assistant a thread definition and bindLaunch refuses
an assistant run without one. The human creates a project and thread first.
No assertion or code changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on 8e2c5c2d0: 4 files, tree 91503e62b8e0ce7b1895ffb7f3b1bbe534b52bcb.
6 of 6 mutants killed, 3 controls pass, cli suite and every scripts/test-*.sh green.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Jason-approved merge, landed unchanged: git diff 07c12b51b..bbf61d6dd
from work/vikunja-19-301. Business reviewer verdict abcd2c87 approve on that
commit; Jason resolved merge decision 29ff6589 via the CLI. No commit on
refactor since 07c12b51b touches these paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, landed unchanged: git diff 07c12b51b..abda248a8
from work/vikunja-19-302. Business reviewer verdict 4998de97 approve on that
commit; Jason resolved merge decision 29ff6589 via the CLI. No commit on
refactor since 07c12b51b touches these paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason-approved merge, landed unchanged: git diff 07c12b51b..b3dd8de83
from work/vikunja-19-297. Business reviewer verdict 0d4c1c68 approve on that
commit; Jason resolved merge decision 29ff6589 via the CLI. No commit on
refactor since 07c12b51b touches these paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rows 135-143 are Vikunja P19 task 314 (Q167). The landing brief covers
Jason's merge decisions 29ff6589, 0eebf83f, 17bb9efc and 12a20e47, landed
through Sage rows as Mos relayed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The update was stamped 19:10Z by estimate. Commit f365a1da9 that
carries it is 19:07:47Z, so the stamp becomes 19:07Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SESSIONS: Darkwing 18:54:54Z and its 18:56:04Z correction, Dewey
19:00:29Z, Sage's landing line. The schema window plan records row 121
landed 2c16d6b93, a rehearsal of the migration on a copy of the backup
(row 121 gate condition 4), and a role_claims check for an open assistant
claim. P19 task 311 holds row 121's follow-ups (sage-0369).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's row 121 round 2 (agents/darkwing/work/queue-121/r2, base
20d2b5788, manifest 710d5086...e67e9b, 30 files), applied unchanged on
d7ebaa9a5; no commit since the base touches any of the 30 paths. Dewey
asked for changes on round 1 in comment 28591 (F1 to F6) and approved
round 2 in 28615. Landing gate on d7ebaa9a5 plus the candidate: all 24
suites passed, 0 failed.
F6 and Dewey's three round 2 notes are follow-ups (sage-0369, P19 task
311). One of them corrects the packet: BUILD.md says the decision-route
setup's projects key is needed, and Dewey's probe showed project.create
reads it only with repo, so the key is dead setup.
The live bus stays at 3b until Mos's schema window migrates it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's row 121 round 2 gate left 201 mosaic-business-* directories.
Filed as a follow-up, not in row 121.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Researcher's row 132 work (agents/researcher/work/queue-132/row132.patch),
rebased unchanged on 73fa83a08 as round 2 (agents/sage/work/queue-132/r2,
manifest 0a0ebbf6...048e). Filbert approved round 1 in comment 28604 and
the round 2 rebase in 28606. Landing gate on 73fa83a08 plus the
candidate: all 24 suites passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Researcher's round 1 patch applied unchanged on 73fa83a08. Only
packages/bus/src/broker.mjs differs from the approved manifest, because
row 133 (a622f426c) landed on it. Full landing gate passed, 24 suites.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sage's row 70 round 2 candidate (agents/sage/work/queue-70/r2, manifest
098b9aef...604e), approved by Filbert in comment 28602. Landing gate on
4a34fb0d6 with rows 133 and 70 applied: all 24 suites passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's row 133 round 2 candidate (agents/dewey/work/queue-133/r2,
manifest 1c0e6f8f...0ef0), approved by Filbert in comment 28600.
Landing gate on 4a34fb0d6 with rows 133 and 70 applied: all 24 suites
passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
F1: the scripts/mosaic header names mosaic-old. Both doc-only follow-ups
are done too, plus the test comment wording. Same base, same 4 files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Researcher's row 132 build found that a role holding organization.change
must send the organization revision, and only the human's limits read
returns it. organization.set fails closed on a stale revision, so this is
a follow-up row, not a blocker for the 3d window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Base 89fd2d6fe. r1's build.patch on the new base gives the r2 tree byte for
byte, so delta.patch is empty; the interdiff shows only index lines and hunk
offsets. Mutants 5/5 killed with controls passing, targeted gate rc 0.
Candidate manifest sha256 1c0e6f8f260ae51b31c2235193c36ed1c005faefe317e1452a351a3bb5690ef0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate 1126314bd522de838aad03689bed41f6b12a33c2d7f30b2eff1b38653f559ae0,
4 files on 48b893871. The first gate stopped at script-queue: the pre-A2
fixture check pinned the old header comment. The check now skips comment
lines (brief amended in 06e964026). Second gate: 24 suites, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window plan records row 49 landed and row 121's round 1 changes, and
adds Darkwing's 28587 note 2: the first start reads every comment list.
Task 304 is Darkwing's note 1, push a task's comment cursor only when
consider runs for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The chief of staff role can revoke a run with a reason (role.revoke) and
write the organization file (organization.set), each under a broker
grant. organization.set checks the grant before the writer runs and
consumes it only after the writer returns, so a refused change leaves no
action.allowed. migrations/3c-3d.sql takes the store from 3c to 3d;
migrate-schema.mjs chains it after 3b to 3c. roles/chief-of-staff.json
and the business vocabulary carry the new actions. The harness tools for
them are row 132, which must land before 3d goes live.
Candidate 255a960582f1018b8d400e8d193e8e95713a078930277652d59ef4153197d9f6,
Researcher packet agents/researcher/work/queue-123/r2 (b959160f9), built
on eaeb5ebf2 plus row 49 r1. Filbert approved round 2 in 28578. Landing
gate at 3a4afdc03: 24 suites green, 0 failed. 14 package suites passed
1611 tests (bus 191, business 92, harness 84, tasks 97, webui 112). The
10 script suites passed (land-work 84, queue 156).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The broker stores the highest comment id seen per business and task in an
append-only table, task_comment_cursors. The tasks adapter restores those
ids before its startup reconcile, and the first reconcile reads each
task's comment list even with no cached count, so a comment posted while
the bus was down is found. Cursor rows, snapshots and comment events
commit in one transaction. migrate-schema.mjs takes a stopped store from
3b to 3c offline; the store refuses a 3b database until it runs.
scripts/land-work/read.mjs reads SCHEMA_VERSION from store.mjs.
The live bus is 3b. From this commit on, land-work refuses it with
bus-schema until Mos migrates in the schema window
(agents/sage/work/window-plan/schema-chain-3c-3e.md).
Candidate 87ef1321a1a08be64a20499889fc24b5549b47d6d33abfd86db95f62fb3cb6d0,
Rocko packet agents/rocko/work/queue-49/r1 (ea5e70fa8), base 7b166a0f1.
Filbert approved in 28579 and Darkwing in 28587. Landing gate at
ebbbf62da: 24 suites green, 0 failed. 14 package suites passed 1598
tests (bus 178, tasks 97, business 92, cli 195, webui 112). The 10
script suites passed (land-work 84, queue 156). Follow-ups: P19 tasks 300
(duplicate cursor) and 304 (push a task's cursor only when consider runs
for it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first row 70 gate stopped at script-queue: dispatch.test.mjs requires
every pre-A2 fixture line, comments included, in scripts/mosaic, and the
fixture holds the header this row corrects.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1 is a code and test review and won't land. land-work fails at
BASE on the read.mjs 3b pin, which row 49 removes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Validator refuses a messaging-limited arbiter; decision.raise refuses a route
that can't message the raiser (decision-route-refused). Candidate on e6e5b41bf,
manifest sha256 edb43aed. Targeted gate green, 5/5 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rows 85 (#1579) and 130 (#1625) changed what the three doc places must
say: the wrapper execs a pinned copy's scripts/mosaic, the human commands
run only from the bus's tree, and queue writes stay on the checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window plan records Filbert's row 49 note 2 (28579): land-work refuses
the live 3b bus from the row 49 landing until Mos migrates. Task 300 is
note 1, the duplicate comment cursor in one pass. Task 299 is row 123's
consume follow-up (28578). Also the row 121 and 131 filing script and
its record.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ruling: the business validator refuses a messaging-limited arbiter, and
decision.raise checks that the route may message the raiser. The close
insert stays unchecked, so no decision is left that its resolver can't
close. Owner dewey, reviewer filbert.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/plugins holds the kind registry and the loader for in-repo
plug-in modules (REQ-PLUG-1). The first two kinds are usage-source and
limit-check (REQ-LIMIT-4). The business file's plugins key turns modules
on per kind, through plugin-settings.mjs and step 5's write path
(REQ-ROLE-3). A refused load is logged and keeps the kind's old modules.
Any filesystem error other than ENOENT or ENOTDIR refuses as unreadable.
The organization writer validates the bytes it writes as well as the
document. stack-copy ships packages/plugins. Nothing is wired into a
host process yet.
Candidate 466c835f7dce79d20c7ab74c197a52b70e7fdeb0efae0456222dc6d0a633f219,
Dewey packet 80797a539 (round 2), base 0bdd573ff. Filbert approved
round 2 in 28568 and Darkwing in 28570. Landing gate at f7d249634: 24 suites green, 0 failed.
14 package suites passed 1586 tests (plugins 21, business 92,
bus 171, cli 195, webui 112). The 10 script suites passed. Follow-up:
P19 task 293, coded writer refusals.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate packet for #1626: build.patch on 0bdd573ff, candidate manifest,
targeted gate (588/0 over plugins, business, bus, webui, cli) and 42/42
mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason via Mos (sage-0204, DECISIONS Q161): "D4 yes". refactor is ready
to merge to next when row 42's acceptance run passes with Jason on the
live mosaic-stack business. The merge itself stays Jason's (R2).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launch line's manifest field is a path, so l.manifest.typed threw a
TypeError in the 2026-10-15 window. Mos caught it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Roles may declare messaging.sendTo and messaging.receiveFrom. The broker
checks them in message.send, review.request and review.verdict before
anything is recorded; a refusal rolls back and logs action.refused.
A role without the field is unchanged. roles/assistant.json declares both.
Candidate ae7515b97bb92fdde63e9d2e6fd8473700d3e0c12d9e70fff67e06129fbac1a9,
Researcher packet f758a8e92, Filbert round 2 approve (comment 28548).
Landing gate at df5b48cae: 23 suites green. The decision-close DECISION
insert stays a follow-up, #1627.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate diff and manifest against 66ea40580; messaging suite now 12 tests;
gate green; 21/21 mutants caught (m16, m17 added). No push.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
launch.max becomes launch.models plus launch.limits (total, providers,
exact provider/model). The launcher re-reads limits on every attempt.
Settings and `mosaic limits` write through one locked, journaled
organization writer. `mosaic limits migrate` is print-only.
Candidate 00b84e311475f97ced0d9a9599fba23a28442971ed15d8d489d9422d84a8812e,
Rocko packet 4b6e50636, Filbert round 2 approve (comment 28542).
Landing gate at 0c56cc2d4: 23 suites green. Decision 123 go from Mos.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Broker send limits for the assistant (REQ-ASSIST-2, #1614): optional role
messaging lists, broker refusals logged both ways. Gate and 19 mutants green
on 85540a74e; patch applies to 9f1c14e57.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
roles/assistant.json and assistant.md are new. The assistant holds only
message.send and relays each ruling verbatim to the chief of staff. The
chief of staff gains role.launch; the PM drops it. Test fixtures move
launch.by from "pm" to a `chief` instance.
This does not change the live host. Jason's organization file names
launch.by "pm", which the new role files refuse, so his edit and the
re-pin happen together in one Mos window (decision 144; the order is in
agents/researcher/work/queue-119/build.md).
Lands Researcher's round 2 candidate, approved by Filbert in comment
28528 (rev 1240). Candidate manifest f77c4917, all 22 files match.
Landing gate on 46117feb8: 23 suites, GATE_PASSED 01:09:35Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the broker answers human-required or tester-required, the CLI
now prints a second line saying the bus accepts these commands only
from its own tree and to run `mosaic` on PATH on a pinned host. The
entry block becomes an exported run(); exit codes are unchanged.
Lands Darkwing's row 130 round 1 candidate, approved by Filbert in
comment 28520 (rev 1228). Candidate manifest 9a5de54d, all four files
match. Landing gate on 9e837ce5f: 23 suites, GATE_PASSED 00:44:07Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PRD version 1.3 records Jason's rulings Q153 to Q155, relayed by Mos
(lead decision 143), and Mos reviewed the text before it landed. Adds
REQ-LIMIT-4 (usage sources and limit checks as the first plug-in kinds;
an unknown turn is never zero, no source switched on means every turn is
unknown, an unenforced limit shows as not enforced) and a Plug-ins
section with REQ-PLUG-1 (kind registry and loader in packages/plugins,
in-repo modules only until Jason rules, Vikunja #286) and REQ-PLUG-2
(new call-outs go through a kind; hard-coded parts convert as rows touch
them). REQ-LIMIT-2 and REQ-LIMIT-3 count through usage sources.
Technical considerations list where each kind is hard-coded today.
requirements.json moves to 1.3 with 41 ids; the tasks and bus id-count
tests follow. The PRD 1.2 build plan gains step 6b, the plug-in layer
(#1626), and rows 124 to 127 ship their counting and checks as modules.
Jason approved the conversion order for hard-coded parts in Q156. Lead
decision 143 and the build plan record it, and the managed-session
config brief notes that row 65 now runs before 64 and 66.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos asked for a row after the #284 go-live: the checkout's scripts/mosaic
is refused tester-required and human-required because the bus proofs
accept only the pinned copy's entry paths. README text, a TOOLS.md
sentence and one stderr hint line; no redirect to the live copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest 68c60a79914692f3f6d4923f7bcff41de109ccca33bf6fba687fb88efa24bcac
from Rocko's round 1 packet (a94a943c9), on BASE 72c9ac36c. No
manifest path changed between BASE and the landing target. Filbert
approved round 1 in 28497 with no findings; the TOOLS.md paragraph
break is a doc-only follow-up (decision 137). Sage reran the full gate
at the landing target with the candidate applied and Docker live.
Nothing is installed or restarted; the live switch is Mos's step after
the SetSpark freeze lifts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest 19ef7133db592787c24fa473357687a5741ccd7823627b970624c930a7611dbc
from Rocko's round 1 packet (a6e4fe747), on BASE 234121b30. No
manifest path changed between BASE and the landing target. The
bus-service.sh drop-in guard now refuses inherited mosaic-.service.d
and service.d drop-ins, the header lists every flag, the unreachable
install-show fakes are gone, and the stack-copy.sh wrapper carries its
copy and lease comments after the line-2 marker. Filbert approved in
28489 with no findings. Sage reran the full gate at the landing target
with the candidate applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest eeb67779c919b7c1f25b4449df7207d85634e5175b16eef315346cf5a284c63c
from Darkwing's round 1 packet (5a68df727), on BASE 2727dd3ec. No
manifest path changed between BASE and the landing target. Dewey
approved in 28479, with no blocking or doc-only findings. Sage reran
the full gate at the landing target with the candidate applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SAFE in packages/seat/src/checkout.mjs adds maintenance.auto=false and
gc.auto=0; probe, tests, mutants m1-m3 (all killed), targeted gate
(seat 132/0, cli 184/0) and the leftover sweep. Candidate is build.patch
against 2727dd3ec; manifest digests check out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest 8b4ab31014a124ec0e3abba44238976d6f4c9af48cbdb36c2c77d18f0137f888
from Darkwing's round 2 packet (0a4076e8e), on BASE 1223a0f37. No
manifest path changed between BASE and the landing target. Filbert
approved round 2 in 28468, closing C1 and D1 from round 1 (28464).
Sage reran the full gate at the landing target with the candidate
applied. The replay acceptance follows ruling sage-0204.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Six cases for Filbert's C1 (f1 split into actor_role and actor_run), the
earlier-merge dry run, a node_modules canary with s27, and the TOOLS.md
usage line. 84 cases, 55 of 56 mutants killed (s26 equivalent), targeted
gate green. Candidate is build.patch against 1223a0f37; not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decision 142 records the PRD 1.2 approval, Mos's rulings on choices 6 to
9, the token-unit draft position, the sage-0188 proxy facts and 9b left
unfiled. The brief file holds one section per build row, steps 2 to 11
with 9a and 9c split out, each tied to its Gitea issue.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PRD version 1.2, approved by Jason through Mos on 2026-10-14 (lead
decisions 141 and 142). Adds REQ-COS-1, REQ-ASSIST-1/2, REQ-LIMIT-1/2/3,
REQ-USER-2 and REQ-THREAD-2, rewrites the role, launch, message, CLI and
WebUI requirements, and quotes Q147 to Q151 in the source line.
REQ-LIMIT-2 marks the token unit for the daily quota as a draft position.
requirements.json moves to 1.2 with 38 ids; the tasks and bus id-count
tests follow. Full gate green at bea5dfa47 plus these four files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing asked how land-work reads the decision. The Console's path is
human-only (human-cli refuses agent environments, #225), so the
conductor can't use it. land-work now opens bus.sqlite readOnly, runs
SELECTs only and refuses bus-schema on a digest mismatch. The brief
records what f71c73c9 holds: task_ref is null, the raise's target and
approvalChoice give the branch and the merge option, and the work ref
comes from the question. agents/sage/work/row116/live-rows.json holds
the live rows for the replay fixtures.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revs 1142-1144 are Darkwing's (row 117 start and in-review), 1145 is
Dewey's approval; the rest are Sage's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 117. At a launch, update() kept a clone that was on another branch
or dirty and ran no git in it, so its refs/remotes/origin/* stayed as
old as the last clean launch. Now those two reasons get the clean
path's fetch, with the same arguments and environment, and no merge.
The result is a new outcome, fetched, with the reason, the clone's
branch and HEAD, and origin { branch, commit } after the fetch. Every
earlier check still keeps the clone with no fetch. The launcher writes
a checkout-fetched launch-log line, and the bundle prompt names a
fetched clone's branch, commit and origin. Nothing in unsafe(),
allowed(), gitEnv() or SAFE changed.
Candidate manifest 9fa839edfb2bd04771bbac1913b142cb16aef227a1265120e0d318fb64d6a502
from Darkwing's round 1 packet (17d801f48), on BASE 559ef2287. Dewey
approved in 28443. Sage reran the full gate at the landing target with
the candidate applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest, patch against 559ef2287, build notes, five killed
mutants and the targeted gate (seat, cli, harness, two scripts).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason's 17:16Z rulings, relayed by Mos: the #271 CI steps (Vikunja
#273-#278) and Q143. Decision 137 is the reviewer rule for Vikunja #279;
the queue README's verdict paragraph points to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 115. When a decision closes by resolve or withdraw, and the closer is
a human or another run, the broker routes a DECISION message to the
raising role's holder: "Decision <id> on <action> was resolved by <by>:
<choice> (<text>)" or "withdrawn by <by>", plus the note if there is one.
A seen op sends nothing. #request falls back to the human.input that
closed the decision, filtered to resolved and withdrawn.
Candidate manifest 1910eb5a672e670cb7c8a197ee3fdd888412dd803fe52a0a15680103c26c078c
from Darkwing's round 2 packet (40e5917f9), on BASE 94ece491f. Filbert
approved in 28428 and Sage in 28427. Sage reran the full gate at the
landing target with the candidate applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on base 94ece491f: 5 files, manifest 1910eb5a; only
decision-close.test.mjs changed since round 1. Mutants m1-m4 killed
(m4 is Filbert's f4); gate passed with no flakes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BUILD-LOG records the squash-merge of decision f71c73c9, the re-pin with
the install-flag correction, and row 116. SESSIONS registers Sage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filed at Mos's request after the PRD v1.1 merge. The slice is conductor-only:
push and any stack-role landing authority stay out of scope.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Squash of the PM's work/vikunja-19-265 branch, net diff 0c3826910..8ae5a1491
(patch sha256 47b34e6054a74eedd8a5b6577e40537a07b2fb8fe5846f309e100146238ef743).
The PRD moves to version 1.1. "Business" becomes "organization" in
REQ-ROLE-3, REQ-ROLE-4 and REQ-LAUNCH-1. REQ-DEC-3 and REQ-DEC-4 name an
authorized human instead of Jason. Non-goal D adds that v1 has no hosted
SaaS, and "Where it runs" says Mosaic Stack stays self-hosted open source.
A new Organizations section adds REQ-ORG-1, REQ-ORG-2, REQ-USER-1,
REQ-THREAD-1, REQ-TPL-1 and REQ-FED-1. packages/tasks/requirements.json
matches at version 1.1 with 30 ids, and the tasks and bus requirements
tests expect 30.
The branch's 010d30cbe (parseRequirementId, task 19/229) is reverted on
the branch and is not part of this squash.
Jason approved the PRD in decision 03130d08 and the merge in decision
f71c73c9 (resolved yes via the CLI, 2026-10-14T16:35:32Z). The stack
reviewer's verdict d3f97f9d approves 8ae5a1491. Sage's gate on 1c9a4910d
plus the patch passed: every package suite and every scripts/test-*.sh.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on base 94ece491f: 5 files, manifest f37bc933. Three mutants
killed; gate passed with no flakes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
T3 messages with role reasoning and an id starting "reasoning:" are now
skipped before any other check and counted as reasoningMessages. Any other
unknown role, including reasoning with another id prefix, still refuses.
Issues updated since the start date are read in pages of 50 until a short
page, at most 10 pages. A full tenth page, a duplicate issue number across
pages, a page over 50 rows or any failed page refuses with exit 2, so
partial totals are never reported. The report prints how many pages it
read. The README describes the skip, the page loop and its refusals.
Candidate manifest 4eed1d6405a505e88cde5062c61f768639d899b8f301019b1403d68fb15d9d36
(Darkwing round 1, packet d6ea56831), BASE cc48b124f. Approved by Filbert
(comment 28407). Sage gate rerun at 644ef5310 plus the candidate passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ledger skips T3 reasoning summaries and reads issue pages with a
10-page ceiling. Candidate manifest, patch against cc48b124f, mutants
(both killed) and a passing full gate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos asked on 2026-10-14 for the connector unit to follow row 85's rule.
install --copy, status names the copy, check --offline, tests and docs.
The live repoint waits for Mos's go after the SetSpark freeze lifts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A recommendation that isn't exactly one option key now refuses
invalid-recommendation, checked after the options, instead of sharing
invalid-options with bad option lists. The raise_decision tool text says
recommendation is one key with the reason in question, and that
approvalChoice defaults to yes. The bus README lists the three codes,
the field each names and the check order; a malformed option object
refuses invalid-request. No schema change.
Candidate manifest 013c82c2fd99d23d92f6bed543a788aa04ff97b164019ccb15252ae808b4138e
(Darkwing round 2, packet fee89d52f), BASE 96656dd21. Approved by Filbert
(comment 28383) and Sage (comment 28384). Sage gate rerun at f63fc20e4
plus the candidate passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
README invalid-options cell lists only count, key, text and duplicates; a
non-{key,text} option refuses invalid-request (Sage 28378, Filbert C1 28379).
Broker test pins the option shape cases. m1 and m2 killed; full gate passed
15:53:56-15:59:33Z on BASE 96656dd21.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two fail-closed refusals stop every full weekly ledger run: T3's new
role 'reasoning' summary rows, and more than 50 issues updated since
the week's start. Owner darkwing after row 112, reviewer filbert.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's start and in-review revs 1103-1105, Sage's round 1 record
1106 (approve, comment 28377) and note 1107 recording its replacement
by changes in comment 28378.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on 96656dd21: broker split refuses invalid-recommendation after the
option checks, raise_decision text asks for one key and names the yes default,
bus README codes table, broker and tools tests. Mutants m1 (old shared line)
and m2 (order swapped) killed; full gate passed 15:42:03-15:47:36Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A tester binds with a process proof from the bus's own tester-cli.mjs
when the business var bus.tester is true. It may send [mos test] messages
to a role, receive replies on the tester address and use the read verbs;
every other verb refuses. Messages store from_role tester and write no
human.input event. turnRequest() marks them as never Jason's instruction.
human.mjs is unchanged.
Candidate manifest bdaf7f476abd0b2b5095ab2f7f43e38540e6ba48ee14a02a4f4995a1469b79df
(Darkwing round 2, packet 9f1563ec1), BASE badfa52b9. Approved by Filbert
(comment 28360) and Sage (comment 28361). Sage gate rerun passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Folds Filbert's N1 per Sage's comment 28355: the envelope test sends a
role cap with a tester proof. The narrowing mutant at server.mjs:50
survives the full bus suite on round 1 and is killed on round 2. Full
gate passed with the real bwrap tester case running.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build notes, patch and candidate manifest on BASE badfa52b9, the gate
logs (GATE_PASSED; the real bwrap tester case in seat sandbox.test.mjs
ran and passed) and the mutant runs (94 killed, b44 and b45 equivalent).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The broker gains a read-only state verb (launch state with the event that set it, open tasks capped). The runner fetches it before each turn and puts a "Current state" block at the top of the request; a failed read fails the turn. turnRequest() adds the message time. roles/pm.md gets one rule: the block is the truth for launch state and task status. Seat session, seat sandbox and cli launcher tests re-anchored to the new request shape.
Built by Researcher, packet 875c515e9. Filbert approved round 3, comment 28333, candidate manifest 0a62225e (11 paths). Sage gate on 9774d70db plus row109.patch: every package node suite and every scripts/test-*.sh green on the first run, skipped 0 in bus, harness and seat. Vikunja #259.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey approve, comment 28331, manifest 5e888f93. Sage gate on be6f2ed8c
plus the patch: every package suite green (cli 178); test-queue failed
review.test.mjs:123 once under load (16s vs 3.7s), passed 28/28 rerun alone;
release 14, task 98. Dewey note a (ended() rejects on a child error and skips
release()) is recorded for the next row that touches host.mjs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Role sessions can reach broker.sock, so "no proof" is withdrawn. tester-cli
shim, path-bound, own nonce; verifyTester in tester.mjs; verifyHuman unchanged;
the real bwrap case must run, not skip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
bus.tester business var, default false; tester socket field with no proof,
refused on role- or reader-bound connections; tester reply address; the
"never Jason's instruction" line in turnRequest() after row 109 lands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate patch (state verb, runner state block, header time, pm.md rule, fake-adapter fixture), six new harness tests, six mutants, full suite outputs, candidate manifest.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Darkwing round 1, approved by Dewey in #1600 comment 28319 on manifest
ae38cbe280aa5885aec7648d38143920753db86ea1ca3934d0a8885fb61e5105.
Sage gate rerun on f63c965f9 plus build.patch: every package suite and
every scripts/test-*.sh green (cli 168, harness 73, test-release 14).
readHostState now treats a host.json that vanishes mid-read as no host
for stopHost and hostStatus too (Dewey note b); Sage accepts it as
correct. Nothing goes live until a re-pin under Mos's go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bus host lock follow-ups: release on a fork or send throw, retry a lock
that vanishes before the read, the have order test fixed to directory
order, the third-racer README sentence and the reviewer wrap. Gate passed
on 0069949ff; 9 mutant runs as expected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1047-1048 Sage: row 106 after none, with a note. The note's time reads "22:2xZ", a placeholder Sage left in. The actual time is in SESSIONS.
1051 Sage: row 104 attribution note (Dewey's 28293 posted under jarvis, confirmed as dewey in 28294).
1052 Sage: row 104 done. The landing note was refused for length, so 0069949ff and #1598 comment 28298 are the landing record.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sandboxArgv sets GIT_AUTHOR_NAME/EMAIL and GIT_COMMITTER_NAME/EMAIL from
the instance and business ids (name = instance id, email
<instance>@<business>.mosaic.invalid); spawnSession and the launcher pass
the ids through. checkout results carry the clone's commit (a kept clone
gets none), and the bundle prompt names it. allowed() is unchanged.
Packet agents/darkwing/work/queue-104/r1/ (manifest f99f2ae7). Dewey
approved round 1 in 28293 (posted under jarvis by mistake; 28294 is
Dewey's confirmation). Sage's gate on f00613106 plus the candidate:
every package suite and scripts/test-*.sh green (test-release failed 3
activation checks once, not reproduced; rerun 14/0).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate against 817ddfea6: git identity in the sandbox environment,
commit field in checkout results, one prompt line. Gate green, 19 of 19
mutants killed. The candidate itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A retried privileged add recorded with no gate owner answers with its
receipt before the --gate-owner check runs; a test pins that order and
a hoisting mutant fails it. The refusal no longer cites row 100.
SEMANTICS stays 2. Packet: agents/researcher/work/queue-107/ (manifest
b70dfe4a). Filbert approved round 1 in comment 28286. Sage's
canonical-root gate: queue 156/0, test-queue 30/0 with queue verify.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 103, the row 100 follow-up. Built by Darkwing, packet
agents/darkwing/work/queue-103/r1/, candidate manifest
6e5ac8c0782bc595d7c0d711d457738ffb42f1ba5f38253912943dd55e55888a.
Sage approved on #1597, comment 28252; Dewey approved, comment 28260.
Host git runs through runGit: a detached group, SIGTERM at the limit,
SIGKILL after the grace, and one more SIGKILL to the group after a
signalled git closes. git-socket close() waits at most 2 s. startHost
claims bus-host/host.lock by hard link before the broker fork.
packages/cli/README.md and tests/host.test.mjs carry row 85's hunks
too; row 85 lands first.
Sage's gate rerun on 3a1e11b83 with rows 85, 102 and 103 applied,
Docker up: node suites bus 128, business 67, cli 165, control-board
124, conversation 182, discord 178, harness 73, ledger 78, mosaic 69,
queue 155, runs 43, seat 113, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 85 round 2. Built by Rocko, packet agents/rocko/work/queue-85/r2/,
candidate manifest
bd7ea899c7cacef43b415b8e9ecb164e1f36718ffbe64347881a2c5653170c98.
Filbert approved on #1579, comment 28256; Darkwing approved, comment
28258.
stack-copy.sh builds shared read-only copies under
XDG_DATA_HOME/mosaic-stack/copies; the bus and Console services render
and install against a copy with --copy and --pm. No live step is run
by this commit; those need Mos's go.
Sage's gate rerun on 3a1e11b83 with rows 85, 102 and 103 applied,
Docker up: node suites bus 128, business 67, cli 165, control-board
124, conversation 182, discord 178, harness 73, ledger 78, mosaic 69,
queue 155, runs 43, seat 113, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 105. Built by a Haiku subagent under Researcher (decision 129),
packet agents/researcher/work/queue-105/r1/, candidate manifest
cd6d5eb3e4093ac157f63d05cb639d243d76c6bde1a03e78efb8f29e191b6b44.
Filbert approved on #1599, comment 28247.
The review.request reply and the review.requested event carry held,
the run id holding the reviewer role or null. The request_review tool
description says what null means. The broker change is live only at
a bus restart.
Sage's gate rerun on 79e126be0 with rows 101 and 105 applied, Docker
up: node suites bus 128, business 67, cli 146, control-board 124,
conversation 182, discord 178, harness 70, ledger 78, mosaic 69,
queue 155, runs 43, seat 109, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 101. Built by a Haiku subagent under Researcher (decision 129),
packet agents/researcher/work/queue-101/r1/, candidate manifest
ee1335a6e44610f676ade116733d180ac6f0b3603a58306110cbd1dd1e9c1f69.
Filbert approved on #1595, comment 28245.
The check runs at write time in store.mjs mutateAs, with no SEMANTICS
bump, so the live Console keeps reading the log.
Sage's gate rerun on 79e126be0 with rows 101 and 105 applied, Docker
up: node suites bus 128, business 67, cli 146, control-board 124,
conversation 182, discord 178, harness 70, ledger 78, mosaic 69,
queue 155, runs 43, seat 109, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Header sentence, line 13 wrap, bidi escapes in three files, inertCheck U+200E/U+200F. Patch and evidence only; the change is not applied to the tree.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Row 93 follow-up: SIGTERM then SIGKILL for host git, rollback-failed,
a total body deadline, fetch-aborted for a reviewer gone mid-fetch,
working refs first in have, and tests for five untested refusals.
Dewey approved in 28226 and Sage in 28225; Sage's full gate passed
16:57:32-17:01:32Z. The host side is live at the next bus restart.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 61 follow-up. Filbert approved round 1 in comment 28214; Sage's
full gate passed 16:18:32-16:22:34Z. The public/bus.js change is the
one aria-label line Mos released under decision 123.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Five paths: tasks fields.mjs and fields.test.mjs, webui README, public/bus.js
(aria-label only) and bus-browser.test.mjs. Phase B: 14 node suites and 9
scripts green, 8 of 8 mutants killed. The literal U+202E is now an escape.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A holder that releases a role without reading what it was delivered leaves
those messages where no later holder receives or reads them. The agents
view now lists them for human and reader sessions under `stranded`, as
{id, from, run, at}, oldest first, at most 50. The bus doesn't redeliver,
route or mark them read; a person who still needs one sends it again.
Round 2 is a mechanical rebase of round 1 onto row 93. Reviewed by Filbert
(28206, approve); Sage gate record 28207. Gate on 7496251c: 14 node suites
and 9 scripts green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Base da6f1166, r1 patch applied clean with --index, +/- and context lines identical.
Bus suite 127/127, mutants 15 of 15 killed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Commits move as v2 bundles over bus-host/git.sock (decision 135). The bus
host keeps one bare working repository per business and project, seeds main
from the project root under a per-repository lock, fetches a pushed bundle
with fsck into a temp ref, requires history shared with main, and moves the
task's work ref only as a fast-forward. Host and runner git run with no
system or global config, hooks at /dev/null and local transports only. The
socket binds only for roles holding git.push.working or review.verdict.
The broker records a push as the action.allowed for git.push.working with
its commit, takes review.request for the latest push and one review.verdict
per request, and refuses task.close on a pushed task without an approve
verdict for the latest push. recordEvent reserves both shapes, so agents
can't forge either.
Reviewed by Dewey (28196) and Sage (28197), both approve. Gate on 7a729aef:
14 node suites and 9 scripts green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lands the Haiku subagent build from agents/researcher/work/queue-84/r1/
(candidate manifest f72730de, base ac6b56ee). roles/chief-of-staff.json
and .md follow decision 104's list; packages/business/tests/
chief-of-staff.test.mjs covers the contract, and the business README
names the role.
Approved by Filbert (comment 28176) and Darkwing (comment 28183). Sage's
gate on 11bf4cb0 plus the patch: 14 node suites and all nine
scripts/test-*.sh green, 15:33:26-15:37:31Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's R1 (comment 28188): the brief requires the packet to name what
the Console would need to show the stranded list. Added as a Console
follow-up section by the subagent, read-only, with unchecked items named.
Also N1 (M12 note) and N2 (round 1 title). Candidate unchanged, manifest
52c41526. Sage corrected one count: M12 failed 6 of 11 tests, not 13 (the
13 was the count of x-marker lines).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stranded delivery list on the bus agents view. Built by Sage's Haiku
subagent (decision 129), rebased onto 57a2ddc5. Phase B, three runs:
the first failed the new test (node:sqlite null-prototype rows, fixed
by mapping to plain objects), the second stopped at cli on a missing
node_modules link, the third passed: 14 node suites, nine scripts,
15/15 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Swept Filbert's rev 926, row 84 round 1 approve on f72730de (comment
28176). Rev 927 moves row 71 to done after the landing and close.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 71. Test 18 gains three steps after the 404/503 step: the child's task
list answers 404 and its confirm read answers 429, 401 or status 0 (the
fake throws fetch failed). Each tick refuses with tracker-not-found and
the child stays. The fake answers a missing kanban view with code 3014,
the live v2.7.0 answer in the tick-62 record.
Built by Researcher's Haiku subagent (decision 129). Approved by Filbert
(28171) and Dewey (28172) on manifest 9ac3a56e. Sage's gate on e635ef1b:
every node suite and scripts/test-*.sh green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rows 71 and 84 moved to in-review as researcher (916, 917, 922, 923; the
seat has no token, so the requests failed). Sage reposted both requests
as jarvis (918, 919: comment 28168; 924, 925: comment 28173). Swept
Filbert's rev 920 and Dewey's rev 921, row 71 approvals on 9ac3a56e.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Haiku subagent build under decision 129. Candidate manifest f72730de
names roles/chief-of-staff.json and .md, the business README and
packages/business/tests/chief-of-staff.test.mjs. Gate and mutants ran
in Sage's slot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Haiku subagent build under decision 129. Candidate manifest 9ac3a56e
names packages/tasks/src/fake.mjs and packages/tasks/tests/tree.test.mjs.
Gate and both named mutants ran in Sage's slot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 97, the row 95 follow-up. Researcher's build, round 3 (agents/researcher/work/queue-97/r3/), a
confirm-only rebase of round 2 onto row 94. Darkwing (comment 28140) and Filbert (comment 28139) approved
round 3. Researcher's check on bb45f75e: webui 107 pass, bus.js mutants 26 and 27 killed (r3/check.md).
Sage's gate rerun on 865a8fc2: all 14 package node suites and all 9 scripts/test-*.sh green. Tests and
README only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 98, the row 77 follow-up. Researcher's build (agents/researcher/work/queue-98/), approved by Filbert
in round 1 on manifest 352a3034 (comment 28110). Tests only: three new mv and rename cases in
pinned-copy.test.mjs beside the fixture repo they need, and added assertions in service.test.mjs.
Sage's gate rerun on 47a8190d: all 14 package node suites and all 9 scripts/test-*.sh green, webui 107 pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict and comment readbacks only; reconstructed candidate/ copies and
queue-91/r2 left untracked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 91, round 3. Darkwing built it, Dewey approved it (comment 28150) on
manifest ca570607 (8 paths, base 2b2a9e14). At each launch the host
makes or updates a git clone of the project in the role's workspace,
and the session prompt names each clone. Round 3 closes Filbert's round
2 escape: a clone with a submodule is kept as it is, and no host git
recurses into one (decision 130 thread). Live at the next bus restart;
the prompt lines appear only when the launcher passes checkouts.
Sage gate on fc6d5822 plus the patch, 14:18:25Z to 14:22:27Z: bus 115,
business 60, cli 131, control-board 124, conversation 182, discord 178,
harness 67, ledger 78, mosaic 69, queue 150, runs 43, seat 89, tasks 89,
webui 104, all fail 0. Every scripts/test-*.sh exited 0; test-release
ran 14 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Built by a Haiku subagent (decision 129). cli 133/0, nine scripts green,
7 of 7 mutants killed. Manifest 5c979e49.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos's ruling: text-mode inbox prints the decisions and a one-line
notice when the pre-94 bus refuses the messages verb. The bus restart
does not move earlier.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 94, round 2. Rocko built it, Dewey approved it (comment 28104) on
manifest 53b01ea9 (16 paths, base e918a915). The bus keeps a messages
view with read state, where delivered but not read stays read:false. The
CLI gains messages and follow, and inbox leaves out messages already
read while inbox --json stays byte-compatible. The Console gets a
Messages page and an unread count in the nav. Mos gave the go under
decision 123 (public/ files). Sage gate on 645489e4 plus the patch,
12:52:14Z to 12:55:44Z: bus 115, cli 129, webui 104, all fail 0. Every
scripts/test-*.sh exited 0 (auth 15, conductor 17, config 24, discord
66, extension-package 18, foundation 44, queue 27, release 14, task 98).
Manifest 16/16 before and after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1's patch on the new base, no change of its own (decision 127).
Manifest 6b58085b, 8 paths; only the two bundle files' digests move,
by row 90's lines. checkout.mjs stays out of RUNNER_FILES: it runs on
the host. Gate 00:07:08Z to 00:10:09Z, seat 83, cli 125, bus 111,
harness 67, tasks 89, every scripts/test-*.sh rc=0 with Docker off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 90, round 2. Dewey built it, Filbert approved it (comment 28068) on
manifest 95611274 (21 paths, base b8539480). The approved PRD's
requirement ids live in packages/tasks/requirements.json. task_create
refuses an id outside it, and the PM session gets a list_requirements
tool. Round 2 adds requirements.mjs to the sandbox's RUNNER_FILES
(decision 127). Sage gate on c047d028 plus the patch, 23:50:16Z to
23:54:26Z: tasks 89, bus 111, harness 66, webui 101, cli 123, seat 33,
business 60, control-board 124, queue 150, conversation 182, discord 178,
ledger 78, runs 43, mosaic 69, all fail 0. Every scripts/test-*.sh
exited 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 69, built by Darkwing, approved by Dewey (comment 28048) and Filbert
(comment 28055) on manifest c68e6582. Sage gate on c6711453: cli 123/0,
queue 150/0, every scripts suite green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1 rebased on row 63 (b8539480) plus one RUNNER_FILES line in
packages/seat/src/sandbox.mjs for row 92 (decision 127).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revs 847-849 are Researcher's (start, in-review move, failed tokenless
request op researcher-r98-review-20261012t231549z); 850-851 are my
request as jarvis.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revs 842-844 are Researcher's (start, in-review move, failed tokenless
request op researcher-r97-review-20261012t231305z); 845-846 are my
request as jarvis.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 63, round 2. Task verbs act in any installed project of the tree;
a project the tree doesn't hold refuses task-project as before, and a
watch-only one refuses with the new code. The harness task_create tool
takes an optional integer project. Round 2 adds Dewey's five proof
tests and Filbert's N1 child-path tests; the code is round 1's.
Candidate manifest 2a22fd3f (agents/darkwing/work/queue-63/r2/), base
cca5f23e, no drift on the six paths. Builder Darkwing. Approved by
Dewey (comment 28024) and Filbert (comment 28026). Sage's gate on
1117d19b plus the patch 2026-10-12T23:07:33Z-23:09:22Z: tasks 84,
harness 65, every scripts/test-*.sh green.
Lands before row 90 (decision 125). The harness change is live at the
next role session launch (decision 109); Mos had the notice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only my revs. Correction: 16d5310b's subject says rev 830, but its
snapshot was rev 832 and carried Rocko's revs 830 (row 94 in-review) and
831 (the failed tokenless request) along with my rev 832.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing found the brief had the host run unconfined git in a clone the
session writes. The brief now requires a clean git environment, hook and
fsmonitor overrides, an lstat walk of .git, a config-key allowlist and a
session-live check, and adds bundle.mjs and a harness test to the files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hidden decide box names the way out, its status line stops promising y,
tests for the narrow-terminal guard, U+18D08 and a cut help or detail
box. Candidate manifest c68e6582. Mutants 6/6 killed; gate 13 suites
green on dad20531 plus build.patch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rev 812 is Rocko's: row 94 briefed to in-progress, on the Codex thread.
Revs 813-818 are Sage's. Rows 97 and 98 go to Researcher to build on
Haiku after row 86 lands (decision 122). Filbert joins row 97 as the GPT
reviewer, and its gate names both reviewers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
c7b07064 appended row 95's follow-up section to the brief. The sections
for rows 94 and 93 are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four child-path tests in verbs.test.mjs for Dewey's and Filbert's round 1
survivors; the task.create README paragraph rewrapped. Code unchanged.
Row 89 (ce104b27) edited tools.mjs and tools.test.mjs after round 1's
base, so this round is built on cca5f23e. Mutants 25 of 25 killed; gate
15 of 15 green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 77, round 3 (round 2 rebased onto row 95). Rocko built it. This
covers row 75's N1-N5 and Vikunja 146 and 147. `install --copy ID`
refuses unless every ExecStart path of the bus unit, read from
`systemctl --user show` so drop-ins count, lies inside that copy. It
also refuses when the unit isn't loaded, no unit is loaded, ExecStart
has no path, or systemctl fails. COPY_PATHS gains packages/tasks/src and
packages/tasks/fields.json. Every copy directory is 0555. TOOLS.md and
the webui README give the removal command and the rollback floor.
Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5
(10 paths, 8 applied by the patch, 2 packet files already committed),
base 2e26fbe1, packet agents/rocko/work/queue-77/r2b/. Approvals on round
3: Darkwing 27979 and Filbert 27985 (Filbert round 2: 27898). Sage's
gate rerun 22:32:51Z-22:35:12Z on 459c4fc6 plus the patch (row 92
included), manifest 10/10: webui 101, control-board 124, and every
scripts/test-*.sh exited 0 (test-release 4, Docker cases skipped at a
dead socket).
Live at the next Console install and restart (decision 109). Mos makes
that call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review notes and the rerun of S1, S2, S4, S8, S9 and S0 in a detached
worktree of ad0bdc20. Verdict comment 27986, rev 803 (swept in 85fd3286).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 92, round 2. Darkwing built it. A role session runs in a bwrap
sandbox that sees only its own workspace, a private /proc, /dev and IPC
namespace, and the variables ENV_ALLOW passes. The launcher checks the
session credential before a Claude launch, records a runner killed from
the host as killed rather than failed, and the bus unit template carries
the credential file (decision 119). The harness gate and the cli, seat
and harness READMEs say what the sandbox does and does not hide.
Candidate manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97
(13 paths), base ac37cd17, packet agents/darkwing/work/queue-92/r2/.
Approvals: Dewey 27978, Filbert 27984 (round 1: Dewey 27942, Filbert
asked for changes in 27951). Sage's gate rerun 22:27:37Z-22:30:35Z on
1b7f3781 plus the patch, manifest 13/13: seat 33, cli 120, harness 65,
bus 108, webui 86, control-board 124, and every scripts/test-*.sh exited 0
(test-release 4, Docker cases skipped at a dead socket).
Live at the next bus restart and the next role session launch (decision
109). Installing the new unit and restarting is Mos's call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 89 landed on packages/harness/README.md. The patch applies cleanly;
gate 14/14 green and 34/34 mutants killed on the new base.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's packet, committed by Sage (Rocko has no credentials). Only
packages/webui/README.md moved, onto row 95's landing; Sage checked
the README's added and removed lines are identical to round 2's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 89, round 1. The broker's `tasks` read takes optional filter and
page arguments and returns a short form. The harness adds `list_tasks`
arguments (open tasks by default), a `get_task` tool, and compact JSON
replies capped below the 50000-character threshold where Claude Code
2.1.296 saves a tool result to a file outside the workspace, which was
the PM's fault in Jason's test. The bundle's tools line names the
harness's own tools.
Candidate manifest b8c5bb3a (agents/dewey/work/queue-89/), base
a3f6e9e6, no drift on the 12 paths. Builder Dewey. Approved by
Darkwing (comment 27940). Sage's gate on f04185fc plus the patch
2026-10-12T22:00:55Z-22:03:38Z: bus 108, harness 65, webui 86, cli
115, control-board 124, every scripts/test-*.sh green.
Live at the next bus restart (broker) and role session launch
(harness), both Mos's call (decision 109); Mos had the notice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's six changes and Dewey's N1-N3: five table rows, two launcher
tests, README sentences. 34/34 mutants killed, 14 suites green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 95, round 1. The Board's first section lists each role a run holds
on the bus (from /api/bus/agents), with a Message button on a Console
started with --send. Machine sessions sit behind a "Show machine
sessions" checkbox, off by default, kept in localStorage; while off the
page never reads /api/board. Every Console fetch passes
redirect: 'manual'; the first opaqueredirect signs the page out: timers
stop, reads are dropped, one line says to reload, and the send form
keeps its draft.
Candidate manifest 246c0557 (agents/filbert/work/queue-95/), base
a3f6e9e6, no drift on the manifest paths. Builder Filbert. Approved by
Darkwing (comment 27953). Mutants 30/35 killed, 5 redundant guards.
Sage's gate 2026-10-12T21:52:13Z-21:54:35Z: webui 86/0, control-board
124/0, every scripts/test-*.sh green.
Live on landing: serve.mjs reads src/public on every request with
no-store (decision 123). Mos gave the go (sage-0115).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build, patch and manifest for the bwrap sandbox, the credential check and
the unit's EnvironmentFile= line. Gate 14 suites rc=0; 26 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on base a3f6e9e6, not committed here. Gate green 21:27:35Z-21:30:19Z;
mutants 30/35 killed, 5 equivalent guards explained in build.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate on base a3f6e9e6: tasks takes filters and paging, list_tasks and get_task,
a 40000-character reply cap with compact JSON, and the built-in tools line per harness.
Gate and 34 mutants in the packet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A non-blocking role.revoke routes to the human without a task; the
tool description and session text say so, and how a run is stopped.
The broker's blocking-needs-a-task rule stays (Mos sage-0107, #228
c91).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The review loop through the bus (findings faults 3 and 5), every
message to the human live (Mos sage-0103), and the Console Board
showing the stack's seats with a signed-out line (fault 8, Mos
sage-0105 and sage-0106). Row 89 also fixes the session text's
built-in tools line for Claude Code (fault 9).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 81. The broker gains a `replies` read verb for human and reader
sessions: `{id}`, a message UUID, returns that business's messages whose
in_reply_to is the id, oldest first, at most 50, and writes no
deliveries row, so the reply still reaches the next `mosaic talk`. An
unknown id refuses message-not-found. The Console serves it through
BUS_VERBS with an id check, and the Sent receipt shows the replies to a
web send. The cli's talk timeout message and README now say the reply
may still come and the next `mosaic talk` prints it.
Dewey's round 2, candidate manifest 3ce7527f, base 1e5f5958, a rebase
after row 61. Approved by Darkwing (27783 round 1, 27821 round 2).
Sage's gate green 19:25:18Z to 19:30:39Z. Live at the next bus restart
(the broker verb) and Console restart (the view), bus first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 61 landed on three shared webui paths; build.patch applied with --3way
clean. Folds in the cli README talk wording and the bus.mjs comment wrap.
Gate green 19:05:31-19:10:13Z; round 1 mutants 17/17 killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Human proof from a copy (bus checks its own human-cli path), row 61's
packages/tasks import missing from COPY_PATHS, directory write bits.
webui 94 pass; 19 mutants, 16 killed, 3 survivors explained.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 61. packages/tasks/fields.json holds the slice 1 field table in two
layers (decision 113): each broker task verb's writer check, and each
field's writing verbs and Vikunja calls. packages/tasks/src/fields.mjs
checks the shape as code and loads it frozen. The tasks test reads the
writer layer from the table and authority from roles/*.json and drives
every task verb against the fake Vikunja. The Console serves the table
at GET /api/task-fields, and the task page shows a "Who may write"
section. The slice 1 doc's field table points to the file.
Darkwing's round 1, candidate manifest 33b96986, base 4f8ac1e7. Approved
by Dewey (27776) and Filbert (27785). Sage's gate green 18:57:31Z to
19:01:58Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The task field-writer table as data: build.patch (12 files on 4f8ac1e7),
candidate manifest, gate summary (all green), mutants 33/33 killed.
The candidate is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's work committed by Sage (Rocko has no git authority). Candidate
manifest sha256 4927de66, base 27a1a2bb.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Console shows the reply to a web send: bus `replies` read (human and
reader, message-not-found on unknown ids, oldest first, cap 50, writes
nothing), /api/bus/replies, the Sent receipt's reply list, talk's timeout
names only mosaic talk. Base ff11aa4b, 12 paths, manifest 12/12 on a fresh
worktree. Gate green 18:29-18:35Z; 17/17 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos sage-0088, fact-finding only. Filed with Sage's own key (user 13)
and read back with the sync key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos sage-0086: bot-mosaic-stack-sage is user 13 with key 13 and
bot-mosaic-stack-ops-01 is user 14 with key 14, both with key 11's
scopes (Q61a cut). Neither reads a project until ops-01 adds them to
team 1 under Mos's go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mint-bot.mjs generalizes mint-mos.mjs (user, name and key title as
arguments; slot written without a trailing newline). bot-launchpad-pm is
user 12, key 12, scopes identical to key 11 (Q61a cut). The key reads no
project until ops-01 adds user 12 to team 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 83. scripts/webui-service.sh render and install take --send, only
with the full preview set; otherwise the script refuses before writing
anything. With --send the rendered ExecStart ends in --send; renders
without it are byte-identical to the base. status says whether the
installed unit sends. The script header, install message, README and
TOOLS.md carry the decision 102 start wording: Jason or Mos starts and
restarts the Console, no other agent.
Dewey's round 1, candidate manifest f764cd9e, base d13b9300. Approved
by Darkwing (27721) and Filbert (27722). Sage's gate green 04:07:30Z to
04:10:47Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
build.patch against d13b9300 (brief blob f8d14d70, rev 630), candidate
manifest, identity and mutant harnesses with results, gate summary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 76 round 2, owner Dewey, candidate manifest 04ecf1b5 on 729da72b.
Carries decisions 99 and 100: Darkwing's and Filbert's round 1 notes,
the README proxy contract line, and a 408 counted as unknown.
Approved by Filbert (27685) and Darkwing (27688); Sage's gate green
03:25-03:29Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebased on row 59 (a8512007). Darkwing's N1-N5 and the Q1 proxy line
(lead decision 99), Filbert's N1-N4 (lead decision 100): a 408 is unknown
on the send box and the board Reply, the receipt shows the code once, focus
and caret kept, a role no longer held kept, Inbox current on a message
trail, README notes. 35 of 35 mutants killed; gate green on 729da72b.
Manifest sha256 04ecf1b57c594a35d34d53277b2df915a571f4fa9f7cc29146dbedfa612f77d4.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 82 round 2, owner Rocko, candidate manifest dbf36e92 on 876f1ffe.
Approved by Filbert (27650); Sage's gate green 02:05-02:09Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 67 round 2 (rebase only), owner Filbert, candidate manifest 4387c8d5 on f38b7f46.
Approved by Dewey (27648) and Darkwing (27649); round 1 by Dewey (27452) and
Darkwing (27627). Sage's gate green 01:58-02:03Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Manifest 4387c8d5. Rebase only: same added and removed lines, three files
re-digested after rows 58, 60 and 59. Gate green, round 1 mutants all killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Built on bbd22f1b, rebased on row 60 (3d298920): the bus.mjs header
comment conflict merged, serve.mjs BUS_VERBS and the README credentials
paragraphs merged cleanly. 21 of 21 mutants killed, full gate green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebase-only round on 3d298920. Merged hunks against rows 58 and 60 keep
both sides; 21 round 1 mutants give round 1's results. Comment 27591.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebase only, per rev 571. Two conflicts resolved: the agents branch in
broker.mjs next to row 58's tasks line, and the bus.css widths line.
Merged hunks in broker.mjs and the bus README are named in build.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The credentials read returns service, account, role, date kind, date and
state for the capability's business, to human and reader sessions only.
It never returns a token, file path or variable name. Author Filbert,
reviewers Dewey (27558) and Darkwing (27575), candidate manifest 13541657.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two default-pin cases (no --claude): the only claude on PATH is mode 0644,
and claude in a PATH dir with a space. render exits 1 with stdout empty
for each. PATH holds only the test's dirs. Author Rocko, reviewer Filbert
(27560), candidate manifest 3016dbf1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rev 583 is mine (filbert-78-review-1). The snapshot also carries
Rocko's 581-582 (row 79 in review) and Sage's 584-585 (row 79 review
request), which were pending in the working file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Packet agents/rocko/work/queue-79/ (packet manifest b6b11e7b, 42 files,
candidate manifest 16b12387), Rocko's BUILD-LOG and SESSIONS appends.
Committed by Sage for Rocko, who has no credential.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Packet agents/rocko/work/queue-78/ (packet manifest 9e067f40, 32 files,
candidate manifest 3016dbf1), Rocko's BUILD-LOG and SESSIONS appends.
Committed by Sage for Rocko, who has no credential.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bus tasks read returns each task's priority and requirement, and the
Console's task list shows them. Files: packages/bus/src/broker.mjs and
README, packages/bus/tests/task-read.test.mjs, packages/webui/README.md,
src/public/bus.css and bus.js, tests/bus.test.mjs and
bus-browser.test.mjs.
Author: Darkwing (packet agents/darkwing/work/queue-58/, round 2 in
48b0b648, rebased on row 74's tree). Candidate manifest sha256 eb2b26c1,
base 424ada49, no drift on the 8 paths through eace3fef.
Approvals, round 2: Filbert comment 27529, Dewey comment 27530 on #1547.
Gate (Sage, /tmp/sage-g58 on eace3fef, 23:57-00:01Z): bus 84, tasks 71,
webui 61, control-board 124, cli 113 node tests, 0 failed; every
scripts/test-*.sh exit 0, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 76 builds as briefed; its brief's 'replies show in the inbox' line
was wrong. A read-only bus read of replies, shown on the Console receipt,
is row 81 (#1575), held behind the cutover. Option (b), a Console
message.receive, is refused because receive marks delivery.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1's build.patch applies cleanly on 424ada49 (row 74 landed as 351464ce).
Only packages/webui/README.md's digest changes; the bullet text is the same.
Manifest eb2b26c1. Gate 23:41:10Z-23:44:25Z: bus 83, tasks 71, webui 61, every
scripts/test-*.sh green. 16 of 17 mutants fail, output identical to round 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The coder-without-a-tracker test in packages/harness/tests/tools.test.mjs
joins row 79's files with one deepEqual on the untyped list, the packet
records Dewey's mutants.sh header correction, and the harness suite
joins the gate. Queue note rev 551.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
roles/coder.json crossRole goes from ["task.reassign", "task.scope.change"]
to ["task.scope.change"]. The broker refuses a non-pm task.reassign at the
writer check before it looks at authority, so the grant could never be
used. A coder's task.reassign now classifies as gated and goes to the
human. Backed by lead decision 90.
Tests: business role and resolve (the narrowing test moves to the pm,
which still holds two cross-role actions), a bus test on the shipped role
files, a harness test that a coder with a tracker gets no task_reassign,
the tasks fixture, and a verbs line showing a coder's task.reassign
refuses field-writer with no action.allowed. mutants.sh is Rocko's packet
rerun script, already committed.
Author: Rocko (packet agents/rocko/work/queue-73/, commit 2137dd7f).
Candidate manifest sha256 bf4e5ed6, base 324c064b, no drift on the
candidate paths through 8a42e819.
Approval: Dewey, round 1, comment 27506 on #1567.
Gate (Sage, /tmp/sage-g73 on 8a42e819, 23:42-23:48Z): tasks 71, bus 81,
business 60, harness 59, cli 113, webui 61, seat 27 node tests, 0 failed;
every scripts/test-*.sh exit 0, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's row 59 review (comment 27487) found that a message delivered
to a run that releases the role unread can't be received or read by the
next holder. Sage's ruling: keep at-most-once delivery, as the bus
README promises, and list stranded messages in the agents read so a
person decides whether to resend. Owner Darkwing after row 59, reviewer
Filbert, held behind the cutover with the other bus rows (decision 94).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
serve takes --public-host NAME --auth-header HEADER --allow-user ID, all
three or none. With them, a request whose Host is NAME must carry HEADER
equal to ID or gets 403; a request that carries HEADER with any other Host
gets 403, so a proxy that rewrites Host can't reach the loopback path with
a signed-in request; every POST is refused read-only. A repeated Host gets
400 in both modes. Host and header names compare case-insensitively
(Traefik sends X-Authentik-Uid). The README states the proxy contract:
the edge forwards Host unchanged and strips client copies of HEADER.
Author: Dewey. Round 2 candidate manifest
17b6d7f62d106a23d99441eefa46e34ab2e9bd1442a1d02e219f126a82a8668d (base
17ea0dbc, no drift on the 4 paths through 60c8dcf9). Approved by Filbert
(comment 27485) and Darkwing (comment 27486) on #1568. Their wording
notes on the README (first signed-in request; the tunnel sentence) go to
row 76, which owns the README next.
Sage gate rerun on a worktree of a55915f0 with the patch: manifest OK;
webui 61/61, control-board 124/124, cli 113/113; test-auth 15, conductor
17, config 24, extension-package 18, foundation 44, queue 27, release 14,
task 98, all 0 failed. test-discord failed one timing test (engine: a
timed-out run outlives the grace, 280 ms at load 30) on the sequential
run and passed 66/66 on a solo rerun of the same worktree; the patch
touches no discord file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate 90d5bddc on f2f2861b. bus 86/0, webui 40/0, every scripts/test-*.sh
green 23:23:30Z-23:26:25Z; 20 of 21 mutants killed, any-role-msg equivalent.
Notes: a claim that outlives its runner, a stranded delivered message (bus
follow-up to Sage), overlap with row 58.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 2 candidate 17b6d7f6 on 17ea0dbc. 22 of 23 mutants killed, joined-value
equivalent; probes cover repeated Host, proxy Host, HEAD and absolute-form.
r1/ holds the round 1 findings sent to Dewey in place of a verdict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 2 per Sage's notes revs 521 and 523: a repeated Host is 400 before
any check; HEADER with a Host other than NAME is 403 proxy Host refused;
HEAD, x.NAME and --auth-header tests; the README proxy contract lines.
Candidate manifest 17b6d7f6, base 17ea0dbc. 34 of 34 mutants killed;
gate green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's packet agents/rocko/work/queue-73/ (candidate manifest bf4e5ed6,
packet manifest 41ffcd5c, 53 files), their BUILD-LOG entry and SESSIONS
line. Sage's SESSIONS line for the review request (comment 27467) and
row 79 (#1573) for the stale text the packet names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's row 73 packet names three places outside the row's files that
still show the coder's task.reassign grant: the business README example,
the tasks README paragraph and live/run.mjs. They go to a follow-up row
after 73, owner Rocko, reviewer Dewey, so row 73's round 1 isn't
restarted for a scope change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One test in packages/cli/tests/host.test.mjs: a directory given with
--claude is refused, and a symlink default stays the symlink path. One
in packages/harness/tests/claude-session.test.mjs: a directory pin exits
2 before claude runs, and a pin under a directory with a space runs.
agents/rocko/work/queue-72/mutants.sh (committed with the packet in
036afdb6) reruns the six row 68 mutants: all killed on the candidate,
all surviving on the base. Accepted deviation: bash's command -v never
returns a directory, so a directory named claude on PATH takes the
no-claude branch, which the test asserts.
Author: Rocko. Candidate manifest e6531a5311f17ea147700e417dfdbd766a9c38acf3c3b44755267c285b78e006
(base a0080da8, no drift on the candidate paths, bus-service.sh or
adapter.sh). Approved by Filbert, comment 27460 on #1566; their N1 is
row 78, #1572.
Sage gate rerun on a worktree of df676fa1 with the patch: manifest OK;
cli 113/113, harness 58/58, 0 skipped; test-auth 15, conductor 17,
config 24, discord 66, extension-package 18, foundation 44, queue 27,
release 14, task 98, all 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's packet agents/rocko/work/queue-72/ (candidate manifest e6531a53,
base a0080da8) with their BUILD-LOG and SESSIONS appends, and Sage's
SESSIONS line for the jarvis review request (comment 27454).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
scripts/webui-service.sh render|install|uninstall|status and the
packages/webui/systemd/mosaic-webui.service.in template, on the
bus-service.sh pattern. install writes the unit to a temp file, renames
it and runs only `systemctl --user daemon-reload`. There is no start,
stop, enable or restart verb: the Console reads the bus through the human
transport, so Jason starts it from his own shell (lead decisions 62 and
94). Serve's flags pass through, including row 74's preview set, each
held to a character set the unit can carry. The unit sets no MOSAIC_*,
CLAUDE*, CODEX* or PI_* variable and does not restart on serve's exit 2.
Tests in packages/webui/tests/service.test.mjs; TOOLS.md entry.
Author: Rocko. Candidate manifest 7dc571dbe87a484f465c82e1965e499c57d9af047bfe702faa3fe041138dd154
(agents/rocko/work/queue-75/, base 43d2328f, no drift on the manifest
paths to 8b351c3d). Approved by Filbert, comment 27430 on #1569, with
five non-blocking notes placed in row 77.
Sage gate rerun on a worktree of 8b351c3d with the patch: manifest OK;
webui 50/50; test-auth 15, conductor 17, config 24, discord 66,
extension-package 18, foundation 44, queue 27, release 14, task 98, all
passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Committed by sage for rocko (no credential). Candidate manifest 7dc571db, packet manifest 0bc9a870, base 43d2328f.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate build.patch on ae428a54, manifest e63e4c4f. Gate on f2f2861b: bus 83,
tasks 71, webui 40, every scripts/test-*.sh exit 0. Mutants 16 of 17 killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decision 94 records Q94 yes and Mos's sage-0054/0055 directions: no broker
row (the Console already reads as the human), Jason starts the Console,
nine rows deferred, S7's line amended, Gate E partial pass on the dogfood
day, no rollback hostname. New brief for the Console service row and the
send-to-a-role row. Row 74's start line and S7's request line amended.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos approved the cuts 2026-10-11: forward auth at the edge, a user unit
beside the bus, a preview hostname, read-only. No code until Jason
answers Q91 (amending lead decision 48).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decision 91 amends 89: the bus pins ~/.local/bin/claude through
MOSAIC_CLAUDE_BIN, so the package upgrade is no longer needed for the
first PM launch. The brief covers two Rocko rows: tests for row 68's
surviving mutants, and dropping the coder's dead task.reassign grant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MOSAIC_CLAUDE_BIN names the claude binary by absolute path instead of
finding it on PATH. scripts/bus-service.sh render|install --claude PATH
writes it into the unit (default: command -v claude in the installing
shell); the claude adapter, the launcher's version read and the seat
session environment all use it. Mos's Q18 ask: the unit's PATH found
/usr/bin/claude 2.1.259, which refuses claude-opus-5-5.
Candidate: round 2 manifest 21c1ab05, round 1 (a2008d8b) rebased onto
f2f2861b after row 55 changed docs/TOOLS.md and packages/cli/README.md;
the changed lines are byte-identical. Approved by Filbert (27349, 27374)
and Dewey (27350, 27375). Sage's gate on f2f2861b plus the patch: cli
112, seat 27, harness 57, nine scripts/test-*.sh 0 failed, Docker on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing found, reading ahead on row 61, that message.send never writes a
tracker comment and that task.scope.change has no writer named. The
prose moves to what the verbs enforce; row 61 rules out changing writers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BUILD-LOG entry and SESSIONS line: feat 390d437b, gate, the live read
(902b3d0e) and the follow-up row 71 (#1565).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick-62.mjs runs the candidate's discover(), reconcile() and tick()
against tasks.woltje.com with the sync key, a real Store and Broker in a
scratch root, and a fetch guard that refuses anything but GET. The
startup scope probes are not sent. Reconcile 15 GETs, 42 snapshots; tick
8 GETs, 0 snapshots; 18, 20 and 21 watch-only. A missing view id on #18
answers 404 code 3014 while #18 reads 200 (the fake says 13001).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With tracker.descendants on, the tracker project is the root of a tree.
Each reconcile reads the sync bot's project list (archived projects and
negative pseudo-project ids left out) and walks it by parent_project_id.
A child that passes the install checks is read through its board, and
one that doesn't is watch-only (bucket 0) and never refuses the
business. A tick reads each project in the tree. A child answering 403
or 404 leaves the tree only if GET /projects/<id> then answers 403 or
404 too, and its open tasks get the one-by-one read. Task refs carry
the project, so a task moved inside the tree shows under its new ref
and the old ref gets its tombstone. tracker.descendants reaches the adapter
through the business var and packages/cli/src/config.mjs (lead decision
87). Off, the adapter reads as v0 did.
Darkwing built it (agents/darkwing/work/queue-62), in three rounds.
Round 3 was approved by Filbert (27317) and Dewey (27338) on manifest
8b81e836, 13 paths. Their non-blocking notes and Darkwing's follow-ups
(two surviving mutants in test 18, the fake's view-404 code) go to a
follow-up row.
Sage's gate on 918a4a19, sequential and teed: node suites tasks 71,
business 60, bus 80, cli 110 and queue 150, 0 failed; webui 39 of 40,
then 40 of 40 on three reruns (row 62 touches no webui file; a timing
test under load 17 with Dewey's gate running beside it); test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98, all 0 failed (Docker on). edc5de71..HEAD
touches none of the 13 paths.
Live read, 2026-10-11T21:28:03Z, tasks.woltje.com v2.7.0, root #19,
descendants on, sync key only, GETs only (the startup scope probes not
sent): reconcile 15 GETs, 42 snapshots; tick 8 GETs, 0 snapshots.
Children 18, 20 and 21 are watch-only. A missing view id on #18 answers
404 (code 3014) while #18 reads 200, as the README says. Record:
agents/sage/work/vikunja-setup/2026-10-11_tick-62.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Includes Sage's pending revs 448 (sage-q81-docs-add) and 449
(sage-70-briefed), and Filbert's rev 451 (filbert-68-review-1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos amended ruling (2): with no environment location for
MOSAIC_LEASE_VERSION_PROBE_COMMAND, the wrapper sends only
`__lease-capability` to ~/.npm-global/bin/mosaic-old, so the old CLI's
lease gate still gets its answer. Every other subcommand reaches the
pinned worktree; undo removes the forward with the wrapper. update now
rewrites the wrapper, and status flags a wrapper that differs from what
the script writes. Checked live at 21:17:58Z: the probe answered
{"name":"lease-runtime-activation","version":1}, exit 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
agents/sage/work/q81/mosaic-cli.sh does install, update, undo and status.
~/.npm-global/bin/mosaic is now a wrapper that execs scripts/mosaic in a
detached worktree at ~/.local/share/mosaic-cli/stack, pinned to pushed
ee129aa6. The estate CLI symlink was renamed in place to mosaic-old.
Only commits on origin/refactor can be pinned. `mosaic queue` refuses
from the worktree (the queue runs only in the canonical checkout), so
queue work stays on scripts/mosaic. status flags a wrapper replaced by
`npm i -g --force`.
lease-gate-notes.md: no single environment location for
MOSAIC_LEASE_VERSION_PROBE_COMMAND exists outside fleet state, so the
old-path lease gate fails closed, as Mos ruled for that case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bare `mosaic` on a terminal opens the app. It has Inbox, Tasks, Agents
and Trail screens, help, and a decide overlay, and it uses the alternate
screen, raw mode, resize and SIGTERM, and restores the terminal on exit.
draw(state) in screens.mjs is pure. cli.mjs exports resolveDecision,
which `mosaic decide` and the app share, so both write the same lines
and ask the same question. A decision resolves on `y` only while
decideShown() says the whole decide box is on screen. The app reads that
at the key. A box that doesn't fit is replaced by "Too small to show this
decision", and any key answers No. scripts/mosaic sends no arguments to
packages/cli (a decided scope addition), so the queue dispatch test
follows. The cli README and docs/TOOLS.md describe the app.
Darkwing built it (agents/darkwing/work/queue-55), in three rounds.
Round 3 was approved by Dewey (27308) and Filbert (27309) on manifest
59f1b9b4, 9 paths. Their non-blocking notes go to row 69 (#1562): a
narrow-terminal test for the decideShown MIN_COLS guard, a way out from
the hidden box, a U+18D08 width test, a cut box keeping Esc back, and
the README row count stated as an example.
Sage's gate on 3634d06e, sequential and teed: node suites cli 110 and
queue 150, 0 failed; test-auth 15, test-conductor 17, test-config 24,
test-discord 66, test-extension-package 18, test-foundation 44,
test-queue 27, test-release 14, test-task 98, all 0 failed (Docker on).
122b1223..3634d06e touches none of the 9 paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert N10-N12 and Dewey's round 3 survivors: a narrow-terminal
decideShown guard test, a way out in the hidden decide box, a U+18D08
width test, a cut-box Esc back test, and the README row count as an
example. Owner Darkwing; reviewers Dewey and Filbert.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R2/B1: after a forbidden or not-found refusal on a child, the tick reads
GET /projects/<id> and drops the child only on 403 or 404; a deleted
kanban view (project read 200) fails the tick as in round 1. Tests 18
and 19, 23/23 mutants killed, gate on ae428a54 green (14 suites).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos withdrew decision 89's package upgrade: Jason's Claude Code is the
native ~/.local/bin/claude (2.1.296), not /usr/bin/claude 2.1.259. The
unit will carry MOSAIC_CLAUDE_BIN and the adapter and manifest version
check will use it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing N1/N2 (27290), Dewey survivors recent-closed-first-event and
web-503-first (27300), and Filbert's trail comment note. Owner Filbert;
reviewers Darkwing and Dewey. No roles LIMIT here; paging is the design
row's call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The broker gains the reader verb `decisions` with `{list: 'roles' |
'recent'}`: the open decisions routed to each role, and the last 50
decisions (RECENT). views.mjs exposes it in the Q1 client module, and
webui serves `GET /api/bus/decisions?list=roles|recent`. Any other list
is 400 and never reaches the bus. No Inbox tab uses the route yet; the
tabs are the design rows' work. The bus and webui READMEs list the verb,
the route and who may call it.
Filbert built it (agents/filbert/work/queue-57). Round 1 approve from
Darkwing (27290) and Dewey (27300) on manifest af3b03cf, 8 paths. Their
non-blocking notes (no seen-then-resolve test, no webui test of ?list=
before the no-bus 503, test 4 compares fewer fields, roles has no LIMIT,
3 of 14 mutants survived) go to a follow-up row.
Sage's gate on 3c79809c, sequential and teed: node suites bus 80 and
webui 40, 0 failed; test-auth 15, test-conductor 17, test-config 24,
test-discord 66, test-extension-package 18, test-foundation 44,
test-queue 27, test-release 14, test-task 98, all 0 failed.
958668a8..3c79809c touches none of the 8 paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One rule for Filbert's R3 and Dewey's B1: y resolves only while the
whole decide box is shown. Adds N9 widths, the start-unguarded and
timeout fixes, and two defects found by the cheap tests (wrap at
graphemes, answer render throw). 51/51 mutants killed; gate on
122b1223 green, cli 110.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's R1 (27279) and Dewey's matching blocking item (27280): a child
that answers 403 or 404 in a tick leaves the tree, and the root still
refuses. Tests for Filbert's N1-N3 and Dewey's three gaps. 18 mutants
killed; all 13 suites green at 122b1223. Manifest sha256 55316d88.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason: "Q80: Have the PM decide. Then fix". project-manager decided to
delete all four and moved their tasks to todo. Mos ruled no standing
delete scope on any bot key. svc-mosaic-stack minted key 10 for the pm
user with only projects.views_buckets_delete and a 30-minute expiry,
held in memory only. The pre-read showed all four buckets empty. Four
DELETEs answered 200, and key 10 was revoked (204) at once. The pm user
now lists only key 8. The read-back shows buckets 57 done, 159 todo
(default), 161 in-progress, 248 in-review and 162 blocked, with task
counts unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GETs only, sync key 9. Project list 200, total 30 (31 with archived #15).
#18, #20 and #21 have parent 19, none archived. GET /tasks with
filter project in 19, 18, 20, 21: 200, 42 tasks across all four. The
per-project open reads give 18, 4, 3 and 1. A follow-up row moves a tick
to the single read after row 62 is done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest sha256 ec3f7a42f3bf6436634a5a83a12bb5678ca1e58a10ff4f66a23dfca7ff3f375f,
13 files on edc5de71. Gate on 6ae8b844: 13 suites green. The candidate
itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R3: below 40x10 a y answers a question the screen does not show. Candidate
3f2d8f8ea3fea1cf7a002297ee9b23059f1522e8060c1d981fba19c85baeabd7.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason ran run.sh (sha256 c609c682) from a human shell on fe5c68fa,
2026-10-11T18:32:45Z to 18:32:54Z. The PM launched coder r2810c6373900,
recorded by pm in the launch log, and mosaic agents shows it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
install-19.mjs renames 57 done, 159 todo, 161 in-progress, 162 blocked,
adds 248 in-review and moves the default to 159; no task moved. Writes go
to /api/v1 (v2.7.0 405s bucket writes under /api/v2). create-33.mjs adds
#35 and #36 under #33. Records hold no secret; shares-read titles removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Project 32 was deleted; the bus refused at startup with tracker-project.
#19's kanban got the five buckets in agents/sage/work/vikunja-setup/
2026-10-11_install-19.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
svc-mosaic-stack created mosaic-agents (team 1, bots pm/cto/coder/reviewer)
and mosaic-sync (team 2, bot sync), made jason.woltje team admin, then left
both teams so a team share gives svc no project access. The pm key gained
projects read_all, read_one and update; no key has task or project delete
or a share route (read back via GET /tokens?owner_id). Old pm key 2 revoked,
now 401. Record holds ids and scopes only; checked for secrets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BUILD.md records Sage's 2026-10-11 ruling that packages/queue/tests/
dispatch.test.mjs is in scope. FOLLOWUP-transport-code.md writes up the
transport's did-not-finish error with no .code, for the 10-12 batch.
The candidate (manifest 93fa48c1cdfe) is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The full-screen terminal app (#1544): BUILD.md, build.patch over fe5c68fa
(9 files), candidate manifest, the gate logs on 23d566d6 and the pty smoke
script. The candidate itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Console gains three read-only views. Queue lists every row from
rows() in packages/queue (lead decision 83: the same lock-free read as
`queue list`, writing nothing), run in a child with a timeout and an
output cap; a row page shows the full row. Business shows names, an
allowlist of vars, arbiters, authority per action and credential
metadata (service, account, role, date, never a value, file or
variable). Settings shows RELEASE, the board origin and the notifier
binding. Every route is GET only, and every string in a body or
refusal passes a redactor: the config directory and dataRoot become
<config> and <dataRoot>, other absolute, ~/ and file:// paths <path>,
and 17 to 20 digit runs <id>. A queue-read that fails to start sends a
fixed message, never node's stderr.
Four fixes to HEAD behaviour, each with a test: the bus view's refresh
timer is cleared at render, a same-page refresh keeps focus on the H1,
#conv-pick is emptied when a conversation opens, and error() returns
focus to <main> only when something had focus. Filbert's T8 tests the
failed first read.
Dewey built it in two rounds. Round 1 (dba2429e) got changes from
Filbert (27185: After rendered [object Object], ~/ and :/ paths leaked)
and Darkwing (27186: Arbiters always none, queue-read import failure
leaked a file:// path and stack). Round 2 (afb2ae0e) was approved by
Filbert (27190) and Darkwing (27191, correction 27192). Sage's gate on
d64f434f plus the candidate: 13 package node suites 0 failed (queue
149, webui 39), every scripts/test-*.sh 0 failed (task 98/0 with
Docker, 26/0 without; release 14/0), build-tokens --check current.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of #1543 candidate dba2429e: gate green with Docker, 31 mutants
(26 killed), Node, import and Chromium probes. Posted as comment 27186.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate manifest dba2429e, 14 files on d64f434f. Request comment 27182,
packet comment 27183 on #1543. No other seat's revision is in this snapshot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The console takes the design package's tokens.css and icons.svg as byte
copies, guarded by a test, and a new shell.css holds the components:
command bar with freshness line, section list (a strip that scrolls
inside itself below 760px), dense table, inspector, status mark, chips,
copy command, toast, Ctrl+K palette, not found, banners and skeleton
rows. JetBrains Mono 2.304 ships from the official archive with sha256
and OFL. Harbor following the system setting is the default palette.
A refusal (403, not-configured, no-bus-host, or the board's 403) fails
closed everywhere: no kept rows, counts, inspector, palette entries or
conversation history, and a later 500 or 503 doesn't bring them back.
Dewey built it in three rounds. Round 1 (75569953) and round 2
(78c3aef9) got changes from Darkwing (27148, 27165) and Filbert (27143,
27163) for refusal leaks outside the main view; round 3 (2562c05d) was
approved by Darkwing (27171) and Filbert (27174). Sage's gate on
ea080fd8 plus the candidate: runs 41/0, queue 148/0, webui 27/0,
conversation 182/0, control-board 124/0, build-tokens --check current,
every scripts/test-*.sh 0 failed (task 98/0 and release 14/0 with
Docker). The round 2 gate had one discord engine timing failure in the
full run, 66/0 alone, filed as #1553.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Refusal leaves board data in the inspector, project tree, footer and
scan line, and bus data in the Inbox count and the Ctrl+K palette.
Gate green, 13 of 20 mutants killed, fonts match the upstream archive.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's row 54 stop: store.show costs ~450 ms a row. Row 54 adds
export function rows(opts) plus a test; queue suites join its gate.
Path redaction and Dewey's recorded view choices accepted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New packages/runs reads run records, result.json, the release pointer
and the activation log without writing, pruning or following a link out
of the data root. mosaic-task.mjs list and show use it, so a malformed
result.json or a run that is a regular file no longer crashes them.
Deliberate deltas are listed in the README.
Rocko built it. Round 1 (ed3c5392) was approved with notes by Darkwing
(27115) and Filbert (27116); round 2 (2727198f, tests and wording only)
was approved by Darkwing (27123) and Filbert (27124). Sage's gate on
c9a25a47 plus the candidate: runs 41/0, queue 148/0, webui 22/0,
conversation 182/0 (181/1 in the full run on the K12 cgroup timing
test under load, 182/0 alone), control-board 124/0, every
scripts/test-*.sh 0 failed (task 98/0 with Docker, 26/0 without).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corrects decision 80's journal claim as a new entry. Brief for the
adapter ready line (#1552) is queued after the Q14 hold ends.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brief for rows 53 to 61 implementing docs/design (Jason's directive via
Mos, DECISIONS 1a312ff). Decision 80 records the Q14 window, rev7 item 6
checks and the freeze; decision 81 the row shape and the design's open
decisions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason accepted the draft 1 direction on 2026-10-10; the refined Relay
mark stays as a placeholder. Adds IMPLEMENTING.md (gap map against
row 40 and the bus/CLI modules, strings, boundaries, acceptance,
proposed rows for Sage), tokens.json and generated tokens.css using the
webui's existing names, icons.svg, and tools/build-tokens.mjs and
tools/check-design.mjs. Coverage statuses corrected against the code
(digest implemented; mosaic stop is row 41; priority and agent state
not in the bus reads). No packages/ change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pausedCommit waits for the child's `close`, not `exit`, so the
`cannot lock ref 'HEAD'` match sees git's stderr. The test-task live
recall pair runs only when `docker info` succeeds, like the three live
blocks above it; otherwise it prints a skip line and counts as neither
a pass nor a failure.
Dewey built it. Darkwing (27099) and Filbert (27100) approved round 1
on manifest 285a4ec1 (3 files). Sage's gate on 560373c3 plus the
candidate: queue 148/0, conversation 182/0, webui 22/0, control-board
124/0, every scripts/test-*.sh 0 failed; test-task 98/0 with Docker and
26/0 without, with the recall-pair skip line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The controller now starts a CHAT-01 `engine-exit` stop when the engine
exits on its own. engineExitCohort proves the cohort from the shim's
recorded engine identity (pid, start ticks, boot) and an empty member
list, under a 5 s deadline that frees the escalation slot. A proven stop
releases the scope; anything else ends `uncertain`. close() no longer
signals a stopped binding.
Dewey built it. Filbert (27087) and Darkwing (27088) approved round 1 on
CHAT-01 cc83ee4f and manifest deac7434 (6 files). Sage's gate on
c92cfb8f plus the candidate: conversation 182/0, webui 22/0,
control-board 124/0, every scripts/test-*.sh 0 failed (task 98/0),
chat-01 PASS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server-started stop for an engine that exits on its own: no request,
no confirmation; it closes admission and supersedes the current stop
(H17), but refuses stop-owned under a force stop or another engine
exit. The binding follows it like a force stop at startStop,
advance-stop and confirm-stopped, and its proof needs a member.
Dispatched or acknowledged input goes delivery-unknown; working input
keeps its state. Five lifecycle sequences and two shape cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 51, owner Dewey, candidate c34039dc (5 files). A claim recorded
stopped on a cohort proof whose scope is still listed is released on
start (classify's stopped branch and the free path's session and seat
heads) and on a confirmed recover, after every recover check. A release
that ended unavailable or still listed is retried; the proof check stays
in #releaseScope. close({ killEngine: true }) no longer signals the
recorded PID after a proven stop. Tests R7-R12 kill relok, closeany,
noprooffkind and nopush; R9 pins the recover order.
Reviews: Filbert approve (27070, 27074), Darkwing changes then approve
(27071, 27075). Gate green on 032b5408 plus the candidate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rev 299 is a failed first request: no credential file was set, so nothing
was sent. Rev 301 posted the request as comment 27068.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
No engine-exit stop over an unfinished force stop, the binding follows
the stop at all three force-stop mode checks, the proof names the engine
as a member, and the proof has a deadline that frees the escalation slot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 51 needs a CHAT-01 rule changed to record `stopped` at engine exit.
The engine-exit proof moves to its own row with a CHAT-01 amendment and
contract review; row 51 keeps the crash window, the close PID fix and
the mutant tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
releaseCohort sends `release` only to a scope shim that answers hello
with the recorded invocation ID, then waits for systemd to drop the
unit. The controller releases once per claim, after a proven force stop
and on close of a proven-stopped binding; every uncertain path keeps the
scope as evidence. The harness's killShims refuses units outside
^mosaic-chat-, R5 checks liveShims positively, and K19's wait on
proc.exited is bounded.
Dewey's candidate, manifest 375594fc (8 files), approved by Filbert
(comment 27053) and Darkwing (comment 27055). Normal engine exit still
leaves the scope; the follow-up is #1537.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Engine exit proof and release, the crash window and release retry,
close's SIGKILL of a recorded PID, and the reviewers' surviving mutants
(relok, closeany, noprooffkind, nopush).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 47, the S5 follow-up. flows.test gains a test for a hold set inside
held input, which kills mutant Mr. claim W5 and W13 reap their own
force-stopped scopes, K19 kills its scope if its kill or release fails,
and the harness gains liveShims, killShims and shimsGone with a final
sweep in reap. claim, cohort and races each end by asserting no shim
outlives the file.
Author: Dewey. Approved round 1 by Filbert (comment 27041) and Darkwing
(comment 27043) on candidate 0587e393. The src side, where a force stop
never sends release, is row 50 (#1536).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's row 47 finding: forceStopCohort and controller close never send
release, so each force stop leaves a mosaic-chat scope with an idle shim.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate agents/filbert/work/s6/candidate-manifest.sha256 (08a78972),
request comment 27033 on #1523.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R4 holds. R5: a relative Pi path resolves against the given workspace in
the gate and against the real path in Pi, so a workspace on a symlink lets
.. reach outside. Comment 27032, queue rev 271.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three follow-up rows from the row 40 and row 41 reviews: the Mr flows
test and shim cleanup (Dewey), commit.test exit wait and the test-task
Docker skip, and persisting the bus comment counts across restarts
(after S6).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's four BUILD-LOG entries for row 40 (started, round 1, round 2,
round 3) and their three candidate manifests under
agents/dewey/work/queue-40/, plus my landing entry with the gate counts
and the Mr follow-up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's round 3 candidate, manifest
agents/dewey/work/queue-40/candidate-manifest-r3.sha256 (d0aa0ded,
27 files, checked OK in the canonical tree).
- WebUI inbox, tasks, agents and trail views, read-only over /api/bus.
The README says the bus proof ends at the Console process.
- CHAT-03 seal: the engine command is fixed, the engine environment is
explicit, SEAL_FLAGS has --no-approve, escalating is cleared on throw.
- Terminal input typed after Ctrl-T or Ctrl-O is held. Only the run whose
own parse set held drains it (T1), and #run catches errors per action.
- DEFERRED keeps N2 and moves F2 to done, citing T1.
Reviews: Filbert approve (comment 27011, rev 260), Darkwing approve
(27013, rev 264). Landing gate on 8cad7722 plus the candidate: webui 22,
conversation 161, control-board 124, every scripts/test-*.sh green,
test-task 98/0. Mutant Mr survives; its flows test is the first
follow-up row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R4: the gate checks a read under every spelling Pi's read would open
(AM/PM U+202F, NFD, U+2019, NFD with U+2019), from the workspace and its
real path, for Pi read and Claude Read. Mr test, launcher test timeouts.
Candidate manifest 9f0593af (42 files), gate at 66b9e082 green but
test-task Docker recall, same on base. Candidate source not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R4: Pi read opens a spelling variant of a missing name, which the gate
never checked; a variant-named workspace link reads outside. Comment 27010.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 41 in-review→in-progress→in-review, round 2 on #1523, candidate
manifest fd21bdc2...eb02, request posted as comment 27004. No other
seat's revs pending.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate 5cbf22c0, comment 27001 on #1522. T1 blocks: the terminal's
input hold is drained by an earlier chunk's run, so text typed after
Ctrl-G then Ctrl-T is refused and cleared. Suites, probes and 16 mutants
(13 killed; the 3 survivors checked by hand) in the packet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict on candidate 5cbf22c0 (comment 27001 on #1522): T1, input held
behind a Ctrl-T is drained by an earlier chunk's run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R1 and R3 block: a late broker reply shifts later replies on the host IPC
channel, and a dangling workspace symlink passes the gate. R2 (close waits
on an open launch.sock client) should be fixed in the same round. Probes,
mutants and suite outputs alongside. Comment 26995 on #1523, queue rev 247.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Request posted as #1523 comment 26991; candidate agents/filbert/work/s6/candidate-manifest.sha256 (9b670e27).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
build.patch dc346027…4454, candidate-manifest 5b3a934d…27f3, base 915e00e5,
41 files, +4302/-55. Gate at 2855e628 in out/: every node suite and every
test-*.sh green except test-task's live recall (needs Docker; the base
fails the same two). The recorded PM launch waits on the Q14 cutover.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/design/ holds the preview page (brand, 13 console views in five
states, 13 terminal screens, 39-feature coverage with status), the
README spec and the Relay mark. Design only; no package code changed.
Open for Jason: default palette and mode, final mark, web resolution,
one mosaic app.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing approved it as is: keys allowed by validateProjectDocument, id
matches the business file's projects.stack, clean-worktree suites green
(business 60/60, cli 66/66, bus 67/67, test-config 24/0). Without it the
bus host gives the broker no task verbs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brand board revisions, the five dashboard mockups with their verify
script and evidence, the slice 1 S5 design note (SLICE1-VIEWS.md, rev 3
plus the S2c section 10 update), the Console mockup of inbox, tasks,
agents and trail (mockups/slice1/) and checks/slice1-verify.mjs (16
checks; check 16 pinned to bus at d27042fa). Also the CHAT-03 progress
log and the 2026-09-26 dirty-tree manifest. Design and evidence only;
no package code. Row 40 (#1522), committed on its own at Sage's
direction, 2026-10-09.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason asked for tasks.mosaicstack.dev operational with agents configured in it.
Sage backed it up, tested the restore, pinned 2.7.0 (infra PR #325) and ran
sections 2 and 3 through the API: owner id 4, svc-mosaic-stack id 5, project 32,
bots 6-10, scoped tokens to 2027-01-07. Probes pass. Scope is Mosaic Stack only
(Mos relaying Jason, his Q9 open). OIDC has been broken since a 2026-04-27
NetworkPolicy; infra PR #326 is with ops-01. BUILD-LOG also records the cert
renewal fix and the last-applied annotation slip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason ruled that agents run the steps his admin grant to the jarvis
Gitea token covers. Sage created the four mosaic-stack bots (ids
114-117, restricted, non-admin), added them as collaborators (W/W/W/R),
and minted one scoped token each (ids 191-194). The tokens were written
0600 outside the repo. Scripts and receipt are in
agents/sage/work/gitea-setup/. The guide and SR brief now say who
runs which section. Sections 2 to 4 (Vikunja) stay with Jason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's three BUILD-LOG entries (row 45 started, round 1 ready, round 2
ready) and their round 2 SESSIONS line, plus my landing entry with the
gate counts and the reviewers' non-blocking follow-ups.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's round 2 candidate, packet agents/rocko/work/s4-follow-up/
(build.patch c8cec070, candidate manifest 5b067a9d, 8/8 OK).
- Definite DM refusals wait the full 30-minute cap, counted from the
journal's last refusal, so five refusals span about two hours before
gave-up (lead decision 73). Unknown outcomes keep doubling.
- Broker close sends at host.mjs:144/150/184/188 pass a callback, which
closes the EPIPE window both reviewers found in round 1.
- README documents manual recovery for an open decision.
- trackers-boot test, X9, X14, and Darkwing's round 1 notes 1-4.
- The append type check stays out; Rocko's reason holds (both reviewers
agree).
Reviews: Darkwing approve (comment 26884), Filbert approve (26886).
Landing gate on b13fef4c plus the patch: every node suite and every
scripts/test-*.sh green, test-task 98/0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
build.patch c8cec070…6756, candidate-manifest 5b067a9d…0e0e, base d539d8d2.
Covers Darkwing 26872, Filbert 26880 and lead decision 73.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes the K1, K3 and K10 race: a child could get SIGTERM before it
installed its handler. Author: Darkwing. Reviewer: Dewey (approve,
comment 26873). BUILD-LOG landing entry with gate counts and the
correction to decision 72's probe claim.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
R1: the guarded close send at host.mjs:144 and :150 can still fail with
EPIPE after a broker SIGKILL; reproduced, fix is a send callback.
R2: give-up lands 7.5 min after the first refusal, against decision 72's
reason; Sage's ruling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Packet for #1527, base 521597bb: build.patch, candidate manifest
b329fdcb, packet manifest, BUILD.md, gate and mutant receipts. The
candidate itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The ignoreTerm tool child was TERMed before Node installed its handler
when the claim store sits on a fast disk. Candidate fixes the fixture;
the patch itself is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decision 72 rules on F2 (five definite refusals stop a DM; unknown
outcomes keep retrying) and J4 (type-check confirmed journal lines),
and opens two rows: the S4 follow-up for Rocko and the K1, K3 and K10
cohort failures for Darkwing. The trackers boot test from the row 39
gate is kept under agents/sage/work/s4-follow-up/ until the follow-up
moves it into packages/cli/tests/.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/cli adds mosaic inbox, decide, tasks, agents and trail over the
human-cli transport, and mosaic bus start, stop and status as the trusted
host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots
packages/bus/src/process.mjs, passes config.trackers from the tracker.*
variables (lead decision 70), and runs a notifier child. The notifier DMs
each open blocking decision once and sends an 08:00 America/Chicago
digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord
ids. A torn journal tail is copied aside and truncated; a malformed line,
a directory looser than 0700 or a symlinked journal refuses (lead
decision 71). packages/discord gains dmRecipient, createDm and notify.mjs.
Candidate agents/rocko/work/slice1-s4, base b9b6cf00, build.patch
b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on
#1521 (comment 26855), Filbert approved round 2 (comment 26856). The
packet's mutant table lists M28 as killed; it survived, and BUILD-LOG
records the correction.
Integration gate in a worktree on 2557e29d with the patch applied:
bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail
on the base; S4 doesn't touch the package. Every scripts/test-*.sh is
green, with test-release 14/14 and test-task 98/98 on the existing gate2
compose network. A scratch test, not in this commit, booted the real
host with trackers against S3's fake Vikunja: the adapter went ready and
a task.close on a missing task answered task-not-found after a Vikunja
read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The seats' uncommitted S2 to S3 entries land with row 38. Rocko's two S4
entries stay uncommitted until row 39 lands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/tasks adds the Vikunja v2 client, the eight task verbs, the
board-plus-cursor poll with its 60 s window and the digest. The broker
gains the task verbs and boots trackers from the boot config (lead
decisions 66 to 68). Due dates are truncated to the second and recorded
as truncated (B1). A write that lands but whose final read fails counts
as landed, in update and in create (B2).
Candidate agents/darkwing/work/slice1-s3, build-r2.patch 71ce87e6,
manifest e10e30e3 (28 files). Filbert approved round 2 on #1520
(comment 26853). Darkwing's post-reset rerun: test-release 14/14,
test-task 98/98 (comment 26857).
Integration gate in a worktree on c4baf779 with the patch applied:
bus 67, business 60, control-board 124, discord 173, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3. The three cohort kill cases (K1, K3,
K10) fail the same on the unpatched base, and the patch doesn't touch
the package. Every scripts/test-*.sh is green. test-release 14/14 and
test-task 98/98 ran on the existing gate2 compose network, because the
host's Docker address pools are exhausted. No network was created or
pruned.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
B1 truncates due dates to the second; B2 records what landed when the
final read fails; tests for V9, V21 and S5. Candidate manifest
e10e30e3, 28 files over c9ef7ee4. Mutants 46/63. Live provider cases
in test-release and test-task failed on a zai usage limit (429).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Candidate for #1520 against 81339889: packages/tasks and the bus task
verbs. Live run rehearsed on a scratch v2.7.0 container; the estate run
waits for T236's base URL.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
human-cli.mjs is the decide transport. S4 owns the broker host and one
notifier for the blocking-decision DM and the 08:00 Central digest.
Darkwing is second reviewer for packages/discord and the host. Trail
prints in broker order. Row 39 no longer waits on S3. SESSIONS gets
Rocko's orientation line and a correction for Sage's estimated stamps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason's R26 via Mos: Claude first, Codex only on gpt-6.1-sol, never
Astra. Rocko's Codex thread ran gpt-6-astra; the seat now runs as T3
thread b84bb264 on Opus 5.5, because its only row, S4, builds mosaic
decide and the gated decision DM. Roster updated. Filbert's launcher
still pins Astra; recorded as a follow-up, unused while Filbert runs in T3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's correction, checked against the 2.7.0 OpenAPI: GET /tokens
returns PaginatedAPIToken, with items possibly null.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's probe-s7f: svc-$BIZ revokes one bot token (204, then 401);
the owner gets 403. A plain GET /tokens shows only svc's own tokens, so
rotation names GET /tokens?owner_id=<bot id> for an old id. Decision 68
records the resolution.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mos: the svc-mosaic-stack password is a credential Jason keeps (R5), and
T236 creates no users. The runbook's Path A now has Jason create it with
the command from the instance's ops doc. Records the R20 confirmation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's row 38 labels probe on the pinned 2.7.0 image: a bot created
from the owner's account reads and attaches every label that account
created, in any project (upstream #3592). Bots owned by svc-<business>,
an account with no labels and no shares, see only labels on the shared
project's tasks. The runbook now creates the bots and mints and revokes
their tokens as svc-$BIZ; the owner keeps the project and the shares.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The estate Vikunja (T236) won't serve by Jason's 10-09 run. S3 builds
against the recorded fake and a scratch container; its gate still needs
the live run. R20's PM and Lead match the slice 1 brief's staffing.
Merge plan step 2 no longer holds for T235 (brief c733ac92b).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason's 2026-10-08 rulings via Mos (thread 1eba59e5). R10: next stays
the trunk; the plan pins refactor b8db39cf and next 2101c9b4, finds a
clean merge-tree, and names the CI gate, approval identity, frozen
next-lane publishing, root .mosaic/repo.json, 13 stale v1 PRs and the
hardcoded ledger branch as the work. Nothing merged.
R8: Path A on the new estate Vikunja meets slice 1 and replaces
decision 61's Path B. The runbook's Path A text names the estate
instance and the share-only isolation rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A within-role message.send stores its decision as a citation: the broker
checks it exists in the business, and doesn't class-match it, consume it
or put it in action.allowed (lead decision 65). Sends that aren't
within-role keep the S2b authority rules. The README wording on
within-role sends is fixed.
Candidate agents/rocko/work/slice1-s2c-r1, build.patch 2c4f8d9f,
manifest aa249ad9. Darkwing approved round 1 on #1526 (comment 26778).
Integration gate in a worktree on ac4a6499: every package green on
Node 26; bus 58/58 on Node 24; every scripts/test-*.sh green. Node 24
failures in conversation, ledger, queue, seat and webui match the base.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict comment 26778, queue rev 156. Within-role citations never reach
the authority check or the grant event; other sends keep S2b. 58/58 on
Node 24 and 26, seven of seven mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
authorize, message.send and role.revoke consume a cited decision once,
inside the write transaction (lead decision 64). A second use refuses
with decision-consumed; a class mismatch refuses with decision-mismatch.
Candidate agents/rocko/work/slice1-s2b-r2, build.patch 56d572fe,
manifest a89d64ca. Darkwing approved round 2 on #1525 (comment 26769).
Integration gate in a worktree on bba75b4a: every package green on
Node 26; bus 55/55 on Node 24; every scripts/test-*.sh green. Node 24
failures in conversation, ledger, queue, seat and webui are identical
on the unpatched base (container /tmp is overlayfs, no jsonschema).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict comment 26769, queue rev 146. R1, R2 and both lead decision 64
rulings are in; round 1 probes now refuse. 55/55 on Node 24 and 26,
nine of ten mutants killed, the survivor equivalent.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict comment 26765, queue rev 140. R1: message.send and role.revoke
verbs use gated decisions without consuming them. R2: a cross-role
decision authorizes without limit once policy makes the action gated.
Two rulings for Sage on cross-role reuse and class drift.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rocko's round 2 candidate, approved by Darkwing (#1519 comment 26757).
build.patch 40d7e838, manifest 61519059, 24 files under packages/bus,
schema v3b (179ffe35, lead decision 60). Integration gate in a git
worktree of 942dca9e (S1 in the tree) plus the patch: bus 43/43 and
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Rulings from lead decisions 62 and 63: the human proof is cooperative in
slice 1, and a self-raised cross-role decision routes to the human.
Single-use gated approvals follow in row 43.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's round 2 candidate, approved by Filbert (#1518 comment 26730).
build-r2.patch a27890d5, manifest 869168c7, 34 files, applied on HEAD and
checked 34/34. Integration gate on an export of HEAD plus the patch:
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Conductor, queue, conversation and discord confirmed in git worktrees of
HEAD with and without the patch, identical results. Lead decision 63
accepts the vocabulary location, the example path and the business
branch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revs 126-129 are Sage's round opening (request comment 26756). Rev 130
records Darkwing's approval, comment 26757, record 1e3c06c7.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 2 of #1519: R1 to R5 fixed and reproduced, 43/43 on Node 26 and
Node 24, 22 of 23 mutants killed against a clean baseline. Records a
reparented-CLI path past the human proof for Sage's ruling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers Filbert's round 1 (#1518 comment 26724): launch.by must hold
role.launch, launch is null when limits.authority drops it, a test for
cross-role narrowing, and the business file checked on its open
descriptor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds ten cc-7 cases (Darkwing's six wrapper cases plus allow, block,
SIGTERM-ignoring gate and inner timeout above the hook timeout), amends
rely-on lines 2 to 4, removes the stale cc-1d-block-bare evidence, adds
compare.sh, reads SDK_ENTRY from the environment, fixes the cc-5c row and
the Pi exit-code sentence.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Record for #1516 comment 26725: 34/34 cases reproduce, and a wrapped
Claude command hook fails closed on crash, missing path, noexec and hang.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lead decision 59. task_current picks each task's current snapshot,
skipping a poll row whose read_at is at or before the latest self row's
at; tasks_open reads it. Prototype on Node 24 and 26, two mutants
caught, and Dewey's fixture replayed: tasks_open now differs from v3a
only on #42 (in-progress). The notes add an S3 poller rule: compare a
read against task_current, not the raw latest row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
S1 candidate for Filbert's review on #1518: build.md, build.patch and
build-manifest.sha256 (34 files at base fef4b362). The candidate itself
is not committed.
proto-v3a-notes-correction: the schema has 36 triggers; the printed 35 is
counted after the tamper check's DROP TRIGGER. Found by Rocko.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pi 0.85.1 and Claude Code 2.1.289 (Agent SDK 0.3.289), 34 cases against a
scripted local model in a loopback-only network namespace. MATRIX.md has
the command, outcome, model-visible message and fail-closed column per
case, and one rely-on line per case. Evidence under evidence/.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict changes, #1517 comment 26712: the Vikunja rotation removes the
owner header before the revoke that needs it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict changes, #1517 comment 26709. Gitea 1.27.1 and Vikunja 2.7.0
source checks for Sage's three questions; two mint defects.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two triggers: every task.* event names its task in subject, and
task.created cites a human.input event and a requirement id. The task
ref pattern is tightened on snapshots, decisions and subjects. Runs on
Node 24.21.0 and 26.8.1 match apart from the version line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
task_snapshots gains via and read_at, an integer-bucket CHECK with a
tombstone exception, the read-ordering rule in task_external_changes and
the tasks_open view. task.missing names its task and a reason. Prototype
rerun on Node 24.21.0 and 26.8.1; a mutant without the read_at rule
reports the broker's own move as external. Lead decision 52 (B3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tokens by hand from a runbook, Gitea bot users per role, slice 1 agents
as Jason's OS user, DMs through the existing connector, digest at 08:00
Central, Gate E with the slice 1 WebUI step. Approval as 1.0 is still
open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Real Vikunja scope map, a read-only sync bot, a two-request poll that
sees column moves and deletions, and task_snapshots changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Researcher ran P1-P7 on a scratch Vikunja 2.7.0. Scope names in
addendum A are wrong, polling on updated misses column moves and
deletions, and If-Match isn't enforced. Decision 51 rules on each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Controller, claim store, live-session guard, engine link and seal,
turn tracker, cohort force stop and recovery, client library,
transcript and mediated terminal, with the fake engine and tests.
Fixtures only; no live cutover.
Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694)
approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files).
Suites on an export: conversation 152/152, control-board 124, webui 14,
seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green.
Follow-ups for I3 are in DEFERRED. Gate E stays with Jason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Verdict comment 26690 on #1507, candidate 2b48e333. B1 and B2 fixed;
follow-ups F1-F3 recorded in the review file, none blocking.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BUILD-LOG phases for the I1 build and the round-1 rework, and Dewey's
SESSIONS line. The candidate itself stays uncommitted until both
reviewers approve round 2.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's schema-v2 adds task_snapshots, decisions.blocking and a
closed events.kind list. Sage reran proto-v2 on Node 26 with matching
output. Decision 50 accepts the five choices beyond addendum A.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's addendum as a record. Broker verbs enforce field ownership,
broker push from a bare repo, polling without webhooks, Vikunja probes
before the adapter interface is fixed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's survey of Pi, Claude Code and Codex hooks as a record. No hook
is a hard block alone; credential scope, verbs, tool ceiling and a
container are. Two DEFERRED items.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's design note and SQLite prototype as records. Sage accepts
seven of eight open questions; the PM launching sessions goes to Jason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1 answers fill users, problems, hard limits, hosting and the v1
done measure. Decision 45 raises Sage's seat limit to 4 Opus 5.5 and 4
Sonnet 5.5 sessions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both reviewers requested changes (26681, 26683). Darkwing's verdict
landed on #1508; pointer 26685 on #1507.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Slice 1 becomes goal 1, working north star sentence on the goals page,
Vikunja local by default with existing or bundled at install, SQLite
for decisions and messages, Pocket ID after slice 1, row 32 parked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vikunja integrate, first roles agreed, Pocket ID recorded as an SSO
candidate, row 32 parking advised.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers the five questions, maps the brain dump against the repo, and
proposes one end-to-end slice before measures. Not ratified.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Built by filbert, approved by darkwing in round 1 (#1508 comment 26651).
Manifest fe7da3ef, 10 paths. Suites green on an index export.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Built by rocko (Gate G run), approved by filbert in round 1 (#1508 comment
26629). Manifest 35199daf. Suites green on an index export.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert posted the verdict as comment 26629 with the seat's own token and
recorded it. The candidate is manifest 35199daf, uncommitted in the working
tree; no file under docs/plans/reviews/ was added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert's n2 from the Piece D round 2 review, written as a queue brief.
It is also the piece a fresh seat starts in Gate G.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert posted the verdict as comment 26589 with the seat's own token and
recorded it (rev 23). The candidate fd72d268 passes verify-commit, and no
file under docs/plans/reviews/ was added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Piece E (fd72d268) moved the weekly routine into packages/ledger/README.md.
Row 7 stays until Jason closes it (lead decision 40).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The ledger prints a queue section above the weekly table. It checks four
things:
- open issues named by done rows;
- owner registrations for active rows;
- closed issues for done rows;
- the age of required rows.
The result is fail, incomplete or reduced pass. It uses its own Gitea
budget of the open list plus at most 10 lookups. A full open page counts
only while an issue in some row's closes has no known state (lead
decision 40). T3 seats are exempt per run with --unsupported-runtime.
The weekly routine is in packages/ledger/README.md.
Built by Darkwing (build.patch ab1f12ca, manifest 0b20bbca). Filbert
reviewed it: round 1 81f26f2e asked for changes (C1, ISO requiredSince
never aged); round 2 ce8ce150 approved. Also carries Filbert's plan
amendment for decision 40 (68a25ffe).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 1 on f539466f: request comment 26577 by darkwing, approval
comment 26579 by filbert, recorded at rev 17. The rev 17 entry went into
f3f48cfd, whose message doesn't mention it. No file was added under
docs/plans/reviews/.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
queue move ID in-review posts the review request as a Gitea comment and
review record reads verdicts back, so reviews stop being files in
docs/plans/reviews/. On a comment round, in-review to waiting-on-jason
now needs every listed reviewer's approval for the current round, the
same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads
the raw per-seat token files (lead decisions 37 to 39): config built and
checked before curl starts, export attribute cleared, fixed base URL.
test-queue.sh skips its live checks outside the canonical root.
Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302)
and corrected the plan (293747cd). Rocko reviewed the helper (e896192f,
2096b0a3), and Sage's lead check passed under decision 38. Manifest
b402fb38, 19 files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AGENTS.md cadence, pointer and recovery rule name `scripts/mosaic queue
next <seat>` and the ratified goal order. The six seat CONTEXT files run
the queue instead of reading CURRENT.md for ownership and gates. The
queue README says why queue-commit.sh calls cli.mjs directly (Filbert
A2 r1 n3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Filbert approved round 1 (f167b85e). Manifest 782bcb62, 21 files, plus
the QUEUE.md markers and the TOOLS.md section. Lead decision 35.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test-foundation.sh and test-discord.sh ran a nested node --test that would
exit 0 on failure under a parent runner. Both clear the variable now, and
each has a check that fails the suite if it comes back. Darkwing wrote it,
Filbert approved it (e464be6c). Nine suites green on an index export.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/queue, scripts/queue-commit.sh, scripts/git-hooks and
scripts/test-queue.sh, plus docs/plans/BRIEF-TEMPLATE.md. There is no
queue.json yet, so verify skips until the genesis commit after A2.
Darkwing built it, and Filbert reviewed R0 (6933b885, changes requested)
and r1 (e464be6c, approved). The 20 files match manifest 85a8a453. The
nine suites passed on an index export, including the new queue suite.
test-queue.sh joins the suite list in AGENTS.md. Lead decisions 20, 23
and 26.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason approved CHAT-03 and gave Sage the SetSpark approver owner choice.
Decision 20 records the queue A1/A2 split. Decision 21 makes the SetSpark
fix Mosaic's through a Sage subagent and keeps the sanctioned pending:
approver markers so the cutover migration still works. Decision 22 charters
CHAT-03 with Dewey writing the brief. QUEUE row 5, DEFERRED and SESSIONS
updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
History opens a seat's conversation from the Waiting card, table row
and inspector. It pages the whole branch through the CHAT-02 board
routes, renders untrusted text inert, polls with the follow cursor, and
marks every switch (branch, newer, reconcile, gone). The WebUI proxy
passes only the two conversation routes' queries upstream.
Dewey authored it. Filbert asked for changes on r1 (24b046af) and
approved r2 (d06de6a7) in review 160dd68d. A relaunch shows 'newer',
not 'reconcile', a deviation from brief 2.3 item 6 that Filbert
accepted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Darkwing's follow-up to the T3 thread source: manifest 382f5bb0 pins
t3.mjs, ledger.test.mjs and README.md. Filbert approved it (review
6fd693b6). Ledger 51/51; the eight suites pass on the index.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/ledger/src/t3.mjs reads ~/.t3/userdata/state.sqlite read-only,
in one transaction. It maps each thread to a seat by title and checks
self-addressed headers. Unmatched threads go in a t3:unmapped row. A
missing or locked database exits 1 and names --no-t3. Gate F is on by
default (lead decision 12). The 6a uppercase-class fix rides here.
Separate item: the Pi session reader splits lines only on \n, so a raw
U+2028 or U+2029 in a string no longer splits a record. Node 26.8.1's
readline split there, and the live ledger refused on HEAD.
Darkwing built to brief R3 (f3c05c1b); manifest ba73a163. Filbert
approved the build (e47ec6da) and the U+2028 fix as its own item; brief
review be1aa414. On an index export: the eight suites
24/90/43/17/14/15/63/18, ledger 47/47. Four nonblocking notes go to a
small follow-up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packages/conversation is a library with no server: safe-fs, the Pi session
parser, CHAT-01 pages, pinned snapshots, cursors and follow. The control
board adds GET /api/conversations and /api/conversation behind the Host
and Origin guard. Both are read-only, their queries are validated, and
each refusal code maps to a status.
Dewey authored it (packet 0cf177b1, revision 2). Filbert reviewed the code:
R1 revise (branch ids moving on append, the assumed-link bridge merging
branches, one unreadable seat directory turning the catalogue into a 500),
then R2 approve (3b14d66c). Darkwing reviewed the routes: R1 approve
(07b10ad1), R2 approve (b9d92003). The package lands with the routes,
because serve.mjs imports the reader at load.
On an index export: the eight suites 24/90/43/17/14/15/63/18,
conversation and control-board 153/153, webui 9/9.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason's live check after the 20:58Z restart posted no Approve button. The
Discord Sage wrote DEC-009's required_approvers as names; the SetSpark
service stores approvers as discord:<id> and accepted the names, and the
connector correctly refused the approval request ("bad approver id").
- binding.mjs derives setspark.approvers from the binding's users (name to
id); a binding-set approvers key and duplicate names are refused. With
setspark set, a user id or name change refuses the reload (pi's approvers
are fixed at start).
- setspark.mjs: record_create/record_update map required_approvers names to
discord:<id> and refuse unknown names, ids, duplicates and non-lists
before any request, without echoing the value. hideIds turns mentions,
discord: values and standalone 17-20 digit runs into the user's name or
"unknown user" in every verb's text and refusal, including the service
message and code before they are cut. The connector's approval request
keeps the bare ids.
- tests: boundary test over nested, keyed, numeric, mention and cut ids;
a local contract fixture from create through validateRequest, with the
old name-stored shape still refused.
Rocko: R1 revise, R2 revise, R3 approve (81379830..., report da75219f...).
Suites on an index export: 24/90/43/17/14/15/63/18; Discord node tests 173/173.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Plan 282fabbb (Filbert) and the six adversarial rounds (Rocko, r6 80cde839).
Lead item 15: Gate F first, then A1 and A2 as separate reviewed commits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brief e8300cb6 (Darkwing), review bb02d8d3 (Filbert, approve with three
nits), R1 record and R1-to-R2 diff. Lead rulings in item 14.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
resolveToolRoots dropped tools.setspark, so the record verbs never reached
pi's --tools list and the connector never built its SetSpark client. The
resolved config now carries only baseUrl, keyFile, principal and timeoutMs,
the keys the extension's loadSetsparkConfig accepts; the extension restores
the response cap. The connector's client uses the validated binding object,
which keeps the cap. README: principal is required, timeoutMs is optional.
Test (failing first): binding -> resolveToolRoots -> JSON -> loadToolsConfig
gives the binding's config, and the verbs reach enabledToolNames. Eight
suites green on an index export. Rocko approved
(agents/rocko/work/row25-setspark-fix-review-2026-09-26.md, a28df89e).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every engine test stops its engine in finally, and commands() tolerates a
log that doesn't exist yet, so a failed assertion no longer leaks a fake pi
and hangs the six-package union. A turn that failed client-side stays at
the front of the queue and holds the next prompt. If pi has sent no
agent_start ABORT_GRACE_MS (30 s) after the failure, the engine marks
itself wedged, fails held prompts with engine-wedged, refuses new ones with
engine-down, and stops pi. The exit reaches onExit, the connector exits 1,
and the unit restarts it. Pi's events carry no prompt id, so R1's approach,
dropping the turn and sending on, let a late run answer the next prompt.
Rocko rejected R1 and approved R2.
Tests: engine 17/17 (R1 fails 4, HEAD fails 5). Union 408/408 and the eight
suites green on the committed index. Record:
agents/darkwing/work/discord-engine-busy/.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
After a DNS rebind, a web page could read /api/board and POST /api/reply,
which pastes into a live seat pane. foreignRequest() now runs first and
returns 403 for a non-loopback Host, a wrong port, userinfo or a path in
Host, or any Origin other than http://<Host>. A missing Origin still passes,
which covers the WebUI proxy. Dewey authored it; Rocko approved de9ff942
(review 5a12f08e) with one low wording finding, now fixed in the notes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason ruled on seven open items (20:27Z-20:45Z): seat Gitea tokens read in
place, one Discord restart after 6b with row 25 live, row 8 limited to the
dev seats, DYOR in dyor-stack-v4 with Sage moved to SetSpark, skills/aws-*
excluded locally, no second WebUI return defect, and go on CHAT-02 only.
Sage persona files name SetSpark as its business work. Darkwing's SOUL drops
harness names that were wrong for T3. DEFERRED adds the slash-prefix paste
hazard and the board Host/Origin gap, and moves the ledger T3 item to Done.
Dewey's approved CHAT-02 brief (636b0fac) and Filbert's review are recorded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
messageKind knew only the tmux preamble, so a prompt opening with the T3
header [from: role (id) -> to: role (id)] counted as human in Table 2. The
first line now matches either form; anything short of the full header stays
human. Filbert approved R1 against the frozen hashes.
Proof: packages/ledger/tests/ledger.test.mjs, 22/22; against HEAD's
ledger.mjs it fails exactly the two new tests. No suite runs it. Eight
suites green on the staged tree.
The fix changes zero current counts: no Pi log under .pi/state contains a
T3 header, and the ledger does not read T3 transcripts. Gate F waits on a
T3 thread source.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- agents/sage/ launch files committed after Dewey's review (R1 revise, R2
approve). The launcher test now covers sage with its zai/glm-5.3 high pin.
The README states the lead role and its limits, and the seat reads but
never writes the old DYOR records under ~/.mosaic.
- N6 (Dewey authored, Sage reviewed against pins): seat personas and the
Rocko launcher name Sage as project lead and Darkwing as a collaborating
engineering seat, per Jason's 2026-09-26 ruling.
- docs/plans/2026-09-26_lead-decisions.md: the push, no merge into next and
its conditions, the board restart, queue-as-data rulings, Gate F waiting
on a T3 source, and what stays with Jason.
Launcher tests 6/6 and 1/1, eight suites green. Not pushed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dewey's return-flow candidate on the #1512 R1 baseline. The inspector used to
show the previous answer while a seat worked on a reply, which looked like the
reply; it now shows a pending notice that clears on the new final answer.
Age shows a relative time beside the ISO time. Filbert approved R2, source
only; the patch reproduces the pinned hashes (app.js d1a51646,
return-flow.test.mjs a598c0d4). webui tests 9/9, all eight suites green.
Known limits are in agents/dewey/work/return-flow-age/NOTES.md. Not pushed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Records Jason's 2026-09-26 ruling: Sage leads the project, Darkwing is a
collaborating seat, development stays in T3, and the old ~/.mosaic fleet is
being retired. The lead role adds no push, merge or deploy authority.
- QUEUE rows 23-25 show their commits and pushes; row 8 links a parked stub
brief listing the rulings Jason must make before fleet seats move.
- Shared records from Darkwing (rows 6, 16, 18, 22, #1511, #1512) and Dewey
(row 5) that were waiting on one owner for the shared files.
- DEFERRED: T3 headers counted as human in the ledger (#1506), #1509 engine
test leak and busy gap, #1512 re-run outcome.
- BUILD-LOG.md rebuilt as HEAD plus the uncommitted entries; the working copy
had dropped the row 23-25 entries. Diff against HEAD is additions only.
All eight suites green. Not pushed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One cumulative control-board, webui and seat state. The four rows edit the
same files (scan.mjs, page.html, README.md, app.js), so they land together,
each on its own receipt:
- Row 18, Discord connector rows on the board (#1509): R3 approved by
Darkwing and Dewey, Gitea comment 26257, manifest 254403b8. Jason
accepted the visual test.
- Row 22, board attention status (#1503): Filbert approved R1, comment
26248, manifest e40b58ec; restart receipt 26249.
- #1511, task attribution (row 6 code phase): R2 approved by Filbert and
Dewey, manifest d4c96395. docs/TOOLS.md carries the approved --by usage
line (tools-usage.patch 86bcba3c).
- #1512, relaunch activity (row 6 pilot): R1 approved by Darkwing and
Dewey, candidate manifest 47769fad. All seven source files match it.
Row 16, internal development bootstrap (#1510): the seven files outside
shared records match Filbert's R1 pins, receipt 26204 (agents/researcher/*,
scripts/test-darkwing-launch.mjs, the bootstrap plan).
packages/webui/src/public/app.js is committed at its #1512 R1 pin ce7d79a4.
The working copy holds Dewey's unreviewed return-flow candidate on top of
that, and it stays uncommitted.
Also: the four row briefs and Darkwing's evidence records under
agents/darkwing/work, including the 2026-09-26 tree manifest and the #1512
re-run against 21e3e908. Serial acceptance command: 397/397, three runs.
The failures that only show when tests run concurrently are in #1509 engine
tests, and they reproduce on clean HEAD.
Suites on the exact staged tree: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63; package union 397/397
(serial); test-darkwing-launch 5/5.
Shared records (BUILD-LOG, QUEUE, CURRENT, DEFERRED, SESSIONS, AGENTS.md,
agents/README.md) follow in Sage's records commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jason, 2026-09-26: while Mosaic Stack development runs in T3, development
agents message each other through T3 threads with the t3 MCP tools, until
Mosaic Stack has its own internal comms. ms-communications now picks the
transport by where the recipient runs, carries the T3 header and reply
rules, and counts a returned turnId as the delivery receipt.
docs/guides/T3-AGENT-COMMS.md is adapted to this repository: status,
thread lookup by agent-name title, replies with wait off, and record
locations; references to guides absent here are replaced.
Suites green before commit: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63. unslop-check clean on
both files. Reviewed by Sage (T3 thread 1ef1e4f8).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Row 25, parts 2a and 2b, against the shared-signals contract a5425a2.
Model side: eight fixed verbs in the pi extension (record_list, record_get,
record_create, record_update, resolve_id, open_approval_request,
get_approval_request, create_document), each one HTTP call with arguments
checked before any request. Writes carry an idempotency key
<principal>:<message id>:<call index> and an audit context. The seat key is
read from a 0600 file on every call and never cached, printed or journaled.
Connector side: append-only approval ledger, Approve button and exact
"approve" reply resolved by the connector against the required approvers,
confirmation message posted as button evidence, bind and add_approval through
the service under connector keys, retry of unknown entries on start.
Evidence: node tests 162 pass, scripts/test-discord.sh 63/63. Review by
rev-code-02, round 1 approved (#1509 comment 26467, tree 7872d8c5).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Row 24. A writable root that is a git work tree may carry a git object in
the binding; the seat then has git_status, git_commit (explicit paths, seat
author, Requested-by trailer from the envelope requester, push at once per
D6), git_pull (ff-only) and git_push (one branch, never force), plus
reserve_id and per-write clone locks under protocol vault. Git children run
with no host config and one credential helper, bin/git-credential.mjs,
reading the 0600 seat token file named in the binding; the fleet helper
serves only the Gitea hosts. Suite 58/58, node 143. rev-code-02 APPROVED
round 1 (#1509 comment 26375, tree 82ab962f).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Row 23. write_file and edit_file for roots marked write: true under the
same fence as reads; web_fetch (https only, public addresses, pinned
connection, capped body) and web_search through SearXNG; extension
renamed to tools.mjs. Engine holds a prompt while pi is busy and sends
it as its own run, so a second message mid-turn no longer folds into
the first (live defect). fake-pi models the real follow-up folding.
Suite 52/52, node tests 129. rev-code-02 APPROVED round 3, comment
26362, tree dbd2ce9a. Records: QUEUE rows 23-24, CURRENT, BUILD-LOG
phase, SESSIONS, row 24 brief (git verbs, D5-D7 ruled).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A binding may declare `tools` with named roots. pi starts with
--no-builtin-tools and the package's own extension, allowlisting
list_dir, read_file and search. src/tools.mjs holds the rules: names
not paths, per-segment lstat walk, one checked descriptor read that
refuses symlinks, swaps, FIFOs, hard links and oversize files, credential
shapes refusing the whole read, and a per-message call budget. The engine
settles on agent_end and records tool calls in the turn record.
Jason's rulings R1-R7 in the brief, section 7. rev-code-02 approved
round 2 (comment 26276) on tree 43f0329b after four round 1 fixes.
Suite 48/48, node tests 116. Not pushed.
Co-Authored-By: Claude Opus 5 <[email protected]>
`reload` validates the binding file and sends SIGHUP to the live owner;
the running connector re-reads it and swaps guildName, channels, users
and limits in place. name, seat, guildId, botUserId, tokenFile, engine
and context are fixed for the life of the process; a change there, an
invalid file or a channel outside the guild refuses the reload and keeps
the old binding. Every attempt is one line in reloads.jsonl. The service
unit maps `systemctl --user reload` to the same signal.
A user entry may carry `channels`, an allowlist of listed channel ids;
absent means every listed channel. Outside the list the message is
dropped as channel-not-for-user; threads count as their parent.
Suite 41/41, 101 node tests. QUEUE rows 19 and 20 opened.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason ran traffic under the unit, SIGKILL recovery, the brake, and the
release, and reported all verified. Receipt in the private evidence dir.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The control board row for the Discord connector is board-side work in
packages/control-board. Jason ruled darkwing builds it under the board
brief; the coordinator answers connector-side questions only.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
QUEUE row 18. The board discovers rows from pi session directories and
decides liveness by tmux; the connector's session and run.lock live
elsewhere and it has no pane since iteration 2. The brief lists what the
board-side change needs. Owner for Jason to rule.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
QUEUE row 17, MVP iteration 2. scripts/discord-service.sh renders and
installs mosaic-discord@<binding> from packages/discord/systemd/. The
unit's main process is `run --supervised`, which applies the new recover
policy first: a lock whose owner is gone is cleared and only the STOP
written for that is removed; an operator STOP or a held binding refuses
with exit 3, which RestartPreventExitStatus never retries. `recover` is
also a CLI verb. First cut used ExecStartPre and looped live, since systemd
honours the never-retry status only from the main process; replaced and
re-verified before any message traffic. Suite 40/40, 95 node tests.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason's message in #sage-admin got the eyes reaction before the reply; the
turn record carries receipt.ok true and a private evidence receipt was
written. CURRENT.md gains the pilot narrative line.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
QUEUE row 15, MVP iteration 1 after the Sage pilot. rest.react is best
effort (2xx true, anything else false, never throws); the connector reacts
at admission before the engine runs and records the outcome in the turn
record as receipt. Drops and refusals get no reaction. Suite 28/28, 90
node tests.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Live pilot receipts summarized in BUILD-LOG (eight steps, private
evidence under the seat's work directory). Jason ruled the replies read
as Sage; QUEUE row 14 done. DISCORD-USER.md now says unlisted senders
are dropped silently rather than refused with a reply, which is what
the connector does.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Zero-dependency Discord connector under packages/discord: binding
validation, REST and gateway clients, pi engine adapter, journal with
append-only inbox, outbox, admissions and notices, and a run.lock
ownership record {pid, start, boot} whose identity is checked three ways
and whose cleanup is gated by STOP. CLI check|run|stop|unlock via
scripts/discord.sh; offline suite scripts/test-discord.sh (28 checks,
87 node tests).
Reviewed by rev-code-02 on #1509 over nine rounds; approved exact tree
4e0feb6758c0a7e4a71483912a8e0d3e3ec95aef at comment 26170. Corrections
(1) to (12) recorded in BUILD-LOG. No listener started, no token read,
no Discord write; the live pilot follows this commit per the brief.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason: the process goes off the rails every time because the entry point is
prose. Rows 9-13 are marked required and cannot be parked.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason could not find what is next without reading the prose plans. QUEUE.md
is one row per piece with owner, issue, state, gate and brief location.
CURRENT.md becomes the narrative log; DEFERRED.md keeps only gaps.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Piece 2 of the MVP (#1503). A one-line reply box and Send in the detail
of rows with a live registration; POST /api/reply runs
tools/tmux/agent-send.sh -s <session> -S <host>:control-board
[-L <socket>] -m <text> once for one seat and returns the exit code,
stdout and stderr. The page shows delivered or failed with the tool's
stderr; other rows say "reply needs a registered seat". No send-keys,
queue, retries, history or broadcast; packages/seat and agent-send.sh
untouched. Every message ends with a fixed trailer telling the seat to
answer in its own session (Jason's refinement after the first Gate C
exchange; the board has no pane). Board suite 98/98.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason asked for the running model (sonnet, opus, gpt-6-astra) on the
board. readSession keeps model and provider from the log's latest
model_change entry or assistant turn, whichever is later, so a /model
switch shows on the next scan; scanAgent exposes both; the page shows the
model under the agent name with the provider in the hover and a Model
detail row. Blank when the log names none.
Live check on a scratch board against the real data root: all 42 rows
carried a model. Board 91/91. Sonnet review caught a double-escaped hover
title; fixed.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Two defects in 69f99323, reported by the professor session and verified.
The darkwing launch test's flock-contention spawn ran without the fixture
config, so launch.sh re-entered scripts/mosaic against the real data root
and wrote fixture records for darkwing, dewey and filbert there. That
spawn now names the fixture config, and both launch test files set
MOSAIC_CONFIG to a nonexistent path and clear MOSAIC_LAUNCH_REGISTERED
process-wide, so a spawn that forgets fails instead of polluting.
A registration is written before the launch script's own checks, so a
refused launch left a record with a dead pid that the board honoured. The
scanner now probes the recorded pid (pidAlive, signal 0); a gone pid makes
the record stale: still on the Registered line with alive false, derived
task, project and workspace win, index gains registrationStale, CLI
summary gains a stale count.
Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15,
launch scripts 5/5. Sonnet review APPROVED.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
New package packages/seat and wrapper scripts/mosaic. `launch <seat>` writes
<dataRoot>/seats/<layout>/<seat>/registration.json and then execs the seat's
launch.sh unchanged; `seat task <seat> <text>` edits the task only. The board
reads registrations, matches by sessions directory, and lets a registered
task, project or workspace override the derived value with a source tag.
The four repository launch scripts register themselves unless already
registered or run with --check. Fleet launchers untouched; one-liner on the
plan page.
Review found the record path keyed by seat name alone (repo and fleet
"darkwing" would collide); fixed by keying on layout. Also: the Pi pin
refusal now names installed and required versions.
Tests: seat 15, control-board 89, launch scripts 5, registry 69, config 24.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Gate A fix asked by the professor session on Jason's behalf. Each row
now carries three derived fields, shown as "unknown" when the log and
tmux do not hold them:
- task: the session's first user message (pi logs have no task envelope)
- workspace: the live pane path of the pane running pi, else session cwd
- activeProject: basename of the nearest git checkout above the workspace
tmuxInspect replaces the bare liveness call in the CLI and returns
{ alive, workspace }; tmuxIsAlive stays as a wrapper. The grouping column
and seen.json keys are unchanged. Fixture test per field, tmux parse
tests, page test; missing launcher signals are recorded in the plan page.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Acceptance rule (plan page, bf641e22): a seat mid-tool-call is working,
never waiting. The scanner already met it through pi's stopReason values;
deriveState now also checks the content for a toolCall block (working),
after the error stop reasons and before "stop" (waiting). Thinking blocks
do not keep a text turn from being waiting. Three JSONL fixture tests and
three state-table cases pin the rule. Live check on the real board:
orch-01 and rev-code-01 mid-tool-call are working, velma's finished
text-only turn is waiting. Sonnet review: APPROVED.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A project header now reads "fleet (13 of 38)" while Hide offline or Hide
seen hides at least one row, and "fleet (38)" when nothing is hidden. The
note under the table still says which filter hid how many. Numbers only,
so nothing new needs escaping. Static test pins the expression and the
removal of the raw-length header. Sonnet review: APPROVED, no findings.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Each project table now has "Hide seen" beside "Hide offline", both on by
default, with a note saying how many rows each one hides. The choice
survives the 10-second refresh. Static test pins the markup, the filter,
the persistence guard, and the change handler. Sonnet review: APPROVED.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason asked for a way to recall rows he marked Seen. The page now lists
them under a collapsed "Seen (N)" section between "Waiting on you" and
"By project", each with Unsee. Open/closed state survives the 10-second
refresh because only the section body is re-rendered. Page-only change
plus one static test. Tests: control-board 64/64, registry 69/69.
Review APPROVED, no findings.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
First step-3 refinement from Jason's daily use. Liveness now lists the
panes of the agent's tmux session and counts it alive only if a pane runs
pi, so killed pi sessions whose tmux session still exists show offline
instead of waiting. A "Seen" button on waiting and error rows stores the
row's lastActivity in <dataRoot>/board/seen.json (clicks only, never
rewritten by a scan, fail closed if corrupt) and drops the row from
"Waiting on you" until the agent writes anything newer; "Unsee" reverses
it. New POST /api/seen route: JSON only, 4 KB limit, 400 on bad input.
Tests: control-board 63/63 (30 new), registry 69/69. Review APPROVED;
receipt docs/plans/reviews/2026-09-12_control-board-step3-seen-marks.md.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Step 2 of the control board MVP (MOSAIC-STACK-D-001): `serve` command starts
a loopback-only local server that serves one self-contained page and re-runs
the status scanner on each /api/board request. The page lists sessions
waiting on Jason first (errors on top), then one table per project with
plain-word states, ages, last messages, expandable detail rows, per-project
hide-offline, and a 10-second auto-refresh with pause.
Tests: control-board 33/33 (10 new: loopback rules, host refusal, all routes,
per-request rescan, 500 path, CLI refusals, live serve, page escaping guard);
registry 69/69 unchanged. Receipt:
docs/plans/reviews/2026-09-12_control-board-step2-review.md.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Step 1 of the control board MVP (decision MOSAIC-STACK-D-001): a plan page,
Gitea #1503, and packages/control-board, which reads each agent's newest pi
session log plus tmux liveness and writes one status file per agent under
<dataRoot>/board/. 23/23 tests; independent review approved after three
fixes (length stopReason as error, unknown liveness state, secrets-boundary
test). CURRENT.md now points at step 2, the page.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Jason reran the two-test fixture demonstration on the canonical checkout
at 5abbabb7 (2/2 pass). Records-only: acceptance receipt, BUILD-LOG,
SESSIONS and CURRENT.md next action (MVP re-plan). No source change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Mosaic concepts pages now own the adapted content; source/license
metadata under docs/reference/concepts. Adds ACT-1 agent-context
planning capture, pinned concept test package + preparation utility,
foundation observation notes (durability, evidence, federation,
onboarding, workflow), and the #1495 consolidation assessment.
TOOLS.md updated for the host-dev launcher.
scripts/agent.sh --host-dev delegates to scripts/agent-host-dev.sh;
agents/darkwing/launch.sh provides the native development TUI
(context, skills, coding tools, /goal). Root SOUL.md is the M14-era
default-collaborator persona captured by the ACT-1 context work.
Launcher regression checks pass (test-darkwing-launch.mjs).
Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
Owner adjudication:
- canonical harness IDs match executables: pi, claude, codex, opencode
- agent.json uses one scalar harness ID; registry/manifest resolution, no
hard-coded schema enum
- target mosaic harness list/detect/install/rm/status lifecycle
- detection recognizes reviewed executables and records compatibility
without reading/copying harness homes
- installs are exact-version/verified, Mosaic-managed, never global
- detected external harness is available but not container-ready until
imported/installed, absent a separately reviewed host adapter
Gate 1 resolved. Remaining P0/review gates stay open; no implementation
authorized. Suites 24/15/90/14/17 + verify green; unslop clean.
Owner direction: the stack must never impact default harness usage.
Correction to M19 as shipped (nothing had been created in ~/.pi — the
move breaks nothing).
- Mosaic-managed accounts: <dataRoot>/auth/<account>.json, perms 0600
enforced (loose perms flagged in listings, refused by --auth — mirrors
gitea-api.sh credential hygiene).
- ~/.pi is read-only to the stack, permanently; the only interaction
remains the existing read-only container mount of the default
credential. Recorded as a ROADMAP standing decision.
- auth.sh is now config-driven (data root from config.json, fail closed,
consistent with every other tool); status reports both sources labeled.
- agent.sh --auth resolution moved after load_config (needs the data
root); missing/symlinked/non-0600 accounts refuse.
- test-auth.sh: 15 no-Docker cases (accounts-create-nothing, loose-perms
refusal, invalid-config refusal added). Test-authoring correction
recorded in BUILD-LOG (fixture-state mismatch caught before running).
Suites 24/15/90/14/17 + verify green.
Owner decision after live verification of M18: an explicit agents-dir
override that cannot resolve the named seat now refuses the launch
(exit 4, names the seat and dir) instead of launching seatless and
unbounded. Unsetting the override keeps the M13 plain governed TUI.
MOSAIC_ROLES_DIR needs no symmetric change - the M18 gate already
refuses unresolvable role contracts.
Task suite 88 -> 90 (refusal + refusal-names-the-seat). TOOLS.md Agent
section documents the refusal.
Suites 24/90/14/17 + verify green.
Role contracts (roles/<role>.json): roleVersion, name bound to filename,
tools ceiling (subset of pi built-ins), network declared (none|api-only|
open; enforced when network policy lands). Strict schema, fail closed -
a non-role document refuses resolution.
mosaic-task.mjs resolve-role: config-free contract validation, emits
MOSAIC_ROLE_TOOLS / MOSAIC_ROLE_NETWORK.
agent.sh: a declared role binds to its contract. Missing/invalid contract
refuses the launch (exit 2, names the role - the under-equipped-seat
failure mode, mirroring M17 skills). Effective tools = ceiling ∩ requested
(CLI --tools or agent.json caps); no request -> ceiling stands; narrowing
and tool-free outcomes loud on stderr. Adapters unchanged; headless M9
chain (mission ∩ task) untouched.
Ships roles/researcher.json (existing seat declares the role; without the
contract the fail-closed gate would refuse its launch).
Task suite 74 -> 88: contract resolution, wrong-kind/name/network/
duplicate/unsupported/missing refusals, ceiling narrowing E2E (mock
adapter), tool-free E2E, missing-contract refusal. Test-authoring
correction recorded in BUILD-LOG (a check that registered on one path
only, caught by count arithmetic).
Suites 24/88/14/17 + verify green.
ms-communications: integrated as-authored - owner preamble restructure +
collaborator delivery-discipline hunks from the #43 calibration (own
session output is not a send path; the tool performs the preamble flip;
receiving rule 3 requires actually running agent-send.sh).
ms-conductor: collaborator redraft integrated (canon-aligned tracking
surfaces, one-action cadence, fail-closed core) with one conductor
remediation - step 3 now distinguishes refusal (fail closed, never
bypass) from runner outage (direct dispatch to a qualified live seat via
ms-communications permitted, recorded loudly as degraded: no sandbox, no
run record; suites still gate integration). Preserves the owner's
outage-dispatch intent inside invariant 6.
docs/TOOLS.md: release.sh ensure row added (M16 subcommand existed in
code but not in the doc - flagged by the collaborator, verified in
release.sh usage).
Authorship: owner (ms-conductor doctrine, preamble restructure) +
ms-test collaborator (delivery hunks, redraft); remediation + integration
by conductor (dragon-lin:darkwing). Suites 24/74/14/17 + verify green;
unslop clean.
Collaborator-authored via conductor-loop calibration: task dispatched to the
live ms-test seat (glm-5.3-flash) over agent-send.sh; diff reviewed line by
line and every documented flag/exit code independently verified against tool
source by the conductor; suites green at integration (config 24 / task 74 /
release 14 / conductor 17 + verify).
- new 'Tools (host-side)' section: agent-send.sh, agent-watch.sh, unslop-check.js
- intro reading guide now points at tools/ (worker-flagged addition, accepted)
- Maintenance suite counts corrected: test-task.sh 58 -> 74
Authored-by: ms-test collaborator (glm-5.3-flash)
Integrated-by: conductor (dragon-lin:darkwing)
- scripts/skill.sh: install (bundled or path) / activate / deactivate /
uninstall (refuses while enabled) / list
- skills-enabled + skills-available dirs under the data root; a skill not
in skills-enabled is not enabled or available for use
- pi adapter: MOSAIC_SKILLS -> --skill per dir; --no-skills when none
- agent.sh: seat definitions declare skills[]; resolution against
skills-enabled refuses the launch loudly when missing
- ms-* skills completed (owner-authored canon, hands-off): ms-tools
adapted to the runtime, ms-file-read/write/agent/conductor bodies
written in the owner's style; ms-agent-watch + ms-unslop untouched
- tasks/USER.md onboarding fixtures; suite hardening (nested def path,
user seed, mock-adapter dispatch evidence)
Suites: config 24, task 74, release 14, conductor 17, verify PASS.
RELEASE 0.0.12 packaged; health-gated activation on merge.
Closes#40, closes#41, closes#42
- release.sh health gate runs with MOSAIC_ENSURE_SKIP=1: the gated task run
cannot re-enter release self-determination
- run-task.sh warns on release drift instead of silently using a stale image
- ROADMAP: M20 decision recorded (packages/* monorepo at usurpation,
continuity-first); restructure sequenced as M20 phase 1
Closes#39
- scripts/agent.sh <name>: launches interactive pi TUI in the container
with contracts + optional mission + agent identity + named session +
optional workspace/tools; the Mosaic alternative to vanilla pi
- pi adapter: MOSAIC_INTERACTIVE branch (clean TUI, no -p, no initial
prompt); headless exec rebuilt via positional args (no word-splitting
on the request); MOSAIC_AGENT_NAME optional in headless
- loader: AGENT IDENTITY section when the launcher names the agent
- compose: fixed command removed (request defaults live in run-agent.sh);
MOSAIC_INTERACTIVE/MOSAIC_AGENT_NAME passthrough
- docs/TOOLS.md: full on-demand tool reference; AGENTS.md routes to it
- RELEASE -> 0.0.8 (container change); build verified
Closes#35
Owner direction: the repository root holds first-class, bootstrap-required
configuration only. conductor-policy.json is a ROLE contract (the
conductor's authority), one of scores of future role contracts
(agent-policy, coder-policy, ...) - such files get a dedicated home.
- roles/conductor-policy.json (git mv)
- conductor-apply.sh + test-conductor.sh read the new path
- CONDUCTOR.md records the roles/ convention
Closes UX follow-up from owner layout review; no issue (convention change).
- mosaic-task.mjs prune [--keep=N] [--yes]: default keep 50; without
--yes lists candidates without deleting
- only r-* directories under the runs root; symlinks skipped;
sessions/workspaces/state/config untouched (asserted by suite sentinels)
- append-only receipt runs/.pruned.log records every pruned id
- test-task.sh: +8 retention cases (dry-run no-delete, keep-N, newest
kept, receipt, isolation, invalid keep, empty no-op)
Also: suite hardening - prune section scopes its config per-command
(no export/unset leaking into later sections); duplicated check()
removed; latest_reason hoisted to helpers; status colors now green OK /
red FAIL (terminal-only, NO_COLOR-aware) per owner UX feedback.
Closes#32
Owner feedback: grep match-highlighting made the word 'policy' red while
status words were plain - counter-indicative. Suites + verify now emit
ANSI colors (green success, red failure) when stdout is a terminal;
piped/machine-parsed output stays plain, honoring NO_COLOR. Word 'ok'
promoted to 'OK' for scannability.
Verified byte-level via forced-pty run; piped output unchanged; suites
41/24/14 + verify green.
- mission schema: optional capabilities.tools (same validation as task)
- merge semantics in runTask: neither -> none; mission only -> mission;
task only -> task; both -> intersection (task narrows, never widens);
empty intersection -> tool-free run with an explicit stderr note
- result.json records EFFECTIVE tools; task/mission snapshots remain the
immutable declaration of intent
- adapters unchanged; host-side only (no image change, 0.0.6 still active)
- task suite +5 cases (41 total): all four merge cases asserted from run
evidence + invalid mission capabilities rejected
Policy decision recorded: missions govern; tasks cannot escalate.
Closes#30
result.json recorded lineage correctly; the human-facing show command
omitted the field. Found by owner test: show | grep retriedFrom was
empty on a run whose result.json contained it.
Closes#29
- retryRun rewrites a snapshot's relative mission path to the run's own
recorded mission.json (absolute) before execution — retries stay
faithful to what originally ran
- runTask accepts options.retriedFrom; retry records lineage in
result.json (additive optional field, no schema break)
- task suite +4 cases: retry succeeds, lineage recorded, mission section
present after retry (36 total), missing-run retry exits 4
Closes#28
Collaboration record (conductor loop, docs/plans/CONDUCTOR.md):
- round 1 (worker session worker-1, 2m28s): retry implemented per spec
- conductor live test exposed spec gap: direct invocation lacked
launcher env exports
- round 2 (same worker session, 59s): spawnEnv made self-sufficient,
but used PI_* where compose interpolates MOSAIC_*
- conductor hotfix: 3-line rename to MOSAIC_PROVIDER/MOSAIC_MODEL/
MOSAIC_DATA_ROOT
Final: node scripts/mosaic-task.mjs retry <runId> re-executes a run's
task snapshot as a new run; live retry replied REMEMBERED; all suites
green (24/32/14 + verify).
Known limitation: retrying a run whose task used a RELATIVE mission path
resolves it against the temp dir; lineage tracking deferred.
Closes#25, closes#26, closes#27
- task schema: optional workspace (absent | :run ephemeral | named
persistent under dataRoot/workspaces) and capabilities.tools (pi
documented tool allowlist); strict validation, traversal-proof names
- runner: creates host workspace, passes MOSAIC_WORKSPACE (container
path) + MOSAIC_TOOLS; result.json records both
- pi adapter: cds into workspace; --tools when allowlist present else
--no-tools
- mock adapter: logs delivered MOSAIC_* vars to stderr as deterministic
plumbing evidence (dash prints 'export K=v', so use env not export)
Closes#20
- load-contracts.sh: MOSAIC_MISSION_FILE (readable) appends a MISSION
(runtime) section — objective + directives — after the immutable
contracts; unreadable path is a hard error, absent env changes nothing
- mosaic-task.mjs: exports MOSAIC_MISSION_FILE as the run snapshot's
container path (/var/lib/mosaic/runs/<id>/mission.json), with an
outside-dataRoot guard; also exports the configured adapter
Verified: contract-only prompt has no mission section; mission-bearing
run shows objective + directives in the generated prompt, snapshot
recorded, real provider returns exactly MOSAIC_HELLO_OK.
Closes#17
- live hello failure dumps latest run result.json + stderr tail before
sandbox cleanup destroys them
- wrong-expectExact case asserts reason == expect-mismatch (was: any
exit 1, which masked compose-level failures)
- repair dangling if/else from the docker-guard refactor
Closes#15
M3 made MOSAIC_IMAGE_TAG required in compose, but run-task.sh never
called load_release — direct task runs failed in compose before any
model call. release.sh paths masked it by exporting the tag to children.
Found by owner-run test-task.sh; failure receipts were in the run
records' stderr.txt.
Closes#14
14 cases: RELEASE validation (valid/invalid/missing), tag consistency,
status on empty state, fault-injected refusal with no pointer + single
valid refusal log line, healthy activation, pointer fields, repeat
activation append-only log, rollback-without-previous refusal.
Harness fix learned the hard way: restore RELEASE from backup inline
after the missing-file case (mv-back restored the mutated file); single
exit trap self-heals the repo state.
Closes#12
- activate: image-presence pre-check + M2 task-runner health gate
(tasks/hello-marker.json exact marker) before atomic pointer replace
(tmp+rename); every attempt appended to activation-log.jsonl
- --fault-injection flips the health expectation to prove the refusal path
- rollback: health-gated re-activation of the previous activated imageTag
from the log; refuses when the image is gone or no previous exists
- status: release, tag, pointer, recent log; safe on empty state
- state lives under <dataRoot>/state/ (config-independent, reset-scoped)
Verified: activate OK; fault-injected refuse with pointer unchanged;
rollback-without-previous refuse.
Closes#11
- RELEASE file: single source of release version (0.0.X until declared stable)
- common.sh load_release(): validates version, derives
MOSAIC_IMAGE_TAG=mosaic-poc-agent:<pi>-r<release> from the pinned pi dep
- compose.yaml: image tag is required env; build/hello/verify call load_release
- verify.sh derives the image name instead of hardcoding it
- package.json version aligned to the same 0.0.X line
Closes#10
- scripts/mosaic-task.mjs: validate | run | list
- Strict v1 schemas: unknown keys rejected; ids/prompt/expectExact/
timeoutSeconds bounds enforced; optional mission file resolved against
the task file and validated too
- run: executes through the config-driven container path with stdin
detached (issue #5 class), SIGKILL timeout (default 120s), trimmed
response capture
- Immutable run records under <dataRoot>/runs/r-<utcstamp>-<rand>/:
task.json + mission.json snapshots (write-once), stderr.txt, result.json
- expectExact gate: mismatch -> status failed, exit 1; result.json is
always written
- scripts/run-task.sh: load_config + bootstrap_runtime_dir before exec
- M2 scope: mission directives are snapshotted for provenance, not yet
injected into the runtime prompt (later policy layer)
Closes#6, closes#7
pi print mode reads piped stdin until EOF; an attached terminal stdin
blocked the one-shot run forever. Automated contexts (closed stdin)
never exposed it. Request text comes from the compose command.
Proven: tail -f /dev/null | scripts/hello.sh now returns MOSAIC_HELLO_OK
in ~4s (previously timed out at 30s); verify.sh remains green.
Closes#5
- verify.sh now calls bootstrap_runtime_dir after load_config; previously a
reset-then-verify flow let Docker auto-create a root-owned mount source
- common.sh: fail with clear guidance when data root exists but is not writable
- README: configuration section, bootstrap usage, selftest entry point
- BUILD-LOG: Phase 5 entries with corrections
E2E (clean slate): 20/20 selftests; bootstrap idempotent; config-driven
hello/verify MOSAIC_HELLO_OK; negative marker exit 1; reset + rerun green;
config checksum unchanged across the entire flow.
Closes#4
- docs/PRD.md: Part I product north star authored from ratified decisions
D1-D14; Part II preserves all active workstream contracts verbatim
(KBN-101, FCM #758, FCOM #766, TESS, #756, MOS-PORT, #1150, #1174, #1194,
RI #1275, M1). Referenced anchors unchanged.
- docs/archive/PRD-v0.1.md: v0.1.0 beta PRD body archived verbatim with
supersession header.
- docs/ROADMAP.md: all phases P0-P5 present from day one per D11
(P2-P5 as explicit placeholders).
- docs/requirements/native-kanban-sot.md: Amendment A1 (D13) - hierarchy
parentage + RBAC chain above workspaces; sections 1-7 untouched.
Per ops-ci-01 (PR #1324 comments 23413/23422) and fred's one-at-a-time
authorization: one serialized run of #1324's tree on the pinned image
(ci-base:lock-9cb7ffcd8828, carried since the cb9a0d1 absorb). This commit
changes no files — the tree is identical to 9af456c; it exists only to mint a
fresh pipeline head instead of restarting 2545.
Per ops-ci-01's procedure (comments 23386/23394, brain D27): pipeline config
travels with the commit under test, so the controlled rerun requires the pinned
anchor in the PR head itself. This merge absorbs everything next took since the
original merge parent (1bdeed62): the ci-base pin (cb9a0d1, lock-9cb7ffcd8828),
the legacy-credential removal (#1325), and the ci-queue-wait statuses:null fix
(#1129 — the branch from the divergence analysis, now landed).
One conflict, same file as round 1: test:framework-shell union — our 54-entry
chain plus next's new test-ci-queue-wait-no-status.sh entry at its position
(55 entries, every target existence-verified). Enumeration guard green:
population 61, enumerated 46, excluded (signed) 16.
Pipeline 2534 (018d96a), two test-step failures plus the format failure
root-caused by rev-code-01 (id 202):
1. pipefail-early-exit.test.mjs: main's window wrote the test but never
wired scripts/*.test.mjs into its own CI, so its tools/install.sh pipes
were never executed against it; next's test wiring runs them and flags
two load-bearing pipes. Both rewritten pipefail-safe via process
substitution (newest_matching_file ls|head, check_fleet_transport
sed|head|tr|awk). Test now 7/7 locally.
2. verify-release.test.mjs: merge kept main's 9-command ci.yml sanitization
but next's 4-command canonical stage. Canonical updated to mirror ci.yml
exactly (9=9, order verified). upgrade-guard checked equal (4=4).
3. format: docs/reports/quality/1099-pipefail-sweep.md (from main f158be8)
prettier-formatted under pinned 3.8.1.
Enumeration guard re-verified: population 60, enumerated 45, excluded 16.
Review stack#1324 id 201 (rev-code-01): the union resolution chained main's #1073
fleet test while keeping next's signed exclusion of it (#1271 burn-down: the test
asserts missing-binary behavior but CI provides pi on PATH — fails by design).
check-test-enumeration CONTRADICTORY EXCLUSION, CI pipeline 2532 terminal failure.
Fix per verified review recommendation: remove the chain entry (55 -> 54 parts),
keeping next's exclusion and #1271 state. Guard now passes:
population 60, enumerated 45, excluded (signed) 16.
The repo format:check glob covers ts/js alongside md; four non-markdown
scripts inside the folded tree were flagged after the md pass. Same pinned
prettier 3.8.1, same markup-only class (verified: node --check still passes
on the js files).
Phase D3 of plan 2026-08-19 (decision S21): skills-local is the local test
bed; promote individually as each proves out. ms-unslop is the only one of
the seven local skills with working enforcement evidence — a checker
(tools/unslop-hook/unslop-check.js), a machine-source list (lists.json), a
19-test suite, a measured corpus, and a regression fixture.
The checker itself stays fleet-local for now (it binds to a specific
harness extension surface); this promotes the skill document only.
Gates verified on the moved file: sanitization denylist clean, prettier
3.8.1 clean (6730 chars was fred's measure at assignment; 7249 as shipped
today — both pass).
Phase D2 of plan 2026-08-19: single package, single install, single command.
The framework installer already treats skills/** as a shipped, manifest-owned
framework subtree, so the folded skills now install into
$MOSAIC_HOME/skills with the rest of the framework — no second repository,
no separate sync step:
- mosaic-sync-skills (bash + powershell): the fetch machinery is gone (clone,
pull, dirty-state migration, rsync from sources/agent-skills). The script
now only links installed skills into runtime homes. --link-only is a compat
no-op; --no-link exits having nothing to do.
- catalog.ts: the sources/agent-skills fallback is dead and removed.
- install.sh, launch.ts, defaults/README.md, README.md, skills/README.md:
references to the second repo rewritten to describe the shipped path.
Verified: clean install into a fresh MOSAIC_HOME produces 102 skills with no
sources/ directory; the linker then links the selected skills into the four
runtime homes with no git involvement.
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.
The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
Four folded skills carried operator identity tokens that the sanitization
gate (verify-sanitized.sh) forbids in the public framework package:
- kickstart: template path pointed at a private brain checkout; now uses the
framework-shipped $MOSAIC_HOME/templates/docs/TASKS.md.template
- mosaic-deploy: dropped one estate-specific stack-name row from the example
table
- mosaic-portainer, mosaic-woodpecker: credentials now name the framework
credentials store (load_credentials <service>) instead of a private
checkout path
Estate-specific values can live in a skills-local override, which the linker
applies with precedence over canonical skills.
Fold the agent-skills repository into the monorepo as the shipped canonical
skills package (plan 2026-08-19 phase D1, decision S19: single package, single
install, single command).
History is preserved by rewriting each commit's paths from skills/ to
packages/mosaic/framework/skills/ (git fast-export/import) and merging the
rewritten history with --allow-unrelated-histories, so the original commits
with their authors, dates, and messages remain reachable. Blob content is
untouched by the rewrite; tree fidelity was verified blob-sha-for-blob-sha.
This change must be merged with a real merge commit (not squash) or the
history link is destroyed.
rev-code-01's non-blocking nit on #1313 round 2. The no-linking-step paragraph
pointed at the bridge explanation as 'described below'; it is above. Now names the
section, which survives further reordering better than a direction word does.
Text-only. Verified with the repo's PINNED prettier (3.8.1 via pnpm-lock.yaml) and
the sanitization gate, both clean.
All four findings reproduced before fixing. rev-code-01 was right on each.
B2 (blocker, mine). SEAT-IDENTITY provisioning step 4 said to symlink the
framework store entry to the seat slot, while the same file says those bridges
must not be recreated. The same bridge, told both ways, in one document. I
rewrote the resolution and token-location sections when the deploy made them
stale and did not carry the change into the numbered steps. Step 4 is gone and
the file now says explicitly that no provisioning step links the store to the
slot, so the omission cannot read as an oversight.
S1 (mine). The guide claimed the helper "attempts a fleet notification" on
refusal. The shipped helper does no such thing — its only reference to
notification is a comment saying an alert built on the record is best-effort, and
there is no send or wake call anywhere in the file. Now: it writes a durable
record, the record is what exists, and nobody should wait for a notification that
nothing sends. A guide that promises an alert is worse than one that promises
nothing.
S2. Estate-local content removed from files that ship to every estate: the
~/.mosaic/fleet/bin script paths (dead paths elsewhere) and the 2026-08-18 dates,
which dated a specific host's migration rather than describing behavior. The
bridge-removal passage now states the ORDERING that matters — remove bridges only
after a seat-aware helper can reach the slot, never before — which is the part
that transfers.
B1. prettier reformatted all three files. Reproduced the pipeline 2515 failure
locally before and confirmed clean after; the other three guides prettier flags
are untouched by this branch (0 changes vs origin/next) and are pre-existing.
Sanitization gate re-run and passing.
Verified for the record, since I could not verify my own work: rev-code-01
confirmed the no-fallback claim TRUE against helper content on origin/next, and
judged the evidence rules actionable on the grounds that each names an executable
replacement.
Pipeline 2514 failed the sanitization gate on 'Jason mints the token into the
seat slot'. The denylist is jarvis|jason|woltje|... and a shipped framework file
must not carry operator identity. My mistake: I generalized the estate paths and
seat names when promoting this guide and did not check the operator name.
Now reads 'the estate operator', with the accompanying rule that an agent does
not ask another agent to mint one either.
Verified by running tools/quality/scripts/verify-sanitized.sh locally rather than
guessing at the pattern: gate passes.
The #1280 prohibition carried an explanation of how the tools misattribute and
why the failure is invisible from inside them. A reader who is not going to use
the tool cannot act on any of it. Same for rule 2's closing clause about what
reviews commonly miss. Both cut to the decree and the corrective action.
Rules 1 and 3-12 keep their trailing sentences: those are corrective actions or
the detail that makes the case recognizable, not justification.
Three guides that existed only as one host's working copy, promoted to framework
templates so every estate gets them. A working copy under ~/.mosaic binds one
host; only a template here binds all of them.
SEAT-IDENTITY.md (new) documents how a seat's git credential is actually
resolved after #1311: identity from MOSAIC_GIT_IDENTITY, then
mosaic.gitIdentity, then the stdin username; host mapped to a store prefix; then
ONE of two stores chosen by whether the seat directory exists, with no
precedence and no fallback between them. A seat with a directory and an empty
slot fails closed rather than reaching the service store, and that is the point.
It also corrects how to find the helper. credential.helper commonly names an
absolute path, so `command -v git-credential-mosaic` answers a different question
than the one git asks, and the two stop agreeing the moment the PATH copy is
removed. Git also tries EVERY configured helper in order, so a fail-closed helper
in front silently hands the request to whatever is configured behind it. The
guide says to read the whole list.
FLEET-COMMS.md (new) documents agent-send.sh: the class table, the addressing
preamble, and the exit codes — including that rc=2 means the text reached the
pane as an unsubmitted draft, so retrying double-sends it. Confirm with
capture-pane instead. It also says to measure the fleet rather than trust
roster.yaml, which on a live host was simultaneously naming a socket that did not
exist, listing seats that were not running, and omitting seats that were.
CODE-REVIEW.md gains an Evidence Discipline section: a green is not a result
until you have shown it could go red, measurement and explanation are separate
sentences, verify by content on the ref that ships rather than by ancestry of a
local sha, and confidence is part of a finding. Plus four shell-measurement rules
earned on #1311, each of which produced a wrong conclusion first — `cmd | tail;
echo rc=$?` reports tail's status, a missed glob under pipefail exits 2 and kills
the run under set -e, nonzero-with-no-output is an environment question before it
is a code question, and `git -C` in a non-repo directory answers from the
enclosing repo.
The estate-specific repository exception that lived in the working copy is not
carried here. The template says an estate may document one, scoped to a named
repository and never precedent for a second.
Both new guides are added to the two routing tables that agents read.
Both defects found in review by rev-code-01 on #1311.
F3 — the JSONL record interpolated every field with a bare %s. An identity comes
from git config or the environment and a cwd is whatever directory git ran in, so
either can contain a quote or a backslash. One such refusal turned the day's spool
into unparseable JSONL, and the operator would only discover it while reading the
record that explains an outage. Fields are now JSON-escaped.
F2 — the diagnostic printed "record: <spool>/<date>.jsonl" unconditionally, but
the record is only written inside the branch where mkdir -p succeeded. When the
spool cannot be created the helper named a file that does not exist, on exactly
the hosts where the escalation was lost. It now reports the real path or says
NOT WRITTEN.
Also: prettier on README.md, which was the format-step failure on pipeline 2508.
It reflowed only the two tables this branch added.
Tests: cases 14 and 15 cover both. Verified discriminating — against the previous
helper with these same tests, case 14 fails with the unparseable record printed
and case 15 fails on both assertions; against this one both pass.
The first draft of case 14 used `ls "$spool"/*.jsonl | head -1`, which under
`set -o pipefail` exits 2 on a missed glob and killed the suite with zero output
— the same silent-nonzero failure rev-code-01 hit from a partial tools/ extraction
and the reason this file exists. Replaced with a glob loop and a comment.
Two changes to one rule: a credential is resolved from exactly one place,
and an identity that cannot be resolved is refused rather than substituted.
FAIL CLOSED. Both readers ended in an unconditional fall-through to the
shared Gitea account whenever an identity did not resolve. Every seat in a
fleet therefore pushed, opened PRs and filed reviews under one account, and
a record made that way cannot be traced to the agent that made it
afterwards. The fallback now applies only where there is no attribution to
lose: a host with no fleet. Where seats exist, an unresolvable request emits
nothing, exits nonzero, explains itself on stderr, and — in the git helper —
appends a record naming the identity, host, reason and cwd, and no token
value, to ${MOSAIC_CREDENTIAL_SPOOL:-~/.local/state/mosaic-credential-escalations}.
A host runs a fleet when <brain>/fleet/agents exists, which is the signal
packages/mosaic/src/fleet/brain-home.ts already uses to decide a brain is
active, resolved the same way (MOSAIC_BRAIN_HOME, else ~/.mosaic). This is
what keeps the change a no-op for an operator who has not provisioned
per-slot tokens: no fleet directory, shared account, unchanged. It is also
why there is no environment variable to restore the old behavior — one would
reintroduce the substitution being removed.
STORE SELECTION. Both readers hardcoded ~/.config/mosaic/secrets/gitea-tokens,
so a seat's own secrets/ slot was invisible to the framework: a seat could
hold a valid credential and still be served the shared account. The store is
now chosen by what the identity is. An identity with a directory under
<brain>/fleet/agents/ is a seat and is read only from
<brain>/fleet/agents/<id>/secrets/; any other identity is a service identity
and is read from the framework store. There is no precedence between them
and no fallback from one to the other, so a seat with an empty slot is
refused even when a same-named token sits in the framework store. Two copies
of one credential are drift rather than redundancy, and drift surfaces as
the stale copy returning 401, which reads as a revoked token and sends
whoever debugs it somewhere else.
detect-platform.sh is in scope alongside git-credential-mosaic because they
are the two readers of these tokens. Patching only the git helper would make
"one credential, one location" true for push and fetch and false for
pr-create.sh, issue-create.sh and pr-review.sh, which is the harder failure
to notice.
TESTS. The three assertions that pinned the shared-account fall-through are
now fail-closed assertions, and a refusal is checked four independent ways:
nonzero exit, empty stdout, a stderr diagnostic naming identity and host,
and no shared token value anywhere in the output. The exit code alone would
pass against a helper that emitted the credential and then failed. Added:
seat-slot resolution, the no-cross-store-fallback case with a control
proving the framework-store file it declines to read is readable, no-identity
on a fleet host, the fleet gate firing on the default ~/.mosaic and not only
on an injected MOSAIC_BRAIN_HOME, and a cross-host leak check. Both suites
were run against the pre-change code as a control and fail there on exactly
the shared-token emission.
shellcheck is not installed on the authoring host, so the rewritten helper
is unlinted locally and CI is the first lint of it.
Squash-merged by topher (jarvis principal) via break-glass API path (wrapper main-only gap, documented). Gates: CI 2492 green at head e19013ed, review 181 APPROVED (fred) at pinned head. Diagnostic merge: the push pipeline now exercises kaniko with the REGISTRY_* secrets - valid creds yield the first fully green gated publish; invalid yield an explicit 401.
Co-authored-by: Jarvis <[email protected]>
Squash-merged by topher (jarvis principal) via break-glass API path (wrapper main-only gap, documented in #1275 log). Gates: CI 2487 green at head 81f500bd, review 180 APPROVED (fred) at pinned head. Unblocks gated publish: verify's no-DB path now skips cleanly.
Co-authored-by: Jarvis <[email protected]>
Squash-merged by topher (jarvis principal) via break-glass: pr-merge.sh hard-codes main-only merge targets and cannot express this repo's next trunk. Gates: CI 2476 green at head 46784c8d, review 177 APPROVED (fred) at pinned head. First gated publish: every publish step now depends on verify-release at the exact commit.
Squash-merged by topher (jarvis principal) via break-glass: pr-merge.sh hard-codes main-only merge targets and cannot express this repo's next trunk. Gates: CI 2475 green at head 758659dd, review 176 APPROVED (fred) at pinned head.
The first PR for this change was filed under the retired mos-dt-0 principal
(pr-create.sh has no --login flag and find_tea_login_for_host returns the first
host match) and was closed and refiled as #1270. That left in-tree references
pointing at a closed duplicate PR rather than at the burn-down issue, which is
the wrong target for them anyway: the open design question belongs on #1271.
The `test` step has failed on every `next` pipeline since #1017 on exactly one
assertion, and it is the same one on unrelated PRs:
FAIL: host provides 'pi' in the system path; missing-binary cases are not
measurable here (framework/tools/fleet/test-start-agent-session.sh:103)
Measured 2026-08-16 across pipelines 2444 (#1256), 2438 (#1240) and 2441
(#1017-quality): exactly one FAIL line in each full log, identical, this line.
Control `zzz-not-present-zzz` -> 0 on all three.
Cause. #1241 (5c35a250) added the guard: the suite shims fake mosaic/pi/npm into
$FAKE_BIN, but the constructed PANE_PATH always ends in the real system path, so
on a host that installs those binaries the missing-binary cases cannot be
measured and a green run would mean nothing. The guard says so instead of
passing. Its own pipeline 2430 was green only because the suite was CI-excluded
at the time, so the guard had never run in CI. #1017 (c56483eb) then enumerated
it and dropped the exclusion. The CI image installs
@earendil-works/[email protected].1 on purpose, so the precondition is
unsatisfiable there. Both commits are mine.
The guard is correct and is not being softened. A check that cannot measure its
property and reports success is the failure mode this repo has been cataloguing
all week; the error was wiring the suite into an image that violates its
precondition, so the wiring is what gets reverted.
Second effect, which is the reason this cost a day rather than an hour:
test:framework-shell is one && chain and this sat at position 44 of 48, so
glpi/test-list-http-status.sh, orchestrator/test-board-roll.sh,
woodpecker/test-ci-wait-exit-matrix.sh and _scripts/test-fleet-transport-check.sh
have not run at all since the merge. The pipeline reported one failure, never
"one failure plus four unrun". All four are green when run directly on
sb-it-1-dt, so the mask hid nothing broken -- but that is a local result on one
host, not a CI-image result.
Verification, with controls:
- enumeration guard OK (population 52, enumerated 36, signed-excluded 16).
- control A, exclusion line removed while unwired -> FAIL UNENUMERATED.
- control B, exclusion line kept while rewired -> FAIL CONTRADICTORY EXCLUSION.
The gate discriminates in both directions, so its OK is load-bearing.
- the four formerly-masked suites: rc=0 each, run directly.
- the full chain cannot be run to completion on sb-it-1-dt: it stops earlier, at
the lease-broker Invariant R test, because this host carries the quarantined
operator-global pi 0.84.2 against a measured 0.84.1. That is host-specific and
out of scope here -- CI pins 0.84.1, and the single FAIL line in those three
pipelines proves positions 1-43 passed there.
Burn-down is to control the tail of PANE_PATH inside the test, not to remove pi
from the image. Recorded in the exclusion reason and in #1269.
Reviewed by scooby via git comms (terminal ACK 32d986). Merge directed by Jason 2026-08-16. Conflicts from #1229 and #1252 resolved on the branch by a pi seat; pre-merge gate verified: tools/install.sh=5d28f773, framework/install.sh=1578c33b, test:framework-shell=48 links with both #1252 and #1245 suites present.
Resolves conflicts from #1229 (tools/install.sh node provisioning) and #1252
(package.json test:framework-shell). tools/install.sh resolved to the reviewed
composite blob 5d28f773; package.json resolved as a union so both #1252's four
suites and #1245's transport-check suite run (48 links).
Reviewed by scooby via git comms (no mosaicstack principal on fomo-lin; review is the comms record, terminal ACK 32d986). Merge directed by Jason 2026-08-16. Part of the five-PR greenfield composite verified E2E on two independent bare boxes.
Reviewed by scooby via git comms (no mosaicstack principal on fomo-lin; review is the comms record, terminal ACK 32d986). Merge directed by Jason 2026-08-16. Part of the five-PR greenfield composite verified E2E on two independent bare boxes.
Reviewed by scooby via git comms (no mosaicstack principal on fomo-lin; review is the comms record, terminal ACK 32d986). Merge directed by Jason 2026-08-16. Part of the five-PR greenfield composite verified E2E on two independent bare boxes.
Reviewed by scooby via git comms (no mosaicstack principal on fomo-lin; review is the comms record, terminal ACK 32d986). Merge directed by Jason 2026-08-16. Part of the five-PR greenfield composite verified E2E on two independent bare boxes.
check-test-enumeration.sh signed four suites as 'likely CI-fit; #1017 burndown'.
Measured all four: each passes standing alone, and each still passes with tmux
removed from PATH entirely (test-start-agent-session.sh writes its own tmux shim
into a fake bin dir, so it never needed the real binary).
Red-first: removing the four exclusion lines makes the guard report exactly four
UNENUMERATED failures. Appending the four to test:framework-shell returns it to
OK, with in-population enumerated going 32 -> 36 and signed exclusions 19 -> 15.
Refs #1017
Review finding from scooby. This case does not use run_start, so
install_pane_binaries' symlinks land under a home its launcher never consults
(HOME is the trusted parent here). It resolves mosaic and pi through
MOSAIC_RUNTIME_BIN=$FAKE_BIN instead. Valid path, valid green — and a trap for
anyone who later drops that env var believing the symlinks cover it, which
would break the #1241 binary check rather than exercise it.
Comment only; no behavior change. Harness rc=0.
Refs #1241.
Review finding from scooby: this PR added a failure branch the harness
structurally could not reach. The fake tmux answered `has-session` only for
`=_holder:0.0`, so every non-holder agent landed in the session-is-gone branch
no matter what — the `elif` (tmux still reports the session, no pane PID after
the retries) had zero coverage and no way to get any.
That is the same shape as the bug this PR exists to fix, one layer down: a code
path shipped green where the gate that should measure it cannot. Less severe,
because the branch fails closed at exit 69 rather than reporting success — but
"the harness can't reach it" is the sentence that precedes the next silent
regression, so it gets closed here rather than filed.
`MOSAIC_TEST_HELD_SESSIONS` lets a case name targets the shim should also
answer for. It answers them only AFTER `new-session`, and that detail is the
whole trick: the launcher asks `has-session` about the same name twice — once
at line 255 where a yes means "already running, exit 0", and once at 417 where
a yes means "the session survived". A shim answering yes to both short-circuits
at the first and never reaches the branch under test. It would have looked like
coverage while measuring the idempotency path.
Both failure modes were measured, not reasoned about:
- toggle absent (the old shim): `code=pane-did-not-survive` — the case lands on
the wrong branch, which is exactly the unreachability being reported.
- toggle answering unconditionally: launcher exits 0 via the idempotency
short-circuit — "launcher reported success over a session with no resolvable
pane PID".
- toggle gated on new-session: `code=pane-pid-unresolved`, exit 69.
The case also asserts the diagnostic is not `pane-did-not-survive` and does not
mention the heartbeat, so the two pane faults cannot collapse into one message.
Gates: bash -n · launcher harness rc=0 · test-fleet-units.sh (real tmux) rc=0 ·
fleet specs 342 passed.
Refs #1241.
`mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every
roster the CLI scaffolds sets `transport: tmux`, but neither `tools/install.sh`
nor `tools/_scripts/mosaic-doctor` contained the string "tmux" at all. A
greenfield host therefore came out of the installer able to install a fleet,
start a fleet, and run no seat, with `mosaic fleet ps` as the operator's first
and only signal.
Measured on mosaic-sbx-dev (Debian, no tmux, framework installed): `mosaic-doctor`
reported 11 warnings and not one of them named the reason no seat could launch.
The installer gets a warning, not a `require_cmd` hard failure: tmux is required
by the fleet, not by mosaic. Hosts that install this to run `mosaic claude` and
never scaffold a roster are common, and failing their install over a binary they
do not need would be wrong. The check runs in `--check` mode too — "what is the
state of this host" is the question `--check` is asked.
Both checks read the roster's own `transport:` rather than assuming tmux, so a
host declaring something else is pointed at the binary it actually needs instead
of at the wrong package.
The two implementations are deliberately parallel and each carries a comment
pointing at the other. They are separate because the installer must answer this
before the framework's own scripts are guaranteed to be on disk. One harness
drives BOTH from the shipped text — the functions are extracted from the scripts
by awk rather than copied — so the pair cannot drift silently, and the test
cannot keep passing after the shipped copy changes.
The harness is wired into `test:framework-shell`. Without that it would have
tripped the #1017 enumeration guard as UNENUMERATED, which is the guard doing
its job: a check nothing runs is not a check.
Evidence:
- red: the harness fails against origin/next ("could not extract
fleet_declared_transport"); `grep -ci tmux` on both files at origin/next = 0.
- green on real hosts, all four branches:
- dev (no tmux, no roster) -> WARN naming tmux, points at `mosaic fleet init`
- dev (no tmux, v2 roster) -> WARN naming the roster, points at `mosaic fleet start`
- dev installer --check -> WARN saying start "reports success and no seat comes up"
- canary (tmux present) -> `[OK] Fleet transport available: tmux` under --verbose,
silent by default (pass() is verbose-gated), installer silent
- harness green on node:24-alpine/busybox, the CI base image.
- `bash -n` x3, `pnpm typecheck` 45/45, fleet specs 342 passed,
enumeration guard OK, its self-test OK, prettier clean.
Refs #1240. Upstream of #1237/#1243 and #1241/#1244: a correct fix for either of
those still leaves this host with no live seat.
`mosaic fleet start` returned 0 over three dead panes. The launcher knew,
and said the wrong thing at the wrong severity to the wrong layer.
The pane runs `mosaic yolo <runtime>` under PANE_PATH with a cleared
environment. When that binary is absent the pane dies in under a second,
tmux destroys the session, and the diagnostic goes with it. The launcher
then found no PANE_PID, printed a WARNING about the *heartbeat sidecar*,
and exited 0 — so systemd logged "Finished ... successfully" and
`fleet start` reported success. `fleet ps` was the only component telling
the truth.
Two changes, both in start-agent-session.sh:
1. Before any effect, resolve `mosaic` and the roster's runtime against
PANE_PATH — the pane's own view of the path, not the launcher's.
`mosaic yolo <runtime>` calls checkRuntime(runtime) and looks for a
binary named exactly like the runtime, so this asks the same question
the pane will ask a moment later, while an operator can still see the
answer. Absent binary -> exit 69, code=missing-binary, no session
created.
2. Replace the dead-pane WARNING+exit-0. An absent session one second
after new-session is a runtime that died on startup, not a heartbeat
problem -> exit 69, code=pane-did-not-survive, with the command to run
by hand to see why. A present session with no pane PID after five
attempts -> code=pane-pid-unresolved. Neither branch kills the
session; destroying a possibly-live pane on a guess is worse than
leaving it for inspection.
Exit 69 (EX_UNAVAILABLE) is deliberate: the 64s already in this file mean
the projection was bad, and here the data is fine and the host is not
ready. Callers separate the cases by `code=`, the same way fail_env's
codes share 64.
This propagates for free. `fleet start` calls runChecked() for the holder
and each agent, and runChecked throws on non-zero, so layers 4 and 5 stop
lying without a TypeScript change. Two adjacent defects are left for a
follow-up issue rather than widened into this diff: the per-agent loop
aborts on the first failure instead of attempting all and reporting an
aggregate, and runChecked's bare throw surfaces the launcher's message
under a Node unhandled-rejection stack trace because program.parse() is
synchronous.
Tests:
- test-start-agent-session.sh gains three cases: `mosaic` absent from the
pane path, the runtime absent from the pane path, and a pane that does
not survive. Each was verified individually red against the unmodified
origin/next launcher.
- The two cases asserting a valid launch now supply a pane PID. Until now
the suite's one success path was itself a dead pane the launcher
reported as fine.
- The harness fakes `npm` so PANE_PATH stops depending on whatever the
host has installed, and fails loudly if the host provides `mosaic` or
`pi` in the system path, where the missing-binary cases would not be
measurable at all.
- test-fleet-units.sh gains a `pi` shim in its runtime bin. The real-tmux
harness named `pi` in its roster and never installed it; the new
preflight caught it.
Refs #1241
Self-inflicted and worth recording rather than quietly amending.
To prove the new tests were red without the fix I ran
`git checkout origin/next -- <fleet.ts> <[email protected]>`. That writes
the *index*, not just the working tree. Copying my versions back afterwards
restored the working tree only, so the unit file sat staged-as-origin/next and
modified-in-tree, and the next commit (67f5014c) committed the index — silently
removing the ConditionPathExists line that 463745e3 had added.
Nothing caught it. The spec reads the file from the working tree, so it stayed
10/10 green against a HEAD that no longer had the guard. Found by reading
`git status` after the push, not by any gate.
Verified by content, not by assumption:
origin/next 0 occurrences
463745e3 1
67f5014c 0 <- the regression
this commit 1
Refs #1237
Two follow-ups from the canary red->green run and scooby's review.
1. The v2 refusal in `add`/`remove` was a bare `throw`, which reaches the CLI
top level uncaught and prints the guidance under a Node stack trace. The
message *is* the point of the refusal, so it now goes through
`command.error()` — the same clean path the roster-config error uses.
Caught on canary, not in review: the unit tests asserted the message text
and passed either way.
2. The unit-template test asserted only that ConditionPathExists is present.
Presence is not effect. Added two tests for the parts that can drift in
code while that assertion still passes: the condition resolving to exactly
the file the fleet writes (%h/%i rendered against a real install), and the
launcher genuinely failing on an absent generated env (exit 64,
`missing-file`) — which is what makes the condition load-bearing rather
than decorative.
systemd is not available in the suite, so the effect itself was measured on
canary (2026-08-16), roster v2 generation 3:
with the condition: start rc=0, Result=success, ConditionResult=no,
journal "skipped, unmet condition check"
condition removed by
drop-in, nothing else: start rc=1, Result=exit-code, ExecMainStatus=64,
unit failed, "agent environment rejected: missing-file"
Canary red->green for the three commands, same v2 roster, side by side:
fleet ps 0.0.50-next.2413 rc=1 -> branch rc=0 (3 agents listed)
fleet install 0.0.50-next.2413 rc=1 -> branch rc=0
fleet remove <name> 0.0.50-next.2413 rc=1 -> branch rc=1, refusal naming
delete + apply
All three previously failed with "Fleet roster has unknown field(s):
generation." The #791 negative was measured too: the six existing
*.env.generated files were untouched by `install` (mtimes 20+ minutes older
than the run).
Gates: typecheck 0, eslint 0, prettier clean, fleet specs 382 passed, new spec
10/10 with the fix and 9/10 red against origin/next (the 10th passes there for
an unrelated reason and is annotated as such). Full suite: only
mutator-gate.acceptance.spec.ts fails, pre-existing on origin/next.
Still true and still worth saying: a correct fix here shows install rc=0 and
start rc=0 and STILL no live seat. #1240 (tmux absent) is upstream, #1241
(start reports lifecycle-complete over dead panes) and the missing agent
runtime are downstream.
Refs #1237
Reviewed-by: scooby (by git comms; cannot file a Gitea review from fomo-lin)
On a roster-v2 fleet, `ps`, `install`, `install-systemd`, `add` and `remove`
all failed in the v1 parser. The consequence was that a greenfield v2 box could
never get its unit templates placed, so nothing downstream could start.
The read-only commands get a narrow version-agnostic view of the roster
(version, socket name, holder session, and per agent name/alias/runtime).
This is deliberately not a v2 -> v1 downshift. A downshifted FleetRoster would
be accepted by generateAgentEnvValues, which would make a third writer of
fleet/agents/<name>.env.generated through the v1 mapping and break the #791
single-SSOT invariant that projectRosterV2AgentGeneratedEnv is documented to
hold. The view is too small to write a roster or an env file back from, so that
misuse is unavailable rather than merely discouraged.
So on a v2 roster `install` places the tool files and the unit templates,
enables the units, and writes no generated env at all. Env belongs to `apply`
and `regen`, both already v2-native.
That change alone would have traded an init-time failure for a boot-time one.
`install` enables mosaic-agent@<name>.service (WantedBy=default.target) without
starting it, so a reboot between `install` and the first `apply` would run
ExecStart against an absent env file and fail every seat unit, further from its
cause. The unit template now carries
ConditionPathExists=%h/.config/mosaic/fleet/agents/%i.env.generated
which skips an enabled-but-unconfigured unit cleanly and starts it on the next
start once the reconciler has written env. On v1 it is a no-op, since v1
`install` writes env itself. Found in review by scooby.
`add` and `remove` are not routed to `create` and `delete`. They are different
operations: the v1 pair edits the roster and drives systemd, the v2 pair is
documented as changing desired state without runtime actions. `add` also
collects four fields where a v2 agent requires eleven, so routing it would mean
inventing an operator's provider, alias, reasoning and tool policy. On v2 both
now fail with the real two-step sequence instead.
Tests: 8 new, 7 of which are red before this change. Includes the greenfield
case scooby asked for — `ps` on a fresh v2 install with nothing running is rc=0
and lists every agent stopped, since that is the command an operator runs to
find out why there is no seat.
Note for anyone verifying this: a correct fix here shows `install` rc=0 and
`start` rc=0 and still no live seat. #1240 (tmux absent) is upstream, #1241
(start reports lifecycle-complete over dead panes) and the missing agent
runtime are downstream. A dead pane after this change is not a regression here.
Refs #1237, #791, #1240, #1241
scooby's review flag 1 on #1242. The other three chmods warn; this one was
`|| true`. It is the one directory holding secrets, so a chmod that fails
silently there is the failure most worth a line in the output.
Comment-and-warn only. No behaviour change on the success path.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
A greenfield install cannot run `mosaic fleet init --write`. It fails with
`unsafe-permissions` on an unnamed `(directory)` and an unhandled Node throw,
and every mutating `mosaic fleet` command fails the same way. Measured on a
reverted-to-greenfield sandbox VM at CLI 0.0.50-next.2413: `~/.config/mosaic`,
`fleet/` and `credentials/` all land at 0775, and 1735 directories under the
framework root carry `mode & 022`.
Two independent causes, and fixing either one alone leaves it broken.
1. The installer inherited the caller's umask. Debian/Ubuntu ship 002, so every
`mkdir -p` produced 0775. Fedora/RHEL ship 022 and produced 0755. The
product therefore worked or did not depending on the operator's login shell,
with nothing in the install output distinguishing the two. 022 is already
what this script assumes it produces — `make_durable_snapshot` restores the
ambient umask specifically so "every later sync copy and new framework dir"
gets 0644/0755 — so pin it rather than inherit it.
2. Even at a correct 0755, three directories are rejected. The fleet code
guards its managed paths with two masks in two languages:
`assertPrivateManagedDirectory` (fleet-reconciler.js, `mode & 0o077`) covers
MOSAIC_HOME and `fleet/` and runs before the roster lock is taken;
`assert_private_directory` (tools/fleet/start-agent-session.sh, `mode & 077`)
covers `fleet/agents` and runs before a pane is spawned. Their laxer
siblings use `mode & 0o022` and accept 0755. The strict mask wins, so the
installer states 0700 outright instead of hoping a umask implies it.
The `find -perm /022 -exec chmod go-w` sweep repairs a tree installed before
this change, which the umask alone cannot reach. It strips group/other WRITE
only — never read or execute — and is scoped to directories, so it corrects the
boundary violation without changing who may traverse or read anything. It is
not sufficient for `fleet/agents`: stripping write from 0755 yields 0750 and
`mode & 077` is still non-zero, which is why that path gets its own chmod.
Reported as #1236. The `fleet/agents` half was found by scooby reading
start-agent-session.sh; the umask framing is theirs too — my first report
blamed the distro rather than the umask.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
Comment-only, no behaviour change. Both raised by scooby in the #1229 review
as non-blocking findings worth writing down rather than fixing here.
F-A: the SHASUMS256.txt check gives integrity, not authenticity. TLS to
$NODE_DIST_BASE is the whole trust root, and MOSAIC_NODE_DIST_BASE widens it
to any mirror with no signature backstop. GPG-verifying SHASUMS256.txt.sig is
filed as its own follow-up so it gets its own review.
F-C: the uname map pulls the glibc build, so musl hosts fail — visibly, via
node_is_suitable, not silently.
This reverts 47e90767. I was wrong: the fix is correct about the cause and
makes the outcome worse.
The acceptance run passed everything I set out to check — greenfield canary
1125, --next --yes, no TTY, rc=0, node v22.23.2 + CLI 0.0.50-next.2413 from a
fresh login shell, and both enforcement hooks wired in ~/.claude/settings.json
where before they were stripped. Then `mosaic doctor` on that same host:
[ERROR] Lease-enforcement hooks (mutator-gate.py, receipt-observer-client.py)
are wired in ~/.claude/settings.json, but broker not healthy
(checkBrokerSupervisorHealth() reports unhealthy). Every gated tool call will
fail closed and BRICK this agent (see #869).
So the change takes a greenfield host from 'enforcement quietly off, agent
works' to 'enforcement wired, broker absent, agent bricks on the first gated
tool call'. The pre-existing behaviour reaches the safe state for the wrong
reason; this reaches the unsafe state for the right one. Safe-for-the-wrong-
reason still wins.
The real defect is underneath both, and it is not an ordering bug:
mosaic __link-claude-settings ... -> rc=0 (leaseEnforcementActivatable:
activatable, wire the hooks)
mosaic doctor -> ERROR (checkBrokerSupervisorHealth:
unhealthy, hooks will brick)
Two capability checks, same host, opposite verdicts. And after a complete
install there is no broker supervisor to be healthy: no systemd --user unit
matching lease/broker, nothing under ~/.mosaic but the bootstrapped node, and
no lease or broker script in ~/.config/mosaic/tools/_scripts/. Lease
enforcement cannot be activated on a greenfield host at all, so
leaseEnforcementActivatable() returning true is the thing that is wrong.
Filing that separately. PR #1229 goes back to exactly the four commits scooby
reviewed.
Co-Authored-By: Claude Opus 5 <[email protected]>
The framework's install.sh ends by running mosaic-link-runtime-assets, which
asks the `mosaic` CLI whether lease enforcement can be activated before
deciding whether to wire the #828 hooks into settings.json. Part 1 (framework)
runs before Part 2 (npm CLI), so on a first install there is no CLI to ask. The
script takes its fail-safe branch, prints a four-line ERROR, and writes
settings.json with mutator-gate.py and receipt-observer-client.py stripped out.
Measured on canary 1125, rolled back to greenfield, `--next --yes`, no TTY:
framework template ~/.config/mosaic/runtime/claude/settings.json
mutator-gate.py 1 occurrence
receipt-observer-client.py 1 occurrence
installed ~/.claude/settings.json after a clean rc=0 install
mutator-gate.py wired: False
receipt-observer-client.py wired: False
So enforcement ends up off because of the order the two halves install in, not
because of anything about the host. Falsified by running the same script by
hand once the CLI existed: rc=0, both hooks wired: True. The guard's real
verdict on that host was 'activatable' the whole time.
This adds one more pass after Part 2. The script is idempotent (unchanged files
are skipped), so on an upgrade — CLI already present, first pass already
correct — it is a no-op. It deliberately does not pass
--allow-inactive-enforcement: Part 1 does not either, and a repair pass must
not be more permissive than the pass it corrects.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two defects found by the second unattended greenfield run on canary (VMID 1125,
rolled back to its greenfield snapshot first).
1. ensure_node() exported the Mosaic-managed Node for the installer process and
nothing wrote it down. The install finished rc=0, put $PREFIX/bin in
~/.profile, and the next login shell found `mosaic` and then died on
env: 'node': No such file or directory
The CLI is a Node script, so a CLI on PATH without its runtime is a
successful install that produces a broken command. persist_node_on_path()
now writes the runtime's bin dir to the same profile, from both the
fresh-install and the already-installed-but-not-on-PATH branches.
2. The 'is it already in a shell rc file' guard was a single
`grep -qslF "$dir" "${rc_files[@]}"` over four paths, most of which do
not exist on a clean host. Handing grep a missing file makes the exit status
implementation-defined: GNU grep 3.11 returns 0 when -q already matched an
earlier file, ugrep 7.5 returns 2 for the missing one regardless. On the 2
path the caller reads 'not present yet' and appends another PATH line, so
every re-install grew the profile. Measured: 3 runs produced 3 duplicate
entries; with the fix, 1.
path_entry_exists() now tests each file for existence and greps it on its
own, so the result does not depend on the grep implementation.
The profile-writing body is factored into persist_on_path(), shared by the CLI
prefix and the Node runtime, since both now need identical treatment.
Verified in a scratch $HOME: fresh write, idempotent across three runs, zsh
routes to .zshenv, an unwritable profile warns and survives set -e, and an
already-on-PATH prefix is a no-op that creates no file. Falsified by restoring
the multi-file grep: duplicates return.
getShellProfilePath() preferred ~/.bashrc when it existed, and ~/.zshrc for
zsh. setupPath() in stages/finalize.ts appends the PATH export to whatever
it returns. Debian's default ~/.bashrc opens with
case $- in *i*) ;; *) return;; esac
so a line appended to the bottom of it never runs for 'bash -lc', for
systemd units, for 'ssh host cmd', or for any agent seat — precisely the
consumers that need the CLI. An install could print its summary and exit 0
while leaving 'mosaic: command not found'. .zshrc has the same problem:
zsh only reads it for interactive shells.
Now ~/.profile, which login shells read and which Debian's copy sources
.bashrc from for interactive shells, so one line covers both. For zsh the
always-sourced file is .zshenv. fish and PowerShell are unchanged.
__tests__/platform/detect.test.ts pins it, including a case asserting that
no shell resolves to an interactive-only rc file. Falsified by inverting
the fix: 5 failed / 1 passed; restored 6/6. Full package suite unchanged at
17 files / 4 tests failing, matching clean origin/next.
tools/install.sh required node and npm and installed neither. Measured on a
snapshot-reverted Debian 13 image with no node, npm or git: the run stopped
at `require_cmd node` with "Required command not found: node", exit 1,
nothing installed, and no indication of how to proceed.
Adds ensure_node() to preflight. It fetches an official Node.js release into
$HOME/.mosaic/node, verifies it against that release's SHASUMS256.txt, and
refuses rather than degrades when the entry is missing or the checksum does
not match. sha256sum on Linux, shasum on macOS. .tar.gz over the smaller
.tar.xz because gzip is universally present and xz is not — a minimal image
is the case this exists to handle.
No-op when a suitable node is already on PATH, so it never fights an
operator's nvm/fnm/distro node. MOSAIC_SKIP_NODE_BOOTSTRAP=1 declines the
download and fails with instructions instead.
Inlined rather than factored into a sibling file because this script is
fetched standalone by curl and has nothing to source.
The three duplicated PATH blocks in tools/install.sh only warned, so an
unattended install finished with rc=0 and left `mosaic: command not found`
— there was no operator to read the advice and act on it. Measured on a
greenfield Debian 13 sandbox: `--next --yes` installed
@mosaicstack/[email protected] successfully and the CLI was still
unreachable.
Replaces all three copies with one ensure_prefix_on_path helper that
appends the export to ~/.profile (~/.zshenv under zsh) and is a no-op when
the prefix is already on PATH or already in a shell profile.
Not ~/.bashrc: Debian's default .bashrc returns early for non-interactive
shells, so a line appended there is unreachable to `bash -lc`, systemd
units and agent seats — the consumers that need the CLI.
runRuntimeLaunchEntry set MOSAIC_HOME to the shipped framework root, so
launch-runtime.py appended its launch ledger to
framework/fleet/run/sessions/events.ndjson — polluting the tree that
manifest.spec.ts walks and failing its completeness check in CI.
Point MOSAIC_HOME at the per-entry temp root instead. Nothing in the
launch chain resolves tools via MOSAIC_HOME (entry scripts resolve via
SCRIPT_DIR); the ledger is its only consumer here.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
The two install-linker-compatibility tests still asserted the pre-isolation
behavior (mosaic skill links planted in $HOME/.claude/skills). This branch
deliberately moved the link farm into the mosaic-owned harness homes
($MOSAIC_HOME/.claude/skills) and demoted the base-install dirs to
cleanup-only legacy targets, so the tests now assert the new topology:
the skill links appear under the harness home, foreign links in the legacy
dir are preserved, and no new mosaic link is planted in the base install.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
The mosaic wrapper makes pane_current_command=node (RUNTIME_ACCEPTABLE_COMMANDS.claude=['claude','node']); the walk matters precisely in that no-shell-wrapper case. Match reality.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
The launcher runs the runtime as a spawnSync CHILD of node(mosaic) (deliberate,
per launch.ts:99 — parent survives to propagate signals), so
MOSAIC_LEASE_SESSION_ID lives on the claude child, not the pane's root pid. The
transport read only pane.pid's /proc/environ and returned RESOLVE_FAILED for
every real 'mosaic claude' seat. Now BFS the pane's process subtree (bounded,
injectable children-reader) and read the first descendant that carries a valid
lease id; fail-closed if none. Unit tests now exercise the real walk (pane=node
without lease -> child=claude with lease) rather than mocking the resolution.
Found by scooby greenfield E2E on fomo-lin with proc-level evidence.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
MAX_LEASE_TTL_SECONDS (daemon cap+default) and DEFAULT_TTL_SECONDS
(lease_promote client) both move to 3600. The 5-minute TTL made
gated-by-default sessions unusable (re-promotion mid-task); 1 hour
matches a working session. Full test:framework-shell RC=0.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
The independent W-0R review of 3592b92e passed but left two PLAUSIBLE
findings: the stderr notice for a legitimately-omitted operator source was
claimed and never asserted (a silent omission is the original defect in
miniature), and the chmod 0o000 unreadable simulations fail spuriously when
euid==0 (CAP_DAC_OVERRIDE). Falsifier for the new assertion: deleting the
notice block turns the suite red (failures=3); restoring returns green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EHYXhcCQsL3J1Lnm7EraGq
build_construction skipped any normative source it could not read
(`except OSError: continue`) and promoted whatever remained. That is not a
degraded binding, it is a forged smaller one: the broker recomputes h_source /
h_payload from the fragments it is SENT (daemon.py:602-616), so an omitted
fragment is internally consistent and PAYLOAD_BINDING_MISMATCH cannot fire. A
partial law promotes exactly like a complete one and nothing downstream can tell
the difference.
Measured before this change, against a seeded home: with only USER.md readable,
the client produced a one-fragment construction with promotion=True. Removing
CONSTITUTION.md, STANDARDS.md or the runtime contract likewise promoted.
The classification mirrors the framework's own file ownership rather than
inventing one:
* CONSTITUTION.md / AGENTS.md / STANDARDS.md are framework-owned and
reconciled every upgrade (install.sh FRAMEWORK_OWNED,
config/file-adapter.ts FRAMEWORK_OWNED_FILES), as is the per-runtime
RUNTIME.md. Absent => IncompleteBinding. A deployment missing one is broken,
not minimal.
* SOUL.md / USER.md are deliberately not seeded by install.sh ("generated by
`mosaic init`") and TOOLS.md is seeded on first install only, so their
absence is legitimate. It is reported on stderr, never silent.
Unreadable is handled separately from absent for EVERY source, optional ones
included: a file that will not open is not a file that was never configured, and
collapsing the two is what let a permission change quietly shrink the law.
Also corrects this module's own docstring, which asserted that a VERIFIED lease
means "this agent is running THIS law". It does not. Both sides of the broker's
comparison originate in this client, so it detects corruption in transit and
nothing else. That overstatement is where the belief spread from; the stronger
claim needs the broker re-reading on-disk sources against a manifest the agent
cannot rewrite.
Test: promotion_binding_unittest.py, enumerated in test:framework-shell (the
enumeration guard's population is *test*.sh and does not cover Python, so an
unenumerated test here would simply never run). Falsifier executed: defeating the
guard while leaving the module API intact turns the suite red (12 failures);
restoring it returns green.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EHYXhcCQsL3J1Lnm7EraGq
This reverts 939f2e04. Keeping the revert rather than dropping the commit,
because the failed attempt is the most useful record on this branch.
The wiring worked mechanically — verified with a live model on sb-it-1-dt: the
receipt was emitted verbatim as a whole message, and the broker token was minted
AND consumed, so observe_receipt and promote_lease both succeeded and the lease
reached VERIFIED.
It failed as a DESIGN, for reasons that are properties of the protocol rather
than of this wiring:
* It puts control-plane traffic in the user-facing conversation channel. An
operator asking "what model are you?" received a receipt string instead of an
answer — the model tried a tool, was blocked, complied with the receipt
instruction, and in one-shot mode that text turn BECAME the reply. Observed
twice, non-deterministically.
* The lease TTL is hard-capped at 300s (MAX_LEASE_TTL_SECONDS; ttl_seconds >
cap raises INVALID_LEASE_TTL). Measured: allowed at T+0, LEASE_EXPIRED at
T+310. So the visible cost recurs every five minutes of mutator activity.
* Model compliance is not guaranteed — one run retried the command instead of
emitting the receipt.
Any model emission is user-visible, so this is not fixable by better wiring; it
needs a design answer about how promotion is triggered and paid for. That is
under adversarial review (docs/scratchpads/lease-remediation/07-liveness-design-brief.md
in the operator's repo). Promotion triggering will return on its own branch once
that lands.
What remains here is independently sound and unblocked: harness-home isolation,
the immutable launch record, the skills relocation, the promotion client itself
(steps 1/4/5), and the #1087 prefix guard.
prune_stale_links_in_target compared "$resolved" == "$canonical_real/"* while
length-checking only $resolved. If $canonical_real were ever empty the pattern
collapses to == "/"* and matches every absolute path.
The failure is precisely inverted, which is what makes it worth fixing rather
than noting: is_mosaic_skill_name already `continue`s for names that ARE current
mosaic skills, so an empty prefix would delete exactly the FOREIGN symlinks in
every target directory and preserve the mosaic ones. On this host that is 4 base
installs, including codex's own .system entry.
Reported by mos-claude as #1087 after I introduced the same guard in the new
legacy-cleanup path in the previous commit and walked past this instance thirty
lines away. Same defect class, same file, one function apart.
$canonical_real is populated by readlink -f after a mkdir -p, so an empty value
requires readlink to fail — unlikely, but the consequence is deleting operator
symlinks across every harness, which is not a risk worth carrying for one test.
Completes the promotion path: the client landed in the previous commit, but
nothing drove step 2 — the model emitting the receipt. This wires it.
LAZY, not at session start. Promotion costs an entire model turn, because the
receipt must be the whole message (hmac.compare_digest, "not a transcript
substring"). Minting at session start would collide with the Constitution's
first-response mode declaration — the two cannot share a message, so requiring
both would be unsatisfiable. Deferring to the first DENIED MUTATOR means the
mode declaration happens first and the receipt gets its own later turn, so no
governance change is needed. A read-only session never pays for promotion at all.
Mechanism: on a MUTATOR_UNVERIFIED denial the tool_call hook mints a challenge
and returns the receipt in the block `reason`, which pi feeds back to the model
as the tool result — the existing injection path already used by
lease-lifecycle.ts. The model emits the receipt as its next message, message_end
ships it to the observer, and the extension then calls observe_receipt +
promote_lease.
Only MUTATOR_UNVERIFIED triggers minting. Other denials (GATE_UNAVAILABLE,
STALE_GENERATION, LEASE_EXPIRED, ANCESTRY_MISMATCH) describe conditions a
receipt cannot fix, and begin_verification revokes before it mints, so minting
there would thrash the broker.
Completion is gated on an EXACT text match against the minted receipt. This is
load-bearing, not defensive: message_end also fires for the message that
CONTAINED the blocked tool call — one turn BEFORE the model answers. An earlier
version completed there, so observe_receipt compared against the wrong text,
failed, and burned the challenge before the model ever emitted it. Matching the
text mirrors the broker's own compare_digest semantics and waits for the right
turn. Confirmed by instrumenting message_end and watching it fire with
pending=yes one message too early.
It never posts the receipt itself. receipt-observer-client.py accepts any
string, so self-posting would satisfy the broker while proving nothing — the
whole point is that a live model echoes a challenge it was given.
Bounded by MAX_PROMOTION_ATTEMPTS: model compliance is not guaranteed (observed
a run where the model retried the command instead of emitting the receipt), so
a non-complying model degrades to today's behaviour — denied mutators — rather
than looping.
Verified with a live model on sb-it-1-dt: receipt emitted verbatim as a whole
message, and the broker token was minted AND consumed, i.e. observe_receipt and
promote_lease both succeeded and the lease reached VERIFIED.
Known limitation: under `pi -p`, the receipt is a text-only turn, which ends the
one-shot loop — so promotion completes but the blocked tool is not retried in
that same invocation. Interactive and durable fleet sessions continue and retry
normally.
The enforcement half of the lease broker ships and denies; the promotion half
has no production caller anywhere in the package. Verified across 0.0.48, 0.0.49
and 0.0.50-next.2207: begin_verification / observe_receipt / promote_lease are
invoked only by broker-test-client.ts, the acceptance spec, unit tests, and two
probes under docs/.
Consequence: no lease on any host can reach VERIFIED, so mutator-gate denies
every mutator with MUTATOR_UNVERIFIED via a gate that nothing shipped can
satisfy. Runtimes that enforce the gate in-process (pi, via mosaic-extension's
tool_call hook) are bricked for mutators; runtimes whose gate is wired through a
settings hook escape only when that hook is absent — i.e. by being ungated.
This adds the client. It implements protocol steps 1, 4 and 5:
1. begin_verification -> mint a challenge, return the exact receipt text
2. the MODEL emits that text verbatim as its entire latest message
3. the runtime adapter ships that message to the observer socket
4. observe_receipt -> PENDING_PROMOTION
5. promote_lease -> VERIFIED
Step 2 is deliberately NOT implemented here, and that is the point.
is_verbatim_receipt uses hmac.compare_digest against the exact minted string —
explicitly "not a transcript substring" — which makes promotion a LIVENESS
PROOF: it requires a live model that received the challenge in its context and
echoed it exactly.
receipt-observer-client.py will post ANY string as the latest assistant message.
A promotion client that posted its own receipt would satisfy the broker while
proving nothing — a gate-disabler indistinguishable from a working fix unless
someone specifically looks. Emitting the receipt therefore belongs to the runtime
adapter, where a real model turn happens. A local diagnostic that posts its own
receipt exists in the operator's repo and is deliberately NOT shipped here.
The construction binds the exact normative source bytes, so a VERIFIED lease
means "this agent is running THIS law", not merely "this session id is known".
h_source/h_payload are derived by importing the framework's own
normative_fragments.build_payload rather than reimplementing it: the broker
derives them the same way and any divergence yields PAYLOAD_BINDING_MISMATCH.
There must be exactly one implementation.
session_identity() prefers the generation FILE over the env var, matching
lease_generation.py. Sending a generation higher than the broker's would revoke
the session's own authority (daemon.py:342-344), so it never guesses.
Verified end-to-end on sb-it-1-dt under a real lease-gated anchor: a mutator
denied rc=2 MUTATOR_UNVERIFIED, then begin -> observe -> promote -> VERIFIED,
then the same mutator allowed rc=0. Negative controls pass: a fresh session is
still denied, and an unrelated session still reads UNVERIFIED — promotion is
per-session and does not leak.
Still open: adapter wiring for step 2. Lazy promotion on first mutator attempt
avoids colliding with the Constitution's first-response mode declaration, since
compare_digest requires the receipt to be the WHOLE message.
Mosaic wrote into the operator's harness base installs — ~/.claude,
~/.pi/agent, ~/.codex, ~/.config/opencode — for settings, instructions, and a
102-symlink skill farm per harness. Any experiment with hooks or gating
therefore mutated the operator's own tooling, and a broken framework change
could take out the very harness needed to repair it.
Harness home isolation
----------------------
Each runtime now reads config from a dedicated mosaic-owned home via the
harness's own config-dir variable:
claude CLAUDE_CONFIG_DIR ~/.config/mosaic/.claude
pi PI_CODING_AGENT_DIR ~/.config/mosaic/.pi (replaces ~/.pi/agent)
codex CODEX_HOME ~/.config/mosaic/.codex
opencode XDG_CONFIG_HOME ~/.config/mosaic/.opencode
These paths are manifest-UNKNOWN, so rule 3 (#791) resolves them to operator
ownership and a keep-mode upgrade can neither overwrite nor prune them.
A bare `claude` / `pi` keeps its own config AND auth, making it a structural
break-glass rather than one depending on restoring a file under pressure.
opencode is blunter than the rest: it has no dedicated variable and follows XDG,
so isolation also relocates XDG lookups for anything it spawns. Documented in
place.
mosaic-sync-skills now links into those homes and cleans the legacy farms it
previously planted in base installs. Ownership is proven by RESOLUTION, not by
name — only symlinks resolving inside the canonical/local skills dirs are
removed, mirroring the refusal already in commands/skill.js. Verified against a
real install: codex's own .system directory survived while its 102 mosaic links
were removed. Both resolution prefixes are length-checked first; an empty prefix
would make "$resolved" == "$prefix/"* match every absolute path and delete
foreign symlinks.
Immutable launch record
-----------------------
Every launch now appends one record to fleet/run/sessions/events.ndjson before
exec. Mandatory, mechanical, no model involvement.
pi rewrites its own argv to a bare `pi`, so /proc/<pid>/cmdline destroys the
launch evidence — that has already produced a confident wrong diagnosis ("this
agent bypassed the launcher"), disproved only by the parent's argv and only
because the parent had not yet exited. A record written before exec is the only
place this survives.
The path is the #797 Runtime Session Ledger, already operator-classified and
already covered by test-upgrade-manifest-guard.sh, which seeds it and proves a
populated ledger survives keep-mode upgrades — but nothing shipped ever wrote
it. This implements it in the shape that guard already asserts (0600 files under
a 0700 dir).
`mosaic` writes session.launch; launch-runtime.py appends lease.register with
the broker session id and activation capability. They correlate by an explicit
MOSAIC_LAUNCH_ID, never by pid: execRuntime uses spawnSync, so the runtime is a
child with a different pid.
Records normative fragment digests (CONSTITUTION/AGENTS/SOUL/USER/STANDARDS/
TOOLS/RUNTIME) — the same set the broker hashes for promotion, so drift is
mechanically detectable rather than a matter of judgement.
Credential-safe: env is captured as PRESENT NAMES ONLY, and argv values over
256 bytes become a sha256 + length rather than being inlined.
Also fixes CLI_VERSION resolution: '@mosaicstack/mosaic/package.json' is not in
the package exports map and always throws ERR_PACKAGE_PATH_NOT_EXPORTED.
resolveTool() uses that same failing specifier, which is why its documented
preference for bundled tools over the deployed ~/.config/mosaic copy has never
once applied — noted in place, not fixed here.
Verified on sb-it-1-dt: isolated homes written and base installs byte-identical
for all four harnesses; 408 legacy symlinks removed with 1 foreign entry
preserved; launch records paired across the spawn boundary. typecheck shows zero
errors in launch.ts (the @mosaicstack/types failures are pre-existing and
reproduce on a pristine origin/main worktree).
invariant_r_unittest.py (landing with the lease-remediation stack, PR
#1109) hard-requires an installed `pi` binary pinned to the measured
version: it boots Pi's real tool registry and proves the broker's
read-only carve-out resolves to real, unshadowed builtins. Absent
runtime fails loud by design — so CI must provide it.
Install @earendil-works/[email protected].1 (the canonical Pi;
@mariozechner/* is embedded-legacy) at step level in the test step.
Step-level rather than baked into Dockerfile.ci because ci-image
publishes are currently blocked on registry UNAUTHORIZED; baking it in
is the follow-up once registry auth is fixed, at which point this line
degrades to a fast no-op guard like the openssl line above it.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
Add the Slice-Zero catalog and selection HTTP surfaces for P3 Task 3:
GET /api/harnesses, GET /api/harnesses/:harnessId/catalog,
GET+PUT /api/chat/preferences/selection. Scope is always server-derived
via scopeFromUser(CurrentUser); selection tuples are validated against the
live catalog with no fallback substitution and persisted in a transitional
owner-scoped in-memory store. HarnessModule is wired into AppModule.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
The `format` step of .woodpecker/ci.yml:89 (`pnpm format:check`) failed on
pipeline 2111 for this branch. Reproduced on a bench with the lockfile-pinned
[email protected] against the repo .prettierrc and .prettierignore, using CI's
exact glob: WRITING-STYLE.md was the only failing file.
The change is mechanical and semantically null: markdown table cell padding
and `*emphasis*` -> `_emphasis_`. Verified by normalizing both revisions
(whitespace removed, `_`/`*` folded, table rules collapsed) — the results are
byte-identical.
This does not address the prose findings published on #965 (P1-P4); those
await a ruling. The `test` step also failed on 2111, on a base ~40 commits
stale — attribution for that failure needs this rerun, and is not claimed here.
Co-authored-by: mos-dt-0 <[email protected]>
issue-comment.sh and pr-review.sh verify a durable write by pinning the
provider-returned object URL's origin and full path. The origin included the
SCHEME verbatim. On a Gitea whose ROOT_URL is configured `http://` while every
client reaches it over `https://`, the provider returns `http://` object URLs,
so the comparison rejects the provider's own truthful answer about a write that
LANDED. The failure is deterministic, not intermittent: every comment, every
time, on such a deployment.
The scheme was never what the check defends. The forgeries it exists to catch —
look-alike host, decoy path prefix, wrong owner/repo/kind/number — all vary the
HOST or the PATH. Both stay strict. `http` and `https` now collapse to one
scheme class; any other scheme (file:, ftp:, javascript:) stays distinguishing,
and an EXPLICIT non-default port still distinguishes, because a different port
is a different service on the same host.
Consequences of the bug, both observed:
- The wrapper reports failure on a comment that is durably on the issue/PR, and
attributes it to #865 ("no durable comment created"). The write landed; the
citation is wrong. Reproduced here: the harness's persisted state contains the
record while the wrapper exits 1.
- pr-review.sh's comment path is worse. On a host where no seat can create a
review OBJECT, comment-form is the only gate-16 review record obtainable, and
this check refuses all of it.
Test gap this closes: every URL fixture in both harnesses was `https://`, and
every negative case varied only host or path. The one axis that fails in
production had zero coverage — the fixtures encoded the assumption that breaks.
Added, in both suites:
- scheme-downgrade (http vs https, otherwise correct) — must be ACCEPTED. Fails
against the unmodified wrappers, passes against the fixed ones; verified in
both directions, and the negative control's captured output is the #865
misattribution above.
- explicit non-default port (`:8443`) — must stay REJECTED.
- non-web scheme (`ftp://`) — must stay REJECTED.
Also fixes test-issue-comment-readback.sh hermeticity (#1007), without which the
suite cannot run on any seat that has a per-agent Gitea token: detect-platform's
step-0 identity lookup reads ~/.config/mosaic/gitea-tokens/<identity>, outside
both XDG_CONFIG_HOME and MOSAIC_CREDENTIALS_FILE, so the suite resolved a
PRODUCTION credential and died at HTTP 401 before case 1. Same two-part fix
already merged for test-pr-review-gitea-comment.sh in #1006: a sandboxed HOME
plus an empty REPO-LOCAL mosaic.gitIdentity to shadow the global. Note the
env-var route does NOT work — detect-platform.sh reads `${MOSAIC_GIT_IDENTITY:-}`
and `:-` treats set-but-empty identically to unset.
The owner-side half of #991 (setting the deployment's Gitea ROOT_URL to https)
is not in scope here and is not made unnecessary by this change; this makes the
wrappers correct against a deployment that returns either scheme.
root cause: emitDecoratorMetadata reflected the third constructor parameter as Function and Nest attempted to resolve it
fix: optional HANDOFF_ID_FACTORY injection token, no production provider, preserving undefined -> crypto.randomUUID() default and unchanged positional construction
TDD: real CoordModule red at Function index [2], then green; test overrides only unrelated AuthGuard because its AUTH provider comes from AppModule's global AuthModule context
Closes#1145
§5 sent the agent to read direct|friendly|formal in USER.md, but the builder
renders prose bullets, not the token — the documented lookup could not key on
the shipped file. Table now keys on the leading bullet USER.md actually
contains. Also: 'concise, technical' -> 'concise, structured' (drop the round-1
residual value name from a rule-9 guide). Docs-only, no code, no scope growth.
Written-by: jarvis (dragon-lin)
Co-Authored-By: Claude Fable 5 <[email protected]>
F1: defaults/USER.md is never installed (generated from templates/USER.md.template
via buildCommunicationPrefs). Editing it was a no-op asserting a phantom setting —
exactly the false-green §2 warns against. Reverted.
F2: the framework already has communicationStyle (direct|friendly|formal). §5 now
maps THOSE values to output instead of inventing technical|prose|brief (rule 9).
Minor: §6 states no mechanical prose check exists today; rule 1 points at §3.4.
Written-by: jarvis (dragon-lin)
Co-Authored-By: Claude Fable 5 <[email protected]>
Adds the agent output standard to the framework SOT so it injects at launch and
is selectable per user (closes the gap: it lived only as a jarvis-brain lab doc + issue #960).
- guides/WRITING-STYLE.md: MOS-STE (adapted ASD-STE100) for docs, Google Style for code,
verification-artifact emphasis, absolute user-voice carve-out. Written in MOS-STE.
- defaults/STANDARDS.md: Output-standards block (always injected via the prompting contract).
- defaults/AGENTS.md: routing row so writing/doc/comms work reaches the guide.
- defaults/USER.md: per-user 'Comms style' option (technical|prose|brief), default technical.
Refs mosaicstack/stack#960. Owner directive (Jason, 2026-07-30): docs->adapted ASD-STE100,
code->Google style, resumes/personal carved out, comms style a per-user choice.
Written-by: jarvis (dragon-lin)
Co-Authored-By: Claude Fable 5 <[email protected]>
My previous commit said four. It is five. `test-issue-comment-readback.sh` has
the same defect and is fixed the same way, and I had already looked straight at
it and filed it as an *unrelated* silent failure. Correcting that here rather
than folding it in quietly.
WHY IT WAS MISSED — the general lesson, not the excuse. `run_comment()` sends
the wrapper's stdout AND stderr to `$OUTPUT_FILE`, and the `EXIT` trap deletes
`$WORK_DIR`. The suite therefore exits 1 with ZERO bytes on stdout and stderr,
and the one line that says what went wrong —
Error: Gitea authenticated-identity read failed with HTTP 401
— lives only inside a directory that no longer exists when anyone looks. Every
oracle I had swept the family with greps for a SYMPTOM in surviving output, so
against this suite all of them returned "nothing found", which I read as "clean"
in the first sweep and as "unrelated pre-existing failure" in the second. A
suite that discards or deletes its own evidence converts a post-hoc assay into a
non-measurement, and I wrote that sentence into the previous commit while it was
already false about a file in the same directory.
HOW IT WAS ACTUALLY FOUND. Intercept the identity read at its SOURCE instead of
grepping for its consequence: a PATH shim over `git` that logs every
`mosaic.gitIdentity` read — args, rc, and resolved value — to a file OUTSIDE any
suite's work dir, then execs the real git. Deletion-proof by construction, and
it measures the defect's cause rather than one of its symptoms. Sweeping all 16
suites with it under an ordinary invocation:
resolves a REAL identity (`mos-dt-0`) before the fix:
test-issue-comment-readback 1 read rc=1 (RED on every seat)
test-pr-review-repo-host-override 6 reads rc=0
test-ci-queue-wait-branch-absent 3 reads rc=0
the four fixed in the previous commit now read empty; the rest never read at all.
The latter two are NOT affected and are deliberately left alone: under a seat
replica (identity set, no per-slot token) neither reaches `get_gitea_token`'s
fail-loud branch, and under a canary HOME neither carries the canary credential
into any surviving artifact. They read the identity and never enter a credential
path. That residual is structural and belongs to the wrapper half of #1007 —
scoping the read with `git -C "$repo"` removes it for everyone at once.
An earlier version of that sweep reported the four fixed suites as still
resolving a real identity. That was my grep, not the suites: `value=\[..*\]` is
satisfied by `value=[] args=[…]`, because `.*` runs past the empty pair and
matches the closing bracket of the NEXT one. `value=\[[^]]` is the correct test.
Recorded because the wrong pattern failed in the direction that would have sent
me re-fixing four already-correct files.
VERIFICATION of this suite, four HOME arms, all rc=0 with zero non-empty
identity reads and the pass line on stdout: real HOME, seat replica, canary
HOME, and an empty HOME with no identity at all. Full 16-suite sweep after the
change: every suite rc=0.
CONSEQUENCE FOR THE FINDING LIST IN THE PREVIOUS COMMIT: item 2 there — the
"silently red, unrelated to #1007" suite — is withdrawn. It was #1007 all along.
Item 1 (`pr-metadata.sh:89-92`, the anonymous fallback that reports an HTTP 200
carrying valid JSON as "unknown API error") stands and is still unfixed here.
Refs #1007
CENSUS CORRECTION: FOUR suites, not the three my own #1007 audit named. The
fourth (test-pr-metadata-gitea.sh) was outside the candidate set that audit
worked from and was found only by sweeping the discriminator across all 16
tools/git/test-*.sh suites. Recording that as a correction to my finding, not
as part of the original claim.
THE DEFECT. get_gitea_token() (detect-platform.sh:502-599) resolves a per-agent
identity at STEP 0, from `git config --get mosaic.gitIdentity`, BEFORE both the
Mosaic credential loader (step 1) and the GITEA_TOKEN env check (step 2). On a
provisioned agent seat that value is set GLOBALLY in ~/.gitconfig and is
inherited by any freshly-`git init`ed repo, so step 0 reads a REAL per-slot
token out of $HOME and returns it without ever consulting the suite's own
MOSAIC_CREDENTIALS_FILE / GITEA_TOKEN fixtures. The suites were running against
production credentials, and the fixture credential each one carefully
constructs was inert.
THE FIX: an empty repo-local `mosaic.gitIdentity`. An empty local value shadows
the global one and reads back empty at rc=0, so step 0 declines. The env route
does NOT work: detect-platform.sh reads "${MOSAIC_GIT_IDENTITY:-}", and `:-`
treats set-but-empty identically to unset.
OPERATIVE vs CONTAINMENT — the two mechanisms are not interchangeable and the
comment in each suite says so. The pin is operative: it prevents the resolution.
The sandboxed HOME each suite now also gets is containment: it bounds a failure
the pin should already have prevented. Conflating them is how this class stays
invisible, because a decoy HOME REMOVES the trigger (~/.gitconfig is where the
global identity lives), so any suite audited under one reads clean however
vulnerable it is. To MEASURE, replicate a seat: a decoy HOME whose .gitconfig
sets mosaic.gitIdentity with no per-slot token, so step 0 reaches its fail-loud
branch. That note is in each file for the next auditor.
SECOND, INDEPENDENT DEFECT in test-pr-metadata-gitea.sh. Applying the pin alone
turned that suite RED — and a control at baseline 826a8b3 under a plain HOME
reproduced the same failure, so it is pre-existing, not introduced. Its
`GITEA_TOKEN="stub-token"` / `GITEA_URL="https://git.example.test"` pair can
never satisfy step 2, because step 2 accepts GITEA_TOKEN only when GITEA_URL
matches the remote host and this repo's origin is git.uscllc.com. The suite had
therefore only ever passed by resolving a REAL credential — step 0 on a seat, or
step 1 from the operator's own credentials.json. A MOSAIC_CREDENTIALS_FILE
fixture is added rather than leaning on the sandboxed HOME making step 1 find
nothing: a test that passes because production configuration is ABSENT fails the
moment it is present. Shipping the pin without this would have moved the failure
rather than removed it.
NO CI ARM. .woodpecker/ci.yml does not run these suites; packages/mosaic/
package.json:28 (test:framework-shell) runs an ENUMERATED list that excludes all
four. They run only by hand — i.e. exclusively on a provisioned seat, the one
environment where the defect is live. "Passes in CI, fails on a seat" does not
apply here; there is no CI observation at all.
VERIFICATION (seat replica = decoy HOME with mosaic.gitIdentity set, no per-slot
token; canary = same plus a marked non-credential at both per-slot paths; plain
= empty HOME; real = ordinary invocation):
- bash -n clean on all four.
- Sweep of all 16 suites at baseline 826a8b3 under the seat replica:
test-gitea-login-resolution rc=1 REACHES-STEP0; test-issue-create-
interactive-auth rc=1 REACHES-STEP0; test-pr-merge-gitea-empty-uid rc=1
REACHES-STEP0; test-pr-metadata-gitea rc=1 REACHES-STEP0.
- Same sweep after: every row rc=0 with step0 absent.
- test-gitea-token-identity flags REACHES-STEP0 in BOTH arms and is NOT a
defect: it runs under `env -i HOME="$FAKE_HOME"` (line 77) and its hit is
its own deliberate assert_failloud fixtures (lines 158-171). The fail-loud
grep matches the intended behaviour as well as the defect, so it needs the
second discriminator; recorded here so the next sweep does not re-file it.
- Durable-argv assay (a PATH shim that tees argv out of each suite's own mock
curl, because test-pr-merge-gitea-empty-uid truncates its log between phases
and its EXIT trap removes the sandbox — a post-hoc read of that suite is a
non-measurement, and "no trace" there is not a clearance):
test-pr-merge-gitea-empty-uid before: canary token in argv, fixture never
used. after: fixture token in argv, canary absent. 5 curl calls both arms.
test-pr-metadata-gitea before: canary in argv. after: both calls
carry the fixture token against git.uscllc.com.
- test-pr-metadata-gitea across seat/canary/plain HOMEs after the fix: rc=0,
rc=0, rc=0.
- All four under the real HOME: rc=0. No regression to ordinary invocation.
The comment block is duplicated across the four files rather than pointing at a
shared note. Deliberate, and matching the merged #1006 precedent
(test-pr-review-gitea-comment.sh:87-95): the reader who needs it is auditing one
file.
TWO FINDINGS DELIBERATELY NOT FIXED HERE (out of this branch's scope, to be
filed):
1. pr-metadata.sh:89-92 — the anonymous curl fallback does not check ^2, so an
HTTP 200 carrying valid JSON is reported as "unknown API error" at rc=1.
2. test-issue-comment-readback.sh exits 1 with ZERO bytes on stdout AND
stderr, dying at its first seed_state python3 heredoc. Reproduces at
baseline 826a8b3 under both a seat replica and the real HOME. Silently red
at main for everyone; unrelated to #1007.
Refs #1007
New skill: lint — zero-tolerance linting enforcement for all code changes.
Detects project linter, fixes ALL violations, never disables rules.
Updated kickstart: linting now explicit standing order #3 in worker template
with "NON-NEGOTIABLE" language and zero-tolerance enforcement.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add npx skills add commands for single, all, and non-interactive install
- Document .git suffix requirement for Gitea-hosted repos
- Add git clone step to manual installation
- Use ln -sf for idempotent symlinks
Co-Authored-By: Claude Opus 4.6 <[email protected]>
From this repository, start a fresh native Pi session:
```sh
bash scripts/goal-dev.sh
```
Canonical source lives under `extensions/`. The launcher first runs `scripts/sync-dev-extensions.sh`, which installs verified ordinary-file copies under `.pi/extensions/`, then loads only the generated goal extension. Global extensions remain unloaded. The launcher keeps your usual native Pi provider authentication; it copies no credentials. Goal state and new conversation files live under `.pi/state/`, which is ignored by Git. Each process gets a fresh incarnation; `/reload` and `/new` in the same process retain its goal. Restarting Pi does not adopt an earlier process's active goal.
Plain `pi` also discovers `.pi/extensions/goal/index.ts` after project trust, but may load global extensions too. Use the launcher to avoid duplicate `/goal` registrations. This is a local development test, not a sandbox or the managed Mosaic runtime. Docker and `~/.mosaic` are unchanged.
## Try it
1. Set `/goal <a long goal with acceptance criteria>`. This starts work immediately.
2. Look below the editor for `Goal: Active`. The old above-editor goal widget is gone.
3. Run bare `/goal`, then press `Alt+G`. Both show the entire stored goal and its status. Tab remains autocomplete.
4. Use `/goal stop` and `/goal resume`. Expect Paused and Active, or Waiting if an untimed wait remains recorded.
5. A blocked `goal_report` displays Blocked. A satisfied report displays Complete and retains the full goal for recall without continuing work.
6.`/goal clear` removes the retained goal. Try `NO_COLOR=1 bash scripts/goal-dev.sh` to check text-only labels.
Use terminal scrollback for recall longer than the screen. At narrow widths Pi may truncate its footer status row; bare `/goal` and Alt+G remain available.
## Checks
```sh
node --test extensions/goal/test/*.test.ts
bash scripts/test-extension-package.sh
python3 scripts/test-goal-native.py
```
Contract tests use ordinary read-only fixture copies in `test/fixtures/skills-local/`, not live brain files. The executive-update fixture SHA-256 matches the parser's pinned contract, `bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319`.
`SOURCE-SNAPSHOT.json` records the original external-source baseline, not the edited candidate. No symlinks are used. Never edit `.pi/extensions/`; the sync script refuses to overwrite installation drift. Make changes under `extensions/`, run the checks, and relaunch. To disable the test, stop its Pi process and remove `.pi/extensions/`. Keep `.pi/state/` only if you need local test state.
Mosaic Stack is a self-hosted, multi-user AI agent platform. It is a TypeScript monorepo with a NestJS gateway, Next.js dashboard, Pi SDK agent runtime, and Discord/Telegram plugin architecture.
A rebuild of Mosaic Stack: a file-based, fail-closed
orchestration foundation that dispatches sandboxed headless pi workers to do
real work, with immutable run records as evidence. Thirteen-plus tagged
milestones (`git tag -l`) from `poc-container-hello-v0` to today; suites
green at every step. Not production software — a proven foundation.
### Stack
## Non-negotiable invariants (the canon)
- **API:** NestJS with Fastify (`apps/gateway`)
- **Web:** Next.js 16 with React 19 (`apps/web`)
- **ORM and database:** Drizzle ORM, PostgreSQL 17, and pgvector (`packages/db`)
Software delivery also requires the applicable tests. Common repository commands are:
- **Register** your session in `docs/SESSIONS.md` — one append-only line
(date, actor, scope, outcome). Never rewrite or remove entries.
- **Cadence**: run `scripts/mosaic queue next <your seat>` first. It names
the row to resume, review or start, or says there is nothing. The goal order
in `docs/plans/2026-09-27_goals-review.md` sets priority, not CURRENT.md.
Open only the brief that row links to. Execute it through every authorized
stage (implement → test → verify against acceptance criteria; commit, push,
or close only when the established plan authorizes each) → move the row with
`scripts/mosaic queue move` (never by editing QUEUE.md) → register in
SESSIONS.md.
- "next" means one action. A batch mandate ("run the queue") repeats the
loop until green or truly blocked under the boundary rules above.
- Substantial work gets a Gitea issue and a BUILD-LOG phase entry
(before/after, with corrections recorded honestly).
```bash
pnpm typecheck # TypeScript checks across the workspace
pnpm lint # ESLint across the workspace
pnpm test# Checkout tests and package Vitest suites
pnpm format:check # Prettier check
pnpm build # Build all packages and applications
```
## Internal development bootstrap
## Database and Local Runtime Safety
Jason's current direction is repository-native development in
`/mnt/storage/src/mosaic-stack`. Sage leads the project (Jason's ruling,
2026-09-26) and coordinates coding, review and research through Darkwing, Dewey,
Filbert, Rocko, Researcher and any further seats Jason launches under `agents/`.
Darkwing is a collaborating agent seat, not the coordinator. Development sessions
run in T3 for now. Work moves to the new stack; the old `~/.mosaic` fleet is being
retired, and a fleet seat acting outside Jason's instructions is the failure this
transition exists to prevent.
Do not assign new development work to fleet seats during this bootstrap phase.
Do not modify `~/.mosaic` launchers, provisioning or other state, or stop/migrate
live fleet processes as part of this work. Preserve existing work and histories.
Use the repository bootstrap/configuration and launch entry points; missing
configuration still fails closed. This changes development coordination, not
managed worker role policy or deployment authority. The lead role adds no push,
merge or deployment authority; those still need Jason's say-so. See
`agents/README.md` for the internal roster.
- Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset.
- PostgreSQL execution is held until KBN-101-00, KBN-101-03, and KBN-101-05 land.
- Do not invoke a migration runner, initialization SQL, or the Compose PostgreSQL service from this checkout.
- Do not start Gateway/Web or run root `pnpm dev` as a local PGlite route. The current dotenv loader can inherit a daemon PostgreSQL DSN; KBN-101-02 must make that path fail closed first.
- Migration artifact generation is offline and does not authorize PostgreSQL access:
For control-board attention, start a completed reply with `Input needed: ` and
one specific nonempty request only when Jason must provide a decision or input.
Put that line at column zero, before other text. Do not use it for routine
completion or a wait on another agent. Ordinary completed replies are idle.
Use code fences or blockquotes when showing this convention as an example.
The signal is advisory status, never permission for a protected action. Seen
acknowledges a request; it does not resolve it. See `packages/control-board/README.md`.
```bash
pnpm --filter @mosaicstack/db db:generate
```
## Role model
## docs/TASKS.md — Schema (CANONICAL)
- **Conductor**: a system-scoped role — not an agent, not a daemon. Holds
verifies, integrates. Protocol: `docs/plans/CONDUCTOR.md`. Exists only
when invoked; push is never automatic.
- **Workers**: headless pi via `scripts/run-task.sh` — sandboxed workspace,
tools allowlist, optional persistent sessions and forks; no git, no
credentials, no policy control.
- Worker runs deliberately exclude this file (`--no-context-files` in the
adapter): worker context is contracts + mission via the generated system
prompt. This file is for conductor-level sessions.
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.
The active rebuild is at this repository's root. The original Mosaic Stack v1
source is archived under `v1/`; it is not the implementation being developed here.
Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi — injecting consistent system prompts, guardrails, skills, and mission context into every session. A NestJS gateway provides the API surface, a Next.js dashboard gives you the UI, and a plugin system connects Discord, Telegram, and more.
| Stable | `bash tools/install.sh` | You want the released Mosaic CLI/framework | npm registry `@mosaicstack/mosaic@latest` + framework archive at `main` |
| Prerelease integration | `bash tools/install.sh --next` | You want the current `next` integration branch | Build-from-source at `next` |
| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are testing a branch before release; `--ref` wins | Build-from-source at the requested ref |
- Unknown keys, unsupported versions/backends, and malformed JSON exit nonzero; nothing is modified.
-`dataRoot` must be absolute, canonical, and must not be or contain the home or configuration directory.
- Validation failures never touch config, state, or images.
-`scripts/test-config.sh` runs the sandboxed config selftests (no Docker required).
`--next` is shorthand for the prerelease integration lane: it enables source-build mode and uses `next` unless an explicit `--ref` or `MOSAIC_REF` is provided.
Run paths (`build/hello/verify/reset`) fail closed when configuration is missing or invalid; they never invent it.
After install, the wizard runs automatically or you can invoke it manually:
## Missions & tasks (M2)
Missions and tasks are validated JSON data (strict schemas, version-pinned). The M2 layer is host-side only: mission directives are recorded for provenance but do not yet reach the runtime system prompt (capability/policy layer comes later).
scripts/run-task.sh run tasks/hello-marker.json # execute; result recorded under dataRoot/runs
scripts/mosaic-task.mjs list # list runs and statuses
scripts/test-task.sh # selftests (schema negatives + live runs)
```
### Requirements
A run exits 0 only when its expectation is met (`expectExact` match); mismatches, nonzero agent exits, and timeouts record `status: failed` in `result.json` and exit 1. Each run gets a unique directory — rerunning never rewrites history.
`RELEASE` single-sources the release version (0.0.X until declared stable); the image tag derives from it plus the pinned Pi version. Activation is health-gated and every event is recorded:
```bash
scripts/release.sh package # build + tag the release image
A failed health check never activates; the previously active release remains deployed. Updating the software therefore cannot corrupt the running installation: package beside, gate, then flip. Verified by the update/refusal/rollback drills in BUILD-LOG Phase 7.
## Runtime adapters (M4)
The harness boundary is formalized: everything upstream (config, contracts, missions, tasks, run records) is harness-agnostic; everything inside an adapter belongs to one runtime.
-`pi` — pinned Pi CLI, noninteractive print mode, ambient discovery off
-`mock` — deterministic test adapter; never for real verification
- Mission directives have a sanctioned injection point: when a task references a mission, the task runner mounts the run snapshot and the generated prompt gains a `MISSION (runtime)` section (objective + directives) after the four immutable contracts
- Adding a harness (Claude, Codex, OpenCode) later means adding one directory — no orchestrator changes
See `adapters/README.md` for the full contract.
## Workspaces, capabilities, sessions (M5/M6)
Optional task fields extend what an agent can do — all defaulting to the previous behavior:
```json
{
"workspace":"demo",// ":run" ephemeral, or persistent dataRoot/workspaces/<name>
"capabilities":{"tools":["bash","read"]},// pi tool allowlist; absent = no tools
"session":"demo"// persistent session at dataRoot/sessions/<name>
}
```
- The adapter runs inside the workspace; files it writes are host-visible (`dataRoot/workspaces/<name>`).
- Sessions persist via pi's documented `--session-dir`; a follow-up run in the same session resumes the conversation (`-c`) and can recall prior context. Distinct names never share state. Ephemeral (`--no-session`) remains the default when no session is declared.
- Selection authority: config for adapter/provider/model; the task file for workspace/capabilities/session.
Inspect anything:
```bash
node scripts/mosaic-task.mjs list # runs with task/workspace/session columns
node scripts/mosaic-task.mjs show <runId> # full record + snapshots + artifacts
scripts/reset.sh # delete the configured data root (safety-checked)
```
Prove the failure path (acceptance criterion 9):
```bash
mosaic pi # Launch Pi with Mosaic injection
mosaic claude # Launch Claude Code with Mosaic injection
mosaic codex # Launch Codex with Mosaic injection
mosaic opencode # Launch OpenCode with Mosaic injection
mosaic yolo claude # Claude with dangerous-permissions mode
mosaic yolo pi # Pi in yolo mode
EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh # must exit nonzero
```
The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments.
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
### TUI & Gateway
```bash
mosaic tui # Interactive TUI connected to the gateway
mosaic gateway login # Authenticate with a gateway instance
mosaic sessions list # List active agent sessions
```
### Gateway Management
```bash
mosaic gateway install # Install and configure the gateway service
mosaic gateway verify # Post-install health check
mosaic gateway login # Authenticate and store a session token
mosaic gateway config rotate-token # Rotate your API token
mosaic gateway config recover-token # Recover a token via BetterAuth cookie
```
If you already have a gateway account but no token, use `mosaic gateway config recover-token` to retrieve one without recreating your account.
### Configuration
Mosaic supports three storage tiers: `local` (PGlite, single-host), `standalone` (PostgreSQL, single-host), and `federated` (PostgreSQL + pgvector + Valkey, multi-host). See [Federated Tier Setup](docs/federation/SETUP.md) for multi-user and production deployments, or [Migrating to Federated](docs/guides/migrate-tier.md) to upgrade from existing tiers.
```bash
mosaic config show # Print full config as JSON
mosaic config get <key> # Read a specific key
mosaic config set <key> <val># Write a key
mosaic config edit # Open config in $EDITOR
mosaic config path # Print config file path
```
### Management
```bash
mosaic doctor # Health audit — detect drift and missing files
mosaic sync # Sync skills from canonical source
mosaic skill list # Audit Claude skill registrations and conflicts
mosaic skill register <name> # Register one canonical skill with Claude Code
mosaic skill unregister <name> # Remove one Mosaic-owned Claude link
mosaic update # Update CLI/framework and auto-register canonical skills
mosaic wizard # Full guided setup wizard
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
mosaic coord init # Initialize a new orchestration mission
mosaic prdy init # Create a PRD via guided session
```
### Sub-package Commands
Each Mosaic sub-package exposes its API surface through the unified CLI:
```bash
# User management
mosaic auth users list
mosaic auth users create
mosaic auth sso
# Agent brain (projects, missions, tasks)
mosaic brain projects
mosaic brain missions
mosaic brain tasks
mosaic brain conversations
# Agent forge pipeline
mosaic forge run
mosaic forge status
mosaic forge resume
mosaic forge personas
# Structured logging
mosaic log tail
mosaic log search
mosaic log export
mosaic log level
# MACP protocol
mosaic macp tasks
mosaic macp submit
mosaic macp gate
mosaic macp events
# Agent memory
mosaic memory search
mosaic memory stats
mosaic memory insights
mosaic memory preferences
# Task queue (Valkey)
mosaic queue list
mosaic queue stats
mosaic queue pause
mosaic queue resume
mosaic queue jobs
mosaic queue drain
# Object storage
mosaic storage status
mosaic storage tier
mosaic storage export
mosaic storage import
# Schema migration is unavailable in this release. The current storage wrapper shells
# directly to `pnpm --filter @mosaicstack/db db:migrate`; it is legacy N-1,
# uncertified, and MUST NOT be invoked pending KBN-101-02/-03/-06/-08 activation.
pnpm typecheck # TypeScript type checking (all packages)
pnpm lint # ESLint (all packages)
pnpm test# Vitest (all packages)
pnpm format:check # Prettier check
pnpm format # Prettier auto-fix
```
### CI
Woodpecker CI runs on every push:
-`pnpm install --frozen-lockfile`
- **Legacy N-1 CI status only — active, uncertified, and non-authorizing as an operator route:** the checked-in job currently invokes `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` against an isolated disposable PostgreSQL CI database. It performs direct DDL in that CI database, is not approved ordinary behavior or an operator route, and remains a known exception pending KBN-101-06 removal/replacement by the certified runner-backed CI path.
-`pnpm test` (Turbo-orchestrated across all packages)
npm packages are published to the Gitea package registry on main merges.
Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding), quality gates, and timeouts. The board review uses multiple AI personas (CEO, CTO, CFO, COO + specialists) to evaluate briefs before committing resources.
## Upgrading
Run the installer again — it handles upgrades automatically:
DTOs go in `*.dto.ts` files at module boundaries. Scratchpads (`docs/scratchpads/`) are mandatory for non-trivial tasks. See `AGENTS.md` for the full standards reference.
## License
Proprietary — all rights reserved.
## Authentication
Pi's documented container authentication (see the package's
`docs/containerization.md`) is used, in this order:
1.**Read-only mounted credential file** (default): the host pi auth file
`~/.pi/agent/auth.json` is bind-mounted read-only to
`/home/node/.pi/agent/auth.json`. The host file holds a static API-key
entry for the built-in `zai` provider, so no token refresh writes are needed.
2.**Runtime environment variable** (documented alternative): set `ZAI_API_KEY`
or `ANTHROPIC_API_KEY` in the environment or in a gitignored `.env`; compose
passes them through. Pi's documented precedence applies.
Credentials are never committed, never copied into the image, and never printed.
Mosaic-managed named accounts (`agent.sh --auth`) live under the data root
(`auth/<account>.json`, 0600) — the stack never writes into `~/.pi`.
`.env.example` contains non-secret settings only.
## Boundaries honored
- No mounts of `~/.mosaic` or `~/.config/mosaic`; no Docker socket mount.
- Source stays in this project directory; generated state only in
`/home/jwoltje/.mosaic-dev` (host) and `/var/lib/mosaic` (container).
- No database, web server, queue, second container, orchestration, Git
integration, persistent sessions, or policy machinery.
-`mock` — deterministic echo of `MOSAIC_MOCK_RESPONSE`. Test-only: never use
it where a real model response is required.
-`claude` — the host's `claude` CLI, for managed sessions only (below):
`MOSAIC_CLAUDE_BIN` when set, else `claude` on `PATH`.
Not a worker adapter: the image has no `claude`, and the adapter refuses
without the bundle's hook and MCP files.
## Managed sessions (slice 1 S6)
The session runner (`packages/harness/src/runner.mjs`) runs one adapter
call per turn on the host, as `/bin/sh <adapters/<name>/adapter.sh>` (the
repository keeps adapters 0644; only the image sets the mode), in the
session's workspace and its own process group. Same contract, plus:
| Variable | Meaning |
|---|---|
| `MOSAIC_WORKSPACE` | The session's workspace; the adapter runs there. |
| `MOSAIC_SESSION_DIR` | The session's persistent directory; later turns resume the session kept there. |
| `MOSAIC_TOOLS` | The built-in tool limit (pi names for `pi`, Claude Code names for `claude`), comma-separated. |
| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. |
| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. |
| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. |
| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. |
| `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. |
| `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. |
The layers each adapter relies on, and what they don't cover, are in
`packages/harness/README.md`.
### A role's skills (row 64)
A managed session's skills reach both adapters as text in the bundle's
`prompt.md`, which they pass with `--system-prompt`. Neither adapter loads a
skill of its own: `pi` keeps `--no-skills`, since role sessions never get
`MOSAIC_SKILLS`, and `claude` keeps `--disable-slash-commands` and
`--restricted`. Sage chose this on 2026-10-19 from a probe, recorded in
`agents/sage/work/queue-64/probe.md`:
- Pi's `--skill` puts only a skill's name and description in the prompt,
and the model has to `read` the SKILL.md itself. The gate refuses every
file-tool path outside the workspace, so the session couldn't open its
own skills. Opening the gate to the bundle would change a security
boundary.
- Claude Code loads a bundle-local `--plugin-dir` skill under `--restricted`
only with its `Skill` tool on, and then it also offers its own bundled
skills. Which ones it offers changes with the Claude Code version, and the
`Skill` tool would pass through the gate hook.
The cost is prompt size: a role's skill text is in every turn, where the
other routes load a skill when it's used. The bundle caps a role's skills at
64 KiB together, since `claude` passes `prompt.md` as one argument and Linux
*)echo"claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2;exit2;;
esac
[ -f "$MOSAIC_CLAUDE_BIN"]&&[ -x "$MOSAIC_CLAUDE_BIN"]||{echo"claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2;exit 2;}
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
mkdir -p "$MOSAIC_WORKSPACE""$MOSAIC_SESSION_DIR"
cd"$MOSAIC_WORKSPACE"
# Session: the first turn names a new session id, later turns resume it.
# The id is kept only after a turn succeeds, so a failed first turn never
# leaves a --resume of a session that doesn't exist.
ID_FILE="$MOSAIC_SESSION_DIR/claude-session-id"
if[ -s "$ID_FILE"];then
SESSION_FLAG=--resume
SESSION_ID=$(cat "$ID_FILE")
else
SESSION_FLAG=--session-id
SESSION_ID=$(cat /proc/sys/kernel/random/uuid)
fi
# Flags:
# -p one-shot: print the answer and exit
# --output-format text stdout is the answer only
# --system-prompt the generated prompt replaces the default
# --effort the instance's thinking var (row 65; 2.1.302 takes
# low, medium, high, xhigh, max)
# --restricted no user/project/local settings files; --settings
| Darkwing | Hands-on engineering; collaborating seat under Sage | Pi, configured Mosaic model | `agents/darkwing/launch.sh` |
| Dewey | Frontend design, UX, accessibility, and UI implementation | Pi, configured Mosaic model | `agents/dewey/launch.sh` |
| Rocko | General development, investigation, testing, and review | T3 session (Codex, gpt-6.1-sol, thread d99326cc, lead decision 122; Claude thread b84bb264 retired 2026-10-12); launcher Claude Code, Sonnet model | `agents/rocko/launch.sh` |
| Filbert | General development, investigation, testing, and review | T3 session (Claude, Opus 5.5, thread 048f3c63, non-sandbox reviews only, lead decisions 130 and 134; Codex threads f712d8af and 50342f6a retired 2026-10-13, Claude thread 9cb9731e retired 2026-10-12); launcher Pi, `openai-codex/gpt-6-astra:low`, not to be used under R26 until it changes (lead decision 69) | `agents/filbert/launch.sh` |
| Researcher | Source-grounded technical research and evidence | T3 session (Claude, Sonnet 5.5, thread 9bac0794, rows 97 and 98, lead decision 124); Haiku rows (71, 80, 84, 88, later 96) run as subagents Sage dispatches with model haiku, effort high and a worktree (lead decision 129); Haiku thread c98aaf4a retired 2026-10-13 unused; launcher Pi, configured Mosaic model | `agents/researcher/launch.sh` |
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
own conversation; a first launch starts one. See each agent's README for
context inputs, authentication, and recovery details. Launch scripts can also
be invoked by absolute path from any directory. No assignment or model request
is submitted by the launcher itself.
The shared Pi helper supports `--provider NAME`, `--model ID`, and
`--thinking LEVEL` as per-launch overrides of the validated system defaults.
Filbert's wrapper appends the required provider, model and thinking flags so
its launch configuration remains fixed, including on resume. Use Filbert's
wrapper to select that configuration; a direct shared-helper invocation uses
its own supplied flags or the system defaults.
All agents follow repository governance and current user direction. Team
leadership does not add deployment or push authority. Coordinate overlapping
work with Sage and preserve other sessions' changes.
Before 2026-09-26 Sage worked only on DYOR strategy and sat outside the
development queue. Jason then made Sage the project lead and Darkwing a
collaborating seat. A separate fleet Sage seat under `~/.mosaic` is being
decommissioned; it does not speak for this seat. Whether the DYOR strategy work
continues is Jason's call. Joe retains DYOR engineering.
{"mode":"race","delayMs":12,"readyMs":null,"termMs":20,"child":3664563,"self":3664322,"show":["Id=dw-r46-race-12-3662746-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=cc912ab9a7fe47dd9e3b97486abf0bc3","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-3.scope","Delegate=yes"],"before":[3664322,3664563],"after":[3664322,3664563],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":69,"child":3665812,"self":3665626,"show":["Id=dw-r46-race-60-3665599-0.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=14681de5ac844879bdfcbcdd3dae8f10","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-0.scope","Delegate=yes"],"before":[3665626,3665812],"after":[3665626,3665812],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":71,"child":3666377,"self":3666179,"show":["Id=dw-r46-race-60-3665599-1.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=3128eba3ed234776ae4a61cb12fe1210","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-1.scope","Delegate=yes"],"before":[3666179,3666377],"after":[3666179,3666377],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":70,"child":3666924,"self":3666728,"show":["Id=dw-r46-race-60-3665599-2.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=836de499898d4b6996739e1b2a433fa0","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-2.scope","Delegate=yes"],"before":[3666728,3666924],"after":[3666728,3666924],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":69,"child":3667387,"self":3667251,"show":["Id=dw-r46-race-60-3665599-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=0ebf8d9d930a43629668e10c08f80e0d","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-3.scope","Delegate=yes"],"before":[3667251,3667387],"after":[3667251,3667387],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":70,"child":3667888,"self":3667727,"show":["Id=dw-r46-race-60-3665599-4.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=9c3594ae3018458594bb3ff996dc0d9a","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-4.scope","Delegate=yes"],"before":[3667727,3667888],"after":[3667727,3667888],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":30,"termMs":38,"child":3668171,"self":3668085,"show":["Id=dw-r46-ready-0-3668072-0.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=4c2c3b3ad52a477e8f735abedf158cbb","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-0.scope","Delegate=yes"],"before":[3668085,3668171],"after":[3668085,3668171],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":22,"termMs":29,"child":3668354,"self":3668284,"show":["Id=dw-r46-ready-0-3668072-1.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=b9da06280a8a4c6682f05a2d4ff118cf","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-1.scope","Delegate=yes"],"before":[3668284,3668354],"after":[3668284,3668354],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":22,"termMs":28,"child":3668425,"self":3668417,"show":["Id=dw-r46-ready-0-3668072-2.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=aab0b83d085e4bac86fdbe1ce65f23dc","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-2.scope","Delegate=yes"],"before":[3668417,3668425],"after":[3668417,3668425],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":23,"termMs":30,"child":3668509,"self":3668482,"show":["Id=dw-r46-ready-0-3668072-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=d076a3b629554b83bded41cb00e667b8","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-3.scope","Delegate=yes"],"before":[3668482,3668509],"after":[3668482,3668509],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":23,"termMs":30,"child":3668635,"self":3668624,"show":["Id=dw-r46-ready-0-3668072-4.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=6b46a0b279ca454eaa034d48a4de7385","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-4.scope","Delegate=yes"],"before":[3668624,3668635],"after":[3668624,3668635],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
✔ W1: two processes acquire the same pair at once; exactly one claim (124.186492ms)
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (11.494379ms)
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (5.019078ms)
✔ W2: acquire while a claim is reserved or active refuses already-active (169.129732ms)
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (231.756886ms)
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (192.859955ms)
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (35.003128ms)
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (21.132326ms)
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (3.242382ms)
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (5566.910492ms)
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (22659.492505ms)
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (201.806859ms)
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (116.242029ms)
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (262.226787ms)
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (220.425841ms)
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (240.127173ms)
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (35.945246ms)
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (98.424488ms)
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (32.141383ms)
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (89.672997ms)
✔ W11: the controller writes no session file; only the fake engine's own appends appear (23.121134ms)
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (54.682806ms)
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (57.719055ms)
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (4.12463ms)
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.12384ms)
✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.84306ms)
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2507.919568ms)
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (153.327228ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2414.676654ms)
✔ K4: two engines; force stop one; the other survives by independent observation (4314.515513ms)
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2190.378919ms)
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2244.796524ms)
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2189.63935ms)
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (4473.315326ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (482.430287ms)
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (408.721794ms)
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (307.075836ms)
✔ K6: recover without proof, without confirmation, or with changed pins is refused (71.309069ms)
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (35.403696ms)
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (45.424183ms)
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3030.941578ms)
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (5.325961ms)
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (30.688596ms)
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (24.893734ms)
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (34.176579ms)
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (30.425031ms)
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (27.293519ms)
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (42.046162ms)
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (21.340651ms)
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (1.34914ms)
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (29.201111ms)
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (138.365864ms)
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (39.366805ms)
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (21.443107ms)
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (31.257304ms)
✔ 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 (26.594189ms)
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.181347ms)
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (26.220793ms)
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (42.195378ms)
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (41.055698ms)
✔ terminal: engine control characters are made visible; a lost connection refuses submit (28.737455ms)
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.53917ms)
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.351376ms)
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.356358ms)
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.168609ms)
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (359.398761ms)
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (56.47649ms)
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (79.112044ms)
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (131.150736ms)
✔ H4: self-takeover is refused (22.440288ms)
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (87.314186ms)
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (104.583327ms)
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (35.050855ms)
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (70.09079ms)
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (129.262166ms)
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (15.147609ms)
✔ H13: a retry with the same request ID and different text is refused (13.200342ms)
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (133.123623ms)
✔ H15: a revoked connection's command is refused; the revocation fence holds (65.378107ms)
✔ H16: a second controller for the same session refuses already-active; the first is untouched (16.417034ms)
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (64.083554ms)
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (56.969236ms)
✔ H18: two prompts before any native output: the second refuses busy; one engine write (15.033626ms)
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (119.397422ms)
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.606652ms)
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (464.542066ms)
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (357.812591ms)
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (337.123938ms)
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2377.836435ms)
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (482.017907ms)
✔ a plain conversation: catalogue row, one page, CHAT-01 records (10.196509ms)
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (3.091521ms)
✔ F1: a malformed line is an unavailable part at its position, and reading continues (2.999666ms)
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (3.746417ms)
✔ F1: an unreadable fork is never merged into another branch's history (3.733154ms)
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (3.881773ms)
✔ F1: a file whose entries are all unreadable shows a notice per line (1.563335ms)
✔ F2: a truncated trailing line marks the view incomplete, not an error (2.494119ms)
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (5.109673ms)
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (4.939351ms)
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (5.516645ms)
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.793786ms)
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (10.497498ms)
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (9.313922ms)
✔ F8: a file swapped for a symlink after the catalogue is refused (2.247728ms)
✔ F9: registrations never add or redirect a root (2.060491ms)
✔ F10: a header cwd naming another project is refused (3.630852ms)
✔ F11: parentSession renders with a marker and the parent is never opened (0.936984ms)
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (4.789444ms)
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.415895ms)
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.349225ms)
✔ F13: compaction is a marker in place, then the retained content (0.756245ms)
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (737.947726ms)
✔ fragments never cut a surrogate pair and keep an empty string (9.768434ms)
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (2.629953ms)
✔ unknown conversations, empty files and non-Pi files refuse (2.988026ms)
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.388415ms)
✔ a seat directory without search permission refuses that root, not the catalogue (2.248255ms)
✔ every page and cursor is a valid CHAT-01 record (847.042088ms)
✔ the engine pin holds for the installed package (2.933293ms)
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (364.994239ms)
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (307.845583ms)
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (80.815781ms)
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (57.148386ms)
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (24.835014ms)
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (21.576912ms)
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (42.350718ms)
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (37.456929ms)
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (119.71406ms)
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (46.194593ms)
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1530.004862ms)
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (15.860593ms)
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (71.692215ms)
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (16.241237ms)
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (16.048665ms)
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (32.548668ms)
✔ N10: fake conformance (30.957332ms)
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (29.834953ms)
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (19.905915ms)
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (66.348845ms)
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (26.705932ms)
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (25.622164ms)
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (19.804122ms)
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (13.437887ms)
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (28.095743ms)
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (113.466843ms)
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (134.279618ms)
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (83.211293ms)
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (14.999139ms)
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (13.807672ms)
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (13.596018ms)
✔ 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 (37.274093ms)
console.log(JSON.stringify({mode,delayMs,readyMs,termMs,child:child.pid,self:process.pid,show,before,after,childInBefore:before.includes(child.pid),childInAfter:after.includes(child.pid),exit:exit??"alive 500 ms after TERM"}));
+// Every test stops its engine in finally: a fake pi left running after a
+// failed assertion keeps the test file from exiting.
+async function withEngine(extra, body) {
+ const started = start(makeRoot(), extra);
+ try {
+ await body(started);
+ } finally {
+ await started.engine.stop();
+ }
}
test("engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file", () => {
@@ -56,8 +69,7 @@ test("engine: with tools, buildPiArgs turns pi's own tools off, loads the extens
assert.equal(rw[rw.indexOf("--tools") + 1], "list_dir,read_file,search,write_file,edit_file", "a writable root adds exactly the two write tools");
});
-test("engine: a run with tool turns settles once, on the answer, with every tool call in the result", async () => {
- const { engine } = start(makeRoot());
+test("engine: a run with tool turns settles once, on the answer, with every tool call in the result", () => withEngine({}, async ({ engine }) => {
const r = await engine.prompt("tools 3");
assert.equal(r.text, "read 3 file(s)");
assert.equal(r.turns, 2);
@@ -71,45 +83,38 @@ test("engine: a run with tool turns settles once, on the answer, with every tool
assert.equal(plain.turns, 1);
await idle(engine);
assert.equal(engine.busy, false);
- await engine.stop();
-});
+}));
-test("engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end", async () => {
- const { engine } = start(makeRoot());
+test("engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end", () => withEngine({}, async ({ engine }) => {
const r = await engine.prompt("toolonly");
assert.equal(r.text, "", "no text: the connector turns this into engine-empty");
assert.equal(r.tools.length, 1);
const again = await engine.prompt("retry");
assert.equal(again.text, "after retry");
- await engine.stop();
-});
+}));
-test("engine: one prompt, one turn, text and usage come back", async () => {
-test("engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order", async () => {
- const { engine, commands } = start(makeRoot());
- const first = engine.prompt("slow 150");
- await new Promise((r) => setTimeout(r, 20));
+test("engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order", () => withEngine({}, async ({ engine, commands }) => {
+ const first = engine.prompt("slow 300");
assert.equal(engine.busy, true);
const second = engine.prompt("second");
assert.equal(engine.pendingCount, 2);
- await new Promise((r) => setTimeout(r, 20));
- assert.equal(commands().filter((c) => c.type === "prompt").length, 1, "the second prompt is not sent while pi is busy");
-test("engine: a held prompt that times out before pi settles fails on its own and is never sent", async () => {
- const { engine, commands } = start(makeRoot());
+test("engine: a held prompt that times out before pi settles fails on its own and is never sent", () => withEngine({}, async ({ engine, commands }) => {
const first = engine.prompt("slow 200");
await new Promise((r) => setTimeout(r, 20));
await assert.rejects(engine.prompt("late one", { timeoutMs: 50 }), (e) => e.details.code === "timeout" && /waiting for the engine/.test(e.message));
@@ -130,50 +133,85 @@ test("engine: a held prompt that times out before pi settles fails on its own an
-test("engine: a malformed JSONL line fails the turn, not the process", async () => {
- const { engine, logs } = start(makeRoot());
+// "mute" is accepted and never run, so no agent_start, agent_end or settle
+// ever comes for it. Unbounded, it would hold every later prompt.
+test("engine: a timed-out turn pi never started holds the next prompt only for the abort grace, then leaves the queue", () => withEngine({ abortGraceMs: 150 }, async ({ engine, commands }) => {
+// Every test stops its engine in finally: a fake pi left running after a
+// failed assertion keeps the test file from exiting.
+async function withEngine(extra, body) {
+ const started = start(makeRoot(), extra);
+ try {
+ await body(started);
+ } finally {
+ await started.engine.stop();
+ }
}
test("engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file", () => {
@@ -56,8 +71,7 @@ test("engine: with tools, buildPiArgs turns pi's own tools off, loads the extens
assert.equal(rw[rw.indexOf("--tools") + 1], "list_dir,read_file,search,write_file,edit_file", "a writable root adds exactly the two write tools");
});
-test("engine: a run with tool turns settles once, on the answer, with every tool call in the result", async () => {
- const { engine } = start(makeRoot());
+test("engine: a run with tool turns settles once, on the answer, with every tool call in the result", () => withEngine({}, async ({ engine }) => {
const r = await engine.prompt("tools 3");
assert.equal(r.text, "read 3 file(s)");
assert.equal(r.turns, 2);
@@ -71,45 +85,38 @@ test("engine: a run with tool turns settles once, on the answer, with every tool
assert.equal(plain.turns, 1);
await idle(engine);
assert.equal(engine.busy, false);
- await engine.stop();
-});
+}));
-test("engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end", async () => {
- const { engine } = start(makeRoot());
+test("engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end", () => withEngine({}, async ({ engine }) => {
const r = await engine.prompt("toolonly");
assert.equal(r.text, "", "no text: the connector turns this into engine-empty");
assert.equal(r.tools.length, 1);
const again = await engine.prompt("retry");
assert.equal(again.text, "after retry");
- await engine.stop();
-});
+}));
-test("engine: one prompt, one turn, text and usage come back", async () => {
-test("engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order", async () => {
- const { engine, commands } = start(makeRoot());
- const first = engine.prompt("slow 150");
- await new Promise((r) => setTimeout(r, 20));
+test("engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order", () => withEngine({}, async ({ engine, commands }) => {
+ const first = engine.prompt("slow 300");
assert.equal(engine.busy, true);
const second = engine.prompt("second");
assert.equal(engine.pendingCount, 2);
- await new Promise((r) => setTimeout(r, 20));
- assert.equal(commands().filter((c) => c.type === "prompt").length, 1, "the second prompt is not sent while pi is busy");
-test("engine: a held prompt that times out before pi settles fails on its own and is never sent", async () => {
- const { engine, commands } = start(makeRoot());
+test("engine: a held prompt that times out before pi settles fails on its own and is never sent", () => withEngine({}, async ({ engine, commands }) => {
const first = engine.prompt("slow 200");
await new Promise((r) => setTimeout(r, 20));
await assert.rejects(engine.prompt("late one", { timeoutMs: 50 }), (e) => e.details.code === "timeout" && /waiting for the engine/.test(e.message));
@@ -130,50 +135,177 @@ test("engine: a held prompt that times out before pi settles fails on its own an
+ assert.ok(await until(() => exited !== null), "pi exits and onExit hears of it");
+ await new Promise((res) => setTimeout(res, 400));
+ assert.ok(!commands().some((c) => c.message === "after stall"), "nothing was sent after the grace");
+ });
});
-test("engine: a malformed JSONL line fails the turn, not the process", async () => {
- const { engine, logs } = start(makeRoot());
+// The same case in memory, after Rocko's reproducer: the old run's events
+// arrive after the grace while pi is still exiting. They land on the failed
+// turn, nothing more is written to pi, and only the exit ends the engine.
+// Pi's response to the old prompt comes either before its timeout or only
+// with the late events.
+for (const lateResponse of [false, true]) test(`engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (${lateResponse ? "late" : "early"} prompt response)`, async () => {
-const clean = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
+export const clean = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
const decimal = value => typeof value === 'number' ? value.toFixed(1) : value;
export function totalsLine(t) {
return `Totals: issues closed ${t.issuesClosed} | median hours open ${decimal(t.medianHoursOpen)} | commits ${t.commits} | follow-ups per issue ${decimal(t.followUpsPerIssue)} | human messages per closed issue ${decimal(t.humanMessagesPerClosedIssue)}`;
@@ -181,5 +222,12 @@ export function formatTable(report) {
const range = dateRange('2026-09-06', '2026-09-12');
+// T3 fixture schema: the live tables, cut to the columns the reader uses plus
+// one it doesn't. `text` allows NULL so a non-text row can be tested.
+const T3_SCHEMA = `
+ create table projection_projects (project_id text primary key, title text not null, workspace_root text not null, deleted_at text);
+ create table projection_threads (thread_id text primary key, project_id text not null, title text not null, archived_at text, deleted_at text);
+ create table projection_thread_messages (message_id text primary key, thread_id text not null, role text not null, text, created_at text not null);
+ create table orchestration_events (sequence integer primary key autoincrement, stream_id text not null, event_type text not null, payload_json text not null, metadata_json text not null);`;
+// Writes a T3 database in WAL mode. Threads default to project p1, which is
+// the fixture root. Returns the open writer when keepOpen is set.
+ if (missing.length) throw new SourceError(`T3 schema changed: missing ${missing.join(', ')}`);
+ // Compared in JavaScript so a declared collation can't loosen the match.
+ const projects = db.prepare('select project_id, workspace_root from projection_projects where deleted_at is null').all()
+ .filter(p => p.workspace_root === root);
+ if (projects.length !== 1) {
+ throw new SourceError(`T3 has ${projects.length ? 'more than one project' : 'no project'} for ${root}; a project opened through a symlink does not match; ${SKIP}`);
+ const messages = db.prepare(`select m.message_id, m.thread_id, m.role, m.text, m.created_at from projection_thread_messages m
+ join projection_threads t on t.thread_id = m.thread_id where t.project_id = ?`).all(projectId);
+ for (const m of messages) {
+ const thread = threads.get(m.thread_id);
+ if (!thread) continue;
+ const where = `T3 message ${clean(m.message_id)} in thread ${clean(m.thread_id)}`;
+ if (m.role !== 'user' && m.role !== 'assistant') throw new SourceError(`${where} has an unknown role`);
+ if (typeof m.text !== 'string') throw new SourceError(`${where} has non-text content`);
+ if (typeof m.created_at !== 'string' || !Number.isFinite(Date.parse(m.created_at))) throw new SourceError(`${where} has an invalid created_at`);
+ if (m.role !== 'user') continue;
+ // A header addressed to its own thread must agree with the title mapping.
+ const header = t3Header(m.text);
+ if (header && header.toId === thread.id) {
+ const to = header.to.toLowerCase();
+ if (thread.seat ? to !== thread.seat : seats.includes(to)) {
+ throw new SourceError(`T3 header conflict: thread ${clean(thread.id)} "${clean(thread.title)}" maps to ${thread.seat ?? 'no seat'}, but a header addresses ${clean(header.to)}`);
run no-project-check $GS' if (!p) throw new Refusal("unknown-project");'''$GT
run no-busy-check $GS'if (active.size >= maxActive) return''if (false) return'$GT
run no-closing-answer $GS$' waiting.set(socket, () => done(new Refusal("host-closing")));\n'''$GT
# The body's total deadline
run no-body-deadline $GS'), deadlineMs);''), 2 ** 31 - 1);'$GT
# fetch-aborted
run abort-as-git-failed $GS'const aborted = () => new Refusal("fetch-aborted",''const aborted = () => new Error('$GT
# A reviewer that hangs up once it has every byte was served. The first cut
# filed any close before 'finish' as fetch-aborted; so does this mutant.
run close-is-aborted $GS'const served = () => !socket.errored;''const served = () => socket.writableFinished;'$GT
# A queued write that fails is fetch-aborted.
run errored-ignored $GS'const served = () => !socket.errored;''const served = () => true;'$GT
# have: working refs first, at most HAVE_MAX
run have-origin-first $GTL'for (const remote of ["refs/remotes/working", "refs/remotes/origin"])''for (const remote of ["refs/remotes/origin", "refs/remotes/working"])'$HT
BASE: 8e0fbca420a3161ce98c5e0f3910f500daabebba. Candidate: build.patch against BASE; candidate-manifest.sha256 lists the files.
## What changed
- packages/seat/src/working.mjs. A git past GIT_TIMEOUT_MS gets SIGTERM, then SIGKILL after GIT_GRACE_MS (5 s). git removes its lockfiles on SIGTERM. A rollback whose update-ref fails refuses rollback-failed, which outranks record's own error. push and review take `limits` so the tests can shorten both timers.
- packages/cli/src/git-socket.mjs.
- readBody has a total deadline, `bodyDeadline(size) = 30000 + ceil(size / 256)` ms (161 s at the 32 MiB cap), beside the 30 s idle timeout. Both refuse body-timeout.
- The bundle send to a reviewer is a loop, `send()`. It used to be pipeline(), whose ERR_STREAM_PREMATURE_CLOSE became git-failed. A close while chunks are still to go is fetch-aborted, and so is a close after a write of a queued chunk failed (EPIPE). A close after every chunk left with no failed write is a served fetch, because the runner reads exactly `size` bytes and hangs up, often before the host's loop sees the file's end or before 'finish'.
- git-failed and rollback-failed go to the log as `git socket: <code>: <message>`.
- close() answers host-closing to a connection still reading its request line, and drops the rest as before.
- `maxActive` and `bodyDeadline` are options for the tests.
- packages/harness/src/git-tools.mjs. fetch_review's `have` lists every clone's working refs, newest first, before any of origin's, and stops at HAVE_MAX.
- packages/seat/README.md: rollback-failed, the SIGTERM/SIGKILL timing, and the lock's one-launcher-per-dataRoot assumption.
- packages/cli/README.md: body-timeout, what nothing-to-fetch, unknown-project and fetch-aborted mean to a reviewer, the log lines, and host-closing.
## Tests
| Brief item | Test |
|---|---|
| not-a-role | git-socket.test: no role, then another business |
| host-closing | git-socket.test: close() while a request line is half sent |
| git-busy | git-socket.test: maxActive 2, a third connection |
| root-unreadable | working.test: a root that doesn't exist |
| unknown-project | git-socket.test: a review naming a project the business lacks |
| timed-out push leaves no lock | working.test: a hook holds the ref lock, SIGTERM, no .lock, next push lands |
| SIGKILL escalation | working.test: a git that ignores SIGTERM |
| rollback-failed | working.test (delete and previous-ref cases) and git-socket.test (logged, trail) |
| trickling client | git-socket.test: body-timeout within the deadline, slot freed |
| fetch-aborted | git-socket.test: a reviewer leaves mid-bundle; no git-failed, no log |
| a served fetch isn't fetch-aborted | git-socket.test: the review loop now closes the socket and checks action.refused is empty; a raw reviewer takes a 1 MiB bundle and hangs up at once, ten times; `send()` against two stand-in sockets, one that never finishes and one whose queued write fails EPIPE |
| have over 32 refs | harness git-tools.test: 53 refs over two clones |
## Mutants
16 mutants, each in `agents/darkwing/work/queue-100/mutants.sh`, run with `mut.mjs` (one exact match, else exit 3) and `run.py` (own process group, killpg at 240 s). All 16 killed:
```
sigkill-first: killed (ℹ fail 1, ✖ a git past its time gets SIGTERM, leaves no ref lock, and the next push lands (10291.116)
no-escalation: killed (TIMEOUT after 240 s, ✖ a git that ignores SIGTERM gets SIGKILL once the grace period is over (20098.619138ms))
no-rollback-check: killed (ℹ fail 2, ✖ a record that fails and can't be undone refuses rollback-failed, which the host logs (39)
rollback-not-logged: killed (ℹ fail 1, ✖ a record that fails and can't be undone refuses rollback-failed, which the host logs (41)
no-root-check: killed (ℹ fail 2, ✖ a root that can't be read refuses root-unreadable (35.076601ms))
no-role-check: killed (ℹ fail 1, ✖ an identity with no role, or of another business, refuses not-a-role and moves no ref (2)
no-business-check: killed (ℹ fail 1, ✖ an identity with no role, or of another business, refuses not-a-role and moves no ref (2)
no-project-check: killed (ℹ fail 1, ✖ a review of a project the business no longer has refuses unknown-project (259.074398ms))
no-busy-check: killed (ℹ fail 2, ✖ past maxActive connections the socket refuses git-busy, and a hung-up connection frees i)
no-closing-answer: killed (ℹ fail 1, ✖ close() answers host-closing to a connection still sending its request line (180.301962m)
no-body-deadline: killed (ℹ fail 1, ✖ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (5212.)
abort-as-git-failed: killed (ℹ fail 2, ✖ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fau)
close-is-aborted: killed (ℹ fail 2, ✖ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (508)
errored-ignored: killed (ℹ fail 1, ✖ send: a close after every byte left is served; one that fails a queued write is fetch-ab)
have-origin-first: killed (ℹ fail 1, ✖ fetch_review's have lists the working refs before origin's, and no more than HAVE_MAX (1)
have-uncapped: killed (ℹ fail 1, ✖ fetch_review's have lists the working refs before origin's, and no more than HAVE_MAX (1)
```
no-escalation hits the 240 s cap: its test fails at its own 20 s timeout, and the wrapper git that ignores SIGTERM keeps node --test alive until run.py kills the group. The sweep after each run found none of my processes left.
### What the mutants found
The first cut of `send()` waited for 'finish' and filed any earlier close as fetch-aborted. abort-as-git-failed showed it: the review loop test logged `git-failed: the reviewer left before the bundle was sent` for both of its fetches, so with the real code each successful fetch was on the trail as `reviewer fetch-aborted`. The review loop test checked the log but not action.refused, so it passed. Instrumenting showed the close usually comes before the loop reads the file's end, not only before 'finish'. The fix is above; the review loop now checks action.refused after close(), and the three tests in the table cover the race. A mutant that counted bytes (`sent === size`) survived as an equivalent: the loop's `destroyed` check already refuses a close with chunks to go, so I dropped the counter.
## Gate
Run in a clean worktree at BASE with the candidate applied and node_modules symlinked, 16:49:50Z to 16:54:16Z on 2026-10-13, by `agents/darkwing/work/queue-100/gate.sh` (test-task under DOCKER_HOST=unix:///nonexistent). Every suite exited 0 with no failures:
```
bus rc=0 16:49:50-16:49:54 ℹ pass 127 ℹ fail 0 ℹ skipped 0 0 marks
business rc=0 16:49:54-16:49:55 ℹ pass 67 ℹ fail 0 ℹ skipped 0 0 marks
packages/ledger has tests but no package.json, so gate.sh prints a MODULE_NOT_FOUND line and "ledger: no test script" for it. I ran `node --test packages/ledger/tests/` on its own; 78 passed. The ledger tests don't touch the row's files.
The cli suite also passed eight runs in a row after the `send()` fix (five before the counter came out, three on the final code).
BASE..origin/refactor (a50c9d23a) touches none of the eight manifest paths. build.patch applied to a fresh BASE checkout reproduces candidate-manifest.sha256.
## Choices
- host-closing comes from the waiting map in close(). The `if (closed)` check after readHeader stays as a guard; with the map it is unreachable in practice.
- body-timeout is a new code for both timeouts. The idle timeout refused invalid-request before. broker.refuse takes any `/^[a-z-]{1,64}$/` code, so there is no schema change. The 30 s idle path has no test of its own; the deadline test uses the same done().
- fetch-aborted isn't logged: a reviewer leaving is not a host fault.
- `have` sorts by committer date, newest first, inside each remote.
## Not owned, for the reviewers
roles/reviewer.md and the harness README and tools.mjs are what a reviewer reads, and row 100 doesn't own them. The nothing-to-fetch text is in packages/cli/README.md. Pointing roles/reviewer.md at it is a follow-up.
## Follow-ups (not in this row)
- packages/seat/src/checkout.mjs git() and the harness runner's git() still use spawn's timeout with SIGKILL.
- The kill goes to the direct git child, not its process group; a hook or helper git spawns may outlive it.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.