diff --git a/docs/plans/M2a/README.md b/docs/plans/M2a/README.md index 725b18c..79b7ba4 100644 --- a/docs/plans/M2a/README.md +++ b/docs/plans/M2a/README.md @@ -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. diff --git a/tools/run-plan.sh b/tools/run-plan.sh new file mode 100755 index 0000000..f1039d2 --- /dev/null +++ b/tools/run-plan.sh @@ -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/ [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."