Files
stack/docs/TOOLS.md
T
jason.woltjeandClaude Opus 5.5 1f2ac5b848 scripts: land-work turns a resolved merge decision into a gated local commit (row 116, #1610)
Candidate manifest 8b4ab31014a124ec0e3abba44238976d6f4c9af48cbdb36c2c77d18f0137f888
from Darkwing's round 2 packet (0a4076e8e), on BASE 1223a0f37. No
manifest path changed between BASE and the landing target. Filbert
approved round 2 in 28468, closing C1 and D1 from round 1 (28464).
Sage reran the full gate at the landing target with the candidate
applied. The replay acceptance follows ruling sage-0204.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 14:24:57 -05:00

550 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```bash
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`:
```bash
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)
```bash
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)
```bash
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`)
```bash
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`)
```bash
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`)
```bash
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`)
```bash
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 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 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`)
```bash
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](#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`)
```bash
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`). 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.