mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 08:36:35 +03:00
- Fix codespell: replace "caf" trigger word in BATS test data - Fix SC2155: separate declare and assign in log functions - Fix SC2129: use grouped redirects instead of individual ones - Fix SC2034: remove unused variables (EXIT_SUCCESS, IMPLEMENT_STATUS, IMPLEMENT_EXIT_CODE, PROCESS_EXIT_CODE, worktree_branch) - Fix SC2034: add shellcheck disable for RESUME_* vars (reserved for future resume functionality) - Fix SC2317/SC2329: add shellcheck disable for trap-invoked release_lock - Fix SC2002: use input redirection instead of useless cat - Remove dead code: unused update_stage() function Co-Authored-By: Claude <claude@anthropic.com>
1673 lines
60 KiB
Bash
Executable File
1673 lines
60 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# implement-issue-orchestrator.sh
|
|
# Orchestrates implement-issue workflow via Claude CLI calls per stage
|
|
#
|
|
# Usage:
|
|
# ./implement-issue-orchestrator.sh --issue 123 --branch test
|
|
# ./implement-issue-orchestrator.sh --issue 123 --branch test --agent patch-engineer
|
|
#
|
|
# Outputs:
|
|
# - status.json: Real-time progress
|
|
# - logs/implement-issue/<timestamp>/: Per-stage logs
|
|
#
|
|
|
|
set -uo pipefail # Note: not -e, we handle errors explicitly
|
|
|
|
# Allow nested Claude CLI invocations when run from within a Claude Code session
|
|
unset CLAUDECODE
|
|
|
|
# =============================================================================
|
|
# CONFIGURATION
|
|
# =============================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SCHEMA_DIR="$SCRIPT_DIR/schemas"
|
|
|
|
# Timeouts and limits
|
|
readonly STAGE_TIMEOUT=3600 # 1 hour per stage
|
|
readonly MAX_TASK_REVIEW_ATTEMPTS=3
|
|
readonly MAX_QUALITY_ITERATIONS=5
|
|
readonly MAX_TEST_ITERATIONS=10
|
|
readonly MAX_PR_REVIEW_ITERATIONS=3
|
|
readonly RATE_LIMIT_BUFFER=60
|
|
readonly RATE_LIMIT_DEFAULT_WAIT=3600
|
|
|
|
# =============================================================================
|
|
# ARGUMENT PARSING
|
|
# =============================================================================
|
|
|
|
ISSUE_NUMBER=""
|
|
BASE_BRANCH=""
|
|
AGENT=""
|
|
STATUS_FILE="status.json"
|
|
RESUME_MODE=""
|
|
RESUME_LOG_DIR=""
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 --issue <number> --branch <name> [options]
|
|
$0 --resume [--status-file <path>]
|
|
$0 --resume-from <log-dir>
|
|
|
|
Options:
|
|
--issue <number> GitHub issue number (required for new runs)
|
|
--branch <name> Base branch for PR (required for new runs)
|
|
--agent <name> Default agent for setup stage (optional)
|
|
--status-file <path> Custom status file path (optional)
|
|
--resume Resume from existing status.json
|
|
--resume-from <dir> Resume from specific log directory
|
|
|
|
Resume modes:
|
|
--resume uses the current status.json (or --status-file path)
|
|
--resume-from reads status.json from the specified log directory
|
|
|
|
Agents are determined per-task from setup output.
|
|
EOF
|
|
exit 3
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--issue)
|
|
[[ -n "${2:-}" ]] || { echo "ERROR: --issue requires a value" >&2; exit 3; }
|
|
ISSUE_NUMBER="$2"
|
|
shift 2
|
|
;;
|
|
--branch)
|
|
[[ -n "${2:-}" ]] || { echo "ERROR: --branch requires a value" >&2; exit 3; }
|
|
BASE_BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
--agent)
|
|
[[ -n "${2:-}" ]] || { echo "ERROR: --agent requires a value" >&2; exit 3; }
|
|
AGENT="$2"
|
|
shift 2
|
|
;;
|
|
--status-file)
|
|
[[ -n "${2:-}" ]] || { echo "ERROR: --status-file requires a value" >&2; exit 3; }
|
|
STATUS_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--resume)
|
|
RESUME_MODE="status"
|
|
shift
|
|
;;
|
|
--resume-from)
|
|
[[ -n "${2:-}" ]] || { echo "ERROR: --resume-from requires a log directory path" >&2; exit 3; }
|
|
RESUME_MODE="logdir"
|
|
RESUME_LOG_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate arguments based on mode
|
|
if [[ -n "$RESUME_MODE" ]]; then
|
|
# Resume mode - issue and branch will be read from status.json
|
|
:
|
|
elif [[ -z "$ISSUE_NUMBER" || -z "$BASE_BRANCH" ]]; then
|
|
echo "ERROR: --issue and --branch are required (or use --resume/--resume-from)"
|
|
usage
|
|
fi
|
|
|
|
# =============================================================================
|
|
# LOGGING FUNCTIONS (defined early so other functions can use log/log_error)
|
|
# Note: LOG_FILE and mkdir happen later after LOG_BASE is set
|
|
# =============================================================================
|
|
|
|
LOG_FILE=""
|
|
STAGE_COUNTER=0
|
|
|
|
log() {
|
|
local msg
|
|
msg="[$(date -Iseconds)] $*"
|
|
if [[ -n "$LOG_FILE" ]]; then
|
|
printf '%s\n' "$msg" >> "$LOG_FILE"
|
|
fi
|
|
printf '%s\n' "$msg" >&2
|
|
}
|
|
|
|
log_error() {
|
|
local msg
|
|
msg="[$(date -Iseconds)] ERROR: $*"
|
|
if [[ -n "$LOG_FILE" ]]; then
|
|
printf '%s\n' "$msg" >> "$LOG_FILE"
|
|
fi
|
|
printf '%s\n' "$msg" >&2
|
|
}
|
|
|
|
next_stage_log() {
|
|
local stage_name="$1"
|
|
STAGE_COUNTER=$((STAGE_COUNTER + 1))
|
|
printf "%02d-%s.log" "$STAGE_COUNTER" "$stage_name"
|
|
}
|
|
|
|
# =============================================================================
|
|
# STATUS FILE MANAGEMENT
|
|
# =============================================================================
|
|
|
|
init_status() {
|
|
jq -n \
|
|
--arg state "initializing" \
|
|
--argjson issue "$ISSUE_NUMBER" \
|
|
--arg base_branch "$BASE_BRANCH" \
|
|
--arg branch "" \
|
|
--arg worktree "" \
|
|
--arg current_stage "setup" \
|
|
--argjson current_task "null" \
|
|
--arg log_dir "$LOG_BASE" \
|
|
'{
|
|
state: $state,
|
|
issue: $issue,
|
|
base_branch: $base_branch,
|
|
branch: $branch,
|
|
worktree: $worktree,
|
|
current_stage: $current_stage,
|
|
current_task: $current_task,
|
|
stages: {
|
|
setup: {status: "pending", started_at: null, completed_at: null},
|
|
research: {status: "pending", started_at: null, completed_at: null},
|
|
evaluate: {status: "pending", started_at: null, completed_at: null},
|
|
plan: {status: "pending", started_at: null, completed_at: null},
|
|
implement: {status: "pending", task_progress: "0/0"},
|
|
quality_loop: {status: "pending", iteration: 0},
|
|
test_loop: {status: "pending", iteration: 0},
|
|
docs: {status: "pending"},
|
|
pr: {status: "pending"},
|
|
pr_review: {status: "pending", iteration: 0},
|
|
complete: {status: "pending"}
|
|
},
|
|
tasks: [],
|
|
quality_iterations: 0,
|
|
test_iterations: 0,
|
|
pr_review_iterations: 0,
|
|
last_update: (now | todate),
|
|
log_dir: $log_dir
|
|
}' > "$STATUS_FILE"
|
|
|
|
log "Initialized status file: $STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
set_stage_started() {
|
|
local stage="$1"
|
|
jq --arg stage "$stage" \
|
|
'.stages[$stage].started_at = (now | todate) |
|
|
.stages[$stage].status = "in_progress" |
|
|
.current_stage = $stage |
|
|
.state = "running" |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
set_stage_completed() {
|
|
local stage="$1"
|
|
jq --arg stage "$stage" \
|
|
'.stages[$stage].completed_at = (now | todate) |
|
|
.stages[$stage].status = "completed" |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
update_task() {
|
|
local task_id="$1"
|
|
local status="$2"
|
|
local review_attempts="${3:-0}"
|
|
|
|
jq --argjson id "$task_id" \
|
|
--arg status "$status" \
|
|
--argjson attempts "$review_attempts" \
|
|
'(.tasks[] | select(.id == $id)).status = $status |
|
|
(.tasks[] | select(.id == $id)).review_attempts = $attempts |
|
|
.current_task = $id |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
set_tasks() {
|
|
local tasks_json="$1"
|
|
jq --argjson tasks "$tasks_json" \
|
|
'.tasks = $tasks |
|
|
.stages.implement.task_progress = "0/\($tasks | length)" |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
set_worktree_info() {
|
|
local worktree="$1"
|
|
local branch="$2"
|
|
jq --arg worktree "$worktree" \
|
|
--arg branch "$branch" \
|
|
'.worktree = $worktree | .branch = $branch | .last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
set_final_state() {
|
|
local state="$1"
|
|
jq --arg state "$state" \
|
|
'.state = $state | .last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
increment_quality_iteration() {
|
|
jq '.quality_iterations += 1 |
|
|
.stages.quality_loop.iteration = .quality_iterations |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
increment_test_iteration() {
|
|
jq '.test_iterations += 1 |
|
|
.stages.test_loop.iteration = .test_iterations |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
increment_pr_review_iteration() {
|
|
jq '.pr_review_iterations += 1 |
|
|
.stages.pr_review.iteration = .pr_review_iterations |
|
|
.last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
}
|
|
|
|
# =============================================================================
|
|
# RESUME FUNCTIONALITY
|
|
# =============================================================================
|
|
|
|
# Validate that a status file has required fields for resumption
|
|
# Returns 0 if valid, 1 if invalid
|
|
validate_resume_status() {
|
|
local status_path="$1"
|
|
|
|
if [[ ! -f "$status_path" ]]; then
|
|
echo "ERROR: Status file not found: $status_path" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Check required fields exist
|
|
local required_fields=("issue" "branch" "worktree" "current_stage" "log_dir")
|
|
local field
|
|
for field in "${required_fields[@]}"; do
|
|
local value
|
|
value=$(jq -r ".$field // empty" "$status_path" 2>/dev/null)
|
|
if [[ -z "$value" || "$value" == "null" ]]; then
|
|
echo "ERROR: Status file missing required field: $field" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
# Check state is resumable (not already completed or in error)
|
|
local state
|
|
state=$(jq -r '.state' "$status_path" 2>/dev/null)
|
|
if [[ "$state" == "completed" ]]; then
|
|
echo "ERROR: Cannot resume - workflow already completed" >&2
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Load resume state from status file
|
|
# Sets global variables: ISSUE_NUMBER, BASE_BRANCH, LOG_BASE, WORKTREE, BRANCH
|
|
# Also sets: RESUME_STAGE, RESUME_TASK, RESUME_TASKS_JSON
|
|
load_resume_state() {
|
|
local status_path="$1"
|
|
|
|
ISSUE_NUMBER=$(jq -r '.issue' "$status_path")
|
|
# Restore BASE_BRANCH from status file (fall back to command-line value if not stored)
|
|
local stored_base_branch
|
|
stored_base_branch=$(jq -r '.base_branch // empty' "$status_path")
|
|
if [[ -n "$stored_base_branch" ]]; then
|
|
BASE_BRANCH="$stored_base_branch"
|
|
elif [[ -z "$BASE_BRANCH" ]]; then
|
|
echo "WARNING: No base_branch in status file and none provided via --branch" >&2
|
|
fi
|
|
BRANCH=$(jq -r '.branch' "$status_path")
|
|
WORKTREE=$(jq -r '.worktree' "$status_path")
|
|
LOG_BASE=$(jq -r '.log_dir' "$status_path")
|
|
|
|
RESUME_STAGE=$(jq -r '.current_stage' "$status_path")
|
|
RESUME_TASK=$(jq -r '.current_task // 0' "$status_path")
|
|
RESUME_TASKS_JSON=$(jq -c '.tasks // []' "$status_path")
|
|
|
|
# Restore iteration counters (reserved for future resume functionality)
|
|
# shellcheck disable=SC2034
|
|
RESUME_QUALITY_ITERATIONS=$(jq -r '.quality_iterations // 0' "$status_path")
|
|
# shellcheck disable=SC2034
|
|
RESUME_TEST_ITERATIONS=$(jq -r '.test_iterations // 0' "$status_path")
|
|
# shellcheck disable=SC2034
|
|
RESUME_PR_ITERATIONS=$(jq -r '.pr_review_iterations // 0' "$status_path")
|
|
|
|
# Get PR number if it exists
|
|
# shellcheck disable=SC2034
|
|
RESUME_PR_NUMBER=$(jq -r '.stages.pr.pr_number // empty' "$status_path")
|
|
}
|
|
|
|
# Check if a stage is completed in status file
|
|
# Returns 0 if completed, 1 if not
|
|
is_stage_completed() {
|
|
local stage="$1"
|
|
local status
|
|
status=$(jq -r ".stages.$stage.status" "$STATUS_FILE" 2>/dev/null)
|
|
[[ "$status" == "completed" ]]
|
|
}
|
|
|
|
# Get count of completed tasks
|
|
get_completed_task_count() {
|
|
jq '[.tasks[] | select(.status == "completed")] | length' "$STATUS_FILE" 2>/dev/null || echo "0"
|
|
}
|
|
|
|
# Check if worktree exists and is valid
|
|
validate_worktree() {
|
|
local worktree_path="$1"
|
|
|
|
if [[ ! -d "$worktree_path" ]]; then
|
|
echo "ERROR: Worktree not found: $worktree_path" >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -d "$worktree_path/.git" && ! -f "$worktree_path/.git" ]]; then
|
|
echo "ERROR: Path is not a git worktree: $worktree_path" >&2
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# =============================================================================
|
|
# RESUME MODE INITIALIZATION
|
|
# =============================================================================
|
|
|
|
# These will be populated in resume mode
|
|
WORKTREE=""
|
|
BRANCH=""
|
|
RESUME_STAGE=""
|
|
RESUME_TASK=""
|
|
RESUME_TASKS_JSON=""
|
|
# shellcheck disable=SC2034 # Reserved for future resume functionality
|
|
RESUME_QUALITY_ITERATIONS=0
|
|
# shellcheck disable=SC2034
|
|
RESUME_TEST_ITERATIONS=0
|
|
# shellcheck disable=SC2034
|
|
RESUME_PR_ITERATIONS=0
|
|
# shellcheck disable=SC2034
|
|
RESUME_PR_NUMBER=""
|
|
|
|
if [[ "$RESUME_MODE" == "logdir" ]]; then
|
|
# Resume from specific log directory
|
|
if [[ ! -d "$RESUME_LOG_DIR" ]]; then
|
|
echo "ERROR: Log directory not found: $RESUME_LOG_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local_status_file="$RESUME_LOG_DIR/status.json"
|
|
if [[ ! -f "$local_status_file" ]]; then
|
|
# Try parent directory's status.json (log_dir may be relative)
|
|
local_status_file="status.json"
|
|
fi
|
|
|
|
if ! validate_resume_status "$local_status_file"; then
|
|
exit 1
|
|
fi
|
|
|
|
load_resume_state "$local_status_file"
|
|
STATUS_FILE="$local_status_file"
|
|
# LOG_BASE was set by load_resume_state
|
|
|
|
if ! validate_worktree "$WORKTREE"; then
|
|
exit 1
|
|
fi
|
|
|
|
elif [[ "$RESUME_MODE" == "status" ]]; then
|
|
# Resume from current status file
|
|
if ! validate_resume_status "$STATUS_FILE"; then
|
|
exit 1
|
|
fi
|
|
|
|
load_resume_state "$STATUS_FILE"
|
|
|
|
if ! validate_worktree "$WORKTREE"; then
|
|
exit 1
|
|
fi
|
|
|
|
else
|
|
# Normal mode - set LOG_BASE
|
|
LOG_BASE="logs/implement-issue/issue-${ISSUE_NUMBER}-$(date +%Y%m%d-%H%M%S)"
|
|
fi
|
|
|
|
# Display mode info
|
|
if [[ -n "$RESUME_MODE" ]]; then
|
|
echo "Implement Issue Orchestrator (RESUME MODE)"
|
|
echo "Resuming from: $STATUS_FILE"
|
|
echo "Issue: #$ISSUE_NUMBER"
|
|
echo "Branch: $BRANCH"
|
|
echo "Worktree: $WORKTREE"
|
|
echo "Resume stage: $RESUME_STAGE"
|
|
[[ -n "$RESUME_TASK" && "$RESUME_TASK" != "null" ]] && echo "Resume task: $RESUME_TASK"
|
|
echo "Log dir: $LOG_BASE"
|
|
else
|
|
echo "Implement Issue Orchestrator"
|
|
echo "Issue: #$ISSUE_NUMBER"
|
|
echo "Branch: $BASE_BRANCH"
|
|
echo "Agent: ${AGENT:-default}"
|
|
echo "Status file: $STATUS_FILE"
|
|
echo "Log dir: $LOG_BASE"
|
|
fi
|
|
|
|
# Create log directories
|
|
mkdir -p "$LOG_BASE/stages" "$LOG_BASE/context"
|
|
LOG_FILE="$LOG_BASE/orchestrator.log"
|
|
STAGE_COUNTER=0
|
|
|
|
# =============================================================================
|
|
# STATUS SYNC TO LOG DIRECTORY
|
|
# =============================================================================
|
|
|
|
# Sync status.json to log directory after every update
|
|
# This ensures status.json exists in LOG_BASE for resume-from functionality
|
|
sync_status_to_log() {
|
|
if [[ -n "$LOG_BASE" && -d "$LOG_BASE" && -f "$STATUS_FILE" ]]; then
|
|
local target="$LOG_BASE/status.json"
|
|
# Avoid copying file to itself (happens with --resume-from)
|
|
if [[ "$(realpath "$STATUS_FILE")" != "$(realpath "$target")" ]]; then
|
|
cp "$STATUS_FILE" "$target"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# GITHUB COMMENT HELPERS
|
|
# =============================================================================
|
|
|
|
REPO=$(git remote get-url origin 2>/dev/null | sed -E 's#.*github\.com[:/]##; s/\.git$//')
|
|
|
|
# comment_issue <title> <body> [agent]
|
|
# If agent is provided, shows "Written by `agent`", otherwise "Posted by orchestrator"
|
|
comment_issue() {
|
|
local title="$1"
|
|
local body="$2"
|
|
local agent="${3:-}"
|
|
local attribution
|
|
|
|
if [[ -n "$agent" ]]; then
|
|
attribution="Written by \`$agent\`"
|
|
else
|
|
attribution="Posted by \`implement-issue-orchestrator\`"
|
|
fi
|
|
|
|
local comment
|
|
comment=$(cat <<EOF
|
|
## $title
|
|
###### *$attribution*
|
|
|
|
$body
|
|
EOF
|
|
)
|
|
|
|
log "Commenting on issue #$ISSUE_NUMBER: $title"
|
|
if ! gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$comment" 2>/dev/null; then
|
|
log_error "Failed to comment on issue #$ISSUE_NUMBER"
|
|
fi
|
|
}
|
|
|
|
# comment_pr <pr_num> <title> <body> [agent]
|
|
# If agent is provided, shows "Written by `agent`", otherwise "Posted by orchestrator"
|
|
comment_pr() {
|
|
local pr_num="$1"
|
|
local title="$2"
|
|
local body="$3"
|
|
local agent="${4:-}"
|
|
local attribution
|
|
|
|
if [[ -n "$agent" ]]; then
|
|
attribution="Written by \`$agent\`"
|
|
else
|
|
attribution="Posted by \`implement-issue-orchestrator\`"
|
|
fi
|
|
|
|
local comment
|
|
comment=$(cat <<EOF
|
|
## $title
|
|
###### *$attribution*
|
|
|
|
$body
|
|
EOF
|
|
)
|
|
|
|
log "Commenting on PR #$pr_num: $title"
|
|
if ! gh pr comment "$pr_num" --repo "$REPO" --body "$comment" 2>/dev/null; then
|
|
log_error "Failed to comment on PR #$pr_num"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# RATE LIMIT DETECTION
|
|
# =============================================================================
|
|
|
|
detect_rate_limit() {
|
|
local output="$1"
|
|
|
|
# Check structured output first
|
|
local status
|
|
status=$(printf '%s' "$output" | jq -r '.structured_output.status // empty' 2>/dev/null)
|
|
|
|
if [[ "$status" == "success" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ "$status" == "rate_limit" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Only check text patterns if there's an actual error
|
|
# (prevents false positives when reviews mention "rate limiting" as a feature)
|
|
local is_error
|
|
is_error=$(printf '%s' "$output" | jq -r '.is_error // false' 2>/dev/null)
|
|
|
|
if [[ "$is_error" != "true" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
# Fallback to text pattern matching (only for errors)
|
|
local result
|
|
result=$(printf '%s' "$output" | jq -r '.result // empty' 2>/dev/null)
|
|
if printf '%s' "$result" | grep -qiE 'rate.limit|429|too many requests|quota.exceeded'; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
extract_wait_time() {
|
|
local output="$1"
|
|
local result
|
|
result=$(printf '%s' "$output" | jq -r '.result // empty' 2>/dev/null)
|
|
local search_text="$result $output"
|
|
|
|
# Try retry-after
|
|
local retry_after
|
|
retry_after=$(printf '%s' "$search_text" | grep -oiE 'retry.after[^0-9]*([0-9]+)' | grep -oE '[0-9]+' | head -1)
|
|
if [[ -n "$retry_after" ]] && (( retry_after > 0 )); then
|
|
printf '%s\n' "$retry_after"
|
|
return
|
|
fi
|
|
|
|
# Try wait X minutes
|
|
local wait_mins
|
|
wait_mins=$(printf '%s' "$search_text" | grep -oiE 'wait[^0-9]*([0-9]+)[^0-9]*min' | grep -oE '[0-9]+' | head -1)
|
|
if [[ -n "$wait_mins" ]] && (( wait_mins > 0 )); then
|
|
printf '%s\n' "$((wait_mins * 60))"
|
|
return
|
|
fi
|
|
|
|
printf '%s\n' "$RATE_LIMIT_DEFAULT_WAIT"
|
|
}
|
|
|
|
handle_rate_limit() {
|
|
local output="$1"
|
|
local wait_time
|
|
wait_time=$(extract_wait_time "$output")
|
|
wait_time=$((wait_time + RATE_LIMIT_BUFFER))
|
|
|
|
local resume_at
|
|
resume_at=$(date -Iseconds -d "+${wait_time} seconds" 2>/dev/null || date -v+${wait_time}S -Iseconds 2>/dev/null)
|
|
|
|
log "Rate limit hit. Waiting ${wait_time}s until $resume_at"
|
|
sleep "$wait_time"
|
|
}
|
|
|
|
# =============================================================================
|
|
# STAGE RUNNERS
|
|
# =============================================================================
|
|
|
|
run_stage() {
|
|
local stage_name="$1"
|
|
local prompt="$2"
|
|
local schema_file="$3"
|
|
local agent="${4:-}"
|
|
|
|
local stage_log
|
|
stage_log="$LOG_BASE/stages/$(next_stage_log "$stage_name")"
|
|
|
|
# Validate schema file exists
|
|
if [[ ! -f "$SCHEMA_DIR/$schema_file" ]]; then
|
|
log_error "Schema file not found: $SCHEMA_DIR/$schema_file"
|
|
echo '{"status":"error","error":"schema not found"}'
|
|
return 1
|
|
fi
|
|
|
|
local schema
|
|
schema=$(jq -c . "$SCHEMA_DIR/$schema_file")
|
|
|
|
log "Running stage: $stage_name"
|
|
log " Schema: $schema_file"
|
|
log " Agent: ${agent:-default}"
|
|
log " Log: $stage_log"
|
|
|
|
local -a agent_args=()
|
|
if [[ -n "$agent" ]]; then
|
|
agent_args=(--agent "$agent")
|
|
fi
|
|
|
|
local output
|
|
local exit_code=0
|
|
|
|
output=$(timeout "$STAGE_TIMEOUT" claude -p "$prompt" \
|
|
"${agent_args[@]}" \
|
|
--dangerously-skip-permissions \
|
|
--output-format json \
|
|
--json-schema "$schema" \
|
|
2>&1) || exit_code=$?
|
|
|
|
{
|
|
printf '%s\n' "=== $stage_name output ==="
|
|
printf '%s\n' "$output"
|
|
printf '%s\n' "=== exit code: $exit_code ==="
|
|
} >> "$stage_log"
|
|
|
|
# Check timeout
|
|
if (( exit_code == 124 )); then
|
|
log_error "Stage $stage_name timed out after ${STAGE_TIMEOUT}s"
|
|
echo '{"status":"error","error":"timeout"}'
|
|
return 1
|
|
fi
|
|
|
|
# Check rate limit
|
|
if detect_rate_limit "$output"; then
|
|
handle_rate_limit "$output"
|
|
# Retry
|
|
output=$(timeout "$STAGE_TIMEOUT" claude -p "$prompt" \
|
|
"${agent_args[@]}" \
|
|
--dangerously-skip-permissions \
|
|
--output-format json \
|
|
--json-schema "$schema" \
|
|
2>&1) || exit_code=$?
|
|
|
|
printf '%s\n' "=== $stage_name retry output ===" >> "$stage_log"
|
|
printf '%s\n' "$output" >> "$stage_log"
|
|
fi
|
|
|
|
# Extract structured output
|
|
local structured
|
|
structured=$(printf '%s' "$output" | jq -c '.structured_output // empty' 2>/dev/null)
|
|
|
|
if [[ -z "$structured" ]]; then
|
|
log_error "No structured output from $stage_name"
|
|
echo '{"status":"error","error":"no structured output"}'
|
|
return 1
|
|
fi
|
|
|
|
printf '%s\n' "$structured"
|
|
}
|
|
|
|
# =============================================================================
|
|
# QUALITY LOOP HELPER
|
|
# =============================================================================
|
|
|
|
# Run the quality loop (simplify -> review -> fix, repeat)
|
|
# Note: Testing is handled separately by run_test_loop after all tasks complete
|
|
# Arguments:
|
|
# $1 - worktree path
|
|
# $2 - branch name
|
|
# $3 - stage prefix for logging (e.g., "task-1" or "pr-fix")
|
|
# Returns:
|
|
# 0 on success (approved)
|
|
# 2 on max iterations exceeded (calls exit 2)
|
|
run_quality_loop() {
|
|
local loop_worktree="$1"
|
|
local loop_branch="$2"
|
|
local stage_prefix="${3:-main}"
|
|
|
|
local loop_approved=false
|
|
local loop_iteration=0 # Per-loop counter (resets each call)
|
|
|
|
while [[ "$loop_approved" != "true" ]]; do
|
|
loop_iteration=$((loop_iteration + 1))
|
|
increment_quality_iteration # Global counter for status tracking
|
|
|
|
if (( loop_iteration > MAX_QUALITY_ITERATIONS )); then
|
|
log_error "Quality loop for $stage_prefix exceeded max iterations ($MAX_QUALITY_ITERATIONS)"
|
|
set_final_state "max_iterations_quality"
|
|
exit 2
|
|
fi
|
|
|
|
log "Quality loop iteration $loop_iteration/$MAX_QUALITY_ITERATIONS (prefix: $stage_prefix)"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# SIMPLIFY → Issue comment #7
|
|
# -------------------------------------------------------------------------
|
|
local simplify_prompt="Run cdd-code-simplifier on modified files in worktree $loop_worktree on branch $loop_branch.
|
|
|
|
IMPORTANT SCOPE CONSTRAINT: This is for issue #$ISSUE_NUMBER. Only simplify code that is directly related to the issue's goals. Do NOT apply general refactoring to files that were only incidentally touched or are outside the issue's focus area.
|
|
|
|
If no relevant files were modified as part of this issue's implementation, make no changes and report 'No changes to simplify'.
|
|
|
|
Simplify code for clarity and consistency without changing functionality. Follow STYLEGUIDE.md conventions for shell scripts.
|
|
Output a summary of changes made."
|
|
|
|
local simplify_result
|
|
simplify_result=$(run_stage "simplify-${stage_prefix}-iter-$loop_iteration" "$simplify_prompt" "implement-issue-simplify.json" "cdd-code-simplifier")
|
|
|
|
local simplify_summary
|
|
simplify_summary=$(printf '%s' "$simplify_result" | jq -r '.summary // "No changes"')
|
|
|
|
# Comment #7: Simplify summary
|
|
comment_issue "Quality Loop [$stage_prefix]: Simplify ($loop_iteration/$MAX_QUALITY_ITERATIONS)" "$simplify_summary" "cdd-code-simplifier"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# REVIEW → Issue comment #9
|
|
# -------------------------------------------------------------------------
|
|
local review_prompt="Review the code changes for task scope '$stage_prefix' in worktree $loop_worktree on branch $loop_branch.
|
|
|
|
IMPORTANT: This is a task-level quality check within the implementation workflow, NOT a full PR review.
|
|
Your job is to verify code quality for the changes made in this task only.
|
|
|
|
Check:
|
|
- Code patterns and standards
|
|
- Consistency with codebase conventions
|
|
- Potential bugs or issues
|
|
- Security concerns
|
|
|
|
DO NOT recommend 'approve and merge' - this is not a PR review.
|
|
Simply output 'approved' if code quality is acceptable, or 'changes_requested' with specific issues to fix."
|
|
|
|
local review_result
|
|
review_result=$(run_stage "review-${stage_prefix}-iter-$loop_iteration" "$review_prompt" "implement-issue-review.json" "code-reviewer")
|
|
|
|
local review_verdict review_summary
|
|
review_verdict=$(printf '%s' "$review_result" | jq -r '.result')
|
|
review_summary=$(printf '%s' "$review_result" | jq -r '.summary // "Review completed"')
|
|
|
|
# Comment #9: Code review results
|
|
local review_icon="✅"
|
|
[[ "$review_verdict" == "changes_requested" ]] && review_icon="🔄"
|
|
comment_issue "Quality Loop [$stage_prefix]: Code Review ($loop_iteration/$MAX_QUALITY_ITERATIONS)" "$review_icon **Result:** $review_verdict
|
|
|
|
$review_summary" "code-reviewer"
|
|
|
|
if [[ "$review_verdict" == "approved" ]]; then
|
|
loop_approved=true
|
|
log "Quality loop for $stage_prefix approved on iteration $loop_iteration"
|
|
else
|
|
local review_comments
|
|
review_comments=$(printf '%s' "$review_result" | jq -r '.comments // "No comments"')
|
|
printf '%s\n' "$review_comments" >> "$LOG_BASE/context/review-comments.json"
|
|
|
|
local fix_prompt="Address code review feedback in worktree $loop_worktree on branch $loop_branch:
|
|
|
|
$review_comments
|
|
|
|
Fix the issues and commit. Output a summary of fixes applied."
|
|
|
|
local fix_result
|
|
fix_result=$(run_stage "fix-review-${stage_prefix}-iter-$loop_iteration" "$fix_prompt" "implement-issue-fix.json")
|
|
|
|
local fix_summary
|
|
fix_summary=$(printf '%s' "$fix_result" | jq -r '.summary // "Fixes applied"')
|
|
|
|
# Comment #10: Fix results (review fix)
|
|
comment_issue "Quality Loop [$stage_prefix]: Review Fix ($loop_iteration/$MAX_QUALITY_ITERATIONS)" "$fix_summary"
|
|
fi
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
# =============================================================================
|
|
# TEST LOOP HELPER
|
|
# =============================================================================
|
|
|
|
# Run the test loop (test -> validate -> fix, repeat until pass)
|
|
# Called once after all tasks complete
|
|
# Flow:
|
|
# 1. Run lint + tests (shellcheck, BATS)
|
|
# 2. If tests fail: fix issues, loop
|
|
# 3. If tests pass: validate test quality (bats-test-validator, scoped to issue)
|
|
# 4. If validation fails: fix issues, loop
|
|
# 5. If validation passes: done
|
|
# Arguments:
|
|
# $1 - worktree path
|
|
# $2 - branch name
|
|
# Returns:
|
|
# 0 on success (tests pass and validated)
|
|
# 2 on max iterations exceeded (calls exit 2)
|
|
run_test_loop() {
|
|
local loop_worktree="$1"
|
|
local loop_branch="$2"
|
|
|
|
local loop_complete=false
|
|
local test_iteration=0
|
|
|
|
log "Starting test loop after all tasks complete"
|
|
|
|
while [[ "$loop_complete" != "true" ]]; do
|
|
test_iteration=$((test_iteration + 1))
|
|
increment_test_iteration # Track iteration in status file
|
|
|
|
if (( test_iteration > MAX_TEST_ITERATIONS )); then
|
|
log_error "Test loop exceeded max iterations ($MAX_TEST_ITERATIONS)"
|
|
set_final_state "max_iterations_test"
|
|
exit 2
|
|
fi
|
|
|
|
log "Test loop iteration $test_iteration/$MAX_TEST_ITERATIONS"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# TEST EXECUTION → Issue comment
|
|
# -------------------------------------------------------------------------
|
|
local test_prompt="Run the test and lint suite in worktree $loop_worktree:
|
|
|
|
1. Run shellcheck on all modified .sh files: git -C $loop_worktree diff $BASE_BRANCH...HEAD --name-only -- '*.sh' | xargs shellcheck
|
|
2. Run actionlint on any modified workflow files: git -C $loop_worktree diff $BASE_BRANCH...HEAD --name-only -- '.github/workflows/*.yml' | xargs actionlint
|
|
3. If BATS tests exist, run them: cd $loop_worktree && bats .claude/scripts/implement-issue-test/
|
|
|
|
Report pass/fail, test counts, and any failures. Output a summary suitable for a GitHub comment."
|
|
|
|
local test_result
|
|
test_result=$(run_stage "test-loop-iter-$test_iteration" "$test_prompt" "implement-issue-test.json" "bats-test-validator")
|
|
|
|
local test_status test_summary
|
|
test_status=$(printf '%s' "$test_result" | jq -r '.result')
|
|
test_summary=$(printf '%s' "$test_result" | jq -r '.summary // "Tests completed"')
|
|
|
|
# Comment: Test results
|
|
local test_icon="✅"
|
|
[[ "$test_status" == "failed" ]] && test_icon="❌"
|
|
comment_issue "Test Loop: Tests ($test_iteration/$MAX_TEST_ITERATIONS)" "$test_icon **Result:** $test_status
|
|
|
|
$test_summary" "bats-test-validator"
|
|
|
|
if [[ "$test_status" == "failed" ]]; then
|
|
log "Tests failed. Getting failures and fixing..."
|
|
local failures
|
|
failures=$(printf '%s' "$test_result" | jq -c '.failures')
|
|
|
|
local fix_prompt="Fix test failures in worktree $loop_worktree on branch $loop_branch:
|
|
|
|
Failures:
|
|
$failures
|
|
|
|
Fix the issues and commit. Output a summary of fixes applied."
|
|
|
|
local fix_result
|
|
fix_result=$(run_stage "fix-tests-iter-$test_iteration" "$fix_prompt" "implement-issue-fix.json" "bash-script-craftsman")
|
|
|
|
local fix_summary
|
|
fix_summary=$(printf '%s' "$fix_result" | jq -r '.summary // "Fixes applied"')
|
|
|
|
# Comment: Fix results
|
|
comment_issue "Test Loop: Test Fix ($test_iteration/$MAX_TEST_ITERATIONS)" "$fix_summary" "bash-script-craftsman"
|
|
continue
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# TEST VALIDATION → Issue comment (only if tests passed)
|
|
# -------------------------------------------------------------------------
|
|
log "Tests passed. Running test validation for issue #$ISSUE_NUMBER..."
|
|
|
|
local validate_prompt="Validate test comprehensiveness and integrity for issue #$ISSUE_NUMBER in worktree $loop_worktree.
|
|
|
|
SCOPE: Only validate tests related to this issue's implementation. Get modified shell scripts with:
|
|
git -C $loop_worktree diff $BASE_BRANCH...HEAD --name-only -- '*.sh' '*.bats'
|
|
|
|
IMPORTANT SCOPE CONSTRAINTS:
|
|
- If NO testable shell scripts were modified (e.g., docs-only, CI config, metadata changes), output 'passed' immediately. Do NOT request new tests for non-script changes.
|
|
- Only validate tests for modified .sh files
|
|
- Do NOT request tests for README, CLAUDE.md, package metadata, or workflow YAML
|
|
|
|
For each modified script that warrants testing, identify the corresponding BATS test file and audit:
|
|
1. Run BATS tests if they exist: cd $loop_worktree && bats .claude/scripts/implement-issue-test/
|
|
2. Check for TODO/FIXME/incomplete tests
|
|
3. Check for hollow assertions (no assertions after run)
|
|
4. Verify edge cases and error conditions are tested
|
|
5. Check for common BATS gotchas (pipes with run, echo mangling, tee pollution)
|
|
|
|
Output:
|
|
- result: 'passed' if tests are comprehensive OR if no testable scripts were modified, 'failed' if issues found
|
|
- issues: array of issues found (if any)
|
|
- summary: suitable for a GitHub comment (note if validation was skipped due to no testable script changes)"
|
|
|
|
local validate_result
|
|
validate_result=$(run_stage "test-validate-iter-$test_iteration" "$validate_prompt" "implement-issue-review.json" "bats-test-validator")
|
|
|
|
local validate_status validate_summary
|
|
validate_status=$(printf '%s' "$validate_result" | jq -r '.result')
|
|
validate_summary=$(printf '%s' "$validate_result" | jq -r '.summary // "Validation completed"')
|
|
|
|
# Comment: Validation results
|
|
local validate_icon="✅"
|
|
[[ "$validate_status" == "changes_requested" || "$validate_status" == "failed" ]] && validate_icon="🔄"
|
|
comment_issue "Test Loop: Validation ($test_iteration/$MAX_TEST_ITERATIONS)" "$validate_icon **Result:** $validate_status
|
|
|
|
$validate_summary" "bats-test-validator"
|
|
|
|
if [[ "$validate_status" == "approved" || "$validate_status" == "passed" ]]; then
|
|
loop_complete=true
|
|
log "Test loop complete on iteration $test_iteration (tests passed and validated)"
|
|
else
|
|
log "Test validation found issues. Fixing..."
|
|
local validate_comments
|
|
validate_comments=$(printf '%s' "$validate_result" | jq -r '.comments // .summary // "Fix test quality issues"')
|
|
|
|
local fix_prompt="Address test quality issues in worktree $loop_worktree on branch $loop_branch:
|
|
|
|
$validate_comments
|
|
|
|
Fix the test quality issues (add missing assertions, remove TODOs, add edge case tests, etc.) and commit.
|
|
Output a summary of fixes applied."
|
|
|
|
local fix_result
|
|
fix_result=$(run_stage "fix-test-quality-iter-$test_iteration" "$fix_prompt" "implement-issue-fix.json" "bash-script-craftsman")
|
|
|
|
local fix_summary
|
|
fix_summary=$(printf '%s' "$fix_result" | jq -r '.summary // "Fixes applied"')
|
|
|
|
# Comment: Fix results
|
|
comment_issue "Test Loop: Validation Fix ($test_iteration/$MAX_TEST_ITERATIONS)" "$fix_summary" "bash-script-craftsman"
|
|
fi
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN FLOW
|
|
# =============================================================================
|
|
|
|
main() {
|
|
# Declare local variables used throughout main
|
|
local worktree branch tasks_json task_count completed_tasks
|
|
|
|
# -------------------------------------------------------------------------
|
|
# RESUME VS FRESH START INITIALIZATION
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]]; then
|
|
log "=========================================="
|
|
log "Implement Issue Orchestrator RESUMING"
|
|
log "=========================================="
|
|
log "Issue: #$ISSUE_NUMBER"
|
|
log "Branch: $BRANCH"
|
|
log "Worktree: $WORKTREE"
|
|
log "Resume stage: $RESUME_STAGE"
|
|
log "Resume task: ${RESUME_TASK:-none}"
|
|
log "Log dir: $LOG_BASE"
|
|
|
|
# Use values from resume state
|
|
worktree="$WORKTREE"
|
|
branch="$BRANCH"
|
|
tasks_json="$RESUME_TASKS_JSON"
|
|
|
|
# Update status to indicate resumption
|
|
jq --arg state "running" \
|
|
'.state = $state | .last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
|
|
# Comment on issue about resumption
|
|
comment_issue "Resuming Automated Processing" "Resuming processing of issue #$ISSUE_NUMBER.
|
|
|
|
**Resuming from stage:** \`$RESUME_STAGE\`
|
|
**Worktree:** \`$worktree\`
|
|
**Branch:** \`$branch\`
|
|
|
|
Log directory: \`$LOG_BASE\`"
|
|
|
|
else
|
|
log "=========================================="
|
|
log "Implement Issue Orchestrator Starting"
|
|
log "=========================================="
|
|
log "Issue: #$ISSUE_NUMBER"
|
|
log "Branch: $BASE_BRANCH"
|
|
log "Agent: ${AGENT:-default}"
|
|
log "Log dir: $LOG_BASE"
|
|
|
|
init_status
|
|
|
|
# -------------------------------------------------------------------------
|
|
# COMMENT #1: Starting automated processing
|
|
# -------------------------------------------------------------------------
|
|
comment_issue "Starting Automated Processing" "Processing issue #$ISSUE_NUMBER against branch \`$BASE_BRANCH\`.
|
|
|
|
**Stages:**
|
|
1. Setup worktree
|
|
2. Research context
|
|
3. Evaluate approach
|
|
4. Create implementation plan
|
|
5. Implement tasks (with per-task quality loop: simplify, review)
|
|
6. Test loop (run tests, fix failures)
|
|
7. Documentation
|
|
8. Create/update PR
|
|
9. PR review loop
|
|
|
|
Log directory: \`$LOG_BASE\`"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: SETUP (worktree creation)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "setup"; then
|
|
log "Skipping setup stage (already completed)"
|
|
else
|
|
set_stage_started "setup"
|
|
|
|
local setup_prompt="Set up worktree for issue #$ISSUE_NUMBER against branch $BASE_BRANCH.
|
|
|
|
You must:
|
|
1. Fetch the issue title and body from GitHub using: gh issue view $ISSUE_NUMBER --repo $REPO --json title,body
|
|
2. Check for existing PR (if re-implementation)
|
|
3. Create or checkout worktree using using-git-worktrees skill
|
|
|
|
Output the worktree path and branch name."
|
|
|
|
local setup_result
|
|
setup_result=$(run_stage "setup" "$setup_prompt" "implement-issue-setup.json" "$AGENT")
|
|
|
|
local setup_status
|
|
setup_status=$(printf '%s' "$setup_result" | jq -r '.status')
|
|
|
|
if [[ "$setup_status" != "success" ]]; then
|
|
local error
|
|
error=$(printf '%s' "$setup_result" | jq -r '.error // "unknown error"')
|
|
log_error "Setup failed: $error"
|
|
set_final_state "error"
|
|
exit 1
|
|
fi
|
|
|
|
worktree=$(printf '%s' "$setup_result" | jq -r '.worktree')
|
|
branch=$(printf '%s' "$setup_result" | jq -r '.branch')
|
|
|
|
set_worktree_info "$worktree" "$branch"
|
|
printf '%s\n' "$setup_result" > "$LOG_BASE/context/setup-output.json"
|
|
|
|
set_stage_completed "setup"
|
|
log "Setup complete. Worktree: $worktree, Branch: $branch"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# VERIFY BUILD TOOLS (needed for build.sh)
|
|
# -------------------------------------------------------------------------
|
|
log "Verifying build tools are available..."
|
|
local missing_tools=()
|
|
for tool in 7z wget wrestool convert dpkg-deb node; do
|
|
if ! command -v "$tool" &>/dev/null; then
|
|
missing_tools+=("$tool")
|
|
fi
|
|
done
|
|
if [[ ${#missing_tools[@]} -gt 0 ]]; then
|
|
log "Warning: Missing build tools: ${missing_tools[*]} (run setup-build-tools skill if needed)"
|
|
else
|
|
log "Build tools verified"
|
|
fi
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: RESEARCH (no comment per user request)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "research"; then
|
|
log "Skipping research stage (already completed)"
|
|
else
|
|
set_stage_started "research"
|
|
|
|
local research_prompt="Research context for issue #$ISSUE_NUMBER in worktree $worktree.
|
|
|
|
You must:
|
|
1. Read the issue details from GitHub
|
|
2. Explore related files and code structure
|
|
3. Identify dependencies and related components
|
|
4. Document relevant context for implementation
|
|
|
|
Output the research findings."
|
|
|
|
local research_result
|
|
research_result=$(run_stage "research" "$research_prompt" "implement-issue-research.json" "$AGENT")
|
|
|
|
printf '%s\n' "$research_result" > "$LOG_BASE/context/research-output.json"
|
|
|
|
set_stage_completed "research"
|
|
log "Research complete."
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: EVALUATE → Issue comment #3
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "evaluate"; then
|
|
log "Skipping evaluate stage (already completed)"
|
|
else
|
|
set_stage_started "evaluate"
|
|
|
|
local evaluate_prompt="Evaluate the best implementation approach for issue #$ISSUE_NUMBER.
|
|
|
|
Based on the research, determine:
|
|
1. The recommended approach
|
|
2. Rationale for this approach
|
|
3. Potential risks or concerns
|
|
4. Alternative approaches considered
|
|
|
|
Output your evaluation with a summary suitable for a GitHub comment."
|
|
|
|
local evaluate_result
|
|
evaluate_result=$(run_stage "evaluate" "$evaluate_prompt" "implement-issue-evaluate.json" "$AGENT")
|
|
|
|
local evaluate_status
|
|
evaluate_status=$(printf '%s' "$evaluate_result" | jq -r '.status')
|
|
|
|
# Comment #3: Evaluate findings (always post, format based on status)
|
|
local eval_summary approach rationale
|
|
eval_summary=$(printf '%s' "$evaluate_result" | jq -r '.summary')
|
|
approach=$(printf '%s' "$evaluate_result" | jq -r '.approach // ""')
|
|
rationale=$(printf '%s' "$evaluate_result" | jq -r '.rationale // ""')
|
|
|
|
if [[ "$evaluate_status" == "success" ]]; then
|
|
local eval_body="**Approach:** $approach
|
|
|
|
**Rationale:** $rationale
|
|
|
|
$eval_summary"
|
|
comment_issue "Evaluation: Best Path" "$eval_body" "${AGENT:-}"
|
|
else
|
|
local eval_error risks_text
|
|
eval_error=$(printf '%s' "$evaluate_result" | jq -r '.error // "Unknown error"')
|
|
risks_text=$(printf '%s' "$evaluate_result" | jq -r '.risks // [] | map("- " + .) | join("\n")')
|
|
|
|
local eval_body="⚠️ **Status:** Error - requires attention
|
|
|
|
**Approach:** $approach
|
|
|
|
**Rationale:** $rationale
|
|
|
|
**Error:** $eval_error"
|
|
|
|
if [[ -n "$risks_text" ]]; then
|
|
eval_body="$eval_body
|
|
|
|
**Risks:**
|
|
$risks_text"
|
|
fi
|
|
|
|
comment_issue "Evaluation: Issue Concerns" "$eval_body" "${AGENT:-}"
|
|
|
|
# Exit early - evaluation found blocking issues
|
|
printf '%s\n' "$evaluate_result" > "$LOG_BASE/context/evaluate-output.json"
|
|
set_stage_completed "evaluate"
|
|
log_error "Evaluation found blocking issues. Exiting."
|
|
set_final_state "blocked"
|
|
exit 1
|
|
fi
|
|
|
|
printf '%s\n' "$evaluate_result" > "$LOG_BASE/context/evaluate-output.json"
|
|
|
|
set_stage_completed "evaluate"
|
|
log "Evaluation complete."
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: PLAN → Issue comments #4 (plan) and #5 (task list)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "plan"; then
|
|
log "Skipping plan stage (already completed)"
|
|
# Load tasks from status file for implement stage
|
|
tasks_json=$(jq -c '.tasks' "$STATUS_FILE")
|
|
else
|
|
set_stage_started "plan"
|
|
|
|
local plan_prompt="Create an implementation plan for issue #$ISSUE_NUMBER in worktree $worktree on branch $branch.
|
|
|
|
Based on the evaluation, you must:
|
|
1. Write a detailed implementation plan
|
|
2. Break down into tasks with agent assignments
|
|
3. Each task should specify: id, description, and agent. Available agents:
|
|
- patch-engineer: minified JS patches in build.sh, sed/regex patterns
|
|
- packaging-specialist: .deb, .rpm, AppImage packaging, control files
|
|
- electron-linux-specialist: BrowserWindow, Wayland/X11, DBus tray, native theme
|
|
- ci-workflow-architect: GitHub Actions workflows, release automation
|
|
- bash-script-craftsman: shell scripts, launcher, build system
|
|
|
|
Output the plan path, task list, a summary of the plan, and a markdown-formatted task list for GitHub."
|
|
|
|
local plan_result
|
|
plan_result=$(run_stage "plan" "$plan_prompt" "implement-issue-plan.json" "$AGENT")
|
|
|
|
local plan_status
|
|
plan_status=$(printf '%s' "$plan_result" | jq -r '.status')
|
|
|
|
if [[ "$plan_status" != "success" ]]; then
|
|
local error
|
|
error=$(printf '%s' "$plan_result" | jq -r '.error // "unknown error"')
|
|
log_error "Plan failed: $error"
|
|
set_final_state "error"
|
|
exit 1
|
|
fi
|
|
|
|
local plan_summary task_list_md plan_path
|
|
tasks_json=$(printf '%s' "$plan_result" | jq -c '.tasks')
|
|
plan_summary=$(printf '%s' "$plan_result" | jq -r '.summary')
|
|
task_list_md=$(printf '%s' "$plan_result" | jq -r '.task_list_markdown')
|
|
plan_path=$(printf '%s' "$plan_result" | jq -r '.plan_path // empty')
|
|
|
|
set_tasks "$tasks_json"
|
|
printf '%s\n' "$plan_result" > "$LOG_BASE/context/plan-output.json"
|
|
printf '%s\n' "$tasks_json" > "$LOG_BASE/context/tasks.json"
|
|
|
|
# Comment #4: Implementation Plan (with full plan in collapsible)
|
|
local plan_body="$plan_summary"
|
|
if [[ -n "$plan_path" && -f "$worktree/$plan_path" ]]; then
|
|
local plan_content
|
|
plan_content=$(cat "$worktree/$plan_path")
|
|
plan_body="$plan_summary
|
|
|
|
<details>
|
|
<summary>Full Implementation Plan</summary>
|
|
|
|
\`\`\`markdown
|
|
$plan_content
|
|
\`\`\`
|
|
|
|
</details>"
|
|
fi
|
|
comment_issue "Implementation Plan" "$plan_body"
|
|
|
|
# Comment #5: Task List
|
|
comment_issue "Task List" "$task_list_md"
|
|
|
|
set_stage_completed "plan"
|
|
log "Plan complete. Tasks: $(printf '%s' "$tasks_json" | jq length)"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: IMPLEMENT (per-task loop)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "implement"; then
|
|
log "Skipping implement stage (already completed)"
|
|
else
|
|
set_stage_started "implement"
|
|
|
|
task_count=$(printf '%s' "$tasks_json" | jq length)
|
|
|
|
# In resume mode, count already completed tasks
|
|
if [[ -n "$RESUME_MODE" ]]; then
|
|
completed_tasks=$(get_completed_task_count)
|
|
log "Resuming implementation: $completed_tasks/$task_count tasks already completed"
|
|
else
|
|
completed_tasks=0
|
|
fi
|
|
|
|
for ((i=0; i<task_count; i++)); do
|
|
local task
|
|
task=$(printf '%s' "$tasks_json" | jq ".[$i]")
|
|
local task_id task_desc task_agent task_status
|
|
task_id=$(printf '%s' "$task" | jq -r '.id')
|
|
task_desc=$(printf '%s' "$task" | jq -r '.description')
|
|
task_agent=$(printf '%s' "$task" | jq -r '.agent')
|
|
|
|
# In resume mode, check if this task is already completed
|
|
if [[ -n "$RESUME_MODE" ]]; then
|
|
task_status=$(jq -r ".tasks[] | select(.id == $task_id) | .status" "$STATUS_FILE" 2>/dev/null)
|
|
if [[ "$task_status" == "completed" ]]; then
|
|
log "Skipping task $task_id (already completed)"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
log "Implementing task $task_id: $task_desc (agent: $task_agent)"
|
|
update_task "$task_id" "in_progress"
|
|
|
|
local review_attempts=0
|
|
local task_approved=false
|
|
|
|
while [[ "$task_approved" != "true" ]] && (( review_attempts < MAX_TASK_REVIEW_ATTEMPTS )); do
|
|
# Implement
|
|
local impl_prompt="Implement task $task_id in worktree $worktree on branch $branch:
|
|
|
|
$task_desc
|
|
|
|
Commit your changes with a descriptive message."
|
|
|
|
local impl_result
|
|
impl_result=$(run_stage "implement-task-$task_id" "$impl_prompt" "implement-issue-implement.json" "$task_agent")
|
|
|
|
local impl_status
|
|
impl_status=$(printf '%s' "$impl_result" | jq -r '.status')
|
|
|
|
if [[ "$impl_status" != "success" ]]; then
|
|
log_error "Task $task_id implementation failed"
|
|
update_task "$task_id" "failed" "$review_attempts"
|
|
break
|
|
fi
|
|
|
|
local commit_sha
|
|
commit_sha=$(printf '%s' "$impl_result" | jq -r '.commit')
|
|
|
|
# Review task
|
|
local review_prompt="Review task $task_id implementation (commit $commit_sha):
|
|
|
|
Task description: $task_desc
|
|
|
|
Did the implementation achieve the task's goal? Are there suggested improvements?"
|
|
|
|
local review_result
|
|
review_result=$(run_stage "task-review-$task_id-attempt-$((review_attempts+1))" "$review_prompt" "implement-issue-task-review.json" "spec-reviewer")
|
|
|
|
local review_verdict suggested_improvements
|
|
review_verdict=$(printf '%s' "$review_result" | jq -r '.result')
|
|
suggested_improvements=$(printf '%s' "$review_result" | jq -r '.suggested_improvements')
|
|
|
|
if [[ "$review_verdict" == "passed" && "$suggested_improvements" != "yes" ]]; then
|
|
task_approved=true
|
|
update_task "$task_id" "completed" "$((review_attempts+1))"
|
|
completed_tasks=$((completed_tasks+1))
|
|
|
|
# Comment #6: Task complete (with summary from implementing agent)
|
|
local impl_summary
|
|
impl_summary=$(printf '%s' "$impl_result" | jq -r '.summary // "Implementation completed"')
|
|
comment_issue "Task $task_id Complete" "**$task_desc**
|
|
|
|
**Commit:** \`$commit_sha\`
|
|
|
|
$impl_summary" "$task_agent"
|
|
|
|
# Run quality loop for this task
|
|
log "Running quality loop for task $task_id"
|
|
run_quality_loop "$worktree" "$branch" "task-$task_id"
|
|
|
|
else
|
|
review_attempts=$((review_attempts+1))
|
|
local review_comments
|
|
review_comments=$(printf '%s' "$review_result" | jq -r '.comments // "No comments"')
|
|
|
|
log "Task $task_id needs fixes (attempt $review_attempts/$MAX_TASK_REVIEW_ATTEMPTS)"
|
|
|
|
# Fix
|
|
local fix_prompt="Fix issues in task $task_id (commit $commit_sha) in worktree $worktree on branch $branch:
|
|
|
|
Review feedback:
|
|
$review_comments
|
|
|
|
Address the issues and commit."
|
|
|
|
run_stage "fix-task-$task_id-attempt-$review_attempts" "$fix_prompt" "implement-issue-fix.json" "$task_agent"
|
|
fi
|
|
done
|
|
|
|
# Update progress
|
|
jq --arg progress "$completed_tasks/$task_count" \
|
|
'.stages.implement.task_progress = $progress | .last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
done
|
|
|
|
set_stage_completed "implement"
|
|
set_stage_completed "quality_loop" # Quality loop ran per-task
|
|
log "Implementation complete. $completed_tasks/$task_count tasks completed (with per-task quality loops)."
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: TEST LOOP (after all tasks complete)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "test_loop"; then
|
|
log "Skipping test_loop stage (already completed)"
|
|
else
|
|
set_stage_started "test_loop"
|
|
log "Running test loop after all tasks complete..."
|
|
|
|
run_test_loop "$worktree" "$branch"
|
|
|
|
set_stage_completed "test_loop"
|
|
log "Test loop complete."
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: LINT (final lint check before PR)
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "docs"; then
|
|
log "Skipping lint stage (already completed)"
|
|
else
|
|
set_stage_started "docs"
|
|
|
|
local lint_prompt="Run final lint checks on all modified files in worktree $worktree on branch $branch.
|
|
|
|
Get modified files with: git -C $worktree diff $BASE_BRANCH...HEAD --name-only
|
|
|
|
For .sh files: run shellcheck
|
|
For .github/workflows/*.yml files: run actionlint
|
|
|
|
If any issues are found, fix them and commit with message: style(issue-$ISSUE_NUMBER): fix lint issues
|
|
|
|
Output a summary of any lint fixes applied."
|
|
|
|
run_stage "lint" "$lint_prompt" "implement-issue-implement.json" "bash-script-craftsman"
|
|
|
|
set_stage_completed "docs"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: PR
|
|
# -------------------------------------------------------------------------
|
|
local pr_number
|
|
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "pr"; then
|
|
log "Skipping PR creation stage (already completed)"
|
|
# Load PR number from status
|
|
pr_number=$(jq -r '.stages.pr.pr_number // empty' "$STATUS_FILE")
|
|
if [[ -z "$pr_number" || "$pr_number" == "null" ]]; then
|
|
log_error "PR stage marked complete but no PR number found in status"
|
|
set_final_state "error"
|
|
exit 1
|
|
fi
|
|
log "Using existing PR #$pr_number"
|
|
else
|
|
set_stage_started "pr"
|
|
|
|
local pr_prompt="Create or update PR for issue #$ISSUE_NUMBER in worktree $worktree.
|
|
|
|
If no PR exists, create one:
|
|
gh pr create --base $BASE_BRANCH --title 'feat(issue-$ISSUE_NUMBER): <description>'
|
|
|
|
If PR exists, push and comment.
|
|
|
|
Include 'Closes #$ISSUE_NUMBER' in the body."
|
|
|
|
local pr_result
|
|
pr_result=$(run_stage "pr" "$pr_prompt" "implement-issue-pr.json")
|
|
|
|
local pr_status
|
|
pr_status=$(printf '%s' "$pr_result" | jq -r '.status')
|
|
pr_number=$(printf '%s' "$pr_result" | jq -r '.pr_number')
|
|
|
|
if [[ "$pr_status" != "success" ]]; then
|
|
log_error "PR creation failed"
|
|
set_final_state "error"
|
|
exit 1
|
|
fi
|
|
|
|
log "PR #$pr_number created/updated"
|
|
|
|
# Store PR info in status
|
|
jq --argjson pr "$pr_number" \
|
|
'.stages.pr.pr_number = $pr | .last_update = (now | todate)' \
|
|
"$STATUS_FILE" > "${STATUS_FILE}.tmp" && mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
|
sync_status_to_log
|
|
set_stage_completed "pr"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: PR REVIEW LOOP
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "pr_review"; then
|
|
log "Skipping pr_review stage (already completed)"
|
|
else
|
|
set_stage_started "pr_review"
|
|
|
|
local pr_approved=false
|
|
|
|
while [[ "$pr_approved" != "true" ]]; do
|
|
increment_pr_review_iteration
|
|
local pr_iteration
|
|
pr_iteration=$(jq -r '.pr_review_iterations' "$STATUS_FILE")
|
|
|
|
if (( pr_iteration > MAX_PR_REVIEW_ITERATIONS )); then
|
|
log_error "PR review loop exceeded max iterations ($MAX_PR_REVIEW_ITERATIONS)"
|
|
set_final_state "max_iterations_pr_review"
|
|
exit 2
|
|
fi
|
|
|
|
log "PR review iteration $pr_iteration"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# SPEC REVIEW → PR comment #11
|
|
# -------------------------------------------------------------------------
|
|
local spec_prompt="Verify PR #$pr_number achieves the goals of issue #$ISSUE_NUMBER.
|
|
|
|
Check goal achievement, not code quality. Flag scope creep.
|
|
Output a summary suitable for a GitHub comment."
|
|
|
|
local spec_result
|
|
spec_result=$(run_stage "spec-review-iter-$pr_iteration" "$spec_prompt" "implement-issue-review.json" "spec-reviewer")
|
|
|
|
local spec_verdict spec_summary
|
|
spec_verdict=$(printf '%s' "$spec_result" | jq -r '.result')
|
|
spec_summary=$(printf '%s' "$spec_result" | jq -r '.summary // "Review completed"')
|
|
|
|
# Comment #11: PR Spec Review Result
|
|
local spec_icon="✅"
|
|
[[ "$spec_verdict" == "changes_requested" ]] && spec_icon="🔄"
|
|
comment_pr "$pr_number" "Spec Review (Iteration $pr_iteration)" "$spec_icon **Result:** $spec_verdict
|
|
|
|
$spec_summary" "spec-reviewer"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CODE REVIEW → PR comment #12
|
|
# -------------------------------------------------------------------------
|
|
local code_prompt="Review code quality of PR #$pr_number against base $BASE_BRANCH.
|
|
|
|
Check patterns, standards, security. Approve or request changes.
|
|
Output a summary suitable for a GitHub comment."
|
|
|
|
local code_result
|
|
code_result=$(run_stage "code-review-iter-$pr_iteration" "$code_prompt" "implement-issue-review.json" "code-reviewer")
|
|
|
|
local code_verdict code_summary
|
|
code_verdict=$(printf '%s' "$code_result" | jq -r '.result')
|
|
code_summary=$(printf '%s' "$code_result" | jq -r '.summary // "Review completed"')
|
|
|
|
# Comment #12: PR Code Review Result
|
|
local code_icon="✅"
|
|
[[ "$code_verdict" == "changes_requested" ]] && code_icon="🔄"
|
|
comment_pr "$pr_number" "Code Review (Iteration $pr_iteration)" "$code_icon **Result:** $code_verdict
|
|
|
|
$code_summary" "code-reviewer"
|
|
|
|
if [[ "$spec_verdict" == "approved" && "$code_verdict" == "approved" ]]; then
|
|
pr_approved=true
|
|
log "PR approved on iteration $pr_iteration"
|
|
else
|
|
log "PR review requested changes. Fixing and re-running quality loop."
|
|
|
|
# Collect feedback
|
|
local spec_comments code_comments
|
|
spec_comments=$(printf '%s' "$spec_result" | jq -r '.comments // ""')
|
|
code_comments=$(printf '%s' "$code_result" | jq -r '.comments // ""')
|
|
|
|
local fix_prompt="Address PR review feedback in worktree $worktree on branch $branch:
|
|
|
|
Spec review:
|
|
$spec_comments
|
|
|
|
Code review:
|
|
$code_comments
|
|
|
|
Fix the issues and commit. Output a summary of fixes applied."
|
|
|
|
local fix_result
|
|
fix_result=$(run_stage "fix-pr-review-iter-$pr_iteration" "$fix_prompt" "implement-issue-fix.json")
|
|
|
|
local fix_summary
|
|
fix_summary=$(printf '%s' "$fix_result" | jq -r '.summary // "Fixes applied"')
|
|
|
|
# Comment #13: PR Fix Result
|
|
comment_pr "$pr_number" "PR Review Fix (Iteration $pr_iteration)" "$fix_summary"
|
|
|
|
# Re-run quality loop after PR review fixes
|
|
log "Re-running quality loop after PR review fixes..."
|
|
run_quality_loop "$worktree" "$branch" "pr-fix"
|
|
|
|
# Push updates
|
|
log "Pushing updates to PR..."
|
|
git -C "$worktree" push origin "$branch" 2>/dev/null || log "Warning: Could not push to origin"
|
|
fi
|
|
done
|
|
|
|
set_stage_completed "pr_review"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# STAGE: COMPLETE → PR comment #14
|
|
# -------------------------------------------------------------------------
|
|
if [[ -n "$RESUME_MODE" ]] && is_stage_completed "complete"; then
|
|
log "Workflow already completed"
|
|
else
|
|
set_stage_started "complete"
|
|
|
|
local complete_prompt="Generate a completion summary for PR #$pr_number implementing issue #$ISSUE_NUMBER.
|
|
|
|
Include:
|
|
- Issue and branch info
|
|
- Decisions made during implementation
|
|
- Reviews passed
|
|
- Final status
|
|
|
|
Output a summary suitable for a GitHub PR comment."
|
|
|
|
local complete_result
|
|
complete_result=$(run_stage "complete" "$complete_prompt" "implement-issue-complete.json")
|
|
|
|
local complete_summary
|
|
complete_summary=$(printf '%s' "$complete_result" | jq -r '.summary // "Implementation completed successfully"')
|
|
|
|
# Comment #14: Implementation complete
|
|
comment_pr "$pr_number" "Implementation Complete" "Issue #$ISSUE_NUMBER has been implemented!
|
|
|
|
**Branch:** \`$branch\`
|
|
**PR:** #$pr_number
|
|
|
|
$complete_summary
|
|
|
|
---
|
|
*This PR is ready for human review and merge.*"
|
|
|
|
set_stage_completed "complete"
|
|
set_final_state "completed"
|
|
fi
|
|
|
|
# Copy final status to log dir
|
|
cp "$STATUS_FILE" "$LOG_BASE/status.json"
|
|
|
|
log "=========================================="
|
|
log "Implement Issue Complete"
|
|
log "=========================================="
|
|
log "Issue: #$ISSUE_NUMBER"
|
|
log "PR: #$pr_number"
|
|
log "Branch: $branch"
|
|
log "Status: completed"
|
|
|
|
exit 0
|
|
}
|
|
|
|
# Run main
|
|
main "$@"
|