`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]>
14 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 contract | Prints MOSAIC_ROLE_TOOLS / MOSAIC_ROLE_NETWORK; 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.
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/gitea-api.sh <METHOD> <path> [body] |
Gitea API helper | 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> [--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. Not the
npm-global mosaic CLI; run by path. 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.
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). Records
under <dataRoot>/discord/<binding>/: inbox.jsonl, outbox.jsonl,
drops.jsonl, admissions.jsonl, notices.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.