Justfile
Rama main · réplica de lectura del repositorio original.
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 <path> 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 <path>] [--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 <path>] [--demo]" >&2
exit 2
;;
esac
done
if [ "$expect_db_path" = true ] || { [ "$persist" = true ] && [ -n "$custom_db_path" ]; }; then
echo "usage: just dev [-p | --db <path>] [--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-<topic>-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="$(<docs/kata-implementation-prompt-template.md)"
prompt="${prompt_template//\{\{issue\}\}/#$issue}"
prompt="${prompt//\{\{issue_body\}\}/$issue_body}"
exec just agent implement -C "$worktree_path" --model "${agent_model_thinking%/*}" --effort "${agent_model_thinking##*/}" -- "$prompt"
# Rebase the current branch through Codex.
[group('agents')]
rebase:
just agent-exec implement {{ quote("$rebase") }}
# Agent-only manual smoke commands should be added temporarily when a concrete uncovered risk remains outside the testscript end-to-end suite.