From 5695279e0d8a7085ba0adbbf7bd57c7a95fbd75b Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Wed, 11 Feb 2026 03:01:48 +0000 Subject: [PATCH] fix: use resolve_ai_cli() and standard dispatch pattern for decomposition worker (t274) - Replace hardcoded "Claude" CLI with resolve_ai_cli() for opencode/claude support - Add PID-based throttle to prevent duplicate decomposition workers per pulse - Use nohup/setsid dispatch + wrapper scripts matching cmd_dispatch() pattern - Fix metadata concatenation to handle empty metadata (avoids leading comma) - Improve subtask detection regex to include cancelled [-] checkbox state - Add explicit PLANS.md edit restriction to decomposition worker prompt --- .agents/scripts/supervisor-helper.sh | 111 +++++++++++++++++++++------ 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/.agents/scripts/supervisor-helper.sh b/.agents/scripts/supervisor-helper.sh index 814f1c092..5d603f9ea 100755 --- a/.agents/scripts/supervisor-helper.sh +++ b/.agents/scripts/supervisor-helper.sh @@ -12543,6 +12543,26 @@ dispatch_decomposition_worker() { return 1 fi + # Check for already-running decomposition worker (throttle) + local pid_file="$SUPERVISOR_DIR/pids/${task_id}-decompose.pid" + if [[ -f "$pid_file" ]]; then + local existing_pid + existing_pid=$(cat "$pid_file" 2>/dev/null || true) + if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then + log_info " $task_id: decomposition worker already running (PID: $existing_pid)" + return 0 + fi + # Stale PID file — clean up + rm -f "$pid_file" + fi + + # Resolve AI CLI (uses opencode with claude fallback) + local ai_cli + ai_cli=$(resolve_ai_cli 2>/dev/null) || { + log_error " $task_id: no AI CLI available for decomposition worker" + return 1 + } + # Build decomposition prompt with explicit TODO.md edit permission local decomposition_prompt read -r -d '' decomposition_prompt </dev/null; then - ( - cd "$repo" || exit 1 - echo "$decomposition_prompt" | Claude > "$worker_log" 2>&1 - local exit_code=$? - echo "Worker exit code: $exit_code" >> "$worker_log" - exit "$exit_code" - ) & - local worker_pid=$! - log_success " Decomposition worker dispatched (PID: $worker_pid)" - - # Update task metadata with worker PID - local escaped_id - escaped_id=$(sql_escape "$task_id") - db "$SUPERVISOR_DB" "UPDATE tasks SET metadata = metadata || ',decomposition_worker_pid=$worker_pid' WHERE id = '$escaped_id';" 2>/dev/null || true + # Build dispatch script for the decomposition worker + local dispatch_script="${SUPERVISOR_DIR}/pids/${task_id}-decompose-dispatch.sh" + { + echo '#!/usr/bin/env bash' + echo "echo 'DECOMPOSE_WORKER_STARTED task_id=${task_id} pid=\$\$ timestamp='\$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "cd '${repo}' || { echo 'DECOMPOSE_FAILED: cd to repo failed: ${repo}'; exit 1; }" + } > "$dispatch_script" + + # Append CLI-specific invocation + if [[ "$ai_cli" == "opencode" ]]; then + { + printf 'exec opencode run --format json --title %q %q\n' \ + "decompose-${task_id}" "$decomposition_prompt" + } >> "$dispatch_script" else - log_error " Claude CLI not found — cannot dispatch decomposition worker" - return 1 + { + printf 'exec claude -p %q --output-format json\n' \ + "$decomposition_prompt" + } >> "$dispatch_script" + fi + chmod +x "$dispatch_script" + + # Wrapper script with cleanup handlers (matches cmd_dispatch pattern) + local wrapper_script="${SUPERVISOR_DIR}/pids/${task_id}-decompose-wrapper.sh" + { + echo '#!/usr/bin/env bash' + echo 'cleanup_children() {' + echo ' local children' + echo ' children=$(pgrep -P $$ 2>/dev/null || true)' + echo ' if [[ -n "$children" ]]; then' + echo ' kill -TERM $children 2>/dev/null || true' + echo ' sleep 0.5' + echo ' kill -9 $children 2>/dev/null || true' + echo ' fi' + echo '}' + echo 'trap cleanup_children EXIT INT TERM' + echo "'${dispatch_script}' >> '${worker_log}' 2>&1" + echo "rc=\$?" + echo "echo \"EXIT:\${rc}\" >> '${worker_log}'" + echo "if [ \$rc -ne 0 ]; then" + echo " echo \"DECOMPOSE_WORKER_ERROR: dispatch exited with code \${rc}\" >> '${worker_log}'" + echo "fi" + } > "$wrapper_script" + chmod +x "$wrapper_script" + + # Launch background process with nohup + setsid (matches cmd_dispatch pattern) + if command -v setsid &>/dev/null; then + nohup setsid bash "${wrapper_script}" &>/dev/null & + else + nohup bash "${wrapper_script}" &>/dev/null & fi + disown 2>/dev/null || true + local worker_pid=$! + + # Store PID for throttle check and monitoring + echo "$worker_pid" > "$pid_file" + log_success " Decomposition worker dispatched (PID: $worker_pid, CLI: $ai_cli)" + + # Update task metadata with worker PID + local escaped_id + escaped_id=$(sql_escape "$task_id") + db "$SUPERVISOR_DB" "UPDATE tasks SET metadata = CASE WHEN metadata IS NULL OR metadata = '' THEN 'decomposition_worker_pid=$worker_pid' ELSE metadata || ',decomposition_worker_pid=$worker_pid' END WHERE id = '$escaped_id';" 2>/dev/null || true return 0 } @@ -12795,8 +12859,9 @@ cmd_auto_pickup() { fi # Check if task already has subtasks (e.g., t001.1, t001.2) + # Matches any checkbox state: [ ], [x], [X], [-] local has_subtasks - has_subtasks=$(grep -E "^[[:space:]]+-[[:space:]]\[[[:space:]xX]\][[:space:]]${task_id}\.[0-9]+" "$todo_file" 2>/dev/null || true) + has_subtasks=$(grep -E "^[[:space:]]+-[[:space:]]\[[ xX-]\][[:space:]]${task_id}\.[0-9]+" "$todo_file" 2>/dev/null || true) if [[ -n "$has_subtasks" ]]; then log_info " $task_id: already has subtasks — skipping auto-decomposition" continue