Darkwing's round 2 candidate, approved by Filbert (#1518 comment 26730). build-r2.patch a27890d5, manifest 869168c7, 34 files, applied on HEAD and checked 34/34. Integration gate on an export of HEAD plus the patch: business 60/60 on Node 24 and 26, every package test and every scripts/test-*.sh green, test-task 98/98 with the live-provider cases. Conductor, queue, conversation and discord confirmed in git worktrees of HEAD with and without the patch, identical results. Lead decision 63 accepts the vocabulary location, the example path and the business branch. Co-Authored-By: Claude Opus 5.5 <[email protected]>
307 lines
17 KiB
Markdown
307 lines
17 KiB
Markdown
# 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.
|
||
|
||
## 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; 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`.
|
||
|
||
## 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.
|