From 290a7761b78708a4d39583588d02053b940bce5c Mon Sep 17 00:00:00 2001 From: satyaborg Date: Tue, 9 Jun 2026 15:53:13 +1000 Subject: [PATCH 1/2] feat: default spec dir to repo-local .devloop/specs --- .gitignore | 1 - AGENTS.md | 2 +- README.md | 6 ++--- devloop | 45 ++++++++++++++---------------------- scripts/devloop_test.sh | 35 ++++++++++++++-------------- skills/devloop-spec/SKILL.md | 2 +- 6 files changed, 39 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 0b32899..bd0d6a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .codex/ .claude/ .devloop/ -.specs/ .DS_Store coverage/ diff --git a/AGENTS.md b/AGENTS.md index 4bc5302..c7610dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index 8f4d376..6f0da7b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/devloop b/devloop index ffa9103..716a2da 100755 --- a/devloop +++ b/devloop @@ -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 @@ -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 @@ -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" @@ -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" @@ -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 ;; @@ -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 diff --git a/scripts/devloop_test.sh b/scripts/devloop_test.sh index 88bb9bd..cc4aa6e 100755 --- a/scripts/devloop_test.sh +++ b/scripts/devloop_test.sh @@ -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" @@ -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" @@ -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" @@ -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" diff --git a/skills/devloop-spec/SKILL.md b/skills/devloop-spec/SKILL.md index 1012ca8..d23ee5e 100644 --- a/skills/devloop-spec/SKILL.md +++ b/skills/devloop-spec/SKILL.md @@ -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-.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-.md`. Do not wrap the spec in a code fence unless the caller explicitly asks for a fenced snippet. From fa4fe9f09c30e79d152b18d2513acd883ec809ff Mon Sep 17 00:00:00 2001 From: satyaborg Date: Tue, 9 Jun 2026 15:53:13 +1000 Subject: [PATCH 2/2] chore: add logo banner and "try: devloop" to installers --- scripts/devloop_test.sh | 4 +++- scripts/install.remote.sh | 28 ++++++++++++++++++++++++++-- scripts/install.sh | 28 ++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/scripts/devloop_test.sh b/scripts/devloop_test.sh index cc4aa6e..e38c6ac 100755 --- a/scripts/devloop_test.sh +++ b/scripts/devloop_test.sh @@ -670,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" @@ -745,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 diff --git a/scripts/install.remote.sh b/scripts/install.remote.sh index 7bb3a2e..1cdb195 100755 --- a/scripts/install.remote.sh +++ b/scripts/install.remote.sh @@ -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] @@ -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 "$@" diff --git a/scripts/install.sh b/scripts/install.sh index 85bf18d..07d2165 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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 @@ -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