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 ontod88613bac(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 on72c3fa598with 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.
43 KiB
TOOLS.md — command and tool reference
On-demand reference for agent sessions (conductors, bootstrapping agents,
reviewers). AGENTS.md routes here; this file carries the depth: usage,
inputs/outputs, exit codes, and safety notes for every entry point.
Reading guide: system entry points are scripts/*.sh (bash) or invoked via
node scripts/mosaic-task.mjs (node). Host-side helpers under tools/
(tmux messaging, watchers, prose checker) are covered under Tools
(host-side) below. Every script fails closed — missing
or invalid configuration/policy refuses the operation with a nonzero exit
and changes nothing.
Lifecycle
| Command | Purpose | Notes |
|---|---|---|
scripts/bootstrap.sh |
Create ~/.config/mosaic-dev/config.json if absent |
Idempotent; existing config validated, never rewritten |
scripts/build.sh |
Build the release image | Tag derived from RELEASE + pinned pi version |
scripts/hello.sh |
One-shot startup request | Prints model response on stdout |
scripts/verify.sh |
Full gated test | Exit 0 only on exact MOSAIC_HELLO_OK; EXPECTED_MARKER overrides for negative drills |
Tasks (missions, runs, evidence)
| Command | Purpose | Notes |
|---|---|---|
scripts/run-task.sh run <task.json> |
Execute a task | Immutable run record under <dataRoot>/runs/ |
scripts/run-task.sh validate <task.json> |
Strict validation | Writes nothing |
node scripts/mosaic-task.mjs show <runId> |
Inspect a run | Full record + snapshots + artifacts |
node scripts/mosaic-task.mjs list |
List runs | task/workspace/session columns |
node scripts/mosaic-task.mjs retry <runId> |
Re-execute a run's snapshot | New run dir; retriedFrom lineage recorded |
node scripts/mosaic-task.mjs prune [--keep=N] [--yes] |
Retention | Dry-run default; receipt in runs/.pruned.log |
node scripts/mosaic-task.mjs resolve-role <roleFile> |
Validate a role file, version 1 or 2 | Prints MOSAIC_ROLE_TOOLS / MOSAIC_ROLE_NETWORK, and MOSAIC_ROLE_CONTRACT for version 2; config-free |
Task fields: prompt (required), mission (path), expectExact,
timeoutSeconds (5–600), workspace (:run or named), capabilities.tools
(allowlist: read write edit bash grep find ls), session,
sessionForkFrom (requires session). Mission fields: objective,
directives[], optional governing capabilities.tools. Policy: a task may
narrow a mission's tools, never widen; empty intersection = tool-free run.
Agent (interactive TUI)
scripts/agent.sh <name> [--mission <file>] [--workspace <ws>] [--session <s>] [--tools <list>]
Launches an interactive pi TUI inside the container with the four immutable
contracts + optional mission + agent identity as its system prompt,
persistent named session, optional workspace. Exit with /quit.
A seat role (agent.json role) binds to roles/<role>.json (M18): the
contract's tools are a ceiling the seat definition or --tools may narrow,
never escalate past. Missing/invalid contract refuses the launch; empty
intersection = loud tool-free seat. An explicit MOSAIC_AGENTS_DIR override
that cannot resolve the named seat also refuses (#46) — unset the override
for the M13 plain governed TUI. --auth <account> injects
auth.<account>.json (beside the active credential file) as the launch's
PI_AUTH_FILE; a missing/invalid account refuses (M19).
For native repository development, opt in with a leading --host-dev:
scripts/agent.sh --host-dev darkwing [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE] [--provider NAME] [--model ID] [--thinking LEVEL]
This mode delegates to scripts/agent-host-dev.sh, uses host Pi and repository
tools/skills plus the development goal extension, and keeps its own sessions
under .pi/state/<name>/. It uses native Pi authentication and does not run
container release alignment or apply managed seat role ceilings. It is a host
development session, not a sandboxed worker. Container-only flags such as
--auth, --mission, and --tools are rejected in this mode. Omitting
--host-dev retains the existing container lifecycle and policy checks;
container failures never trigger a host fallback. Darkwing's agent-local shim
selects host development explicitly. See agents/darkwing/README.md.
Auth (credentials)
Credential checkpoint over pi's auth model (one auth.json keyed by
provider; resolution order --api-key → auth.json → env → models.json).
No credential material is ever printed — provider names, credential types,
and env var NAMES only.
Ownership rule (#48): ~/.pi is read-only to the stack, permanently. The
only interaction is the existing read-only container mount of the default
credential (PI_AUTH_FILE, default ~/.pi/agent/auth.json). Mosaic-managed
accounts live under the data root: <dataRoot>/auth/<account>.json, perms
0600 (mirroring scripts/gitea-api.sh hygiene — loose perms are flagged in
listings and refused by --auth).
| Command | Purpose | Notes |
|---|---|---|
scripts/auth.sh status |
Report both credential sources | Default harness credential (read-only) + mosaic-managed accounts; never prints material |
scripts/auth.sh accounts |
List mosaic-managed accounts | Under the data root; marks the active one; flags non-0600 |
agent.sh --auth <account> injects <dataRoot>/auth/<account>.json as the
launch's PI_AUTH_FILE; missing/symlinked/non-0600 accounts refuse.
Headless task runs keep the default credential.
Release
| Command | Purpose | Notes |
|---|---|---|
scripts/release.sh package |
Build + tag the release image | Tag: mosaic-poc-agent:<pi>-r<release> |
scripts/release.sh activate |
Health gate → atomic pointer swap | --fault-injection proves the refusal path |
scripts/release.sh rollback |
Health-gated return to previous | Refuses if image missing |
scripts/release.sh status |
Release, tag, active pointer, log | Safe on empty state |
scripts/release.sh ensure |
Self-determination: align active pointer to RELEASE |
Fast path restores a missing/mismatched pointer without a gate; slow path packages + health-gates first. Invoked automatically at launch |
Conductor (worker patches)
scripts/conductor-apply.sh <runId> [--dry-run]
Auto-applies a worker's patch under roles/conductor-policy.json:
succeeded run → clean target tree → path allowlist → syntax gates →
apply → policy suites → attribution commit. Any failure reverts.
Push is never automatic.
Conductor (landing approved work)
scripts/land-work.sh <decision-id> [--base refactor] [--dry-run] [--subject TEXT]
[--attribution LINE] [--business ID] [--project ID]
Turns a merge decision the human resolved into one gated local commit on
refactor (#1610). It reads <dataRoot>/bus/bus.sqlite read-only through
scripts/land-work/read.mjs and checks that the human picked the raise's
approval option, that the question names one work/vikunja-<p>-<t>, and
that the task's latest review.verdict approves a commit C that the work
ref in the working repository still points at. It then applies
git diff $(merge-base refactor C) C in a detached worktree, checks the
patch-id, runs every package suite except queue and every
scripts/test-*.sh, and commits. The subject is the approval option's
text unless --subject overrides it, and --attribution replaces the last
line, Applied-under: scripts/land-work.sh (conductor). --business and
--project are needed only when the config has more than one business file
or the business more than one project. The tool prints the line for the PM
and doesn't send it. --dry-run runs every check and the gate and writes
no ref. A refusal names its reason and exits 2; the reasons are listed at
the top of the script. The worktree, its node_modules link and its Docker
network are removed on every exit. The gate log directory stays and the
tool names it. Keep TMPDIR short: the gate inherits it, the cli suite
puts unix sockets under it, and a 46-byte TMPDIR failed 8 of those tests.
Push is a separate act.
Since #1631 the gate, its steps and the root-file rule live in
packages/seat/src/landing.mjs, and the decision checks in
packages/bus/src/landing.mjs. The bus host's land op uses both to land
an agent's task on its working repository's main, never on refactor
(packages/cli/README.md, "Git socket"). A decision raised for one target
can't land the other. land-work still accepts only a decision the human
resolved through the CLI.
Maintenance
| Command | Purpose | Notes |
|---|---|---|
scripts/reset.sh |
Delete the data root | Triple-safety-checked (path, symlink, ownership marker) |
scripts/test-config.sh |
Config selftests (no Docker) | 24 cases |
scripts/test-task.sh |
Task selftests + live cases | 90 cases |
scripts/test-release.sh |
Release selftests | 14 cases |
scripts/test-conductor.sh |
Auto-apply selftests (sandboxed) | 17 cases |
scripts/test-auth.sh |
Auth checkpoint selftests (no Docker) | 13 cases |
scripts/test-land-work.sh |
Land-work selftests (sandboxed, fake Docker) | 84 cases |
scripts/gitea-api.sh <METHOD> <path> [body] |
Gitea API helper; reads the JSON credential file or a raw per-seat token file | Token never on argv/stdout |
Seat launch with registration (scripts/mosaic)
scripts/mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...]
scripts/mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]
Runs a seat's launch.sh unchanged (arguments after -- pass through) and
leaves one registration record at <dataRoot>/seats/<layout>/<seat>/registration.json
that the control board reads: seat, project, task, workspace, tmux session,
harness, start time, pid. Rewritten on every launch. seat task changes the
task field only. The four repository seats (agents/*/launch.sh) register
themselves through this path unless called with --check. Run it by
path from the checkout. On a pinned host, mosaic on PATH is a wrapper
around a pinned copy's scripts/mosaic
(Stack copies) and carries the human
commands; on Jason's host the estate CLI it replaced is mosaic-old.
Queue commands stay on scripts/mosaic queue in the canonical checkout,
because a copy refuses them (One pinned
copy). Exit
codes: the launch script's own once it runs; before that 1 could not
start, 2 invalid config or seat, 4 usage. Details and the record's
fields: packages/seat/README.md.
Work queue (scripts/mosaic queue)
scripts/mosaic queue list | show ID | next [SEAT]
scripts/mosaic queue add|move|release|assign|note|set ... --op ID [--by NAME]
scripts/mosaic queue verify [--current] | render [--check] | sync | unlock [--check-gate]
scripts/mosaic queue review request|resolve|abandon|record ... --op ID [--by NAME]
scripts/mosaic queue review verify-commit ID REF
scripts/queue-commit.sh -m MSG
docs/plans/queue.json holds the rows and an append-only log; the table in
docs/plans/QUEUE.md between the mosaic-queue markers is rendered from it.
Canonical checkout only. Every change needs an --op ID chosen before the
first attempt and reused on retry; only an op whose ok <op> rev N receipt
printed is done. The lead commits queue changes with scripts/queue-commit.sh;
the pre-commit guard refuses any other commit that stages the two queue files.
Moving a row with reviewers to in-review posts one review request comment
on its issue as the acting seat, with the seat's own token file in
MOSAIC_GITEA_CREDENTIAL_FILE; reviewers then run review record.
Exit codes: 0 ok · 1 failed · 2 invalid or refused · 3 uncertain,
retry the same op (for a review request, the retry sends nothing; resolve or
abandon it) · 4 usage. Details: packages/queue/README.md.
Business files and role instances (scripts/mosaic business)
scripts/mosaic business validate <business>
scripts/mosaic business resolve <business> <instance> [--project <id>]
Reads businesses/<business>.json next to the system config
($MOSAIC_CONFIG or ~/.config/mosaic-dev/), the version 2 role files in
roles/ (or $MOSAIC_ROLES_DIR) and each project's .mosaic/project.json.
Writes nothing. validate checks the whole business, stat-checks every
credential reference without opening it and prints one digest per role
instance. resolve prints one instance's merged variables, their sources,
its narrowed tools, network and authority, and its credential references.
The business file must belong to you and not be group or other writable.
Exit codes: 0 ok · 2 invalid file or credential reference · 3 system
config problem · 4 usage or a required file missing. Details:
packages/business/README.md.
Inbox, decisions and the bus host (scripts/mosaic)
scripts/mosaic # no argument: the full-screen app
scripts/mosaic inbox | tasks | agents [--business <id>] [--json]
scripts/mosaic inbox --read <id>... | --read-all [--business <id>]
scripts/mosaic decide <decision> <option> [--note <text>] [--yes]
scripts/mosaic trail <task|decision> [--json]
scripts/mosaic bus start <business> [--pm] | stop | status [--json]
scripts/mosaic talk <role> <text> [--wait SECONDS] [--business <id>]
scripts/mosaic project create <name> [--repo <repo>] | list [--json]
scripts/mosaic threads [--project <project>] [--json]
scripts/mosaic thread create <project> <name> | open <thread> | read <thread> [--json]
scripts/mosaic talk --thread <thread> <text> [--wait SECONDS | --follow]
scripts/mosaic follow --thread <thread>
scripts/mosaic stop <run>
scripts/mosaic launches off | on [--business <id>]
scripts/mosaic launches list [--json]
scripts/bus-service.sh render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
scripts/bus-service.sh install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
scripts/bus-service.sh uninstall | status <business>
The human commands read and resolve through the broker's human transport
(packages/bus/src/human-cli.mjs), never the SQLite file. They refuse inside
an agent run. bus start is the host the systemd user unit
mosaic-bus@<business> runs. It boots the broker, starts the Discord
notifier, and needs <dataRoot>/notify/<business>/notify.json. That
directory must be 0700: it also holds the notifier's journal sent.jsonl
(0600, never a symlink). On open the notifier copies a torn final line to
torn-<UTC stamp>.bin and truncates the journal to its last newline; a
malformed complete line refuses with exit 3.
bus-service.sh pins the Claude Code binary managed sessions run as the
unit's MOSAIC_CLAUDE_BIN, from --claude PATH or else command -v claude
(packages/cli/README.md, "Unit").
bus start also serves the session launcher (slice 1 S6): a PM session's
launch tool starts role sessions through it, each a runner under its own
PID namespace with a generated bundle, and --pm launches the business's
launch.by instance at start. A host that starts ends the launches a
crashed host left as host-lost and refuses with exit 3 on a
sessions.json it can't read. talk sends a REQUEST and prints only the
reply to it, marking only that read (polls every 2 s, --wait default 900;
0 only sends); other messages stay unread for mosaic inbox, and
--follow and follow print and read everything. inbox --read <id>...
marks unread messages read by the ids inbox prints, and --read-all marks
every one it lists (row 150); the cli README, "Human commands", says how
refusals print. When no run holds the
role, a line after sent says whether launch.by was told, or that the role
isn't in the launch block and launch.by can't launch it (row 152); the cli
README, "Human commands", lists the lines. stop sends SIGTERM to
a run's runner after checking it in
sessions.json. launches off|on revokes and restores role.launch for
the business; running sessions keep running. launches list reads
sessions.json and needs no broker. talk, stop and launches off|on
refuse inside an agent run.
With no argument on a terminal, scripts/mosaic opens the full-screen app:
Inbox, Tasks, Agents, Trail and Help, deciding through the same flow and
strings as decide. In an agent run it exits 3; without a terminal on
stdin and stdout it exits 4 with the usage.
Exit codes: 0 ok · 1 failed or outcome unknown · 2 invalid input ·
3 refused or config problem · 4 usage. Details: packages/cli/README.md.
Stack copies (scripts/stack-copy.sh)
scripts/stack-copy.sh copy COMMIT [--copies DIR] [--allow-worktree]
scripts/stack-copy.sh wrapper ID [--copies DIR] [--bin DIR]
scripts/stack-copy.sh check [--copies DIR] [--bin DIR] [--business ID]
One read-only tree supplies the bus, Console and human CLI (row 85,
#1579). Copies default to ${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies/<full id>.
copy requires a canonical checkout (.git directory); --allow-worktree
is a test override. COMMIT must be reachable from both local refactor and
origin/refactor. It archives the runtime/import closure, adapters, roles,
RELEASE, package.json and package-lock.json; refuses missing manifests;
runs npm ci inside the temporary copy; removes write permissions; and
renames beside the target. A published existing copy stays unchanged.
Dependencies use the exact lock and carry Pi's installed lock record.
Failure publishes nothing and removes the temporary tree.
wrapper is a live switching step, reserved for Jason or Mos. It accepts
only an existing copy and a regular non-symlink mosaic wrapper with the
Q81 marker on line 2 and its exact lease line to mosaic-old. It preserves
both lines, adds comments naming the copy root and Q81's mosaic-old lease
probe, and atomically replaces the wrapper's final exec with the
copy's scripts/mosaic. --bin is a fixture override; the default is
~/.npm-global/bin. Neither the Q81 worktree nor mosaic-old is removed.
check changes nothing: it reads effective systemd WorkingDirectory and
ExecStart (including drop-ins) for the bus and Console, plus the wrapper
exec. It prints all three paths and succeeds only on the same existing
copy; absent/failed reads and a mismatched ExecStart fail. Business defaults
to mosaic-stack. Queue work continues on the canonical scripts/mosaic.
Bus render/install accept --copy ID, --copies DIR, --pm and the unchanged --claude pin. Without --copy the rendered unit stays the checkout unit. --pm adds the PM startup; a refusal exits 3, and RestartPreventExitStatus prevents a retry. Install refuses any busy bus instance, even with --no-reload; a failed state/drop-in read also refuses. Any template or instance drop-in, or inherited mosaic-.service.d/ or service.d/ drop-in, assigning ExecStart or WorkingDirectory refuses, naming the file. Status reads effective roots, --pm and DropInPaths, reports checkout or copy identity and refactor drift, and writes nothing.
Pinned stack update
scripts/stack-release.sh adopt BUSINESS
scripts/stack-release.sh activate ID BUSINESS [--pm] [--migrate] [--timeout S] [--plan]
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
scripts/stack-release.sh status BUSINESS
common options: --copies DIR, --bin DIR
This is an operator plan, not an action agents run. Both reviews and the
landing come before it. Mos grants each live run first. A bus restart ends
running sessions, so run it only when no PM turn is in flight. --plan and
status change nothing and need no go.
The script keeps <dataRoot>/state/stack-active.json (the live copy, business,
PM flag, claude pin, Console flags, schema and activation) and appends every
run to <dataRoot>/state/stack-activation-log.jsonl: a started line, then
one final line with result ok, refused, gate-failed or
rollback-failed. A refusal exits 3, a gate failure that rolled back exits 1,
a failed rollback exits 2, usage exits 4. Only one run at a time holds
state/stack-release.lock (run-locked).
- Once per host,
adopt BUSINESSrecords the running stack as the pointer: the copystack-copy.sh checkreports, the bus unit's--pmandMOSAIC_CLAUDE_BIN, the Console's flags without--checkout, and the copy's schema version. It refuses when the check fails, the bus unit has noMOSAIC_CLAUDE_BINor a pointer exists. - Make the pushed target copy with
stack-copy.sh copy COMMITand note its full id. - Run
activate ID BUSINESS --planand take the output to Mos. It names the store's schema and the target's, any migration with--migrateand the decision 96 condition 2 line (Jason present), the commits the run adds, and the three install commands. It refusescopies-unrelatedwhen neither copy is an ancestor of the other. - Mos removes any bus drop-in that overrides ExecStart or WorkingDirectory;
activaterefusesdrop-in-overrideand names the file. - With Mos's go,
activate ID BUSINESS, plus--migratewhen the plan says so. Before it stops anything, it refuses a missing, unpushed or unreadable copy, a busymosaic-bus@instance, and unit flags that differ from the pointer. Then it saves the unit files, drop-ins and wrapper understate/stack-rollback/<activation>/, stops the Console then the bus, and reads the stopped store. A-walor-shmbeside it refusesstore-uncleanand leaves the store alone. A migration takes an owner-only backup under<dataRoot>/backups/stack/<activation>/first. It installs the bus, the Console and the wrapper from the pointer's flags, starts the bus then the Console, and runs the gate: both units active, a live bus host for this start, Console/healthz,stack-copy.sh checkand, with--pm, exactly onelaunchedjournal line for a live run of this start. A pass writes the pointer. - A gate failure puts back the saved units and wrapper, restores the backup
when a migration ran, keeps the store it replaced as
bus.post-attempt-<stamp>, starts the old copy and gates it. If that gate fails too, both units stay stopped, the log saysrollback-failed, and Jason or Mos settles it from the rollback set the message names. rollback BUSINESSreturns to the copy before the pointer's, taken from the log. When the schemas differ it needs--restore ACTIVATION; events written after that backup need--events Nwith the count it prints, and Jason decides first.- Jason's plain
mosaic talk pm "..."must print sent and a reply. Keep thestatusoutput with the receipts.
A started line with no final line is an interrupted run. status shows
it, and adopt, activate and rollback refuse until Jason or Mos settles
it. Nothing prunes copies, rollback sets, backups or post-attempt stores.
After a switch, the human commands and --tester work only through mosaic
on PATH; the checkout's scripts/mosaic exits 3 with human-required or
tester-required, and queue writes stay on it (packages/cli/README.md, One
pinned copy).
Console service (scripts/webui-service.sh)
scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy
scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit
scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
scripts/webui-service.sh status [--dir DIR]
# FLAGS: [--port N] [--business ID] [--board URL]
# [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
The systemd user unit mosaic-webui.service runs the Console,
node packages/webui/src/cli.mjs serve, with serve's flags as given:
--port defaults to 7332 (7330 is the operator's own Console), and the
preview set goes all three or none. --send (row 83) goes only with the
preview set and is refused otherwise (exit 1, nothing written); ExecStart
then ends with --send, and the Console needs the system config at start
(--send needs the bus configured, exit 2). Without --send the unit is
read-only. The template is
packages/webui/systemd/mosaic-webui.service.in.
Under --send the Console's Docs page (row 148) can also save an existing
spec/north-star.md or docs/NAME.md of a project: one local commit authored by the
business's human, refused stale-base if the file changed since the editor opened it.
It is the human bus verb project.file.save through the human transport; an agent
refuses it. Nothing is pushed. Without --send the page reads and previews only. See
packages/webui/README.md ("Spec and docs editor"). The page opens in a visual editor
built from reviewable source (row 154): third_party/editor/ is unmodified upstream
source with its licenses and PROVENANCE.json, and npm run build:editor in
packages/webui rebuilds src/public/editor.js and editor.css with esbuild 0.28.2;
the save, the verb and the routes are unchanged. See "Visual editor" there. A save of spec/north-star.md also
sends the business's pm and cto one notice naming the commit (row 143); a BLOCK from
either goes to the chief of staff and on to the assistant. See packages/bus/README.md
("Spec review notices").
The unit runs a pinned copy, not the working checkout (row 77, #1571).
copy COMMIT resolves COMMIT to its full id, refuses one not reachable
from both refs/heads/refactor and refs/remotes/origin/refactor, and
delegates to stack-copy.sh for the archive and pinned npm install into DIR/<id>/, by default
${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies/<id>/. It writes a
temp directory beside the target, makes every file and directory read-only
(so a writer that replaces by rename can't change it either), then renames
it, so the copy's path is always whole. A copy that exists is left alone
(exists:). install --copy ID and render --copy ID point
WorkingDirectory and ExecStart at the copy and pass serve
--checkout <this checkout>, where the queue read still runs; RELEASE,
the roles, the config check and the bus child come from the copy. The bus
child is the human transport, packages/bus/src/human-cli.mjs, and the bus
checks that path against its own, so the bus must run from the same copy
(below). install refuses without --copy, or when the copy is missing.
render without --copy prints the unit for the checkout, as before.
install --copy ID also refuses unless the bus runs from the same copy
(round 2, #225). The bus accepts a human proof only from its own
packages/bus/src/human-cli.mjs, so a Console on a copy whose bus runs
from the checkout would fail every bus call with human-required. With
--business ID, install reads [email protected]; without it, every
loaded mosaic-bus@ unit. It reads effective WorkingDirectory and ExecStart from
systemctl --user show -p LoadState -p WorkingDirectory -p ExecStart, so a drop-in counts,
and every ExecStart path must lie inside the copy. It refuses, naming both
paths, when one doesn't; when the unit is not installed or none is loaded;
when ExecStart has no path; and when the systemctl call fails. The check
runs before the unit state read and changes nothing.
install writes a temp file, renames it and runs
systemctl --user daemon-reload. install and uninstall refuse while the
unit is in any state but inactive or failed (systemctl --user list-units),
and say Jason or Mos stops it first; --no-reload skips only the
daemon-reload, never that check. If the state read itself fails they refuse
with one line naming the call, and change nothing. copy and install
refuse outside the canonical checkout (a .git directory, not a worktree's
.git file); --allow-worktree overrides that for the tests only. status
prints the unit state, the copy id it runs, whether refactor points at
that commit or another, whether the installed unit sends, and
GET /healthz on the installed port. It changes nothing.
Each value is held to a plain character set the unit can carry (no space,
%, $, quote, backslash, & or |), and the preview values must start
with a letter or digit; anything else is refused. With --auth-header X-authentik-uid, in any letter case, --allow-user must be the Authentik
uid, ^[0-9a-f]{64}$ (lead decision 101). The unit sets no MOSAIC_*,
CLAUDE*, CODEX* or PI_* variable. Serve's exit 2 (usage, config, or a
port already in use) is not restarted.
The update/rollback sequence is Pinned stack update above. The queue view reads the checkout and stays current. Copies remain until an operator removes them; nothing here prunes or migrates copies.
There is no start, stop, enable or restart verb. The Console reads the bus
through the human transport, so whoever starts it holds the human's
capability. Jason starts, stops and restarts it
(systemctl --user enable --now mosaic-webui), and so may Mos under lead
decision 102, which accepts that sends from a Console Mos started enter the
trail as Jason (human-cli). Any other agent starting it, even through
systemd, works around the human proof (lead decisions 62 and 94).
Exit codes: 0 ok · 1 failed or refused · 4 usage.
Discord connector (scripts/discord.sh)
scripts/discord.sh check <binding>
scripts/discord.sh run <binding> [--supervised]
scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
scripts/discord.sh recover <binding>
scripts/discord.sh reload <binding>
scripts/discord-service.sh render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
One seat's conversation reachable from listed channels of one Discord
server, chat only. <binding> names <dataRoot>/discord/<binding>.json
(0600, never committed; shape in packages/discord/fixtures/binding.example.json).
check validates the binding, token file mode, context files and pi, reads
the bot, guild and channels over REST, opens and closes one gateway
connection; it sends nothing. run serves turns until stop, SIGTERM or
SIGINT and refuses to start when STOP exists or a delivery cannot be
reconciled. stop writes STOP and signals the running process. unlock
writes STOP and removes a run.lock left by a dead process; it refuses
while the owner is alive with an unverifiable identity or its record cannot be
parsed. run refuses a stale lock rather than reclaiming it, and releases a
claim that meets STOP. recover is the supervised pre-start, and
run --supervised does it first in the same process: it refuses (exit 3)
while STOP is present or the binding is held, clears a lock whose owner
is gone and removes only the STOP it wrote for that. reload validates
the binding file and sends SIGHUP to the running connector, which applies
guildName, channels, users and limits in place and refuses anything
else, keeping the old binding; attempts are journaled in reloads.jsonl. A
user entry may carry channels, an allowlist of listed channel ids.
scripts/discord-service.sh install renders and writes the systemd user
unit [email protected] (one instance per binding, restart on
failure, exit 3 never retried, SIGTERM on systemctl --user stop, SIGHUP on
systemctl --user reload). With
install --copy ID [--copies DIR], a full id selects an existing shared
copy holding the Discord CLI and pi; ExecStart runs Node on that CLI with
--repo and WorkingDirectory in the copy. Without --copy the checkout unit
is unchanged. Status names its effective cwd/copy id, including drop-ins.
check --offline validates local binding/context/tool roots/pi/token mode
and reports journal/STOP without REST or gateway calls; run refuses this
flag. For a Mos-approved re-pin after the SetSpark freeze: make the copy,
run node <copy>/packages/discord/src/cli.mjs check <binding> --repo <copy> --offline,
save the installed unit, install --copy, then restart on Mos's go. Context
files resolve inside the copy and missing files refuse. Rollback restores
the saved unit and daemon-reloads/restarts on Mos's go; keep both copies.
The connector's pin is independent of stack-copy.sh check. An optional tools key in the binding declares
file roots; pi then runs with its own tools off and the package's
extension providing list_dir, read_file and search confined to those
roots, plus write_file and edit_file for roots marked "write": true
(temp file and rename, no folder creation, no dot-prefixed path, no
credential shapes), and web_fetch and web_search when tools.web
names a SearXNG instance (https only, public addresses only, three
redirects, capped body, html to text), with each call in the turn record.
A writable root that is a git work tree may carry git (branch,
identity, tokenFile, author, optional protocol: "vault") and
gains git_status, git_commit (explicit paths, seat author,
Requested-by: trailer, push at once), git_pull (ff-only) and
git_push (one branch, never force), with reserve_id under the vault
protocol; git runs with no host config and the package's own credential
helper reading a 0600 token file, never printed. tools.setspark names the
SetSpark record service and adds record_list, record_get,
record_create, record_update, resolve_id, open_approval_request,
get_approval_request and create_document, one fixed path each, writes
keyed per turn and call; approvals of its proposals are an Approve button
or an exact approve reply, checked by the connector against the
request's approvers, never by the model, with one Discord message per
approval as evidence. tools is a fixed key: changing
it needs a stop and start. Records
under <dataRoot>/discord/<binding>/: inbox.jsonl, outbox.jsonl,
drops.jsonl, admissions.jsonl, notices.jsonl, reloads.jsonl, approvals.jsonl, write-once turns/<id>.json. Suite:
scripts/test-discord.sh.
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 3
refused by a brake (a supervisor must not retry), 4 usage. Details:
packages/discord/README.md.
Tools (host-side)
Host-side helpers under tools/, outside the scripts/ command surface.
Per-tool READMEs: tools/tmux/README.md and tools/unslop-hook/README.md.
| Command | Purpose | Notes |
|---|---|---|
tools/tmux/agent-send.sh |
Inter-agent tmux message with addressing preamble | Reliable submit (bracketed paste, Enter flush, draft detection); ships send-message.sh over ssh for remote panes (remote needs only bash + tmux + base64) |
tools/agent-watch/agent-watch.sh |
Condition watcher per agent seat | One transient systemd --user timer + service per watch; fires agent-send.sh when the condition command exits 0 |
node tools/unslop-hook/unslop-check.js <file> |
Mechanical AI-tell prose check | Dependency-free node CLI + module driven by lists.json; extension.ts is the pi extension wrapper |
agent-send.sh prepends the preamble
[<src_host>:<src_session> -> <dst_host>:<dst_session>]; -C/--class adds
a class=<CLASS> token (terminal-log, actionable, human, reaction,
digest; consumers treat an absent class as actionable). Flags: -s dst
session (required) · -H ssh target for a remote pane · -L named tmux
socket · -n dst hostname for the preamble · -m/-f/stdin message body ·
-S source-label override · -r N Enter-flush attempts (default 2) · -v
verbose · -h help. Exit codes: 0 delivered/queued · 1 target not found ·
2 still draft · 3 usage error · 4 ambiguous socket (the session exists
on more than one tmux server; disambiguate with -L or MOSAIC_TMUX_SOCKET).
agent-watch.sh subcommands: start --name <id> --session <session> --when '<shell command; exit 0 = met>' --message <text> with --class,
--interval (default 30), --timeout (default 3600), --repeat,
--quiet-timeout, --socket · list · status [--json] · stop <name> ·
log <name> · meta-install [--interval 300] [--unit-name <unit>] ·
meta-remove [--unit-name <unit>]. Interval floor is 10s (a watcher is a
fallback cadence, never a tight poll); hidden _tick/_scan subcommands run
inside the systemd services. status exit codes: 0 clean · 3 any stale
watch or dead meta-watch · 6 systemd user bus unreachable. Delivery goes
through agent-send.sh; rc 2 means the text reached the pane as an
unsubmitted draft, which counts as delivered and is not retried (other
failures retry twice, then the watch gives up). Watches are one-shot by
default; --repeat re-arms. Notices carry a [watch:<name>] prefix.
unslop-check.js checks a file (or stdin) against the word, phrase,
punctuation-density, and pattern lists in lists.json, stripping fenced and
inline code first so a quoted mention never flags. Invocation:
node tools/unslop-hook/unslop-check.js <file>; UNSLOP_LISTS=<path>
overrides the lists location. Exit codes: 0 clean · 1 violations (findings
printed as JSON on stdout) · 2 gate broken (invalid lists or unreadable
input; error on stderr, never a clean verdict).
Exit-code convention
0 success · 1 operation failed · 2 invalid data/configuration ·
3 configuration missing for a read operation · 4 usage/file/environment
problem. Scripts print diagnostics on stderr; model responses (and only
model responses) on stdout.
Agent limits (scripts/mosaic limits)
View: scripts/mosaic limits --business ID --json. Set: scripts/mosaic limits set --business ID --file PATCH, with the current revision and replacement
limits and/or models, or instances mapping role names to model-table keys,
or assistants mapping an assistant instance to any of its vars, tracker
(null removes it) and definition (row 155, #1649, Jason's ruling Q169). The
definition stays an assistant one; holder and credentials stay hand edits.
Both use the human bus proof. Settings offers the same edits in signed-in send
mode. Unset means unlimited, zero blocks new launches, and running sessions
remain alive when a limit is lowered. Total, provider and exact model limits
are checked for each launch; counts have no hard-coded policy ceiling.
Token limits (row 125) are limits.tokens.instances.NAME, providers.NAME,
models.PROVIDER.MODEL and keys.NAME, each { "limit": N }. The writer adds
and keeps since. scripts/mosaic limits reset WORD... [--business ID] (for
example reset tokens providers anthropic) moves one limit's since to now,
logged with its actor. A token limit is enforced only by a switched-on
limit-check plug-in (token-limit ships): it refuses a launch at the limit
and stops the session at the end of the turn that reaches it. Otherwise the
read shows it as not enforced; a failing check refuses and stops (fail
closed). Without --json the read prints JSON without tokens followed by
token limits: text lines, so scripts must pass --json.
The Console's Assistant section (signed-in send mode) also sets an instance's
seat name, as scripts/mosaic name does, and stops the assistant's runs in the
human's threads. The chief of staff makes the same changes with its
change_organization, set_name and end_run tools. Until row 134 gives it a
revision read, the revision comes from the human.
Convert an old organization file with scripts/mosaic limits migrate --file ORGANIZATION --models MAPPING --roles-dir roles > CONVERTED. It needs no bus,
never edits the input or system config and has no write flag. Inherited models
require a valid system config and an exact matching provider/model mapping.
Mapping JSON contains models,
families and optional per-role instances selections. Jason checks and installs
the output in Mos's window with the PM run idle; re-pin in that same window.
Never migrate the live file from an agent session.
Local project records (mosaic project)
mosaic project create and mosaic project list work on the bus record of a
business's projects; mosaic project init and mosaic project check work on
the repository on disk. Threads (mosaic threads, mosaic thread create|open|read,
mosaic talk --thread, mosaic follow --thread) are described in
packages/cli/README.md; a plain mosaic talk to the threaded instance exits 2
and asks for --thread.
mosaic project init <business> <project> creates the declared project's
separate local record repository, with layout version 1, package-validated JSON
templates, Markdown spec/docs and its first commit authored as the business's
human. Init refuses an agent environment, an undeclared project or a non-empty
directory. Failed init cleans up its own layout and allows retry, preserving a
pre-existing empty target and unrelated files. A changed owned path refuses
project-init-cleanup for inspection. Records may not overlap any declared
project's code root. No bus or remote is involved.
mosaic project check <business> <project> validates project metadata, every
record, filename/ID agreement and the fixed layout, naming each problem. It
refuses project-no-commit if the layout has no readable HEAD commit. It
exits 0 when valid, 2 for project/record problems (including an undeclared
project), 3 for configuration loading problems and 4 for usage. The optional business project repo is a normalized absolute
record-repository path; the default is
<dataRoot>/projects/<business>/<project>/. root remains the code checkout.
The package README describes schemas and the shared agent record writer; agent
writes never enter spec/, never absorb pending edits and refuse remotes.
Token usage (row 124)
mosaic usage [--business ID] [--json] reads token accounting from the bus.
Each total, run, instance, model, provider and account group shows input,
output, cache write, cache read and unknown turns. A malformed or unavailable
transcript contributes an unknown turn, never a zero-token reading. Accounts
are unassigned until account tracking ships. Exact totals larger than the
safe integer range are decimal strings in JSON. Settings has the same
read-only Token usage section. Counts are a best reading, not proof: the
session can edit its own transcript. This command changes no setting and
performs no token-limit enforcement.
Console Chat (row 129, #1623)
The Console's Chat section uses #/chat, #/chat/projects/UUID and
#/chat/threads/UUID. Create and name a project and its threads, then send a
message in that thread's conversation. Names are creation-only. The project
catalog is business-wide; thread lists and histories belong to the current
human. This is separate from the Board's seat reply route.
GET/POST /api/chat/projects lists/creates projects; GET/POST /api/chat/threads
lists/creates threads (project=UUID optionally filters the GET).
GET /api/chat/threads/UUID/messages?limit=100&after=UUID pages history forward;
POST on that messages route sends {body} as a REQUEST.
POST /api/chat/threads/UUID/open takes {} and explicitly resumes the assistant.
GET never writes, opens a thread or marks messages delivered/read. No browser
owner, tenant, proof or arbitrary bus verb is accepted. A same-business thread
owned by someone else is 403 thread-not-owned; foreign/missing ids are 404.
The four chat POSTs use the existing signed-in --send boundary: named Host,
one exact proxy user header, exact HTTPS Origin, same-origin Fetch Metadata,
strict JSON and 16 KiB. Read-only mode disables them. Bodies are inert text;
drafts are memory-only per thread and cleared on sign-out. History starts
oldest-first in pages of 100, with a 200-message display window. Sending does
not claim an assistant answer; an uncertain receipt is never retried. Read the
current list/history before trying again. Activation still needs Mos's separate
window; changing these files does not authorize a live Console action.