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.
237 lines
8.5 KiB
Bash
Executable File
237 lines
8.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# mosaic-worktree.sh — the only supported way to create and dispose of a git
|
|
# worktree on a fleet host.
|
|
#
|
|
# Why this exists as a helper and not as a rule: the rule already existed, in
|
|
# the framework's own words ("Big work → /var/tmp"), and 255 GB accumulated in
|
|
# $HOME across 842 directories anyway. Five placement conventions were live on
|
|
# one fleet host simultaneously. Every one was a decision an agent had to make,
|
|
# and a decision an agent has to make is a decision that drifts.
|
|
#
|
|
# So this script makes NO placement decision available. The caller supplies a
|
|
# branch name. Every path is DERIVED:
|
|
#
|
|
# main worktree <- git worktree list --porcelain (never cwd, which may
|
|
# itself already be a worktree)
|
|
# REPO_NAME <- basename of the main worktree
|
|
# REPO_PARENT <- dirname of the main worktree
|
|
# WT_ROOT <- $REPO_PARENT/$REPO_NAME-worktrees
|
|
# SLUG <- branch with '/' replaced by '-'
|
|
# WT_PATH <- $WT_ROOT/$SLUG
|
|
#
|
|
# The derivation puts the worktree on the same filesystem as the object store
|
|
# it shares, as a sibling of the repo, under one root per repo. Those are the
|
|
# properties that make the checkout cheap and — via `git worktree list` —
|
|
# enumerable, which is the only reason automated cleanup can ever be safe.
|
|
#
|
|
# Usage:
|
|
# mosaic-worktree.sh new <branch> [--from <base>] create (branch may exist)
|
|
# mosaic-worktree.sh path <branch> print derived path, no side effect
|
|
# mosaic-worktree.sh list this repo's worktrees + state
|
|
# mosaic-worktree.sh rm <branch> [--force] remove; refuses to lose work
|
|
# mosaic-worktree.sh gc [--apply] report/remove clean+pushed worktrees
|
|
#
|
|
# `rm` and `gc` refuse to delete a worktree with uncommitted changes or with
|
|
# commits absent from every remote. That check is by EVIDENCE, never by size or
|
|
# age. --force overrides it and is yours to type deliberately.
|
|
#
|
|
# Run from anywhere inside the repo, or pass --repo <path>.
|
|
|
|
set -euo pipefail
|
|
|
|
die() { printf 'mosaic-worktree: %s\n' "$*" >&2; exit 1; }
|
|
|
|
REPO_HINT=""
|
|
ARGS=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--repo) REPO_HINT="${2:-}"; shift 2 ;;
|
|
*) ARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
|
|
|
CMD="${1:-}"
|
|
[ -n "$CMD" ] || die "no command. Try: new | path | list | rm | gc"
|
|
shift || true
|
|
|
|
# ---- mechanical derivation -------------------------------------------------
|
|
# The FIRST entry of `git worktree list --porcelain` is always the main
|
|
# worktree, regardless of which worktree we are standing in. Deriving from cwd
|
|
# would nest worktrees inside worktrees.
|
|
resolve_repo() {
|
|
local start="${REPO_HINT:-$PWD}"
|
|
git -C "$start" rev-parse --git-dir >/dev/null 2>&1 \
|
|
|| die "not inside a git repository: $start"
|
|
MAIN_WT="$(git -C "$start" worktree list --porcelain | awk '/^worktree /{print substr($0,10); exit}')"
|
|
[ -n "$MAIN_WT" ] || die "could not resolve the main worktree"
|
|
REPO_NAME="$(basename -- "$MAIN_WT")"
|
|
REPO_PARENT="$(dirname -- "$MAIN_WT")"
|
|
WT_ROOT="$REPO_PARENT/$REPO_NAME-worktrees"
|
|
}
|
|
|
|
slugify() { printf '%s' "$1" | tr '/' '-'; }
|
|
|
|
derive_path() {
|
|
local branch="$1"
|
|
[ -n "$branch" ] || die "branch name required"
|
|
printf '%s/%s' "$WT_ROOT" "$(slugify "$branch")"
|
|
}
|
|
|
|
# A worktree root under $HOME defeats the entire point: wrong filesystem, and
|
|
# $HOME is for configuration and state, not work products. Refuse rather than
|
|
# silently produce the layout we are trying to eliminate.
|
|
assert_not_home() {
|
|
local p="$1" home_real repo_real
|
|
home_real="$(cd "$HOME" && pwd -P)"
|
|
repo_real="$(cd "$(dirname -- "$p")" 2>/dev/null && pwd -P || dirname -- "$p")"
|
|
case "$repo_real/" in
|
|
"$home_real"/*)
|
|
die "refusing: derived path is under \$HOME ($p).
|
|
The repo itself lives under \$HOME, so its worktrees would too. Move the repo
|
|
to a work filesystem (e.g. /src/$REPO_NAME) and re-run. \$HOME holds
|
|
configuration, credentials, state and caches — not checkouts." ;;
|
|
esac
|
|
}
|
|
|
|
# ---- work-loss evidence ----------------------------------------------------
|
|
# Two independent questions, both answered from git, neither from size or age:
|
|
# dirty — anything uncommitted in the tree
|
|
# unpushed — commits reachable from HEAD that no remote ref contains
|
|
wt_dirty() { git -C "$1" status --porcelain 2>/dev/null | head -200 | wc -l; }
|
|
wt_unpushed() { git -C "$1" rev-list --count HEAD --not --remotes 2>/dev/null || echo "?"; }
|
|
|
|
wt_state() {
|
|
local wt="$1" d u
|
|
d="$(wt_dirty "$wt")"; u="$(wt_unpushed "$wt")"
|
|
if [ "$d" -eq 0 ] && [ "$u" = "0" ]; then
|
|
printf 'SAFE\tclean; 0 unpushed'
|
|
else
|
|
printf 'PRESERVE\t%s uncommitted; %s unpushed' "$d" "$u"
|
|
fi
|
|
}
|
|
|
|
# ---- commands --------------------------------------------------------------
|
|
cmd_path() { resolve_repo; derive_path "${1:-}"; echo; }
|
|
|
|
cmd_new() {
|
|
local branch="${1:-}" base=""
|
|
shift || true
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in --from) base="${2:-}"; shift 2 ;; *) die "unknown flag: $1" ;; esac
|
|
done
|
|
[ -n "$branch" ] || die "usage: mosaic-worktree.sh new <branch> [--from <base>]"
|
|
|
|
resolve_repo
|
|
local path; path="$(derive_path "$branch")"
|
|
assert_not_home "$path"
|
|
|
|
if [ -e "$path" ]; then
|
|
echo "exists: $path"
|
|
echo "(already checked out — reuse it, or 'rm' it first)"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$WT_ROOT"
|
|
|
|
# Existing branch -> check it out. New branch -> create from base (default:
|
|
# the remote's default branch if resolvable, else current HEAD).
|
|
if git -C "$MAIN_WT" show-ref --verify --quiet "refs/heads/$branch" \
|
|
|| git -C "$MAIN_WT" show-ref --verify --quiet "refs/remotes/origin/$branch"; then
|
|
git -C "$MAIN_WT" worktree add "$path" "$branch"
|
|
else
|
|
if [ -z "$base" ]; then
|
|
base="$(git -C "$MAIN_WT" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
|
[ -n "$base" ] || base="HEAD"
|
|
fi
|
|
git -C "$MAIN_WT" worktree add -b "$branch" "$path" "$base"
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
worktree: $path
|
|
branch: $branch
|
|
|
|
Removal is part of this task, not a later chore. When the work is pushed:
|
|
mosaic-worktree.sh rm $branch
|
|
EOF
|
|
}
|
|
|
|
cmd_list() {
|
|
resolve_repo
|
|
printf 'repo: %s\nroot: %s\n\n' "$MAIN_WT" "$WT_ROOT"
|
|
git -C "$MAIN_WT" worktree list --porcelain \
|
|
| awk '/^worktree /{print substr($0,10)}' \
|
|
| while read -r wt; do
|
|
[ "$wt" = "$MAIN_WT" ] && { printf '%-10s %s (main)\n' "-" "$wt"; continue; }
|
|
printf '%-10s %s\t%s\n' "$(wt_state "$wt" | cut -f1)" "$wt" "$(wt_state "$wt" | cut -f2)"
|
|
done
|
|
}
|
|
|
|
cmd_rm() {
|
|
local branch="${1:-}" force=0
|
|
shift || true
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in --force) force=1; shift ;; *) die "unknown flag: $1" ;; esac
|
|
done
|
|
[ -n "$branch" ] || die "usage: mosaic-worktree.sh rm <branch> [--force]"
|
|
|
|
resolve_repo
|
|
local path; path="$(derive_path "$branch")"
|
|
[ -d "$path" ] || die "no worktree at $path"
|
|
|
|
local d u
|
|
d="$(wt_dirty "$path")"; u="$(wt_unpushed "$path")"
|
|
if [ "$force" -eq 0 ] && { [ "$d" -ne 0 ] || [ "$u" != "0" ]; }; then
|
|
die "refusing to remove $path
|
|
uncommitted files: $d
|
|
unpushed commits: $u
|
|
Commit and push first — that is the contract. If this work is genuinely
|
|
disposable, re-run with --force."
|
|
fi
|
|
|
|
# NB: ${force:+--force} would expand for force=0 too ("0" is non-empty).
|
|
if [ "$force" -eq 1 ]; then
|
|
git -C "$MAIN_WT" worktree remove --force "$path"
|
|
else
|
|
git -C "$MAIN_WT" worktree remove "$path"
|
|
fi
|
|
git -C "$MAIN_WT" worktree prune
|
|
echo "removed: $path"
|
|
rmdir "$WT_ROOT" 2>/dev/null || true
|
|
}
|
|
|
|
cmd_gc() {
|
|
local apply=0
|
|
[ "${1:-}" = "--apply" ] && apply=1
|
|
resolve_repo
|
|
git -C "$MAIN_WT" worktree prune
|
|
git -C "$MAIN_WT" worktree list --porcelain \
|
|
| awk '/^worktree /{print substr($0,10)}' \
|
|
| while read -r wt; do
|
|
[ "$wt" = "$MAIN_WT" ] && continue
|
|
local_state="$(wt_state "$wt")"
|
|
case "$local_state" in
|
|
SAFE*)
|
|
if [ "$apply" -eq 1 ]; then
|
|
git -C "$MAIN_WT" worktree remove "$wt" && echo "removed: $wt"
|
|
else
|
|
echo "reclaimable (clean + fully pushed): $wt"
|
|
fi ;;
|
|
*) echo "preserved: $wt [$(printf '%s' "$local_state" | cut -f2)]" ;;
|
|
esac
|
|
done
|
|
git -C "$MAIN_WT" worktree prune
|
|
[ "$apply" -eq 1 ] || echo $'\n(report only — re-run with --apply to remove the reclaimable ones)'
|
|
}
|
|
|
|
case "$CMD" in
|
|
new) cmd_new "$@" ;;
|
|
path) cmd_path "$@" ;;
|
|
list) cmd_list "$@" ;;
|
|
rm) cmd_rm "$@" ;;
|
|
gc) cmd_gc "$@" ;;
|
|
-h|--help|help) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ;;
|
|
*) die "unknown command: $CMD (new | path | list | rm | gc)" ;;
|
|
esac
|