Files
stack/docs/TOOLS.md
T
jason.woltjeandClaude Fable 5.1 caaef941e6 feat(discord): binding reload without a restart, and a per-user channel allowlist (#1509)
`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]>
2026-09-13 18:59:31 -05:00

14 KiB
Raw Blame History

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 (5600), 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-keyauth.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.