framework: make tool discoverability, workspace placement and model tiering mechanical

An undocumented tool is, from inside an agent session, indistinguishable from a
tool that was never written. The framework shipped 26 git wrappers and named 6 of
them in its resident index docs — 23% discoverability, with pr-review.sh among the
missing. The observable consequence was an agent obeying Constitution gate 7 as
best it could see it, reaching for raw curl, sending GitHub's APPROVE to a Gitea
host, and getting HTTP 200 with the review silently filed PENDING. Three times.
That is not a discipline failure and no amount of prose fixes it.

Four changes, each converting a rule that decayed into a mechanism that cannot:

- check-tools-index.sh (new, CI-blocking): every tool in an enforced suite must be
  named in a resident index doc, and every tool an index names must exist. The git
  suite is enforced now; other suites report coverage without failing, so the
  ratchet tightens one reviewed PR at a time instead of landing as one sweep. The
  enforced list is framework-owned rather than a marker inside operator-owned
  TOOLS.md — a doc marker would let an operator silence the gate on exactly the
  host where it matters most. Carries --self-test, because a checker that only
  ever passes is indistinguishable from one that is not running.

- TOOLS-REFERENCE.md: complete 28-entry git index, plus the APPROVED/APPROVE
  dialect note that explains why pr-review.sh is not a formality.

- mosaic-worktree.sh + wrapper-guard.sh (upstreamed): 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 placement conventions on one
  host. The helper therefore exposes no placement decision — given a branch name,
  every path is derived from `git worktree list --porcelain`. Worktrees rather than
  clones because enumerability is the only thing that makes reclaim safe, and
  reclaim is by evidence (clean tree + no unpushed commits), never by size or age.
  The guard blocks three mechanically-detectable mistakes and nothing else:
  a checkout into $HOME, a raw provider-API write to an endpoint that has a
  wrapper, and the literal APPROVE event. Reads pass untouched.

- STANDARDS.md: model tiering as a standard, named by capability class so it
  survives a model generation. Start cheapest, escalate on evidence, benchmark
  before demoting a task class, and keep the class->model binding in operator
  config with the DB-backed config service as the end state.

Registering the guard in runtime/claude/settings.json is the point of upstreaming
it: ~/.claude/settings.json is a framework-managed copy, so a hand-added hook there
is destroyed by the next upgrade. In the template it survives, and it reaches every
host instead of one.
This commit is contained in:
Hermes Agent
2026-08-12 16:51:17 -05:00
parent ec260e678f
commit e3a0ee87b3
7 changed files with 790 additions and 8 deletions
@@ -11,22 +11,105 @@ All tool suites are located at `~/.config/mosaic/tools/`.
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-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
# Issues
~/.config/mosaic/tools/git/issue-create.sh
~/.config/mosaic/tools/git/issue-close.sh
~/.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
```
# PRs
~/.config/mosaic/tools/git/pr-create.sh
~/.config/mosaic/tools/git/pr-merge.sh
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.
# Milestones
~/.config/mosaic/tools/git/milestone-create.sh
`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. `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)