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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ Uninstall with `./scripts/uninstall.sh` (`--dry-run` to preview).
| `devloop status` | Show run status |
| `devloop clean` | Remove run artifacts |

Each run writes an HTML report, spec, and reviews under `.devloop/`.
Each run writes a Markdown report, spec, and reviews under `.devloop/`.
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

A good spec is short, concrete, and verifiable. Start from [`skills/devloop-spec/references/spec-template.md`](skills/devloop-spec/references/spec-template.md). The bundled `devloop-spec` skill can also render a sibling HTML companion with [`skills/devloop-spec/scripts/render.sh`](skills/devloop-spec/scripts/render.sh).
A good spec is short, concrete, and verifiable. Start from [`skills/devloop-spec/references/spec-template.md`](skills/devloop-spec/references/spec-template.md).

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

## Skills

Devloop ships two agent skills, installed into `~/.claude/skills` and `~/.agents/skills`:

- [`devloop-spec`](skills/devloop-spec/SKILL.md) — turns a rough idea, notes, a URL, or an interview into one concrete, devloop-ready spec, with optional HTML rendering.
- [`devloop-spec`](skills/devloop-spec/SKILL.md) — turns a rough idea, notes, a URL, or an interview into one concrete, devloop-ready spec.
- [`devloop-review`](skills/devloop-review/SKILL.md) — judges each pass against the spec and engineering quality gates, returning ACCEPT, REJECT, or UNCLEAR with fix instructions.

## Runtime
Expand Down
113 changes: 34 additions & 79 deletions devloop
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ Common commands:
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

Expand All @@ -202,7 +201,6 @@ Options:
--plain force plain output
--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
--in-place run in the current worktree
Expand Down Expand Up @@ -237,7 +235,6 @@ welcome_tui() {
printf ' %-30s %s\n' "--plain" "force plain output"
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"
printf ' %-30s %s\n' "--in-place" "run in the current worktree"
Expand All @@ -249,7 +246,7 @@ welcome_tui() {
}

usage() {
printf '%s\n' "usage: devloop [--version] [--plain|--tui] [--in-place] [--no-strict] [--create-pr|--pr] [--no-shell|--stay|--shell|--enter-worktree] [--coder codex|claude] [--reviewer codex|claude] [--report-format html|markdown] [--timeout-minutes N] <spec.md> [max=5]"
printf '%s\n' "usage: devloop [--version] [--plain|--tui] [--in-place] [--no-strict] [--create-pr|--pr] [--no-shell|--stay|--shell|--enter-worktree] [--coder codex|claude] [--reviewer codex|claude] [--timeout-minutes N] <spec.md> [max=5]"
printf '%s\n' "agents: Codex or Claude Code"
}

Expand Down Expand Up @@ -1295,13 +1292,12 @@ format_elapsed() {
run_header() {
local spec="$1"
local max="$2"
local report_format="$3"
local strict="$4"
local use_worktree="$5"
local coder="$6"
local reviewer="$7"
local create_pr="$8"
local timeout_minutes="${9:-$DEFAULT_TIMEOUT_MINUTES}"
local strict="$3"
local use_worktree="$4"
local coder="$5"
local reviewer="$6"
local create_pr="$7"
local timeout_minutes="${8:-$DEFAULT_TIMEOUT_MINUTES}"
if [ "$USE_TUI" != true ]; then return; fi
ui_header "devloop" "$spec"
ui_print_key_values \
Expand All @@ -1311,7 +1307,6 @@ run_header() {
"passes" "$max" \
"timeout" "$timeout_minutes minutes" \
"strict" "$strict" \
"report" "$report_format" \
"pr" "$create_pr"
}

Expand Down Expand Up @@ -1528,7 +1523,6 @@ interactive_create_pr_choice() {

interactive_run_setup() {
local spec="$1"
local report_format="html"
local strict=true
local use_worktree=true
local coder
Expand All @@ -1550,8 +1544,8 @@ interactive_run_setup() {
ui_go_back
return 0
fi
run_header "$spec" "$max" "$report_format" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$report_format" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_header "$spec" "$max" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
code=$?
maybe_enter_worktree
return "$(final_exit_code "$code")"
Expand Down Expand Up @@ -1782,21 +1776,13 @@ list_artifact_files() {
while IFS= read -r root; do
dir="$root/$subdir"
if [ -d "$dir" ]; then
find "$dir" -type f | LC_ALL=C sort
find "$dir" -type f -name '*.md' | LC_ALL=C sort
fi
done
}

view_file() {
local file="$1"
case "$file" in
*.html|*.htm)
if [ "$USE_TUI" = true ] && [ -t 0 ]; then
if command -v open >/dev/null 2>&1 && open "$file" >/dev/null 2>&1; then return 0; fi
if command -v xdg-open >/dev/null 2>&1 && xdg-open "$file" >/dev/null 2>&1; then return 0; fi
fi
;;
esac
if ui_has_gum; then
gum pager < "$file"
return $?
Expand Down Expand Up @@ -1870,12 +1856,8 @@ track_report_path() {
local slug dir report
slug="$(basename "$track" .md)"
dir="$(cd "$(dirname "$track")/.." >/dev/null 2>&1 && pwd -P)/reports"
for report in "$dir/$slug.html" "$dir/$slug.md"; do
if [ -f "$report" ]; then
printf '%s\n' "$report"
return
fi
done
report="$dir/$slug.md"
if [ -f "$report" ]; then printf '%s\n' "$report"; fi
}

print_track_status() {
Expand Down Expand Up @@ -1956,15 +1938,14 @@ remove_devloop_worktree() {

run_from_track() {
local track="$1"
local worktree spec max report_format strict coder reviewer create_pr timeout_minutes old_pwd code next_pass
local worktree spec max strict coder reviewer create_pr timeout_minutes old_pwd code next_pass
track="$(absolute_existing_file "$track")" || {
printf 'track not found: %s\n' "$track" >&2
return 2
}
worktree="$(track_value "worktree" "$track")"
spec="$(track_value "spec" "$track")"
max="$(track_value "max" "$track")"
report_format="$(track_value "report-format" "$track")"
strict="$(track_value "strict" "$track")"
coder="$(track_value "coder" "$track")"
reviewer="$(track_value "reviewer" "$track")"
Expand All @@ -1974,7 +1955,6 @@ run_from_track() {
if [ -z "$worktree" ]; then worktree="$(cd "$(dirname "$track")/../.." >/dev/null 2>&1 && pwd -P)"; fi
if [ -z "$spec" ]; then spec="$(track_value "source-spec" "$track")"; fi
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="$(devloop_coder)"; fi
if [ -z "$reviewer" ]; then reviewer="$(devloop_reviewer)"; fi
Expand All @@ -1995,8 +1975,8 @@ run_from_track() {
old_pwd="$PWD"
cd "$worktree" || return 2
RUN_START_PASS="$next_pass"
run_header "$spec" "$max" "$report_format" "$strict" false "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$report_format" "$strict" false "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_header "$spec" "$max" "$strict" false "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$strict" false "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
code=$?
RUN_START_PASS=1
maybe_enter_worktree
Expand Down Expand Up @@ -2045,7 +2025,6 @@ will_enter_worktree_shell() {
}

run_command() {
local report_format="html"
local strict=true
local use_worktree=true
local coder
Expand All @@ -2064,16 +2043,6 @@ run_command() {
arg="$1"
shift
case "$arg" in
--report-format)
if [ "$#" -eq 0 ]; then usage >&2; return 2; fi
value="$1"
shift
case "$value" in
html) report_format="html" ;;
markdown|md) report_format="markdown" ;;
*) usage >&2; return 2 ;;
esac
;;
--coder)
if [ "$#" -eq 0 ]; then printf '%s\n' "coder must be Codex or Claude Code" >&2; usage >&2; return 2; fi
value="$(normalize_agent "$1")"
Expand All @@ -2096,8 +2065,6 @@ run_command() {
fi
shift
;;
--html) report_format="html" ;;
--markdown|--md) report_format="markdown" ;;
--no-strict) strict=false ;;
--strict) strict=true ;;
--in-place) use_worktree=false ;;
Expand Down Expand Up @@ -2136,8 +2103,8 @@ run_command() {
if [ "$max" -lt 1 ]; then max=1; fi
if [ "$max" -gt 10 ]; then max=10; fi

run_header "$spec" "$max" "$report_format" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$report_format" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_header "$spec" "$max" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
run_devloop "$spec" "$max" "$strict" "$use_worktree" "$coder" "$reviewer" "$create_pr" "$timeout_minutes"
local code=$?
maybe_enter_worktree
return "$(final_exit_code "$code")"
Expand All @@ -2146,13 +2113,12 @@ run_command() {
run_devloop() {
local spec_arg="$1"
local max="$2"
local report_format="$3"
local strict="$4"
local use_worktree="$5"
local coder="$6"
local reviewer="$7"
local create_pr="$8"
local timeout_minutes="${9:-$DEFAULT_TIMEOUT_MINUTES}"
local strict="$3"
local use_worktree="$4"
local coder="$5"
local reviewer="$6"
local create_pr="$7"
local timeout_minutes="${8:-$DEFAULT_TIMEOUT_MINUTES}"

MAX="$max"
CODER="$coder"
Expand Down Expand Up @@ -2262,15 +2228,11 @@ run_devloop() {
run_branch="$(git -C "$repo" branch --show-current)"
FINAL_BRANCH="$run_branch"
TRACK=".devloop/tracks/$slug.md"
if [ "$report_format" = "html" ]; then
REPORT=".devloop/reports/$slug.html"
else
REPORT=".devloop/reports/$slug.md"
fi
REPORT=".devloop/reports/$slug.md"
local coder_session=".devloop/sessions/$slug-coder-$coder.id"
local reviewer_session=".devloop/sessions/$slug-reviewer-$reviewer.id"

init_track "$repo/$TRACK" "$run_spec" "$spec" "$PWD" "$SOURCE_REPO" "$repo" "$base" "$source_branch" "$run_branch" "$max" "$report_format" "$strict" "$coder" "$reviewer" "$WORK_TYPE" "$WORK_BREAKING" "$create_pr" "$timeout_minutes"
init_track "$repo/$TRACK" "$run_spec" "$spec" "$PWD" "$SOURCE_REPO" "$repo" "$base" "$source_branch" "$run_branch" "$max" "$strict" "$coder" "$reviewer" "$WORK_TYPE" "$WORK_BREAKING" "$create_pr" "$timeout_minutes"

if [ "$create_pr" = true ]; then
event_step "pull-request-lookup" "checking for existing PR"
Expand Down Expand Up @@ -2448,7 +2410,7 @@ run_devloop() {

CODER_SESSION_ID="$(read_first_line "$repo/$coder_session")"
REVIEWER_SESSION_ID="$(read_first_line "$repo/$reviewer_session")"
synthesize_report "$repo" "$slug" "$reviewer" "$run_spec" "$spec" "$spec_text" "$SOURCE_REPO" "$repo" "$TRACK" "$REPORT" "$STATUS" "$PASSES" "$max" "$base" "$source_branch" "$FINAL_BRANCH" "$FINAL_COMMIT" "$FINAL_COMMIT_MESSAGE" "$PULL_REQUEST" "$PULL_REQUEST_ERROR" "$coder" "$repo/$reviewer_session" "$CODER_SESSION_ID" "$REVIEWER_SESSION_ID" "$report_format"
synthesize_report "$repo" "$slug" "$reviewer" "$run_spec" "$spec" "$spec_text" "$SOURCE_REPO" "$repo" "$TRACK" "$REPORT" "$STATUS" "$PASSES" "$max" "$base" "$source_branch" "$FINAL_BRANCH" "$FINAL_COMMIT" "$FINAL_COMMIT_MESSAGE" "$PULL_REQUEST" "$PULL_REQUEST_ERROR" "$coder" "$repo/$reviewer_session" "$CODER_SESSION_ID" "$REVIEWER_SESSION_ID"

if [ "$create_pr" = true ] && [ -n "$PULL_REQUEST" ] && [ "$STATUS" != "pr-error" ]; then
event_step "pr-final-report" "posting final report to PR"
Expand Down Expand Up @@ -3176,14 +3138,13 @@ init_track() {
local branch="$8"
local worktree_branch="$9"
local max="${10}"
local report_format="${11}"
local strict="${12}"
local coder="${13}"
local reviewer="${14}"
local type="${15}"
local breaking="${16}"
local create_pr="${17}"
local timeout_minutes="${18}"
local strict="${11}"
local coder="${12}"
local reviewer="${13}"
local type="${14}"
local breaking="${15}"
local create_pr="${16}"
local timeout_minutes="${17}"
local track_name
if [ -f "$file" ]; then return; fi
track_name="$(basename "$file" .md)"
Expand All @@ -3203,7 +3164,6 @@ init_track() {
- type: $type
- breaking: $breaking
- max: $max
- report-format: $report_format
- strict: $strict
- create-pr: $create_pr
- timeout-minutes: $timeout_minutes
Expand Down Expand Up @@ -4103,17 +4063,12 @@ synthesize_report() {
local reviewer_session_file="${22}"
local coder_session_id="${23}"
local reviewer_session_id="${24}"
local format="${25}"
local title subtitle reviews metadata body prompt
title="$(report_title "$spec_text" "$slug")"
subtitle="$(report_subtitle "$spec_text" "$title")"
reviews="$(list_reviews "$slug" "$pass" "$max")"
metadata="$(report_metadata "$status" "$pass" "$max" "$repo" "$run_spec" "$source_spec" "$source_repo" "$worktree" "$base" "$initial_branch" "$branch" "$commit" "$commit_message" "$pull_request" "$pull_request_error" "$coder" "$reviewer" "$coder_session_id" "$reviewer_session_id" "$track" "$reviews")"
if [ "$format" = "html" ]; then
body="Write the report to $report as valid standalone HTML. Use a readable document layout with embedded CSS, set the HTML <title> to the report title, render the report title and subtitle before Metadata, render a topical three-line haiku immediately after the subtitle, use a compact metadata table, and add substantive sections after it. Include these visible section headings: Metadata, The shape of the problem, What was built, What the review caught (and why it mattered), What to remember next time, Residual risk, Pointers. Do not optimize away substance: explain the decisions, tradeoffs, evidence, and transferable lessons clearly enough that the reader learns from the run."
else
body="Write the report to $report in markdown. Start with the report title as the H1, put the subtitle directly below it, put a topical three-line haiku immediately after the subtitle, then include these headings: Metadata, The shape of the problem, What was built, What the review caught (and why it mattered), What to remember next time, Residual risk, Pointers. Do not optimize away substance: explain the decisions, tradeoffs, evidence, and transferable lessons clearly enough that the reader learns from the run."
fi
body="Write the report to $report in plain Markdown. Start with the report title as the H1, put the subtitle directly below it, put a topical three-line haiku immediately after the subtitle, then include these headings: Metadata, The shape of the problem, What was built, What the review caught (and why it mattered), What to remember next time, Residual risk, Pointers. Use terminal-friendly Markdown only: no HTML tags, embedded CSS, Mermaid, SVG, images, or external assets. If a diagram clarifies the run, draw it as ASCII inside a plain code fence. Do not optimize away substance: explain the decisions, tradeoffs, evidence, and transferable lessons clearly enough that the reader learns from the run."
prompt="$(cat <<EOF
You are writing a learning-oriented post-mortem for a developer who just ran a devloop.

Expand Down
Loading
Loading