Add tools/run-plan.sh to run a plan's tasks in fresh OpenCode sessions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 14:02:49 -07:00
co-authored by Claude Fable 5.1
parent 76ccc251cd
commit db0ba081db
2 changed files with 70 additions and 1 deletions
+3 -1
View File
@@ -62,7 +62,9 @@ In `~/src/boxmaker`, start a fresh OpenCode session with Laguna S 2.1 and send:
> Read `docs/plans/M2a/01-proto-sha256.md` and do exactly that task.
Then the next file in a new session. If a session ends with a `stopped` row in
Then the next file in a new session. Or let `tools/run-plan.sh docs/plans/M2a` do that: it runs each
task in a fresh `opencode run` session with Laguna, and stops at the first task that does not end
with a commit, a clean tree and a `done` row. If a session ends with a `stopped` row in
`docs/implementer-log.md`, do not start the next task. Task 13 talks to straylight: Ornith must be
loadable and slot 0 should not be in heavy use while it runs.
+67
View File
@@ -0,0 +1,67 @@
#!/bin/sh
# Runs the tasks of a plan one after another, each in its own fresh OpenCode session, and stops
# at the first task that does not end cleanly. This is the owner's tool; the implementing model
# never runs it.
#
# Usage: tools/run-plan.sh docs/plans/M2a [FIRST] FIRST is a task number such as 04
# Options (environment):
# BOXMAKER_MODEL model to use (default straylight/laguna-s-2.1; OpenCode's own default differs)
# OPENCODE_FLAGS extra flags for `opencode run`, for example "--pure" or "--auto"
# DRY_RUN=1 print what would be run and stop
set -eu
plan="${1:?usage: tools/run-plan.sh docs/plans/<milestone> [FIRST]}"
first="${2:-00}"
model="${BOXMAKER_MODEL:-straylight/laguna-s-2.1}"
log=docs/implementer-log.md
milestone=$(basename "$plan")
runs=".state/runs/$milestone"
[ -d "$plan" ] || { echo "run-plan: $plan is not a directory" >&2; exit 2; }
[ -f "$log" ] || { echo "run-plan: run this from the repository root" >&2; exit 2; }
mkdir -p "$runs"
for task in "$plan"/[0-9][0-9]-*.md; do
name=$(basename "$task" .md)
number=$(echo "$name" | cut -c1-2)
[ "$number" -ge "$first" ] || continue
prompt="Read \`$task\` and do exactly that task."
if [ "${DRY_RUN:-0}" = 1 ]; then
echo "opencode run -m $model ${OPENCODE_FLAGS:-} --title $milestone/$name \"$prompt\""
continue
fi
if [ -n "$(git status --porcelain)" ]; then
echo "run-plan: the working tree is not clean before $name; stopping" >&2
exit 1
fi
before=$(git rev-parse HEAD)
echo "=== $milestone/$name: started $(date '+%H:%M:%S')"
# A new `opencode run` is a new session: nothing carries over from the task before.
# shellcheck disable=SC2086
if ! opencode run -m "$model" ${OPENCODE_FLAGS:-} --title "$milestone/$name" "$prompt" \
> "$runs/$name.log" 2>&1; then
echo "run-plan: opencode exited with an error in $name; see $runs/$name.log" >&2
exit 1
fi
echo "=== $milestone/$name: session ended $(date '+%H:%M:%S')"
# The task must have left exactly what AGENTS.md asks for: a commit, a clean tree, and a
# `done` row in the log. Anything else needs a person, so stop.
if [ "$(git rev-parse HEAD)" = "$before" ]; then
echo "run-plan: $name made no commit; stopping" >&2
exit 1
fi
if [ -n "$(git status --porcelain)" ]; then
echo "run-plan: $name left uncommitted changes; stopping" >&2
exit 1
fi
if ! grep -Eq "^\| *$milestone/$name *\|[^|]*\| *done *\|" "$log"; then
echo "run-plan: no 'done' row for $milestone/$name in $log; stopping" >&2
grep -E "^\| *$milestone/$name *\|" "$log" >&2 || true
exit 1
fi
echo "=== $milestone/$name: done ($(git log -1 --format=%h))"
done
echo "run-plan: every task from $first on ended cleanly. Ready for review."