Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
122 changes: 103 additions & 19 deletions devloop
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 $?
Expand All @@ -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 $?
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")"
Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions scripts/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading