From 2856055e2e1450b595608f42473de9f92a1845af Mon Sep 17 00:00:00 2001 From: cexll Date: Sun, 25 Jan 2026 18:04:47 +0800 Subject: [PATCH] fix: support concurrent tasks with unique state files - Generate unique task_id (timestamp-pid-random) for each /do invocation - State files now use pattern: do.{task_id}.local.md - Stop hook scans all state files, aggregates blocking reasons - Auto-cleanup completed task state files Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai --- skills/do/README.md | 6 +- skills/do/SKILL.md | 6 +- skills/do/hooks/stop-hook.sh | 138 ++++++++++++++++++++-------------- skills/do/scripts/setup-do.sh | 5 +- 4 files changed, 93 insertions(+), 62 deletions(-) diff --git a/skills/do/README.md b/skills/do/README.md index 45043e2..24773eb 100644 --- a/skills/do/README.md +++ b/skills/do/README.md @@ -54,7 +54,7 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri 3. **Phase 5 requires approval** - stop after Phase 4 if not approved 4. **Pass complete context forward** - every agent gets the Context Pack 5. **Parallel-first** - run independent tasks via `codeagent-wrapper --parallel` -6. **Update state after each phase** - keep `.claude/do.local.md` current +6. **Update state after each phase** - keep `.claude/do.{task_id}.local.md` current ## Context Pack Template @@ -80,7 +80,7 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri ## Loop State Management -When triggered via `/do `, initializes `.claude/do.local.md` with: +When triggered via `/do `, initializes `.claude/do.{task_id}.local.md` with: - `active: true` - `current_phase: 1` - `max_phases: 7` @@ -102,7 +102,7 @@ To abort early, set `active: false` in the state file. ## Stop Hook A Stop hook is registered after installation: -1. Creates `.claude/do.local.md` state file +1. Creates `.claude/do.{task_id}.local.md` state file 2. Updates `current_phase` after each phase 3. Stop hook checks state, blocks exit if incomplete 4. Outputs `DO_COMPLETE` when finished diff --git a/skills/do/SKILL.md b/skills/do/SKILL.md index 5ec5782..23b9865 100644 --- a/skills/do/SKILL.md +++ b/skills/do/SKILL.md @@ -16,7 +16,7 @@ When triggered via `/do `, **first** initialize the loop state: "${SKILL_DIR}/scripts/setup-do.sh" "" ``` -This creates `.claude/do.local.md` with: +This creates `.claude/do.{task_id}.local.md` with: - `active: true` - `current_phase: 1` - `max_phases: 7` @@ -24,7 +24,7 @@ This creates `.claude/do.local.md` with: ## Loop State Management -After each phase, update `.claude/do.local.md` frontmatter: +After each phase, update `.claude/do.{task_id}.local.md` frontmatter: ```yaml current_phase: phase_name: "" @@ -44,7 +44,7 @@ To abort early, set `active: false` in the state file. 3. **Phase 5 (Implementation) requires explicit approval.** Stop after Phase 4 if not approved. 4. **Pass complete context forward.** Every agent invocation includes the Context Pack. 5. **Parallel-first.** Run independent tasks via `codeagent-wrapper --parallel`. -6. **Update state after each phase.** Keep `.claude/do.local.md` current. +6. **Update state after each phase.** Keep `.claude/do.{task_id}.local.md` current. ## Agents diff --git a/skills/do/hooks/stop-hook.sh b/skills/do/hooks/stop-hook.sh index 0623f53..d506fba 100755 --- a/skills/do/hooks/stop-hook.sh +++ b/skills/do/hooks/stop-hook.sh @@ -26,9 +26,13 @@ json_escape() { } project_dir="${CLAUDE_PROJECT_DIR:-$PWD}" -state_file="${project_dir}/.claude/do.local.md" +state_dir="${project_dir}/.claude" -if [ ! -f "$state_file" ]; then +shopt -s nullglob +state_files=("${state_dir}"/do.*.local.md) +shopt -u nullglob + +if [ ${#state_files[@]} -eq 0 ]; then exit 0 fi @@ -38,7 +42,7 @@ if [ ! -t 0 ]; then fi frontmatter_get() { - local key="$1" + local file="$1" key="$2" awk -v k="$key" ' BEGIN { in_fm=0 } NR==1 && $0=="---" { in_fm=1; next } @@ -52,72 +56,96 @@ frontmatter_get() { exit } } - ' "$state_file" + ' "$file" } -active_raw="$(frontmatter_get active || true)" -active_lc="$(printf "%s" "$active_raw" | tr '[:upper:]' '[:lower:]')" -case "$active_lc" in -true|1|yes|on) ;; -*) exit 0 ;; -esac +check_state_file() { + local state_file="$1" -current_phase_raw="$(frontmatter_get current_phase || true)" -max_phases_raw="$(frontmatter_get max_phases || true)" -phase_name="$(frontmatter_get phase_name || true)" -completion_promise="$(frontmatter_get completion_promise || true)" + local active_raw active_lc + active_raw="$(frontmatter_get "$state_file" active || true)" + active_lc="$(printf "%s" "$active_raw" | tr '[:upper:]' '[:lower:]')" + case "$active_lc" in + true|1|yes|on) ;; + *) return 0 ;; + esac -current_phase=1 -if [[ "${current_phase_raw:-}" =~ ^[0-9]+$ ]]; then - current_phase="$current_phase_raw" -fi + local current_phase_raw max_phases_raw phase_name completion_promise + current_phase_raw="$(frontmatter_get "$state_file" current_phase || true)" + max_phases_raw="$(frontmatter_get "$state_file" max_phases || true)" + phase_name="$(frontmatter_get "$state_file" phase_name || true)" + completion_promise="$(frontmatter_get "$state_file" completion_promise || true)" -max_phases=7 -if [[ "${max_phases_raw:-}" =~ ^[0-9]+$ ]]; then - max_phases="$max_phases_raw" -fi + local current_phase=1 + if [[ "${current_phase_raw:-}" =~ ^[0-9]+$ ]]; then + current_phase="$current_phase_raw" + fi -if [ -z "${phase_name:-}" ]; then - phase_name="$(phase_name_for "$current_phase")" -fi + local max_phases=7 + if [[ "${max_phases_raw:-}" =~ ^[0-9]+$ ]]; then + max_phases="$max_phases_raw" + fi -if [ -z "${completion_promise:-}" ]; then - completion_promise="DO_COMPLETE" -fi + if [ -z "${phase_name:-}" ]; then + phase_name="$(phase_name_for "$current_phase")" + fi -phases_done=0 -if [ "$current_phase" -ge "$max_phases" ]; then - phases_done=1 -fi + if [ -z "${completion_promise:-}" ]; then + completion_promise="DO_COMPLETE" + fi -promise_met=0 -if [ -n "$completion_promise" ]; then - if [ -n "$stdin_payload" ] && printf "%s" "$stdin_payload" | grep -Fq -- "$completion_promise"; then - promise_met=1 - else - body="$( - awk ' - BEGIN { in_fm=0; body=0 } - NR==1 && $0=="---" { in_fm=1; next } - in_fm==1 && $0=="---" { body=1; in_fm=0; next } - body==1 { print } - ' "$state_file" - )" - if [ -n "$body" ] && printf "%s" "$body" | grep -Fq -- "$completion_promise"; then + local phases_done=0 + if [ "$current_phase" -ge "$max_phases" ]; then + phases_done=1 + fi + + local promise_met=0 + if [ -n "$completion_promise" ]; then + if [ -n "$stdin_payload" ] && printf "%s" "$stdin_payload" | grep -Fq -- "$completion_promise"; then promise_met=1 + else + local body + body="$( + awk ' + BEGIN { in_fm=0; body=0 } + NR==1 && $0=="---" { in_fm=1; next } + in_fm==1 && $0=="---" { body=1; in_fm=0; next } + body==1 { print } + ' "$state_file" + )" + if [ -n "$body" ] && printf "%s" "$body" | grep -Fq -- "$completion_promise"; then + promise_met=1 + fi fi fi -fi -if [ "$phases_done" -eq 1 ] && [ "$promise_met" -eq 1 ]; then + if [ "$phases_done" -eq 1 ] && [ "$promise_met" -eq 1 ]; then + rm -f "$state_file" + return 0 + fi + + local reason + if [ "$phases_done" -eq 0 ]; then + reason="do loop incomplete: current phase ${current_phase}/${max_phases} (${phase_name}). Continue with remaining phases; update ${state_file} current_phase/phase_name after each phase. Include completion_promise in final output when done: ${completion_promise}. To exit early, set active to false." + else + reason="do reached final phase (current_phase=${current_phase} / max_phases=${max_phases}, phase_name=${phase_name}), but completion_promise not detected: ${completion_promise}. Please include this marker in your final output (or write it to ${state_file} body), then finish; to force exit, set active to false." + fi + + printf "%s" "$reason" +} + +blocking_reasons=() +for state_file in "${state_files[@]}"; do + reason="$(check_state_file "$state_file")" + if [ -n "$reason" ]; then + blocking_reasons+=("$reason") + fi +done + +if [ ${#blocking_reasons[@]} -eq 0 ]; then exit 0 fi -if [ "$phases_done" -eq 0 ]; then - reason="do loop incomplete: current phase ${current_phase}/${max_phases} (${phase_name}). Continue with remaining phases; update ${state_file} current_phase/phase_name after each phase. Include completion_promise in final output when done: ${completion_promise}. To exit early, set active to false." -else - reason="do reached final phase (current_phase=${current_phase} / max_phases=${max_phases}, phase_name=${phase_name}), but completion_promise not detected: ${completion_promise}. Please include this marker in your final output (or write it to ${state_file} body), then finish; to force exit, set active to false." -fi - -printf '{"decision":"block","reason":"%s"}\n' "$(json_escape "$reason")" +combined_reason="${blocking_reasons[*]}" +printf '{"decision":"block","reason":"%s"}\n' "$(json_escape "$combined_reason")" exit 0 diff --git a/skills/do/scripts/setup-do.sh b/skills/do/scripts/setup-do.sh index b09cc0f..64f17b4 100755 --- a/skills/do/scripts/setup-do.sh +++ b/skills/do/scripts/setup-do.sh @@ -81,7 +81,9 @@ fi project_dir="${CLAUDE_PROJECT_DIR:-$PWD}" state_dir="${project_dir}/.claude" -state_file="${state_dir}/do.local.md" + +task_id="$(date +%s)-$$-$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')" +state_file="${state_dir}/do.${task_id}.local.md" mkdir -p "$state_dir" @@ -107,5 +109,6 @@ $prompt EOF echo "Initialized: $state_file" +echo "task_id: $task_id" echo "phase: 1/$max_phases ($phase_name)" echo "completion_promise: $completion_promise"