diff --git a/README.md b/README.md index 59a4606..0714658 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Uninstall with `./scripts/uninstall.sh` (`--dry-run` to preview). | `devloop clean` | Remove run artifacts | Each run writes an HTML report, spec, and reviews under `.devloop/`. -When you pick a spec from the interactive menu, devloop uses the standard run defaults and only prompts for PR mode. Use CLI flags such as `--coder`, `--reviewer`, `--in-place`, or `--timeout-minutes` when you need to override those defaults. +When you pick a spec from the interactive menu, devloop uses your configured run defaults and only prompts for PR mode. The interactive **Settings** menu sets the default coder and reviewer agents (Codex or Claude Code), spec path, and run timeout, saved to `~/.devloop/config`. Use CLI flags such as `--coder`, `--reviewer`, `--in-place`, or `--timeout-minutes` to override those defaults per run. ## Specs diff --git a/devloop b/devloop index 3f46ecd..1a0d96a 100755 --- a/devloop +++ b/devloop @@ -7,6 +7,8 @@ CLAUDE_MODEL_ARGS=(--model claude-opus-4-8) CLAUDE_EFFORT_ARGS=(--effort max) DEFAULT_TIMEOUT_MINUTES=30 DEFAULT_SPEC_DIR=".devloop/specs" +DEFAULT_CODER="codex" +DEFAULT_REVIEWER="claude" SCRIPT_PATH="${BASH_SOURCE[0]}" while [ -L "$SCRIPT_PATH" ]; do @@ -198,8 +200,8 @@ Common commands: Options: --tui force terminal UI output --plain force plain output - --coder codex|claude choose Codex or Claude Code for implementation - --reviewer codex|claude choose Codex or Claude Code for review + --coder codex|claude choose Codex or Claude Code for implementation (default from Settings) + --reviewer codex|claude choose Codex or Claude Code for review (default from Settings) --report-format html|markdown choose report format --timeout-minutes N cap one run, default 30 --no-strict weaken strict review gates @@ -233,8 +235,8 @@ welcome_tui() { gum style --foreground "$UI_ACCENT_COLOR" --bold "Options" printf ' %-30s %s\n' "--tui" "force terminal UI output" printf ' %-30s %s\n' "--plain" "force plain output" - printf ' %-30s %s\n' "--coder codex|claude" "choose implementation agent" - printf ' %-30s %s\n' "--reviewer codex|claude" "choose review agent" + printf ' %-30s %s\n' "--coder codex|claude" "choose implementation agent (default from Settings)" + printf ' %-30s %s\n' "--reviewer codex|claude" "choose review agent (default from Settings)" printf ' %-30s %s\n' "--report-format html|markdown" "choose report format" printf ' %-30s %s\n' "--timeout-minutes N" "cap one run, default 30" printf ' %-30s %s\n' "--no-strict" "weaken strict review gates" @@ -545,7 +547,7 @@ write_config_value() { local value="$3" local file tmp case "$key" in - spec_dir|timeout_minutes) ;; + spec_dir|timeout_minutes|coder|reviewer) ;; *) printf 'unknown config key: %s\n' "$key" >&2; return 2 ;; esac file="$(devloop_config_file_for_scope "$scope")" || return $? @@ -563,7 +565,7 @@ remove_config_value() { local key="$2" local file tmp case "$key" in - spec_dir|timeout_minutes) ;; + spec_dir|timeout_minutes|coder|reviewer) ;; *) printf 'unknown config key: %s\n' "$key" >&2; return 2 ;; esac file="$(devloop_config_file_for_scope "$scope")" || return $? @@ -812,6 +814,58 @@ remove_config_timeout_minutes() { remove_config_value "$scope" timeout_minutes } +devloop_agent_setting() { + local key="$1" + local default="$2" + local scope file value normalized + ensure_global_config >/dev/null 2>&1 || true + for scope in local global; do + file="$(devloop_config_file_for_scope "$scope" 2>/dev/null)" || continue + value="$(config_file_value "$key" "$file" || true)" + [ -n "$value" ] || continue + normalized="$(normalize_agent "$value")" + [ -n "$normalized" ] || continue + printf '%s\n' "$normalized" + return 0 + done + printf '%s\n' "$default" +} + +write_config_agent() { + local key="$1" + local scope="global" + local value normalized + if [ "$#" -eq 3 ]; then + scope="$2" + value="$3" + else + value="$2" + fi + normalized="$(normalize_agent "$value")" + if [ -z "$normalized" ]; then + printf '%s must be Codex or Claude Code\n' "$key" >&2 + return 2 + fi + write_config_value "$scope" "$key" "$normalized" || return $? + printf '%s\n' "$normalized" +} + +devloop_coder() { + devloop_agent_setting coder "$DEFAULT_CODER" +} + +devloop_reviewer() { + devloop_agent_setting reviewer "$DEFAULT_REVIEWER" +} + +write_config_coder() { + write_config_agent coder "$@" +} + +write_config_reviewer() { + write_config_agent reviewer "$@" +} + ui_has_gum() { [ "$USE_TUI" = true ] && command -v gum >/dev/null 2>&1 } @@ -1355,31 +1409,39 @@ interactive_create_spec() { } interactive_settings() { - local choice custom_spec_dir custom_timeout scope saved value timeout_display code - local default_spec_dir + local choice custom_spec_dir custom_timeout scope saved value timeout_display code agent + local default_spec_dir coder_display reviewer_display local choices=() while true; do custom_spec_dir="$(configured_spec_dir || true)" custom_timeout="$(configured_timeout_minutes || true)" timeout_display="$(devloop_timeout_minutes) minutes" default_spec_dir="$(devloop_default_spec_dir)" - ui_header "Settings" "Global spec path and run timeout" + coder_display="$(agent_label "$(devloop_coder)")" + reviewer_display="$(agent_label "$(devloop_reviewer)")" + ui_header "Settings" "Global spec path, agents, and run timeout" if [ -n "$custom_spec_dir" ]; then ui_print_key_values \ "configured" "$custom_spec_dir" \ "fallback" "$default_spec_dir" \ + "coder" "$coder_display" \ + "reviewer" "$reviewer_display" \ "timeout" "$timeout_display" - choices=("Remove spec path" "Set timeout" "Back") + choices=("Remove spec path") else ui_print_key_values \ "default" "$default_spec_dir" \ + "coder" "$coder_display" \ + "reviewer" "$reviewer_display" \ "timeout" "$timeout_display" - choices=("Add spec path" "Set timeout" "Back") + choices=("Add spec path") fi + choices+=("Set coder" "Set reviewer" "Set timeout") if [ -n "$custom_timeout" ]; then - choices=("${choices[@]:0:${#choices[@]}-1}" "Remove timeout" "Back") + choices+=("Remove timeout") fi - choice="$(ui_choose "Spec paths" "${choices[@]}")" + choices+=("Back") + choice="$(ui_choose "Settings" "${choices[@]}")" code=$? if [ "$code" -ne 0 ]; then ui_go_back @@ -1401,6 +1463,24 @@ interactive_settings() { UI_NOTICE="spec path removed" fi ;; + "Set coder") + agent="$(ui_choose "Coder" "Codex" "Claude Code" "Back")" || continue + [ "$agent" = "Back" ] && continue + if saved="$(write_config_coder global "$(agent_choice_value "$agent")")"; then + UI_NOTICE="coder saved: $(agent_label "$saved")" + else + UI_NOTICE="coder not saved" + fi + ;; + "Set reviewer") + agent="$(ui_choose "Reviewer" "Codex" "Claude Code" "Back")" || continue + [ "$agent" = "Back" ] && continue + if saved="$(write_config_reviewer global "$(agent_choice_value "$agent")")"; then + UI_NOTICE="reviewer saved: $(agent_label "$saved")" + else + UI_NOTICE="reviewer not saved" + fi + ;; "Set timeout") value="$(ui_input "Timeout minutes, 1-1440" "$(devloop_timeout_minutes)")" || continue if saved="$(write_config_timeout_minutes global "$value")"; then @@ -1451,12 +1531,14 @@ interactive_run_setup() { local report_format="html" local strict=true local use_worktree=true - local coder="codex" - local reviewer="claude" + local coder + local reviewer local create_pr=false local max="5" local timeout_minutes local code + coder="$(devloop_coder)" + reviewer="$(devloop_reviewer)" timeout_minutes="$(devloop_timeout_minutes)" if [ "$USE_TUI" = true ]; then @@ -1894,8 +1976,8 @@ run_from_track() { if [ -z "$max" ]; then max=5; fi if [ -z "$report_format" ]; then report_format="html"; fi if [ -z "$strict" ]; then strict=true; fi - if [ -z "$coder" ]; then coder="codex"; fi - if [ -z "$reviewer" ]; then reviewer="claude"; fi + if [ -z "$coder" ]; then coder="$(devloop_coder)"; fi + if [ -z "$reviewer" ]; then reviewer="$(devloop_reviewer)"; fi if [ -z "$create_pr" ]; then create_pr=false; fi if ! timeout_minutes="$(normalize_timeout_minutes "$timeout_minutes")"; then timeout_minutes="$(devloop_timeout_minutes)"; fi next_pass="$(next_pass_from_track "$track")" @@ -1966,14 +2048,16 @@ run_command() { local report_format="html" local strict=true local use_worktree=true - local coder="codex" - local reviewer="claude" + local coder + local reviewer local create_pr=false local timeout_minutes local spec="" local max_raw="5" local max_set=false local arg value + coder="$(devloop_coder)" + reviewer="$(devloop_reviewer)" timeout_minutes="$(devloop_timeout_minutes)" while [ "$#" -gt 0 ]; do diff --git a/scripts/devloop_test.sh b/scripts/devloop_test.sh index 861d8de..7fa3ef5 100755 --- a/scripts/devloop_test.sh +++ b/scripts/devloop_test.sh @@ -614,6 +614,9 @@ printf '%s\n' "spec_dir=.specs" > "$global_repo/.devloop/config" equals "$(cd "$global_repo" && HOME="$global_home" devloop_spec_dir)" ".specs" "repo .specs overrides global spec dir" equals "$(cd "$global_repo" && HOME="$global_home" configured_spec_dir)" ".specs" "repo .specs reported as override" equals "$(cd "$global_repo" && HOME="$global_home" configured_spec_dir_scope)" "local" "repo .specs override scope" +equals "$(cd "$global_repo" && HOME="$global_home" devloop_coder)" "codex" "global coder applies without local override" +printf '%s\n' "coder=claude" >> "$global_repo/.devloop/config" +equals "$(cd "$global_repo" && HOME="$global_home" devloop_coder)" "claude" "local coder overrides global" default_scope_repo="$work/default-scope-repo" default_scope_home="$work/default-scope-home" @@ -649,6 +652,20 @@ equals "$(cd "$config_repo" && HOME="$config_home" configured_timeout_minutes_sc (cd "$config_repo" && HOME="$config_home" remove_config_timeout_minutes) equals "$(cd "$config_repo" && HOME="$config_home" devloop_timeout_minutes)" "30" "removed timeout falls back" +equals "$(cd "$config_repo" && HOME="$config_home" devloop_coder)" "codex" "default coder" +equals "$(cd "$config_repo" && HOME="$config_home" devloop_reviewer)" "claude" "default reviewer" +equals "$(cd "$config_repo" && HOME="$config_home" write_config_coder Claude)" "claude" "write coder defaults to global and normalizes label" +equals "$(grep '^coder=' "$config_home/.devloop/config")" "coder=claude" "coder persisted to config" +equals "$(cd "$config_repo" && HOME="$config_home" devloop_coder)" "claude" "configured coder" +equals "$(cd "$config_repo" && HOME="$config_home" write_config_reviewer global codex)" "codex" "write reviewer" +equals "$(cd "$config_repo" && HOME="$config_home" devloop_reviewer)" "codex" "configured reviewer" +if (cd "$config_repo" && HOME="$config_home" write_config_coder global nope) >/dev/null 2>&1; then fail "write_config_coder accepted invalid agent"; fi +if (cd "$config_repo" && HOME="$config_home" write_config_value global bogus x) >/dev/null 2>&1; then fail "write_config_value accepted unknown key"; fi +(cd "$config_repo" && HOME="$config_home" remove_config_value global coder) +(cd "$config_repo" && HOME="$config_home" remove_config_value global reviewer) +equals "$(cd "$config_repo" && HOME="$config_home" devloop_coder)" "codex" "removed coder falls back" +equals "$(cd "$config_repo" && HOME="$config_home" devloop_reviewer)" "claude" "removed reviewer falls back" + lint_spec_text=$'---\ntype: feat\n---\n# Title\n\n## Acceptance criteria\n1. Thing' lint_spec_file "$criteria_file" "$lint_spec_text" 1 true || fail "lint_spec_file rejected valid spec" bad_lint_spec=$'---\ntype: invalid\n---\n# Title\n\n## Acceptance criteria\n1. Thing' @@ -722,6 +739,32 @@ if ! create_spec_output="$( )"; then fail "create spec launch failed"; fi equals "$create_spec_output" "--agent codex" "create spec launches immediately" if ! ( ui_choose() { printf '%s\n' "Back"; }; UI_BACK=false; interactive_settings >/dev/null 2>&1; [ "$UI_BACK" = true ] ); then fail "settings back navigation"; fi +settings_choose_state="$work/settings-choose-state" +: > "$settings_choose_state" +settings_agents_home="$work/settings-agents-home" +mkdir -p "$settings_agents_home" +if ! ( + HOME="$settings_agents_home" + USE_TUI=false + UI_BACK=false + ui_header() { :; } + ui_print_key_values() { :; } + ui_choose() { + local count + count="$(wc -l < "$settings_choose_state" | tr -d ' ')" + printf 'x\n' >> "$settings_choose_state" + case "$count" in + 0) printf '%s\n' "Set coder" ;; + 1) printf '%s\n' "Claude Code" ;; + 2) printf '%s\n' "Set reviewer" ;; + 3) printf '%s\n' "Codex" ;; + *) printf '%s\n' "Back" ;; + esac + } + interactive_settings >/dev/null 2>&1 +); then fail "settings agent selection flow failed"; fi +equals "$(HOME="$settings_agents_home" devloop_coder)" "claude" "settings menu writes coder to config" +equals "$(HOME="$settings_agents_home" devloop_reviewer)" "codex" "settings menu writes reviewer to config" if ! run_setup_output="$( HOME="$config_home" USE_TUI=false @@ -733,6 +776,23 @@ if ! run_setup_output="$( interactive_run_setup "spec.md" )"; then fail "run setup defaults failed"; fi equals "$run_setup_output" "spec.md 5 html true true codex claude false 60" "run setup launches with defaults" +configured_agents_home="$work/agents-home" +configured_agents_repo="$work/agents-repo" +mkdir -p "$configured_agents_home" "$configured_agents_repo/.devloop/specs" +HOME="$configured_agents_home" write_config_coder global claude >/dev/null +HOME="$configured_agents_home" write_config_reviewer global codex >/dev/null +if ! agents_setup_output="$( + cd "$configured_agents_repo" + HOME="$configured_agents_home" + USE_TUI=false + UI_BACK=false + interactive_create_pr_choice() { printf '%s\n' "false"; } + run_header() { :; } + run_devloop() { printf '%s\n' "$*"; return 0; } + maybe_enter_worktree() { :; } + interactive_run_setup "spec.md" +)"; then fail "configured agents run setup failed"; fi +contains "$agents_setup_output" " claude codex " "run setup honors configured coder and reviewer" if ! ( ui_choose() { return 130; }; UI_BACK=false; interactive_create_spec >/dev/null 2>&1; [ "$UI_BACK" = true ] ); then fail "create spec escape navigation"; fi if ! ( interactive_create_pr_choice() { return 130; }; UI_BACK=false; interactive_run_setup "spec.md" >/dev/null 2>&1; [ "$UI_BACK" = true ] ); then fail "run setup PR prompt navigation"; fi if ! ( ui_choose() { printf '%s\n' "Quit"; }; UI_BACK=false; interactive_menu >/dev/null 2>&1 ); then fail "menu quit failed"; fi