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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ By default, Codex makes the change, Claude Code reviews it, and Codex retries un

## Install

Prereqs: Bash, git, and the agent CLIs you want to use. The default pairing requires `codex` and `claude`.
For the full interactive UI, install [`gum`](https://github.com/charmbracelet/gum) and [`fzf`](https://github.com/junegunn/fzf). They are optional; `devloop` falls back to plain terminal output when they are missing.
Required dependencies: Bash, git, Homebrew, `codex`, `claude`, `gum`, and `fzf`.
`install.sh` installs missing `gum` and `fzf` with Homebrew. Install the Codex and Claude Code CLIs before running a loop, then verify everything with `devloop doctor`.

```sh
git clone https://github.com/satyaborg/devloop.git
Expand Down Expand Up @@ -139,11 +139,11 @@ When stdout is a terminal, running `devloop` without arguments opens a menu:
- `Continue a run`: pick a prior `.codex/tracks/*.md` and continue in that worktree.
- `Open reports`: pick a prior report from any registered worktree.
- `Settings`: view or change the shared spec path and set the run timeout.
- `Doctor`: verify required commands, optional UI tools, and installed skills.
- `Doctor`: verify required dependencies and installed skills.

Nested menu screens keep `Back` as the final option, and Esc/cancel also returns to the previous menu without exiting Devloop. Interactive screens redraw in place instead of appending a fresh UI after each selection.

`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports.
`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports. Both are required and installed by `install.sh` when missing.

## What Devloop Does

Expand Down
2 changes: 1 addition & 1 deletion devloop
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ welcome_tui() {
gum style --foreground "$UI_ACCENT_COLOR" --bold "Usage"
printf ' devloop [options] <spec.md> [max=5]\n\n'
gum style --foreground "$UI_ACCENT_COLOR" --bold "Common commands"
printf ' %-42s %s\n' "devloop doctor" "check required and optional tools"
printf ' %-42s %s\n' "devloop doctor" "check required tools"
printf ' %-42s %s\n' "devloop reports" "open previous run reports"
printf ' %-42s %s\n' "devloop status" "summarize tracked runs"
printf ' %-42s %s\n' "devloop clean" "show safe cleanup candidates"
Expand Down
39 changes: 38 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@ BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}"
TARGET="$BIN_DIR/devloop"
SOURCE="$ROOT/devloop"
SKILL_STATUS=0
TOOL_STATUS=0

install_required_ui_tools() {
local missing=()
local tool

for tool in gum fzf; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing+=("$tool")
fi
done

if [ "${#missing[@]}" -eq 0 ]; then
echo "required UI tools ready"
return 0
fi

if ! command -v brew >/dev/null 2>&1; then
echo "missing required UI tools: ${missing[*]}" >&2
echo "install Homebrew, then rerun ./install.sh" >&2
return 1
fi

echo "installing required UI tools: ${missing[*]}"
brew install "${missing[@]}"

for tool in "${missing[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "failed to install required UI tool: $tool" >&2
return 1
fi
done
}

if [ ! -f "$SOURCE" ]; then
echo "missing devloop executable: $SOURCE" >&2
Expand All @@ -29,6 +62,7 @@ chmod +x "$SOURCE"
ln -sfn "$SOURCE" "$TARGET"

echo "installed devloop -> $SOURCE"
install_required_ui_tools || TOOL_STATUS=$?
devloop_install_skills "$ROOT" || SKILL_STATUS=$?

case ":${PATH:-}:" in
Expand All @@ -42,4 +76,7 @@ esac

echo
echo "try: devloop doctor"
exit "$SKILL_STATUS"
if [ "$TOOL_STATUS" -ne 0 ] || [ "$SKILL_STATUS" -ne 0 ]; then
exit 1
fi
exit 0
19 changes: 3 additions & 16 deletions skill_helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,6 @@ devloop_doctor_command() {
return 1
}

devloop_doctor_optional_command() {
local command="$1"
local detail="$2"
local resolved
resolved="$(command -v "$command" 2>/dev/null || true)"
if [ -n "$resolved" ]; then
printf '[ok] optional %s: %s (%s)\n' "$command" "$resolved" "$detail"
return 0
fi
printf '[skip] optional %s: %s\n' "$command" "$detail"
}

devloop_doctor_skills() {
local root="$1"
local skills_dir status
Expand Down Expand Up @@ -241,14 +229,13 @@ devloop_doctor() {
local status=0

printf 'devloop doctor\n'
printf 'Required\n'
printf 'Required dependencies\n'
devloop_doctor_command devloop || status=1
devloop_doctor_command git || status=1
devloop_doctor_command codex || status=1
devloop_doctor_command claude || status=1
printf '\nOptional UI\n'
devloop_doctor_optional_command gum "rich prompts, confirmations, status, and paging"
devloop_doctor_optional_command fzf "searchable spec, track, and report pickers"
devloop_doctor_command gum || status=1
devloop_doctor_command fzf || status=1
printf '\nSkills\n'
devloop_doctor_skills "$root" || status=1

Expand Down
44 changes: 39 additions & 5 deletions tests/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ contains() {
[[ "$haystack" == *"$needle"* ]] || fail "$label missing: $needle"
}

not_contains() {
local haystack="$1"
local needle="$2"
local label="$3"
[[ "$haystack" != *"$needle"* ]] || fail "$label should not contain: $needle"
}

equals() {
local actual="$1"
local expected="$2"
Expand Down Expand Up @@ -492,9 +499,31 @@ ok "release helpers"

bin_dir="$work/bin"
install_home="$work/install-home"
DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out
tool_bin="$work/tool-bin"
mkdir -p "$tool_bin"
cat > "$tool_bin/brew" <<'BREW'
#!/usr/bin/env bash
set -euo pipefail
if [ "${1:-}" != "install" ]; then exit 1; fi
shift
tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
for formula in "$@"; do
case "$formula" in
gum|fzf)
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$formula"
chmod +x "$tool_dir/$formula"
;;
*) exit 1 ;;
esac
done
BREW
chmod +x "$tool_bin/brew"
install_path="$tool_bin:/usr/bin:/bin:/usr/sbin:/sbin"
DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out
[[ -x "$REPO_ROOT/devloop" ]] || fail "devloop is not executable"
[[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink"
PATH="$install_path" command -v gum >/dev/null 2>&1 || fail "installer did not make gum available"
PATH="$install_path" command -v fzf >/dev/null 2>&1 || fail "installer did not make fzf available"
[[ -f "$install_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "installer did not install Codex spec skill"
[[ -f "$install_home/.agents/skills/devloop-spec/references/spec-template.md" ]] || fail "installer did not install Codex spec template reference"
[[ -f "$install_home/.agents/skills/devloop-review/SKILL.md" ]] || fail "installer did not install Codex review skill"
Expand All @@ -507,11 +536,11 @@ contains "$(cat /tmp/devloop-help-test.out)" "Spec-driven code and review loop."
ok "installer"

printf '%s\n' "user edit" >> "$install_home/.agents/skills/devloop-review/SKILL.md"
DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1
DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1
contains "$(cat /tmp/devloop-install-skip.out)" "skipping modified skill" "installer modified skill guard"
contains "$(cat /tmp/devloop-install-skip.out)" "try: devloop doctor" "installer guidance after skill skip"
contains "$(cat "$install_home/.agents/skills/devloop-review/SKILL.md")" "user edit" "installer modified skill preserved"
DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out
DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out
if grep -q "user edit" "$install_home/.agents/skills/devloop-review/SKILL.md"; then fail "installer force did not restore skill"; fi
ok "installer skill updates"

Expand All @@ -520,10 +549,15 @@ mkdir -p "$fake_bin"
printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/codex"
printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/claude"
chmod +x "$fake_bin/codex" "$fake_bin/claude"
doctor_output="$(HOME="$install_home" PATH="$bin_dir:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)"
doctor_output="$(HOME="$install_home" PATH="$bin_dir:$tool_bin:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)"
contains "$doctor_output" "devloop doctor: ready" "doctor"
contains "$doctor_output" "Required dependencies" "doctor"
contains "$doctor_output" "[ok] codex:" "doctor"
contains "$doctor_output" "[ok] claude:" "doctor"
contains "$doctor_output" "[ok] skill devloop-spec" "doctor"
contains "$doctor_output" "Optional UI" "doctor"
contains "$doctor_output" "[ok] gum:" "doctor"
contains "$doctor_output" "[ok] fzf:" "doctor"
not_contains "$doctor_output" "Optional UI" "doctor"
contains "$doctor_output" "$install_home/.agents/skills/devloop-spec" "doctor Codex skill"
contains "$doctor_output" "$install_home/.claude/skills/devloop-spec" "doctor Claude skill"
ok "doctor"
Expand Down