Files
stack/docs/TOOLS.md
T
jason.woltjeandClaude Opus 5.5 a1ddeabdfc discord: unit can run from a pinned copy; check --offline; status reads the effective tree (row 114, #1608)
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]>
2026-10-14 15:43:46 -05:00

32 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 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.

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. 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.

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 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 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 what arrives until the reply (polls every 2 s, --wait default 900; 0 only sends). 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

This is an operator plan, not an action agents run. Both reviews and the landing precede it. Mos grants each live step first. A bus restart ends running sessions; do this only when no PM turn is in flight.

  1. Make the pushed target copy with stack-copy.sh copy COMMIT and note its full id. Before the first update, preserve the current Q81 CLI wrapper (bytes and mode), bus and Console unit files and drop-ins, and the effective WorkingDirectory/ExecStart output from each status command. Record the bus's --claude pin and the Console's preview/send flags. Keep the bus pm.conf and the existing Console copy or checkout: the first rollback needs them. On later updates, also record the prior shared copy's full id.
  2. With Mos's go, stop the Console, then stop the bus.
  3. Mos removes overriding bus drop-ins identified by status. The first update removes ~/.config/systemd/user/[email protected]/pm.conf. Reload systemd after removal; keep unrelated environment drop-ins.
  4. With Mos's go, install the bus with bus-service.sh install --copy ID --pm and its existing --claude pin, then the Console with webui-service.sh install --copy ID and its existing preview/send flags.
  5. With Mos's go, run stack-copy.sh wrapper ID to switch the human CLI.
  6. Mos starts the bus first, then the Console. Read stack-copy.sh check, both service status commands, Console /healthz and the canonical scripts/mosaic launches list; the latter must show the PM.
  7. Jason's plain mosaic talk pm "..." must print sent and a reply before #225 closes. Keep the receipts for the three paths and reply.

The first rollback restores the pre-update mixed setup, which has no shared copy id. Each live step needs Mos's go: stop the Console then the bus; reinstall the checkout bus unit without --copy or --pm, retaining its saved --claude pin; restore the saved pm.conf and other bus drop-ins; restore the saved Console unit and drop-ins pointing to its original checkout or mosaic-console copy; restore the saved Q81 wrapper bytes and mode; reload systemd; start the bus then the Console. Check their effective paths against the saved status output, Console /healthz and the PM launch. The shared-copy check is expected to refuse this restored mixed setup; #225 remains open.

Later rollback repeats steps 2-6 with the prior shared copy's full id, each live step again approved by Mos. Nothing prunes copies or removes mosaic-old.

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.

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.