# Machine-Level Tool Reference

Centralized reference for tools, credentials, and CLI patterns available across all projects.
Project-specific tooling belongs in the project's `AGENTS.md`, not here.

All tool suites are located at `~/.config/mosaic/tools/`.

## Tool Suites

### Git Wrappers (Use First)

Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands.

This index is complete and is kept complete mechanically: `tools/quality/scripts/check-tools-index.sh`
fails CI when a wrapper ships without an entry here, or when an entry here names a wrapper that no
longer exists. A wrapper missing from this list is, from inside an agent session, indistinguishable
from a wrapper that was never written — which is how the APPROVE/APPROVED incident below happened.

Every command takes `--help`. All of them accept `--login <account>` to pin the acting identity;
supply it explicitly on any host where the provider CLI's default account is an admin.

| Issues             |                                   |
| ------------------ | --------------------------------- |
| `issue-create.sh`  | Create an issue (Gitea or GitHub) |
| `issue-view.sh`    | Show one issue                    |
| `issue-list.sh`    | List issues                       |
| `issue-edit.sh`    | Edit title/body/labels/milestone  |
| `issue-comment.sh` | Add a comment                     |
| `issue-assign.sh`  | Assign or unassign                |
| `issue-close.sh`   | Close an issue                    |
| `issue-reopen.sh`  | Reopen a closed issue             |

| Pull requests    |                                                           |
| ---------------- | --------------------------------------------------------- |
| `pr-create.sh`   | Open a pull request                                       |
| `pr-edit.sh`     | Edit PR title, body, base branch, or draft/ready state    |
| `pr-view.sh`     | Show one PR                                               |
| `pr-list.sh`     | List PRs                                                  |
| `pr-diff.sh`     | Fetch a PR's diff                                         |
| `pr-metadata.sh` | PR metadata as JSON (head SHA, base, state, mergeability) |
| `pr-review.sh`   | **Place a review verdict — see the dialect note below**   |
| `pr-ci-wait.sh`  | Block until the PR's CI reaches a terminal state          |
| `pr-merge.sh`    | Merge a PR                                                |
| `pr-close.sh`    | Close a PR without merging                                |

| Milestones            |                    |
| --------------------- | ------------------ |
| `milestone-create.sh` | Create a milestone |
| `milestone-list.sh`   | List milestones    |
| `milestone-close.sh`  | Close a milestone  |

| Gates and guards        |                                                                                                           |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `ci-queue-wait.sh`      | CI queue guard — required before push/merge (see below)                                                   |
| `push-guard.sh`         | Refuse verifications that pass for the wrong reason (e.g. green against an unpushed tree)                 |
| `mutate-push-guard.sh`  | Regenerate the guard's mutation-coverage table from measurement, so the table cannot drift from the guard |
| `verify-clean-clone.sh` | Prove the **committed** artifact runs, from a clean clone — not the working tree                          |

| Context              |                                                                                          |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `detect-platform.sh` | Resolve the provider (Gitea vs GitHub) for the current repo; every other wrapper uses it |
| `lane-brief.sh`      | Live dispatch brief for a repo "lane" (milestone/label) straight from the provider       |

| Workspace            |                                                                          |
| -------------------- | ------------------------------------------------------------------------ |
| `mosaic-worktree.sh` | Create/list/remove git worktrees — **the only supported way**; see below |
| `wrapper-guard.sh`   | PreToolUse hook that enforces the two rules above; not called by hand    |

**Workspace placement is derived, not chosen.** `mosaic-worktree.sh new <branch>` takes a branch
name and nothing else. Every path comes out of `git worktree list --porcelain` — main worktree,
repo name, parent dir, then `<parent>/<repo>-worktrees/<branch-slug>`. There is no placement flag
because a decision an agent has to make is a decision that drifts: the rule "big work goes on a work
filesystem" already existed in prose and 255 GB accumulated in `$HOME` across 842 directories
anyway, under five simultaneous conventions on a single host.

```bash
~/.config/mosaic/tools/git/mosaic-worktree.sh new <branch> [--from <base>]
~/.config/mosaic/tools/git/mosaic-worktree.sh path <branch>   # derived path, no side effect
~/.config/mosaic/tools/git/mosaic-worktree.sh list            # this repo's worktrees + state
~/.config/mosaic/tools/git/mosaic-worktree.sh rm <branch>     # removal is part of the task
~/.config/mosaic/tools/git/mosaic-worktree.sh gc [--apply]    # reclaim clean + fully-pushed ones
```

Worktrees rather than clones, because `git worktree list` makes every checkout enumerable — a bare
clone dropped somewhere on disk can never be safely reclaimed, so it is never reclaimed. `rm` and
`gc` decide by **evidence, never by size or age**: a worktree is reclaimable only when
`git status --porcelain` is empty _and_ `git rev-list --count HEAD --not --remotes` is 0. Anything
else is preserved and reported. `--force` exists and is yours to type deliberately.

`wrapper-guard.sh` is registered as a Claude Code `PreToolUse` hook on `Bash` (see
`runtime/claude/settings.json`). It blocks exactly three things and lets everything else through:
a `git clone`/`git worktree add` targeting `$HOME`; a raw provider-API **write** to an endpoint that
already has a wrapper above (reads are untouched — they are how you gather evidence); and the
literal `"event": "APPROVE"`. For a genuine gap no wrapper can express, prefix
`MOSAIC_WRAPPER_OVERRIDE=1`. Reaching for the override twice for the same call means the wrapper has
a missing flag — extend the wrapper.

```bash
~/.config/mosaic/tools/git/issue-create.sh --help
~/.config/mosaic/tools/git/pr-review.sh --pr 42 --event APPROVED --body "..."

# CI queue guard (required before push/merge; defaults to the checked-out branch)
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge
```

**Review dialect — the reason `pr-review.sh` is not optional.** Gitea's approve event is
`APPROVED`; GitHub's is `APPROVE`. Send GitHub's spelling to a Gitea host and it answers **HTTP
200**, files the review as PENDING, and then rejects the submit with `422 review stay pending` — the
verdict looks placed and is not. (`REQUEST_CHANGES` is spelled identically on both, so only the
approve path carries the trap.) `pr-review.sh` sends the correct token for the detected provider.
Whatever you use, re-read `GET /pulls/{n}/reviews` and assert the state before reporting a verdict
placed.

The guard exits nonzero for any provider-asserted non-green, missing, or malformed CI state. If credentials or the provider are unavailable, it emits `CANNOT_ASSERT` and writes a JSONL audit record. Push degrades to exit 0 so recovery work is not bricked; merge holds with retryable exit 75 until the provider recovers, then self-clears without manual reset. Neither outcome is evidence that CI was clear. For a repository with no CI configured at all, `pr-merge.sh --no-ci-expected` is the sanctioned merge path: it forwards to `ci-queue-wait.sh --no-ci-expected`, which reclassifies a zero-context merge head as queue-clear only when the acting token holds repository admin and `MOSAIC_GIT_IDENTITY` names the asserting identity (a caller without one is refused with exit 78 before the admin lookup), and records the assertion (or its refusal) in the same JSONL audit log. `pr-merge.sh` automatically inspects the exact PR head repository and full commit SHA rather than its `main` base; this also handles fork PRs without branch-name ambiguity. Pass `--expect-head <approved-full-sha>` to bind a commit-specific review or merge-gate verdict; Gitea uses atomic `head_commit_id` and GitHub uses `--match-head-commit`.

### Code Review (Codex)

```bash
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
```

### Infrastructure — Portainer

```bash
~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name>
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name>
~/.config/mosaic/tools/portainer/stack-list.sh
~/.config/mosaic/tools/portainer/endpoint-list.sh
```

### Infrastructure — Coolify (DEPRECATED)

> Coolify has been superseded by Portainer Docker Swarm in this stack.
> Tools remain for reference but should not be used for new deployments.

```bash
# DEPRECATED — do not use for new deployments
~/.config/mosaic/tools/coolify/project-list.sh
~/.config/mosaic/tools/coolify/service-list.sh
~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k KEY -v VALUE
```

### Identity — Authentik

```bash
~/.config/mosaic/tools/authentik/user-list.sh
~/.config/mosaic/tools/authentik/user-create.sh -u <username> -n <name> -e <email>
~/.config/mosaic/tools/authentik/group-list.sh
~/.config/mosaic/tools/authentik/app-list.sh
~/.config/mosaic/tools/authentik/flow-list.sh
~/.config/mosaic/tools/authentik/admin-status.sh
```

### CI/CD — Woodpecker

Multi-instance support: `-a <instance>` selects a named instance. Omit `-a` to use the default from `woodpecker.default` in credentials.json.

| Instance           | URL                | Serves                             |
| ------------------ | ------------------ | ---------------------------------- |
| `mosaic` (default) | ci.mosaicstack.dev | Mosaic repos (git.mosaicstack.dev) |
| `usc`              | ci.uscllc.com      | USC repos (git.uscllc.com)         |

```bash
# List recent pipelines
~/.config/mosaic/tools/woodpecker/pipeline-list.sh [-r owner/repo] [-a instance]

# Check latest or specific pipeline status
~/.config/mosaic/tools/woodpecker/pipeline-status.sh [-r owner/repo] [-n number] [-a instance]

# Trigger a build
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh [-r owner/repo] [-b branch] [-a instance]
```

Instance selection rule: match `-a` to the git remote host of the target repo. If the repo is on `git.uscllc.com`, use `-a usc`. If on `git.mosaicstack.dev`, use `-a mosaic` (or omit, since it's the default).

### DNS — Cloudflare

Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal`, `work`). Omit `-a` to use the default from `cloudflare.default` in credentials.json.

```bash
# List zones (domains)
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]

# List DNS records (zone by name or ID)
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-a instance] [-t type] [-n name]

# Create DNS record
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl] [-P priority]

# Update DNS record
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl]

# Delete DNS record
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id> [-a instance]
```

### IT Service — GLPI

```bash
~/.config/mosaic/tools/glpi/ticket-list.sh
~/.config/mosaic/tools/glpi/ticket-create.sh -t <title> -c <content>
~/.config/mosaic/tools/glpi/computer-list.sh
~/.config/mosaic/tools/glpi/user-list.sh
```

### Health Check

```bash
# Check all configured services
~/.config/mosaic/tools/health/stack-health.sh

# Check a specific service
~/.config/mosaic/tools/health/stack-health.sh -s portainer

# JSON output for automation
~/.config/mosaic/tools/health/stack-health.sh -f json
```

### Shared Credential Loader

```bash
# Source in any script to load service credentials
source ~/.config/mosaic/tools/_lib/credentials.sh
load_credentials <service-name>
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare, turbo-cache, openbrain
```

### OpenBrain — Semantic Memory (PRIMARY)

Self-hosted semantic brain backed by pgvector. Primary shared memory layer for all agents across all sessions and harnesses. Stores and retrieves decisions, context, and observations via semantic search.

**Credentials:** `load_credentials openbrain` → exports `OPENBRAIN_URL`, `OPENBRAIN_TOKEN`

Configure in your credentials.json:

```json
"openbrain": {
  "url": "https://<your-openbrain-host>",
  "api_key": "<your-api-key>"
}
```

**REST API** (any language, any harness):

```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials openbrain

# Search by meaning
curl -s -X POST -H "Authorization: Bearer $OPENBRAIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"query": "your search", "limit": 5}' "$OPENBRAIN_URL/v1/search"

# Capture a thought
curl -s -X POST -H "Authorization: Bearer $OPENBRAIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"content": "...", "source": "agent-name", "metadata": {}}' "$OPENBRAIN_URL/v1/thoughts"

# Recent activity
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/thoughts/recent?limit=5"

# Stats
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/stats"
```

**Python client** (if the OpenBrain client is on your PYTHONPATH):

```bash
python tools/openbrain_client.py search "topic"
python tools/openbrain_client.py capture "decision or observation" --source agent-name
python tools/openbrain_client.py recent --limit 5
python tools/openbrain_client.py stats
```

**MCP (Claude Code sessions):** When connected, `mcp__openbrain__capture/search/recent/stats` tools are available natively — prefer those over CLI when in a Claude session.

**Rule: capture when you LEARN something. Never when you DO something.**

| Trigger                                   | Action                                    | Retention             |
| ----------------------------------------- | ----------------------------------------- | --------------------- |
| Session start                             | `search` + `recent` to load prior context | —                     |
| Architectural or tooling decision made    | Capture with rationale                    | `long` or `permanent` |
| Gotcha or non-obvious behavior discovered | Capture immediately                       | `medium`              |
| User preference stated or confirmed       | Capture                                   | `permanent`           |
| Cross-project pattern identified          | Capture                                   | `permanent`           |
| Prior decision superseded                 | UPDATE existing thought                   | (keep tier)           |

**Never capture:** task started, commit pushed, PR opened, test results, file edits, CI status.

Full protocol and cleanup tools: `~/.config/mosaic/guides/MEMORY.md`
Smart capture wrapper (enforces schema + dedup): `~/.config/mosaic/tools/openbrain/capture.sh`

### Excalidraw — Diagram Export (MCP)

Headless `.excalidraw` → SVG export via `@excalidraw/excalidraw`. Available as MCP tools in Claude Code sessions.

**MCP tools (when connected):**

| Tool                                      | Input                                         | Output                                               |
| ----------------------------------------- | --------------------------------------------- | ---------------------------------------------------- |
| `mcp__excalidraw__excalidraw_to_svg`      | `elements` JSON string + optional `app_state` | SVG string                                           |
| `mcp__excalidraw__excalidraw_file_to_svg` | `file_path` to `.excalidraw`                  | SVG string + writes `.svg` alongside                 |
| `mcp__excalidraw__list_diagrams`          | (none)                                        | Available templates (requires `EXCALIDRAW_GEN_PATH`) |
| `mcp__excalidraw__generate_diagram`       | `name`, optional `output_path`                | Path to generated `.excalidraw`                      |
| `mcp__excalidraw__generate_and_export`    | `name`, optional `output_path`                | Paths to `.excalidraw` and `.svg`                    |

**Diagram generation** (`list_diagrams`, `generate_diagram`, `generate_and_export`) requires `EXCALIDRAW_GEN_PATH` env var pointing to `excalidraw_gen.py`. Set in environment or shell profile:

```bash
export EXCALIDRAW_GEN_PATH="$HOME/.config/mosaic/tools/excalidraw/excalidraw_gen.py"
```

**Manual registration:**

```bash
mosaic-ensure-excalidraw           # install deps + register with Claude
mosaic-ensure-excalidraw --check   # verify registration
```

## Git Providers

| Instance                      | URL | CLI | Purpose |
| ----------------------------- | --- | --- | ------- |
| (add your git providers here) |     |     |         |

## Credentials

**Location:** (configure your credential file path)
**Loader:** `source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials <service>`

**Never expose actual values. Never commit credential files.**

## CLI Gotchas

(Add platform-specific CLI gotchas as you discover them.)

## Safety Defaults

- Prefer `trash` over `rm` when available — recoverable beats gone forever
- Never run destructive commands without explicit instruction
- Write it down — "mental notes" don't survive session restarts; files do
