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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.codex/
.claude/
.devloop/
.specs/
.DS_Store
coverage/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This is a Bash CLI project. The active runtime is the root `devloop` executable.

- `bash scripts/devloop_test.sh`: run the shell test suite.
- `./scripts/install.sh`: link `devloop` into `~/.local/bin` or `DEVLOOP_BIN_DIR`.
- `./devloop --plain .specs/change.md`: example local CLI invocation from a target git worktree.
- `./devloop --plain .devloop/specs/change.md`: example local CLI invocation from a target git worktree.
- `./scripts/release.sh patch --dry-run`: validate the release path without changing files.

## Coding Style & Naming Conventions
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ devloop doctor
```sh
devloop
devloop spec "add retry behavior to the chat sender"
devloop .specs/change.md
devloop --create-pr .specs/change.md
devloop .devloop/specs/change.md
devloop --create-pr .devloop/specs/change.md
devloop continue
devloop status
devloop clean
Expand All @@ -50,7 +50,7 @@ devloop spec --agent claude notes.md

Strict mode is on by default. Specs need `## Acceptance criteria`, and reviews must pass both the spec gate and engineering quality gate.

Devloop stores shared settings in `~/.devloop/config`. The default spec directory is `~/Projects/specs/`; the picker also searches the current repo's `.specs/` directory.
Devloop stores shared settings in `~/.devloop/config`. The default spec directory is the current repo's `.devloop/specs/`. Set a custom `spec_dir` (global or per-repo) to point elsewhere, for example `~/Projects/specs`; the picker searches both the custom directory and the repo's `.devloop/specs/`.

## PR mode

Expand Down
45 changes: 17 additions & 28 deletions devloop
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ CODEX_REASONING_ARGS=(-c 'model_reasoning_effort="xhigh"')
CLAUDE_MODEL_ARGS=(--model claude-opus-4-8)
CLAUDE_EFFORT_ARGS=(--effort max)
DEFAULT_TIMEOUT_MINUTES=30
DEFAULT_SPEC_DIR_SUFFIX="Projects/specs"
DEFAULT_SPEC_DIR=".devloop/specs"

SCRIPT_PATH="${BASH_SOURCE[0]}"
while [ -L "$SCRIPT_PATH" ]; do
Expand Down Expand Up @@ -178,12 +178,12 @@ Common commands:
devloop continue
devloop menu
devloop spec "add retry behavior to the chat sender"
devloop .specs/change.md
devloop --tui .specs/change.md
devloop --plain .specs/change.md
devloop --report-format markdown .specs/change.md 3
devloop --coder claude --reviewer codex .specs/change.md
devloop --create-pr .specs/change.md
devloop .devloop/specs/change.md
devloop --tui .devloop/specs/change.md
devloop --plain .devloop/specs/change.md
devloop --report-format markdown .devloop/specs/change.md 3
devloop --coder claude --reviewer codex .devloop/specs/change.md
devloop --create-pr .devloop/specs/change.md

Options:
--tui force terminal UI output
Expand Down Expand Up @@ -216,8 +216,8 @@ welcome_tui() {
printf ' %-42s %s\n' "devloop continue" "resume a tracked run"
printf ' %-42s %s\n' "devloop menu" "open the guided UI"
printf ' %-42s %s\n' 'devloop spec "add retry behavior"' "launch a spec agent"
printf ' %-42s %s\n' "devloop .specs/change.md" "run a spec"
printf ' %-42s %s\n' "devloop --create-pr .specs/change.md" "open and maintain a draft PR during the loop"
printf ' %-42s %s\n' "devloop .devloop/specs/change.md" "run a spec"
printf ' %-42s %s\n' "devloop --create-pr .devloop/specs/change.md" "open and maintain a draft PR during the loop"
printf '\n'
gum style --foreground "$UI_ACCENT_COLOR" --bold "Options"
printf ' %-30s %s\n' "--tui" "force terminal UI output"
Expand Down Expand Up @@ -328,32 +328,22 @@ config_file_value() {
}

devloop_default_spec_dir() {
if [ -n "${HOME:-}" ]; then
printf '%s/%s\n' "${HOME%/}" "$DEFAULT_SPEC_DIR_SUFFIX"
local root
if root="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null)"; then
printf '%s/%s\n' "${root%/}" "$DEFAULT_SPEC_DIR"
else
printf '%s\n' ".specs"
printf '%s\n' "$DEFAULT_SPEC_DIR"
fi
}

ensure_global_config() {
local file value spec_dir
local file value
file="$(devloop_global_config_file 2>/dev/null)" || return 0
mkdir -p "$(dirname "$file")" || return 1
if [ ! -f "$file" ]; then
spec_dir="$(devloop_default_spec_dir)"
mkdir -p "$spec_dir" || return 1
{
printf 'spec_dir=%s\n' "$spec_dir"
printf 'timeout_minutes=%s\n' "$DEFAULT_TIMEOUT_MINUTES"
} > "$file"
printf 'timeout_minutes=%s\n' "$DEFAULT_TIMEOUT_MINUTES" > "$file"
return 0
fi
value="$(config_file_value spec_dir "$file" || true)"
if [ -z "$value" ]; then
spec_dir="$(devloop_default_spec_dir)"
mkdir -p "$spec_dir" || return 1
printf 'spec_dir=%s\n' "$spec_dir" >> "$file"
fi
value="$(config_file_value timeout_minutes "$file" || true)"
if [ -z "$value" ]; then
printf 'timeout_minutes=%s\n' "$DEFAULT_TIMEOUT_MINUTES" >> "$file"
Expand Down Expand Up @@ -498,7 +488,7 @@ spec_search_dirs() {
local configured dir seen
configured="$(devloop_spec_dir)"
seen=""
for dir in "$configured" ".specs"; do
for dir in "$configured" "$(devloop_default_spec_dir)"; do
[ -n "$dir" ] || continue
case "$seen" in
*"|$dir|"*) continue ;;
Expand Down Expand Up @@ -1204,13 +1194,12 @@ interactive_settings() {
if [ -n "$custom_spec_dir" ]; then
ui_print_key_values \
"configured" "$custom_spec_dir" \
"fallback" ".specs" \
"fallback" "$default_spec_dir" \
"timeout" "$timeout_display"
choices=("Remove spec path" "Set timeout" "Back")
else
ui_print_key_values \
"default" "$default_spec_dir" \
"fallback" ".specs" \
"timeout" "$timeout_display"
choices=("Add spec path" "Set timeout" "Back")
fi
Expand Down
39 changes: 20 additions & 19 deletions scripts/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -323,30 +323,29 @@ equals "$(clean_candidate_status "$clean_status_track" "$clean_status_repo" "$wo

config_repo="$work/config-repo"
config_home="$work/config-home"
config_default_specs="$config_home/Projects/specs"
mkdir -p "$config_repo/.specs" "$config_repo/.devloop/specs" "$config_home"
config_repo_real="$(cd "$config_repo" && pwd)"
git init -q "$config_repo"
config_repo_real="$(cd "$config_repo" && pwd -P)"
config_default_specs="$config_repo_real/.devloop/specs"
mkdir -p "$config_repo/.devloop/specs" "$config_home"
equals "$(HOME="$config_home" devloop_config_file)" "$config_home/.devloop/config" "default config file"
printf '%s\n' "# Default" > "$config_repo/.specs/default.md"
printf '%s\n' "# Devloop" > "$config_repo/.devloop/specs/devloop.md"
config_specs="$(cd "$config_repo" && HOME="$config_home" list_spec_files)"
[[ -f "$config_home/.devloop/config" ]] || fail "global config was not created"
contains "$(cat "$config_home/.devloop/config")" "spec_dir=$config_default_specs" "global config default spec dir"
if grep -q "spec_dir=" "$config_home/.devloop/config"; then fail "global config seeded a default spec dir"; fi
contains "$(cat "$config_home/.devloop/config")" "timeout_minutes=30" "global config default timeout"
[[ -d "$config_default_specs" ]] || fail "global default spec dir was not created"
contains "$config_specs" ".specs/default.md" "default spec search"
if printf '%s\n' "$config_specs" | grep -Fq ".devloop/specs/devloop.md"; then fail "default spec search included .devloop/specs"; fi
equals "$(cd "$config_repo" && HOME="$config_home" devloop_spec_dir)" "$config_default_specs" "default global spec dir"
equals "$(cd "$config_repo" && HOME="$config_home" spec_search_label)" "$config_default_specs, .specs" "spec search label"
if (cd "$config_repo" && HOME="$config_home" configured_spec_dir) >/dev/null 2>&1; then fail "default global spec dir reported as custom"; fi
contains "$config_specs" "$config_default_specs/devloop.md" "default spec search uses repo .devloop/specs"
equals "$(cd "$config_repo" && HOME="$config_home" devloop_spec_dir)" "$config_default_specs" "default spec dir is repo .devloop/specs"
equals "$(cd "$config_repo" && HOME="$config_home" spec_search_label)" "$config_default_specs" "spec search label is single default dir"
if (cd "$config_repo" && HOME="$config_home" configured_spec_dir) >/dev/null 2>&1; then fail "default spec dir reported as custom"; fi
equals "$(cd "$config_repo" && HOME="$config_home" write_config_spec_dir local "custom-specs")" "custom-specs" "write local config spec dir"
equals "$(cd "$config_repo" && HOME="$config_home" devloop_spec_dir)" "custom-specs" "configured spec dir"
equals "$(cd "$config_repo" && HOME="$config_home" configured_spec_dir)" "custom-specs" "custom spec dir"
equals "$(cd "$config_repo" && HOME="$config_home" configured_spec_dir_scope)" "local" "custom spec dir scope"
[[ -d "$config_repo/custom-specs" ]] || fail "configured spec dir was not created"
printf '%s\n' "# Custom" > "$config_repo/custom-specs/custom.md"
configured_specs="$(cd "$config_repo" && HOME="$config_home" list_spec_files)"
contains "$configured_specs" ".specs/default.md" "configured spec search includes default dir"
if printf '%s\n' "$configured_specs" | grep -Fq ".devloop/specs/devloop.md"; then fail "configured spec search included .devloop/specs"; fi
contains "$configured_specs" "custom-specs/custom.md" "configured spec search includes custom dir"
contains "$configured_specs" "$config_default_specs/devloop.md" "configured spec search still includes default"
if (cd "$config_repo" && HOME="$config_home" write_config_spec_dir "../bad") >/dev/null 2>&1; then fail "write_config_spec_dir accepted path traversal"; fi

absolute_specs="$work/shared-specs"
Expand All @@ -357,13 +356,13 @@ equals "$(cd "$config_repo" && HOME="$config_home" configured_spec_dir)" "$absol
printf '%s\n' "# Shared" > "$absolute_specs/shared.md"
absolute_configured_specs="$(cd "$config_repo" && HOME="$config_home" list_spec_files)"
contains "$absolute_configured_specs" "$absolute_specs/shared.md" "configured spec search includes absolute dir"
contains "$absolute_configured_specs" ".specs/default.md" "absolute spec search includes default dir"
contains "$absolute_configured_specs" "$config_default_specs/devloop.md" "absolute spec search still includes default"
equals "$(spec_dir_status "$absolute_specs")" "exists" "spec dir status exists"
equals "$(spec_dir_status "$work/missing-specs")" "missing" "spec dir status missing"
(cd "$config_repo" && HOME="$config_home" remove_config_spec_dir local)
if (cd "$config_repo" && HOME="$config_home" configured_spec_dir) >/dev/null 2>&1; then fail "custom spec dir was not removed"; fi
equals "$(cd "$config_repo" && HOME="$config_home" devloop_spec_dir)" "$config_default_specs" "removed custom spec dir falls back"
equals "$(cd "$config_repo" && HOME="$config_home" spec_search_label)" "$config_default_specs, .specs" "removed custom spec search label"
equals "$(cd "$config_repo" && HOME="$config_home" spec_search_label)" "$config_default_specs" "removed custom spec search label"

global_repo="$work/global-repo"
global_home="$work/global-home"
Expand Down Expand Up @@ -405,7 +404,7 @@ raw_tilde_repo="$work/raw-tilde-repo"
raw_tilde_home="$work/raw-tilde-home"
mkdir -p "$raw_tilde_repo/.devloop" "$raw_tilde_home"
printf '%s\n' "spec_dir=~/raw-specs" > "$raw_tilde_repo/.devloop/config"
equals "$(cd "$raw_tilde_repo" && HOME="$raw_tilde_home" devloop_spec_dir)" "$raw_tilde_home/Projects/specs" "raw tilde config falls back"
equals "$(cd "$raw_tilde_repo" && HOME="$raw_tilde_home" devloop_spec_dir)" ".devloop/specs" "raw tilde config falls back to default"

equals "$(normalize_timeout_minutes 1)" "1" "timeout lower bound"
equals "$(normalize_timeout_minutes 30)" "30" "timeout normalize"
Expand Down Expand Up @@ -488,8 +487,8 @@ if ! ( cd "$empty_spec_repo" && UI_BACK=false; interactive_open_report >/dev/nul
USE_TUI="$old_use_tui"

cancel_spec_repo="$work/cancel-spec-repo"
mkdir -p "$cancel_spec_repo/.specs"
printf '%s\n' "# Cancel" > "$cancel_spec_repo/.specs/cancel.md"
mkdir -p "$cancel_spec_repo/.devloop/specs"
printf '%s\n' "# Cancel" > "$cancel_spec_repo/.devloop/specs/cancel.md"
if ! ( cd "$cancel_spec_repo" && ui_pick_from_file() { return 130; }; UI_BACK=false; interactive_run_spec >/dev/null 2>&1; [ "$UI_BACK" = true ] ); then fail "interactive_run_spec escape navigation"; fi

picker_file="$work/picker.txt"
Expand Down Expand Up @@ -671,6 +670,8 @@ contains "$remote_install_output" "$remote_default_bin is not on PATH" "remote i
contains "$remote_install_output" "export PATH=\"$remote_default_bin:\$PATH\"" "remote install PATH guidance"
contains "$remote_install_output" "[ok] gum:" "remote install UI check"
contains "$remote_install_output" "[ok] codex:" "remote install agent check"
contains "$remote_install_output" "devloop $remote_version installed" "remote install banner version"
contains "$remote_install_output" "try: devloop" "remote install banner try line"
[[ -f "$remote_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "remote installer did not install Codex spec skill"
[[ -f "$remote_home/.agents/skills/devloop-review/.devloop-checksum" ]] || fail "remote installer did not write Codex skill checksum"
[[ -f "$remote_home/.claude/skills/devloop-spec/SKILL.md" ]] || fail "remote installer did not install Claude spec skill"
Expand Down Expand Up @@ -746,7 +747,7 @@ ok "installer"
printf '%s\n' "user edit" >> "$install_home/.agents/skills/devloop-review/SKILL.md"
DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$SCRIPTS_DIR/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 /tmp/devloop-install-skip.out)" "try: devloop" "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" PATH="$install_path" "$SCRIPTS_DIR/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
Expand Down
28 changes: 26 additions & 2 deletions scripts/install.remote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ YES=false
NO_SKILLS=false
DRY_RUN=false

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_ACCENT=$'\033[38;5;141m'
C_DIM=$'\033[38;5;244m'
C_BOLD=$'\033[1m'
C_RESET=$'\033[0m'
else
C_ACCENT=""
C_DIM=""
C_BOLD=""
C_RESET=""
fi

print_banner() {
local version="$1"
printf '\n%s' "$C_ACCENT"
cat <<'EOF'
░█▀▄░█▀▀░█░█░█░░░█▀█░█▀█░█▀█
░█░█░█▀▀░▀▄▀░█░░░█░█░█░█░█▀▀
░▀▀░░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀░░
EOF
printf '%s\n' "$C_RESET"
printf ' %sdevloop %s installed%s\n' "$C_DIM" "$version" "$C_RESET"
printf ' %stry:%s %s%sdevloop%s\n\n' "$C_DIM" "$C_RESET" "$C_BOLD" "$C_ACCENT" "$C_RESET"
}

usage() {
cat <<'EOF'
usage: install.remote.sh [options]
Expand Down Expand Up @@ -352,8 +377,7 @@ main() {
check_ui_tools "$ui_missing"
check_agent_tools "$agent_missing"
print_path_guidance
info ""
info "try: devloop doctor"
print_banner "$version"
}

main "$@"
28 changes: 26 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ SOURCE="$ROOT/devloop"
SKILL_STATUS=0
TOOL_STATUS=0

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_ACCENT=$'\033[38;5;141m'
C_DIM=$'\033[38;5;244m'
C_BOLD=$'\033[1m'
C_RESET=$'\033[0m'
else
C_ACCENT=""
C_DIM=""
C_BOLD=""
C_RESET=""
fi

print_banner() {
local version="$1"
printf '\n%s' "$C_ACCENT"
cat <<'EOF'
░█▀▄░█▀▀░█░█░█░░░█▀█░█▀█░█▀█
░█░█░█▀▀░▀▄▀░█░░░█░█░█░█░█▀▀
░▀▀░░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀░░
EOF
printf '%s\n' "$C_RESET"
printf ' %sdevloop %s installed%s\n' "$C_DIM" "$version" "$C_RESET"
printf ' %stry:%s %s%sdevloop%s\n\n' "$C_DIM" "$C_RESET" "$C_BOLD" "$C_ACCENT" "$C_RESET"
}

install_required_ui_tools() {
local missing=()
local tool
Expand Down Expand Up @@ -76,8 +101,7 @@ case ":${PATH:-}:" in
;;
esac

echo
echo "try: devloop doctor"
print_banner "$(cat "$ROOT/VERSION" 2>/dev/null)"
if [ "$TOOL_STATUS" -ne 0 ] || [ "$SKILL_STATUS" -ne 0 ]; then
exit 1
fi
Expand Down
2 changes: 1 addition & 1 deletion skills/devloop-spec/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ Keep every standard section present, remove leftover placeholders, and list the

## Output

When a caller provides an output path, write the spec there. Otherwise, write only the markdown spec to stdout or save it under the caller's requested default spec directory, usually `.specs/YYYY-MM-DD-<slug>.md`. Do not wrap the spec in a code fence unless the caller explicitly asks for a fenced snippet.
When a caller provides an output path, write the spec there. Otherwise, write only the markdown spec to stdout or save it under the caller's requested default spec directory, usually `.devloop/specs/YYYY-MM-DD-<slug>.md`. Do not wrap the spec in a code fence unless the caller explicitly asks for a fenced snippet.
Loading