Skip to content

Commit 219a2f0

Browse files
outdoorseaclaude
andcommitted
feat(plugins): implement 4 spec-only plugins with run.sh scripts
These plugins had complete specifications in plugin.md but no executable run.sh. Each implementation follows the existing plugin pattern: cooldown gate, bd wisp recording, and escalation on failure. - **stuck-agent-dog**: Context-aware stuck/crashed agent detection for polecats and deacon. Checks tmux session + process health, notifies witness for restarts, escalates on mass death (>=3 agents). Never touches crew/mayor/witness/refinery sessions. - **rebuild-gt**: Safe binary rebuild from gastown source. Uses `gt stale --json` to verify staleness and safety before building. Only rebuilds forward on main branch — prevents the crash-loop incident where a bad rebuild broke every session's startup hook. - **git-hygiene**: Stale branch, stash, and gc cleanup across all rig repos. Deletes merged local branches, orphan agent branches (polecat/*, dog/*, fix/*) with no remote, merged remote branches on GitHub via `gh api`, clears stashes, and runs `git gc --prune=now`. - **dolt-snapshots**: Build/run wrapper for the existing Go binary (main.go) that tags Dolt databases at convoy boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe11b9e commit 219a2f0

File tree

4 files changed

+460
-0
lines changed

4 files changed

+460
-0
lines changed

plugins/dolt-snapshots/run.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# dolt-snapshots/run.sh — Build and run the dolt-snapshots Go binary.
3+
set -euo pipefail
4+
5+
PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
6+
BINARY="$PLUGIN_DIR/dolt-snapshots"
7+
8+
# Build if binary doesn't exist or source is newer
9+
if [ ! -f "$BINARY" ] || [ "$PLUGIN_DIR/main.go" -nt "$BINARY" ]; then
10+
echo "[dolt-snapshots] Building binary..."
11+
(cd "$PLUGIN_DIR" && go build -o dolt-snapshots .) || {
12+
echo "[dolt-snapshots] Build failed"
13+
exit 1
14+
}
15+
fi
16+
17+
exec "$BINARY" "$@"

plugins/git-hygiene/run.sh

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env bash
2+
# git-hygiene/run.sh — Clean stale branches, stashes, and loose objects.
3+
#
4+
# Runs across all rig repos. Covers:
5+
# - Merged local branches
6+
# - Orphan local branches (polecat/*, dog/*, fix/*, etc. with no remote)
7+
# - Merged remote branches on GitHub
8+
# - Stale stashes
9+
# - Git garbage collection
10+
11+
set -euo pipefail
12+
13+
log() { echo "[git-hygiene] $*"; }
14+
15+
# --- Enumerate rig repos -----------------------------------------------------
16+
17+
RIG_JSON=$(gt rig list --json 2>/dev/null) || {
18+
log "SKIP: could not get rig list"
19+
exit 0
20+
}
21+
22+
RIG_PATHS=$(echo "$RIG_JSON" | python3 -c "
23+
import json, sys
24+
rigs = json.load(sys.stdin)
25+
for r in rigs:
26+
p = r.get('repo_path') or ''
27+
if p: print(p)
28+
" 2>/dev/null)
29+
30+
if [ -z "$RIG_PATHS" ]; then
31+
log "SKIP: no rigs with repo paths found"
32+
exit 0
33+
fi
34+
35+
RIG_COUNT=$(echo "$RIG_PATHS" | wc -l | tr -d ' ')
36+
log "Found $RIG_COUNT rig repo(s) to clean"
37+
38+
# --- Process each rig repo ----------------------------------------------------
39+
40+
TOTAL_LOCAL_MERGED=0
41+
TOTAL_LOCAL_ORPHAN=0
42+
TOTAL_REMOTE=0
43+
TOTAL_STASHES=0
44+
TOTAL_GC=0
45+
46+
while IFS= read -r REPO_PATH; do
47+
[ -z "$REPO_PATH" ] && continue
48+
49+
if ! git -C "$REPO_PATH" rev-parse --git-dir >/dev/null 2>&1; then
50+
log "SKIP: $REPO_PATH is not a git repo"
51+
continue
52+
fi
53+
54+
log ""
55+
log "=== Cleaning: $REPO_PATH ==="
56+
57+
# Detect default branch
58+
DEFAULT_BRANCH=$(git -C "$REPO_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \
59+
| sed 's|refs/remotes/origin/||')
60+
if [ -z "$DEFAULT_BRANCH" ]; then
61+
DEFAULT_BRANCH="main"
62+
fi
63+
CURRENT_BRANCH=$(git -C "$REPO_PATH" branch --show-current 2>/dev/null)
64+
65+
# Step 1: Prune remote tracking refs
66+
log " Pruning remote tracking refs..."
67+
git -C "$REPO_PATH" fetch --prune --all 2>/dev/null || true
68+
69+
# Step 2: Delete merged local branches
70+
log " Deleting merged local branches..."
71+
MERGED_BRANCHES=$(git -C "$REPO_PATH" branch --merged "$DEFAULT_BRANCH" 2>/dev/null \
72+
| grep -v "^\*" \
73+
| grep -v "^+" \
74+
| grep -v -E "^\s*(main|master)$" \
75+
| sed 's/^[[:space:]]*//' || true)
76+
77+
LOCAL_MERGED=0
78+
while IFS= read -r BRANCH; do
79+
[ -z "$BRANCH" ] && continue
80+
if [ "$BRANCH" = "$CURRENT_BRANCH" ] || [ "$BRANCH" = "$DEFAULT_BRANCH" ]; then
81+
continue
82+
fi
83+
case "$BRANCH" in
84+
refinery-patrol|merge/*) continue ;;
85+
esac
86+
log " Deleting merged: $BRANCH"
87+
git -C "$REPO_PATH" branch -d "$BRANCH" 2>/dev/null && LOCAL_MERGED=$((LOCAL_MERGED + 1))
88+
done <<< "$MERGED_BRANCHES"
89+
TOTAL_LOCAL_MERGED=$((TOTAL_LOCAL_MERGED + LOCAL_MERGED))
90+
91+
# Step 3: Delete stale unmerged orphan branches
92+
log " Deleting stale orphan branches..."
93+
STALE_PATTERNS="polecat/|dog/|fix/|pr-|integration/|worktree-agent-"
94+
ALL_BRANCHES=$(git -C "$REPO_PATH" branch 2>/dev/null \
95+
| grep -v "^\*" \
96+
| grep -v "^+" \
97+
| sed 's/^[[:space:]]*//' || true)
98+
99+
LOCAL_ORPHAN=0
100+
while IFS= read -r BRANCH; do
101+
[ -z "$BRANCH" ] && continue
102+
if ! echo "$BRANCH" | grep -qE "^($STALE_PATTERNS)"; then
103+
continue
104+
fi
105+
if [ "$BRANCH" = "$CURRENT_BRANCH" ] || [ "$BRANCH" = "$DEFAULT_BRANCH" ]; then
106+
continue
107+
fi
108+
case "$BRANCH" in
109+
main|master|refinery-patrol|merge/*) continue ;;
110+
esac
111+
if git -C "$REPO_PATH" rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then
112+
continue
113+
fi
114+
log " Deleting orphan: $BRANCH"
115+
git -C "$REPO_PATH" branch -D "$BRANCH" 2>/dev/null && LOCAL_ORPHAN=$((LOCAL_ORPHAN + 1))
116+
done <<< "$ALL_BRANCHES"
117+
TOTAL_LOCAL_ORPHAN=$((TOTAL_LOCAL_ORPHAN + LOCAL_ORPHAN))
118+
119+
# Step 4: Delete merged remote branches on GitHub
120+
log " Deleting merged remote branches..."
121+
REMOTE_DELETED=0
122+
123+
GH_REPO=$(git -C "$REPO_PATH" remote get-url origin 2>/dev/null \
124+
| sed -E 's|.*github\.com[:/]||; s|\.git$||')
125+
126+
if [ -n "$GH_REPO" ]; then
127+
REMOTE_BRANCHES=$(git -C "$REPO_PATH" branch -r 2>/dev/null \
128+
| grep -v HEAD \
129+
| grep -v "origin/$DEFAULT_BRANCH" \
130+
| grep -v "origin/dependabot/" \
131+
| grep -v "origin/refinery-patrol" \
132+
| grep -vE "origin/merge/" \
133+
| sed 's|^[[:space:]]*origin/||' || true)
134+
135+
REMOTE_PATTERNS="polecat/|fix/|pr-|integration/|worktree-agent-"
136+
137+
while IFS= read -r RBRANCH; do
138+
[ -z "$RBRANCH" ] && continue
139+
if ! echo "$RBRANCH" | grep -qE "^($REMOTE_PATTERNS)"; then
140+
continue
141+
fi
142+
if git -C "$REPO_PATH" merge-base --is-ancestor "origin/$RBRANCH" "origin/$DEFAULT_BRANCH" 2>/dev/null; then
143+
log " Deleting remote: origin/$RBRANCH"
144+
gh api "repos/$GH_REPO/git/refs/heads/$RBRANCH" -X DELETE 2>/dev/null && REMOTE_DELETED=$((REMOTE_DELETED + 1))
145+
fi
146+
done <<< "$REMOTE_BRANCHES"
147+
fi
148+
TOTAL_REMOTE=$((TOTAL_REMOTE + REMOTE_DELETED))
149+
150+
# Step 5: Clear stale stashes
151+
log " Clearing stashes..."
152+
STASH_COUNT=$(git -C "$REPO_PATH" stash list 2>/dev/null | wc -l | tr -d ' ')
153+
if [ "$STASH_COUNT" -gt 0 ]; then
154+
log " Clearing $STASH_COUNT stash(es)"
155+
git -C "$REPO_PATH" stash clear 2>/dev/null
156+
TOTAL_STASHES=$((TOTAL_STASHES + STASH_COUNT))
157+
fi
158+
159+
# Step 6: Garbage collect
160+
log " Running git gc..."
161+
git -C "$REPO_PATH" gc --prune=now --quiet 2>/dev/null && TOTAL_GC=$((TOTAL_GC + 1))
162+
163+
log " Done: $LOCAL_MERGED merged, $LOCAL_ORPHAN orphan, $REMOTE_DELETED remote, $STASH_COUNT stash(es)"
164+
done <<< "$RIG_PATHS"
165+
166+
# --- Report -------------------------------------------------------------------
167+
168+
SUMMARY="$RIG_COUNT rig(s): $TOTAL_LOCAL_MERGED merged, $TOTAL_LOCAL_ORPHAN orphan, $TOTAL_REMOTE remote, $TOTAL_STASHES stash(es), $TOTAL_GC gc"
169+
log ""
170+
log "=== Git Hygiene Summary ==="
171+
log "$SUMMARY"
172+
173+
bd create "git-hygiene: $SUMMARY" -t chore --ephemeral \
174+
-l type:plugin-run,plugin:git-hygiene,result:success \
175+
-d "$SUMMARY" --silent 2>/dev/null || true

plugins/rebuild-gt/run.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
# rebuild-gt/run.sh — Rebuild gt binary from gastown source if stale.
3+
#
4+
# SAFETY: Only rebuilds forward (binary is ancestor of HEAD) and only
5+
# from main branch. A bad rebuild caused a crash loop (every session's
6+
# startup hook failed, witness respawned, loop repeated every 1-2 min).
7+
8+
set -euo pipefail
9+
10+
TOWN_ROOT="${GT_TOWN_ROOT:-$(gt town root 2>/dev/null)}"
11+
RIG_ROOT="${TOWN_ROOT}/gastown/mayor/rig"
12+
13+
log() { echo "[rebuild-gt] $*"; }
14+
15+
# --- Detection ---------------------------------------------------------------
16+
17+
log "Checking binary staleness..."
18+
STALE_JSON=$(gt stale --json 2>/dev/null) || {
19+
log "gt stale --json failed, skipping"
20+
exit 0
21+
}
22+
23+
IS_STALE=$(echo "$STALE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stale', False))" 2>/dev/null || echo "False")
24+
SAFE=$(echo "$STALE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('safe_to_rebuild', False))" 2>/dev/null || echo "False")
25+
26+
if [ "$IS_STALE" != "True" ]; then
27+
log "Binary is fresh. Nothing to do."
28+
bd create "rebuild-gt: binary is fresh" -t chore --ephemeral \
29+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:success \
30+
--silent 2>/dev/null || true
31+
exit 0
32+
fi
33+
34+
if [ "$SAFE" != "True" ]; then
35+
log "Not safe to rebuild (not on main or would be a downgrade). Skipping."
36+
bd create "Plugin: rebuild-gt [skipped]" -t chore --ephemeral \
37+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:skipped \
38+
-d "Skipped: not safe to rebuild" --silent 2>/dev/null || true
39+
exit 0
40+
fi
41+
42+
# --- Pre-flight checks -------------------------------------------------------
43+
44+
log "Pre-flight checks..."
45+
46+
if [ ! -d "$RIG_ROOT" ]; then
47+
log "Rig root $RIG_ROOT does not exist. Skipping."
48+
exit 0
49+
fi
50+
51+
DIRTY=$(git -C "$RIG_ROOT" status --porcelain 2>/dev/null)
52+
if [ -n "$DIRTY" ]; then
53+
log "Repo is dirty, skipping rebuild."
54+
bd create "Plugin: rebuild-gt [skipped]" -t chore --ephemeral \
55+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:skipped \
56+
-d "Skipped: repo has uncommitted changes" --silent 2>/dev/null || true
57+
exit 0
58+
fi
59+
60+
BRANCH=$(git -C "$RIG_ROOT" branch --show-current 2>/dev/null)
61+
if [ "$BRANCH" != "main" ]; then
62+
log "Not on main branch (on $BRANCH), skipping rebuild."
63+
bd create "Plugin: rebuild-gt [skipped]" -t chore --ephemeral \
64+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:skipped \
65+
-d "Skipped: not on main branch (on $BRANCH)" --silent 2>/dev/null || true
66+
exit 0
67+
fi
68+
69+
# --- Build -------------------------------------------------------------------
70+
71+
OLD_VER=$(gt version 2>/dev/null | head -1 || echo "unknown")
72+
log "Rebuilding gt from $RIG_ROOT..."
73+
74+
if (cd "$RIG_ROOT" && make build && make safe-install) 2>&1; then
75+
NEW_VER=$(gt version 2>/dev/null | head -1 || echo "unknown")
76+
log "Rebuilt: $OLD_VER -> $NEW_VER"
77+
bd create "rebuild-gt: $OLD_VER -> $NEW_VER" -t chore --ephemeral \
78+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:success \
79+
--silent 2>/dev/null || true
80+
else
81+
ERROR="make build/safe-install failed"
82+
log "FAILED: $ERROR"
83+
bd create "Plugin: rebuild-gt [failure]" -t chore --ephemeral \
84+
-l type:plugin-run,plugin:rebuild-gt,rig:gastown,result:failure \
85+
-d "Build failed: $ERROR" --silent 2>/dev/null || true
86+
gt escalate "Plugin FAILED: rebuild-gt" -s medium 2>/dev/null || true
87+
exit 1
88+
fi

0 commit comments

Comments
 (0)