import? "~/.justfile" set shell := ["bash", "-euo", "pipefail", "-c"] set windows-shell := ["pwsh", "-NoLogo", "-Command"] # Shared role defaults; exported overrides survive nested recipes and agent sessions. # See docs/agents/model-configuration.md for Bash usage and manual fleet selection. implement_model := env("MINA_DEV_IMPLEMENT_MODEL", "gpt-6.1-sol") implement_effort := env("MINA_DEV_IMPLEMENT_EFFORT", "high") plan_model := env("MINA_DEV_PLAN_MODEL", "gpt-6.1-sol") plan_effort := env("MINA_DEV_PLAN_EFFORT", "high") fleet_model := env("MINA_DEV_FLEET_MODEL", "claude-opus-5-5") fleet_effort := env("MINA_DEV_FLEET_EFFORT", "high") fleet_codex_model := env("MINA_DEV_FLEET_CODEX_MODEL", "gpt-6.1-sol") fleet_codex_effort := env("MINA_DEV_FLEET_CODEX_EFFORT", "medium") review_codex_model := env("MINA_DEV_REVIEW_CODEX_MODEL", "gpt-6.1-sol") review_codex_effort := env("MINA_DEV_REVIEW_CODEX_EFFORT", "high") review_claude_model := env("MINA_DEV_REVIEW_CLAUDE_MODEL", "claude-opus-5-5") review_claude_effort := env("MINA_DEV_REVIEW_CLAUDE_EFFORT", "high") aggregate_model := env("MINA_DEV_AGGREGATE_MODEL", "gpt-6-luna") aggregate_effort := env("MINA_DEV_AGGREGATE_EFFORT", "xhigh") validate_model := env("MINA_DEV_VALIDATE_MODEL", "gpt-6.1-sol") validate_effort := env("MINA_DEV_VALIDATE_EFFORT", "high") garden_model := env("MINA_DEV_GARDEN_MODEL", "gpt-6.1-sol") garden_effort := env("MINA_DEV_GARDEN_EFFORT", "high") guardian_model := env("MINA_DEV_GUARDIAN_MODEL", "gpt-6.1-sol") guardian_effort := env("MINA_DEV_GUARDIAN_EFFORT", "high") export MINA_DEV_IMPLEMENT_MODEL := implement_model export MINA_DEV_IMPLEMENT_EFFORT := implement_effort export MINA_DEV_PLAN_MODEL := plan_model export MINA_DEV_PLAN_EFFORT := plan_effort export MINA_DEV_FLEET_MODEL := fleet_model export MINA_DEV_FLEET_EFFORT := fleet_effort export MINA_DEV_FLEET_CODEX_MODEL := fleet_codex_model export MINA_DEV_FLEET_CODEX_EFFORT := fleet_codex_effort export MINA_DEV_REVIEW_CODEX_MODEL := review_codex_model export MINA_DEV_REVIEW_CODEX_EFFORT := review_codex_effort export MINA_DEV_REVIEW_CLAUDE_MODEL := review_claude_model export MINA_DEV_REVIEW_CLAUDE_EFFORT := review_claude_effort export MINA_DEV_AGGREGATE_MODEL := aggregate_model export MINA_DEV_AGGREGATE_EFFORT := aggregate_effort export MINA_DEV_VALIDATE_MODEL := validate_model export MINA_DEV_VALIDATE_EFFORT := validate_effort export MINA_DEV_GARDEN_MODEL := garden_model export MINA_DEV_GARDEN_EFFORT := garden_effort export MINA_DEV_GUARDIAN_MODEL := guardian_model export MINA_DEV_GUARDIAN_EFFORT := guardian_effort agent_model_thinking_levels := implement_model + "/" + implement_effort + "\n" + review_claude_model + "/" + review_claude_effort + "\n" + review_claude_model + "/xhigh\n" + review_codex_model + "/medium\n" + review_codex_model + "/" + review_codex_effort + "\n" + garden_model + "/" + garden_effort + "\n" + garden_model + "/xhigh" [private] @default: just --list # Install local development hooks. [group('dev-tooling')] init: command -v mise >/dev/null || { echo "missing required tool: mise" >&2; exit 1; } mise install just frontend-install mise exec -- prek install --hook-type pre-commit # Validate GitHub Actions workflow syntax. # TODO: Move actionlint back to go.mod's tool block after actionlint supports yaml/v4 rc.6 (https://github.com/rhysd/actionlint/pull/730); the versioned go run isolates actionlint v1.7.12's yaml/v4 rc.3 dependency from golangci-lint v2.13.2's gosec dependency on rc.6. [group('dev-tooling')] workflow-check: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # Format Go source files. [group('dev-tooling')] fmt: go fmt ./... # Check Go source formatting. [group('dev-tooling')] fmt-check: files="$(git ls-files '*.go' | while IFS= read -r file; do [ -f "$file" ] && gofmt -l "$file"; done)"; if [ -n "$files" ]; then printf 'Go files need formatting:\n%s\n' "$files" >&2; exit 1; fi # Normalize prose in eligible tracked Markdown documents. [group('dev-tooling')] [working-directory: 'frontend'] prose-fmt: #!/usr/bin/env bash set -euo pipefail paths=() while IFS= read -r -d '' path; do case "$path" in LICENSE.md|README.md|SCOPE.md|docs/plans/*) continue ;; esac [[ -e "../$path" ]] || continue [[ -L "../$path" ]] && continue paths+=("../$path") done < <(git -C .. ls-files -z '*.md') ((${#paths[@]})) || exit 0 mise exec -- pnpm exec prettier --write --prose-wrap never "${paths[@]}" # Apply Go source fixes. [group('dev-tooling')] fix: go fix ./... # Run static analysis checks. [group('dev-tooling')] lint: go tool golangci-lint run ./... go run ./internal/tools/archlint # Regenerate OpenAPI server and client code. [group('codegen')] openapi: go tool oapi-codegen -config api/oapi-codegen.yaml api/openapi.yaml go tool oapi-codegen -config api/oapi-codegen-httpclient.yaml api/openapi.yaml go run ./internal/tools/surfacegen # Validate OpenAPI client-surface exposure decisions. [group('codegen')] surface-check: go run ./internal/tools/surfacegen -verify # Validate OpenAPI and generated code freshness. [group('codegen')] openapi-check: surface-check go run github.com/getkin/kin-openapi/cmd/validate api/openapi.yaml tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; awk -v output="$tmpdir/server/openapi.gen.go" '/^output:/ { print "output: " output; next } { print }' api/oapi-codegen.yaml > "$tmpdir/oapi-codegen.yaml"; mkdir -p "$tmpdir/server"; go tool oapi-codegen -config "$tmpdir/oapi-codegen.yaml" api/openapi.yaml; cmp -s "$tmpdir/server/openapi.gen.go" internal/httpapi/openapi/openapi.gen.go || { echo 'generated OpenAPI server output is stale; run `just openapi`' >&2; diff -u internal/httpapi/openapi/openapi.gen.go "$tmpdir/server/openapi.gen.go" >&2; exit 1; }; awk -v output="$tmpdir/client/openapi.gen.go" '/^output:/ { print "output: " output; next } { print }' api/oapi-codegen-httpclient.yaml > "$tmpdir/oapi-codegen-httpclient.yaml"; mkdir -p "$tmpdir/client"; go tool oapi-codegen -config "$tmpdir/oapi-codegen-httpclient.yaml" api/openapi.yaml; cmp -s "$tmpdir/client/openapi.gen.go" internal/httpclient/openapi.gen.go || { echo 'generated OpenAPI client output is stale; run `just openapi`' >&2; diff -u internal/httpclient/openapi.gen.go "$tmpdir/client/openapi.gen.go" >&2; exit 1; } # Regenerate the pristine current accounting-schema DDL artifact. [group('codegen')] accounting-schema: go run ./internal/tools/accountingschema # Verify the checked-in accounting-schema DDL artifact is current. [group('codegen')] accounting-schema-check: main_ref="$(git rev-parse --verify 'main^{commit}' 2>/dev/null || git rev-parse --verify 'origin/main^{commit}' 2>/dev/null)" || { echo 'cannot verify migration immutability: main ref is missing' >&2; exit 1; }; changed_migrations="$(git diff --no-renames --diff-filter=MD --name-only "$main_ref" -- internal/store/migrations/)"; [[ -z "$changed_migrations" ]] || { echo 'migrations already on main are immutable; add a new migration instead:' >&2; echo "$changed_migrations" >&2; exit 1; }; tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; go run ./internal/tools/accountingschema -output "$tmpdir/schema.sql"; cmp -s internal/services/accountingschema/schema.sql "$tmpdir/schema.sql" || { echo 'generated accounting schema is stale; run `just accounting-schema`' >&2; diff -u internal/services/accountingschema/schema.sql "$tmpdir/schema.sql" >&2; exit 1; } # Tidy Go module files. [group('dev-tooling')] tidy: go mod tidy # Install the frontend package dependencies. [group('frontend')] [working-directory: 'frontend'] frontend-install: mise exec -- pnpm install # Format frontend source files. [group('frontend')] [working-directory: 'frontend'] frontend-fmt: mise exec -- pnpm run format # Check frontend source formatting. [group('frontend')] [working-directory: 'frontend'] frontend-fmt-check: mise exec -- pnpm run format:check # Run frontend static analysis checks. [group('frontend')] [working-directory: 'frontend'] frontend-lint: mise exec -- pnpm run lint # Run frontend TypeScript compiler checks. [group('frontend')] [working-directory: 'frontend'] frontend-typecheck: mise exec -- pnpm run typecheck # Build frontend assets. [group('frontend')] [working-directory: 'frontend'] frontend-build: mise exec -- pnpm run build # Start the Vite dev server against a running Mina backend. [group('frontend')] [working-directory: 'frontend'] frontend-dev backend_url="http://127.0.0.1:8080": MINA_FRONTEND_BACKEND_URL={{ quote(backend_url) }} mise exec -- pnpm run dev # Run all frontend static checks. [group('frontend')] frontend-check: frontend-openapi-check frontend-fmt-check frontend-lint frontend-typecheck # Check Docker builder versions against development tool declarations. [group('docker')] docker-version-check: scripts/check-docker-versions.sh # Check that a remote image index includes every supported deployment platform. [group('docker')] docker-manifest-check image: #!/usr/bin/env bash set -euo pipefail image={{ quote(image) }} [ -n "$image" ] || { echo "usage: just docker-manifest-check IMAGE" >&2; exit 2; } manifest="$(docker buildx imagetools inspect "$image")" grep -Eq 'Platform:[[:space:]]+linux/amd64' <<<"$manifest" || { echo "$image is missing linux/amd64" >&2; exit 1; } grep -Eq 'Platform:[[:space:]]+linux/arm64' <<<"$manifest" || { echo "$image is missing linux/arm64" >&2; exit 1; } printf '%s includes linux/amd64 and linux/arm64\n' "$image" # Run the real Docker image and Compose lifecycle test. [group('docker')] test-docker: scripts/docker-service-test.sh # Regenerate frontend REST client code. [group('codegen')] [working-directory: 'frontend'] frontend-openapi: mise exec -- pnpm exec openapi-ts -f openapi-ts.config.ts --no-log-file # Verify frontend REST client generated code freshness. [group('codegen')] frontend-openapi-check: tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; cd frontend; mise exec -- pnpm exec openapi-ts -f openapi-ts.config.ts -o "$tmpdir/generated" --no-log-file >/dev/null; diff -qr src/api/generated "$tmpdir/generated" >/dev/null || { echo 'generated frontend OpenAPI client output is stale; run `just frontend-openapi`' >&2; diff -ru src/api/generated "$tmpdir/generated" >&2; exit 1; } # Build the mina binary without rebuilding frontend assets. [group('dev-tooling')] build-go: test -f internal/webui/dist/index.html || { echo 'missing embedded frontend assets; run `just frontend-build` or `just build`' >&2; exit 1; } mkdir -p bin go build -buildvcs=false -ldflags="-X github.com/mishamsk/mina/internal/runtime.buildCommitHash=$(git rev-parse --verify HEAD 2>/dev/null || true) -X github.com/mishamsk/mina/internal/runtime.buildRepositoryURL=$(git remote get-url origin 2>/dev/null || true)" -o bin/mina ./cmd/mina # Build frontend assets and the mina binary. [group('dev-tooling')] build: frontend-build build-go [doc('''Start the REST API in the background; pass -p to persist data in build/dev/mina.db. Pass --db to use a specific Mina database. Pass --demo to seed deterministic demo data and enable login plus API authentication.''')] [group('demo')] dev mode="" extra="" value="": build #!/usr/bin/env bash set -euo pipefail dev_dir="build/dev" pid_file="$dev_dir/mina.pid" stdout_log="$dev_dir/stdout.log" stderr_log="$dev_dir/stderr.log" access_log="$dev_dir/access.log" tailscale_port_file="$dev_dir/tailscale-port" db_path="$dev_dir/mina.db" config_path="$dev_dir/config.toml" auth_path="$dev_dir/auth.toml" default_port=8080 persist=false demo=false custom_db_path="" expect_db_path=false port_in_use() { (exec 3<>"/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1 } select_dev_port() { if ! port_in_use "$default_port"; then echo "$default_port" return fi for port in $(seq 49152 65535); do if ! port_in_use "$port"; then echo "$port" return fi done echo "no free high port found" >&2 exit 1 } for arg in {{ quote(mode) }} {{ quote(extra) }} {{ quote(value) }}; do if [ "$expect_db_path" = true ]; then if [ -z "$arg" ]; then echo "usage: just dev [-p | --db ] [--demo]" >&2 exit 2 fi custom_db_path="$arg" expect_db_path=false continue fi case "$arg" in "") ;; "-p") persist=true ;; "--demo") demo=true ;; "--db") expect_db_path=true ;; *) echo "usage: just dev [-p | --db ] [--demo]" >&2 exit 2 ;; esac done if [ "$expect_db_path" = true ] || { [ "$persist" = true ] && [ -n "$custom_db_path" ]; }; then echo "usage: just dev [-p | --db ] [--demo]" >&2 exit 2 fi mkdir -p "$dev_dir" if [ -f "$pid_file" ]; then pid="$(cat "$pid_file")" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "mina already running at http://127.0.0.1:8080 with pid $pid" >&2 exit 1 fi rm -f "$pid_file" if [ -f "$tailscale_port_file" ]; then tailscale_port="$(cat "$tailscale_port_file")" tailscale serve --https="$tailscale_port" off rm -f "$tailscale_port_file" fi fi port="$(select_dev_port)" : > "$stdout_log" : > "$stderr_log" mina_command=(./bin/mina) serve_args=(serve --host 127.0.0.1 --port "$port" --access-log "$access_log") if [ "$persist" = true ]; then serve_args+=(--db "$db_path" --yes) elif [ -n "$custom_db_path" ]; then serve_args+=(--db "$custom_db_path" --yes) fi if [ "$demo" = true ]; then printf 'auth_file = "auth.toml"\n' > "$config_path" chmod 0600 "$config_path" install -m 0600 scripts/fixtures/demo-auth.toml "$auth_path" mina_command+=(--config-file "$config_path") serve_args+=(--demo) fi nohup "${mina_command[@]}" "${serve_args[@]}" > "$stdout_log" 2> "$stderr_log" & pid="$!" echo "$pid" > "$pid_file" disown "$pid" for _ in {1..50}; do if grep -q "listening http://127.0.0.1:$port" "$stdout_log"; then if ! tailscale serve --https="$port" --bg "http://127.0.0.1:$port"; then kill -TERM "$pid" 2>/dev/null || true rm -f "$pid_file" echo "failed to serve Mina over Tailscale" >&2 exit 1 fi echo "$port" > "$tailscale_port_file" echo "mina listening at http://127.0.0.1:$port with pid $pid" if [ "$port" != "$default_port" ]; then echo "default port $default_port was busy; selected high port $port" fi if [ "$demo" = true ]; then echo "login: admin@local / password" echo "api token: mina-token" fi echo "logs: $stdout_log $stderr_log $access_log" exit 0 fi if ! kill -0 "$pid" 2>/dev/null; then rm -f "$pid_file" echo "mina failed to start; see $stdout_log and $stderr_log" >&2 exit 1 fi sleep 0.1 done echo "mina did not report readiness; see $stdout_log and $stderr_log" >&2 exit 1 # Stop the REST API demo server. [group('demo')] dev-kill: #!/usr/bin/env bash set -euo pipefail pid_file="build/dev/mina.pid" tailscale_port_file="build/dev/tailscale-port" stop_tailscale_serve() { if [ ! -f "$tailscale_port_file" ]; then return fi tailscale_port="$(cat "$tailscale_port_file")" tailscale serve --https="$tailscale_port" off rm -f "$tailscale_port_file" } print_detected_mina_serve() { detected_pids="$(pgrep -f '(^|.*/)mina serve([[:space:]]|$)' || true)" if [ -z "$detected_pids" ]; then echo "no mina serve processes detected" return fi echo "detected mina serve processes:" while IFS= read -r detected_pid; do command_line="$(ps -p "$detected_pid" -o command= 2>/dev/null || true)" listen_addrs="$(lsof -nP -a -p "$detected_pid" -iTCP -sTCP:LISTEN 2>/dev/null | awk 'NR > 1 { print $9 }' | paste -sd, - || true)" if [ -n "$listen_addrs" ]; then echo " pid $detected_pid listening $listen_addrs: $command_line" else echo " pid $detected_pid: $command_line" fi done <<< "$detected_pids" echo "to stop all detected mina serve processes: kill -TERM $(echo "$detected_pids" | paste -sd' ' -)" } if [ ! -f "$pid_file" ]; then stop_tailscale_serve echo "no background mina pid file found" print_detected_mina_serve exit 0 fi pid="$(cat "$pid_file")" if [ -z "$pid" ]; then rm -f "$pid_file" stop_tailscale_serve echo "removed empty mina pid file" print_detected_mina_serve exit 0 fi if ! kill -0 "$pid" 2>/dev/null; then rm -f "$pid_file" stop_tailscale_serve echo "removed stale mina pid file for pid $pid" print_detected_mina_serve exit 0 fi kill -TERM "$pid" for _ in {1..50}; do if ! kill -0 "$pid" 2>/dev/null; then rm -f "$pid_file" stop_tailscale_serve echo "stopped mina pid $pid" exit 0 fi sleep 0.1 done kill -KILL "$pid" rm -f "$pid_file" stop_tailscale_serve echo "force-stopped mina pid $pid" # Run all Go tests. [group('dev-tooling')] test: go test ./... # Compare test inventories with the existing main worktree; use --worktree for uncommitted changes. [group('dev-tooling')] [positional-arguments] test-diff *args: #!/usr/bin/env bash set -euo pipefail exec go run ./internal/tools/testdiff "$@" # Inventory app tests without executing test bodies (also used against the main worktree). [private] test-list-go root: @cd {{ quote(root) }} && go test -json -list '^Test' ./internal/apptest/runtime # Inventory browser tests without building Mina or installing/launching browsers. [private] test-list-frontend root: @cd {{ quote(root) }}/frontend && mise exec -- pnpm exec playwright test --list --reporter=json --forbid-only --pass-with-no-tests # Run focused concurrent app tests with the Go race detector. [group('dev-tooling')] test-race-concurrency: go test -race ./internal/apptest/runtime -run '^TestConcurrent' -count=1 # Run REST integration tests. [group('dev-tooling')] test-integration: frontend-build go test -tags=integration ./cmd/mina -run TestIntegrationScripts -count=1 # Run frontend browser end-to-end tests against the embedded UI. [group('dev-tooling')] [working-directory: 'frontend'] [positional-arguments] test-frontend-e2e *playwright_args: frontend-install build #!/usr/bin/env bash set -euo pipefail playwright_install_args=(chromium webkit) if [[ "$(uname -s)" == Linux ]]; then playwright_install_args=(--with-deps "${playwright_install_args[@]}") fi mise exec -- pnpm exec playwright install "${playwright_install_args[@]}" if [[ $# -eq 1 && $1 == "" ]]; then set -- fi while IFS= read -r mina_environment_name; do if [[ $mina_environment_name == MINA_* ]]; then unset "$mina_environment_name" fi done < <(compgen -e) # Bypass `pnpm exec`: its SIGINT forwarding can exit Playwright before web-server cleanup finishes. exec env MISE_NO_ENV=1 mise exec -- node node_modules/@playwright/test/cli.js test "$@" # Run a manual local REST responsiveness benchmark with default script settings. [group('dev-tooling')] bench-rest: build scripts/rest-benchmark.sh # Run configured pre-commit checks against all tracked files. [group('dev-tooling')] pre-commit: prek run --all-files # Serve recorded UI evidence from build/ on a fixed local port. [group('dev-tooling')] evidence-serve: go run ./internal/tools/evidencegallery # Start an interactive agent: role [-C workdir] [--prompt-file path | prompt]. [group('agents')] [positional-arguments] agent role *args: @just _agent interactive "$@" # Run a headless agent with the same role settings as agent. [group('agents')] [positional-arguments] agent-exec role *args: @just _agent exec "$@" [private] [positional-arguments] _agent mode role *args: #!/usr/bin/env bash set -euo pipefail mode="$1" role="$2" shift 2 case "$role" in implement|fix) model="$MINA_DEV_IMPLEMENT_MODEL"; effort="$MINA_DEV_IMPLEMENT_EFFORT" ;; plan) model="$MINA_DEV_PLAN_MODEL"; effort="$MINA_DEV_PLAN_EFFORT" ;; fleet) model="$MINA_DEV_FLEET_MODEL"; effort="$MINA_DEV_FLEET_EFFORT" ;; fleet-codex) model="$MINA_DEV_FLEET_CODEX_MODEL"; effort="$MINA_DEV_FLEET_CODEX_EFFORT" ;; review-codex) model="$MINA_DEV_REVIEW_CODEX_MODEL"; effort="$MINA_DEV_REVIEW_CODEX_EFFORT" ;; review-claude) model="$MINA_DEV_REVIEW_CLAUDE_MODEL"; effort="$MINA_DEV_REVIEW_CLAUDE_EFFORT" ;; aggregate) model="$MINA_DEV_AGGREGATE_MODEL"; effort="$MINA_DEV_AGGREGATE_EFFORT" ;; validate) model="$MINA_DEV_VALIDATE_MODEL"; effort="$MINA_DEV_VALIDATE_EFFORT" ;; guardian) model="$MINA_DEV_GUARDIAN_MODEL"; effort="$MINA_DEV_GUARDIAN_EFFORT" ;; garden) model="$MINA_DEV_GARDEN_MODEL"; effort="$MINA_DEV_GARDEN_EFFORT" ;; *) echo "unknown agent role: $role" >&2; exit 2 ;; esac workdir=. prompt="" prompt_file="" prompt_set=false while [ "$#" -gt 0 ]; do case "$1" in -C|--prompt-file|--model|--effort) [ "$#" -ge 2 ] || { echo "$1 requires a value" >&2; exit 2; } case "$1" in -C) workdir="$2" ;; --prompt-file) prompt_file="$2" ;; # Picker overrides affect only this launch, not inherited role defaults. --model) model="$2" ;; --effort) effort="$2" ;; esac shift 2 ;; --) shift [ "$#" -eq 1 ] && [ "$prompt_set" = false ] || { echo "expected one prompt after --" >&2; exit 2; } prompt="$1"; prompt_set=true; shift ;; -*) echo "unknown agent option: $1" >&2; exit 2 ;; *) [ "$prompt_set" = false ] || { echo "provide only one prompt; use -C for the working directory" >&2; exit 2; } prompt="$1"; prompt_set=true; shift ;; esac done cd -- "$workdir" if [ -n "$prompt_file" ]; then [ "$prompt_set" = false ] || { echo "provide prompt or --prompt-file, not both" >&2; exit 2; } prompt="$(cat -- "$prompt_file")" fi if [ "$mode" = exec ] && [ -z "$prompt" ]; then echo "headless agents require a prompt or --prompt-file" >&2 exit 2 fi case "$model" in gpt-*) args=(codex) if [ "$mode" = exec ]; then args+=(exec); fi args+=(-m "$model" -c "model_reasoning_effort=$effort") ;; claude-*) args=(claude) if [ "$mode" = exec ]; then args+=(-p --output-format text); fi args+=(--model "$model" --effort "$effort") ;; *) echo "unsupported model provider: $model (use gpt- or claude-)" >&2; exit 2 ;; esac if [ -n "$prompt" ]; then args+=(-- "$prompt"); fi printf 'agent %s: %s / %s (%s) in %s\n' "$role" "$model" "$effort" "$mode" "$PWD" >&2 exec "${args[@]}" # Garden eligible documentation with parallel Codex agents. [group('agents')] garden-docs limit="" codex=(garden_model + "/" + garden_effort): #!/usr/bin/env bash set -euo pipefail limit={{ quote(limit) }} args=(--codex {{ quote(codex) }}) if [ -n "$limit" ]; then args+=(--limit "$limit") fi go run ./internal/tools/gardendocs "${args[@]}" # Run the repository-local review loop through local Codex and Claude CLI sessions. [group('agents')] review-loop mode context branch_or_commit="" base_ref="" claude_model=review_claude_model codex_reviewer=(review_codex_model + "/" + review_codex_effort) codex_aggregator=(aggregate_model + "/" + aggregate_effort) codex_validator=(validate_model + "/" + validate_effort) codex_fixer=(implement_model + "/" + implement_effort) claude_effort=review_claude_effort codex_guardian=(guardian_model + "/" + guardian_effort): #!/usr/bin/env bash set -euo pipefail branch_or_commit={{ quote(branch_or_commit) }} base_ref={{ quote(base_ref) }} set -- \ --claude-model {{ quote(claude_model) }} \ --claude-effort {{ quote(claude_effort) }} \ --codex-reviewer {{ quote(codex_reviewer) }} \ --codex-aggregator {{ quote(codex_aggregator) }} \ --codex-validator {{ quote(codex_validator) }} \ --codex-fixer {{ quote(codex_fixer) }} \ --codex-guardian {{ quote(codex_guardian) }} if [ -n "$base_ref" ]; then set -- "$@" --base "$base_ref" fi log_dir="build/review-loop" mkdir -p "$log_dir" log_file="$log_dir/review-loop-$(date -u +%Y%m%dT%H%M%SZ)-$$.log" echo "review loop log: $log_file" review_command=(go run ./internal/tools/reviewloop "$@" {{ quote(mode) }} {{ quote(context) }}) if [ -n "$branch_or_commit" ]; then review_command+=("$branch_or_commit") fi "${review_command[@]}" 2>&1 | tee "$log_file" # Tail the most recently updated review-loop log with optional tail arguments. [group('agents')] [positional-arguments] review-loop-tail *tail_args: #!/usr/bin/env bash set -euo pipefail log_dir="build/review-loop" latest_log="" shopt -s nullglob for log_file in "$log_dir"/review-loop-*.log; do if [ -z "$latest_log" ] || [ "$log_file" -nt "$latest_log" ]; then latest_log="$log_file" fi done if [ -z "$latest_log" ]; then echo "no review-loop logs found in $log_dir" >&2 exit 1 fi echo "tailing review loop log: $latest_log" tail "$@" "$latest_log" # Run Codex against an implementation plan. [group('agents')] codex-goal plan_file="": #!/usr/bin/env bash set -euo pipefail plan_file={{ quote(plan_file) }} if [ -z "$plan_file" ]; then command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; } if ! plan_file="$( find docs/plans -maxdepth 1 -type f -name '*.md' | sort | fzf \ --prompt='Plan> ' \ --preview='sed -n "1,120p" {}' )"; then echo "no plan selected" >&2 exit 1 fi [ -n "$plan_file" ] || { echo "no plan selected" >&2; exit 1; } fi exec just agent implement "/goal Implement @${plan_file}. The plan's completion criteria define when the goal is complete." # Run a Codex operator against a sequential fleet plan. [group('agents')] codex-goal-fleet plan_file="": #!/usr/bin/env bash set -euo pipefail plan_file={{ quote(plan_file) }} command -v codex >/dev/null || { echo "missing required tool: codex" >&2; exit 1; } if [ -z "$plan_file" ]; then command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; } if ! plan_file="$( find docs/plans -maxdepth 1 -type f -name '*-fleet.md' | \ while IFS= read -r candidate; do if [[ "$candidate" =~ ^docs/plans/[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*-fleet\.md$ ]]; then printf '%s\n' "$candidate" fi done | sort | fzf \ --prompt='Fleet plan> ' \ --preview='sed -n "1,160p" {}' )"; then echo "no fleet plan selected" >&2 exit 1 fi [ -n "$plan_file" ] || { echo "no fleet plan selected" >&2; exit 1; } fi [[ "$plan_file" =~ ^docs/plans/[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*-fleet\.md$ ]] || { echo "fleet plan must match docs/plans/YYYY-MM-DD--fleet.md" >&2 exit 1 } [ -f "$plan_file" ] || { echo "fleet plan not found: $plan_file" >&2; exit 1; } exec just agent fleet-codex "/goal Operate @${plan_file}. The plan's completion criteria define when the goal is complete." # List currently actionable, unclaimed parent Kata issues as tab-separated rows. [private] kata-open-issues: #!/usr/bin/env bash set -euo pipefail command -v jq >/dev/null || { echo "missing required tool: jq" >&2; exit 1; } command -v kata >/dev/null || { echo "missing required tool: kata" >&2; exit 1; } jq -r -s ' (.[1].issues | map(select(.parent != null) | .parent.short_id) | unique) as $parents | .[0].issues | map(select((.short_id as $id | $parents | index($id) | not))) | group_by(.priority // 5)[] | sort_by(.updated_at) | reverse[] | [ .short_id, ("P" + (.priority | tostring)), .updated_at, .title ] | @tsv ' <(kata ready --unowned --json --limit 0) <(kata list --status all --limit 1000 --json) # Start a kata plan-only worktree through Codex. [group('agents')] codex-kata-plan: #!/usr/bin/env bash set -euo pipefail command -v codex >/dev/null || { echo "missing required tool: codex" >&2; exit 1; } command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; } if ! selected="$( just --quiet kata-open-issues | fzf \ --prompt='Kata> ' \ --delimiter=$'\t' \ --with-nth=1,2,4 \ --preview='kata show {1} --agent' \ --preview-window='right:50%:wrap-word' )"; then echo "no kata issue selected" >&2 exit 1 fi [ -n "$selected" ] || { echo "no kata issue selected" >&2; exit 1; } issue="${selected%%$'\t'*}" exec just agent-exec plan "\$kata-plan-worktree #${issue}" # Create a worktree for a selected Kata issue and launch an implementation agent. [group('agents')] kata-implement: #!/usr/bin/env bash set -euo pipefail command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; } command -v git >/dev/null || { echo "missing required tool: git" >&2; exit 1; } command -v gt >/dev/null || { echo "missing required tool: gt" >&2; exit 1; } command -v jq >/dev/null || { echo "missing required tool: jq" >&2; exit 1; } command -v kata >/dev/null || { echo "missing required tool: kata" >&2; exit 1; } if ! selected="$( just --quiet kata-open-issues | fzf \ --prompt='Kata> ' \ --delimiter=$'\t' \ --with-nth=1,2,4 \ --preview='kata show {1} --agent' \ --preview-window='right:50%:wrap-word' )"; then echo "no kata issue selected" >&2 exit 1 fi [ -n "$selected" ] || { echo "no kata issue selected" >&2; exit 1; } issue="${selected%%$'\t'*}" if ! agent_model_thinking="$( printf '%s\n' {{ quote(agent_model_thinking_levels) }} | fzf --prompt='Agent model / thinking> ' )"; then echo "no agent model and thinking level selected" >&2 exit 1 fi [ -n "$agent_model_thinking" ] || { echo "no agent model and thinking level selected" >&2; exit 1; } case "${agent_model_thinking%/*}" in gpt-*) agent_cli=codex ;; claude-*) agent_cli=claude ;; *) echo "unsupported agent model: ${agent_model_thinking%/*}" >&2; exit 2 ;; esac command -v "$agent_cli" >/dev/null || { echo "missing required tool: $agent_cli" >&2; exit 1; } kata claim "$issue" --agent issue_json="$(kata show "$issue" --json)" title="$(jq -r '.issue.title' <<<"$issue_json")" issue_body="$(jq -r '.issue.body // ""' <<<"$issue_json")" slug="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" [ -n "$slug" ] || { echo "could not create a branch slug from Kata issue title" >&2; exit 1; } branch="${issue}-${slug}" git fetch origin main gt "$branch" main -x true worktree_path="$( git worktree list --porcelain | awk -v branch="refs/heads/$branch" ' $1 == "worktree" { path = $2 } $1 == "branch" && $2 == branch { print path; exit } ' )" [ -n "$worktree_path" ] || { echo "could not find worktree for branch $branch" >&2; exit 1; } prompt_template="$(