Skip to content

Commit 384e233

Browse files
author
gluck
committed
skills: commit constantly, present deliberately, sweep the scratch
Recoverability and coherence are different jobs and one commit stream cannot do both. Reading "commits are itemized and self-contained" as "do not commit until the slice is presentable" produced the state it was meant to prevent: forty modified files held for an hour with no restore point, at which point `git checkout <path>` is file-granular and an agent's edits are not. It ate two files on 10-07, both rebuilt by hand. So maintaining.md now says the two jobs out loud and gives the shape that serves both: throwaway commits all day, a dated `scratch/` ref marking the mess, `reset --soft`, then the commit you would hand over. With three clarifications that cost us time to work out: a scratch ref is a safety net and never a SOURCE of merges (merging mess yields mess, and you rebuild from the tree regardless, which is why there is no branch dance here); the presented history is REORDERED, since discovery order is rarely explanatory order; and a canary wants `git stash` or a commit as its restore point, never a `.bak` copy for some files and git for others. scripts/scratch-gc.sh is the sweeper, because a policy without one is a preference: there were 223 branches in .bare today. Dry by default, refuses a ref any worktree has checked out, optionally gates on --merged, and uses -D knowingly (a scratch ref is deliberately never merged, so -d declines every one). Each guard was canaried against minted fixtures: the old ref pruned, the young one kept for being young, the same ref kept for being checked out, kept again for not being in main, and its sha still reachable through the reflog afterwards. The whole section is named a MENU, twice, because it is: Gluck's instruction outranks it and "just put it on main" is a sentence he says. Also, agents.md pointed at internal/credo/default_credo.md for the voice. No such file, and no default credo in this tree at all: the credo is the owner's own config, which is worth saying since editing it reaches every aria on the machine.
1 parent 9b27efd commit 384e233

4 files changed

Lines changed: 153 additions & 2 deletions

File tree

‎agents.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ Doesn't duplicate `README.md` or `ARCHITECTURE.md`: points at them and adds the
88

99
- **What this is.** Go CLI coding agent, supervisor architecture, JSON-RPC over Unix sockets. See `README.md`.
1010
- **How it's wired.** Process model, packages, data flow live in `ARCHITECTURE.md`. Treat as authoritative; if you contradict it, update it in the same change.
11-
- **Personality.** `internal/credo/default_credo.md` shapes the user-facing voice. Match lightly; don't parody.
11+
- **Personality.** The credo shapes the user-facing voice, and it is the
12+
owner's file (`$FIGARO_CONFIG_DIR/credo.md`), not the repo's: there is no
13+
default credo in this tree. Match lightly; don't parody. Editing it changes
14+
every aria on the machine, so say so when you do.
1215

1316
## Build, run, test
1417

‎scripts/scratch-gc.sh‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env bash
2+
# scratch-gc.sh: prune the scratch refs a worked branch leaves behind.
3+
#
4+
# See skills/figaro/contributing/maintaining.md, "Commit constantly, present
5+
# deliberately". A scratch ref holds the WIP history a presented commit was
6+
# rebuilt from: it is a safety net, it is never merged, and it is garbage once
7+
# the work it protected has landed.
8+
#
9+
# scripts/scratch-gc.sh # report what is prunable, delete nothing
10+
# scripts/scratch-gc.sh --days 14 # a different horizon
11+
# scripts/scratch-gc.sh --prune # actually delete
12+
# scripts/scratch-gc.sh --prune --merged main
13+
#
14+
# DRY BY DEFAULT. A sweeper that deletes on its first run teaches you to not
15+
# run it.
16+
set -uo pipefail
17+
18+
DAYS=7
19+
PRUNE=0
20+
MERGED=""
21+
PREFIX="refs/heads/scratch/"
22+
23+
usage() {
24+
sed -n '2,13p' "$0" | sed 's/^# \?//'
25+
exit "${1:-0}"
26+
}
27+
28+
while [ $# -gt 0 ]; do
29+
case "$1" in
30+
--days) DAYS="${2:?--days <n>}"; shift 2 ;;
31+
--prune) PRUNE=1; shift ;;
32+
--dry-run) PRUNE=0; shift ;;
33+
--merged) MERGED="${2:?--merged <branch>}"; shift 2 ;;
34+
--prefix) PREFIX="refs/heads/${2:?--prefix <dir>}/"; shift 2 ;;
35+
-h|--help) usage 0 ;;
36+
*) echo "scratch-gc: unknown flag $1" >&2; usage 1 ;;
37+
esac
38+
done
39+
40+
git rev-parse --git-dir >/dev/null 2>&1 || {
41+
echo "scratch-gc: not a git repository" >&2; exit 1
42+
}
43+
44+
cutoff=$(( $(date +%s) - DAYS * 86400 ))
45+
kept=0 doomed=0
46+
47+
# A ref still checked out by a worktree is live work, whatever its age: the
48+
# sweeper must never delete the branch someone is standing on.
49+
checked_out="$(git worktree list --porcelain | sed -n 's/^branch //p')"
50+
51+
while IFS=$'\t' read -r ref when subject; do
52+
[ -n "$ref" ] || continue
53+
name="${ref#refs/heads/}"
54+
age_days=$(( ( $(date +%s) - when ) / 86400 ))
55+
if printf '%s\n' "$checked_out" | grep -qxF "$ref"; then
56+
printf 'keep %-44s %3dd checked out\n' "$name" "$age_days"
57+
kept=$((kept + 1))
58+
continue
59+
fi
60+
if [ -n "$MERGED" ] && ! git merge-base --is-ancestor "$ref" "$MERGED" 2>/dev/null; then
61+
printf 'keep %-44s %3dd not in %s\n' "$name" "$age_days" "$MERGED"
62+
kept=$((kept + 1))
63+
continue
64+
fi
65+
if [ "$when" -gt "$cutoff" ]; then
66+
printf 'keep %-44s %3dd younger than %dd\n' "$name" "$age_days" "$DAYS"
67+
kept=$((kept + 1))
68+
continue
69+
fi
70+
doomed=$((doomed + 1))
71+
if [ "$PRUNE" = 1 ]; then
72+
# -D, not -d: a scratch ref is deliberately never merged, so -d refuses
73+
# every one of them. The reflog keeps the sha reachable for 90 days.
74+
sha="$(git rev-parse --short "$ref")"
75+
git branch -D "$name" >/dev/null && printf 'prune %-44s %3dd was %s\n' "$name" "$age_days" "$sha"
76+
else
77+
printf 'PRUNE %-44s %3dd %s\n' "$name" "$age_days" "${subject:0:40}"
78+
fi
79+
done < <(git for-each-ref --sort=committerdate \
80+
--format='%(refname)%09%(committerdate:unix)%09%(contents:subject)' "$PREFIX")
81+
82+
if [ "$doomed" = 0 ] && [ "$kept" = 0 ]; then
83+
echo "scratch-gc: no refs under ${PREFIX#refs/heads/}"
84+
exit 0
85+
fi
86+
if [ "$PRUNE" = 1 ]; then
87+
echo "scratch-gc: pruned $doomed, kept $kept (sha stays in the reflog ~90 days)"
88+
else
89+
echo "scratch-gc: $doomed prunable, $kept kept. --prune to delete."
90+
fi

‎skills/figaro/contributing/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ subject is in play.
1212

1313
| File | When to read it |
1414
|---|---|
15-
| [maintaining.md](maintaining.md) | Always, before the first change. Worktrees, dev shells, the loop, handing work back, and cutting a release, which goes through `scripts/release.sh` because a tag alone ships to nobody. |
15+
| [maintaining.md](maintaining.md) | Always, before the first change. Worktrees, dev shells, the loop, how to commit (constantly) and how to present (deliberately), handing work back, and cutting a release, which goes through `scripts/release.sh` because a tag alone ships to nobody. |
1616
| [updating-docs.md](updating-docs.md) | You are about to edit any file in the skill tree. Before, not after. |
1717

1818
## Testing what paints

‎skills/figaro/contributing/maintaining.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,64 @@ is disk, though it survives reboot, so clean up after yourself.
134134
Commits are itemized and self-contained. There is one real user, so a clean
135135
design beats a compatibility shim.
136136

137+
### Commit constantly, present deliberately
138+
139+
**Two different jobs, and one commit stream cannot do both.**
140+
141+
| Need | Wants |
142+
|---|---|
143+
| RECOVERABILITY, do not lose bytes | frequent, cheap, thoughtless, now |
144+
| COHERENCE, a history someone can read | rewritten, ordered, late |
145+
146+
Conflating them is how an agent comes to hold forty modified files for an hour
147+
with no restore point, which is the state in which `git checkout <path>` eats
148+
work. Measured, 2026-10-07: two files reverted whole to undo one sabotaged
149+
line each, and both had to be rebuilt by hand from the transcript.
150+
151+
So: **commit on every green step, with a throwaway message.** Then, when a
152+
slice is done, mark the mess and rebuild it as the thing you would hand over:
153+
154+
```sh
155+
git commit -am wip # constantly, all day
156+
git branch scratch/<topic>-$(date +%Y%m%d) HEAD # the net
157+
git reset --soft <last presented commit> # tree intact, history gone
158+
git commit -F - # one idea, green alone
159+
```
160+
161+
Three things this is NOT:
162+
163+
- **Not a second working branch.** A scratch branch cannot be a SOURCE of
164+
merges: merging mess yields mess, and to get one coherent commit out of five
165+
WIP commits you rebuild it from the tree regardless. The scratch ref is a
166+
safety net, never an input. Once that is true you never switch branches
167+
while working, which is the point: a branch dance costs a rebuild and loses
168+
the working-tree continuity that made `cp /tmp/x.bak` tempting.
169+
- **Not a squash.** The presented history is REORDERED. Discovery order is
170+
almost never explanatory order, and each commit has to be one idea and green
171+
on its own, which is rarely how it was found.
172+
- **Not a place for apology archaeology.** "I tried X, it failed, so Y"
173+
belongs nowhere. Y with its reason is the commit. Same law as the comment
174+
rule: the record that outlives the work carries the reason, not the
175+
scaffolding.
176+
177+
**The canary needs a restore point, not a backup file.** `git stash` → break
178+
it → run → `git stash pop`, or commit → break → `git checkout .`. Mixing a
179+
`.bak` copy for some files and `git checkout` for others is how a revert
180+
reaches past its target: git's unit is the FILE and an agent's edits are not.
181+
Standing rule: never `git checkout <path>` while that path holds uncommitted
182+
work you mean to keep.
183+
184+
**Scratch refs are garbage and need a sweeper.** Dated in the name, pruned by
185+
`scripts/scratch-gc.sh` (`--days`, `--dry-run`). A policy with no sweeper is a
186+
preference: there were 223 branches in `.bare` the day this was written, which
187+
is what nothing-prunes-anything looks like after a year.
188+
189+
**All of this is a MENU, not a ceremony.** Gluck's instruction outranks it
190+
entirely, and "just put it on main" is a sentence he says. Reach for the full
191+
shape when the work is long, exploratory, or someone else will read it; skip
192+
straight to one commit when the change is one change. Use your judgement and
193+
say which you did.
194+
137195
### Start red
138196

139197
Reproduce on current HEAD before you read anyone's proposed fix, including

0 commit comments

Comments
 (0)