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 <noreply@swe-agent.ai>
This commit is contained in:
cexll
2026-01-25 18:04:47 +08:00
parent a9c1e8178f
commit 2856055e2e
4 changed files with 93 additions and 62 deletions

View File

@@ -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 <task>`, initializes `.claude/do.local.md` with:
When triggered via `/do <task>`, 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 `<promise>DO_COMPLETE</promise>` when finished

View File

@@ -16,7 +16,7 @@ When triggered via `/do <task>`, **first** initialize the loop state:
"${SKILL_DIR}/scripts/setup-do.sh" "<task description>"
```
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: <next phase number>
phase_name: "<next 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

View File

@@ -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,49 +56,55 @@ 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
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)"
local current_phase=1
if [[ "${current_phase_raw:-}" =~ ^[0-9]+$ ]]; then
current_phase="$current_phase_raw"
fi
fi
max_phases=7
if [[ "${max_phases_raw:-}" =~ ^[0-9]+$ ]]; then
local max_phases=7
if [[ "${max_phases_raw:-}" =~ ^[0-9]+$ ]]; then
max_phases="$max_phases_raw"
fi
fi
if [ -z "${phase_name:-}" ]; then
if [ -z "${phase_name:-}" ]; then
phase_name="$(phase_name_for "$current_phase")"
fi
fi
if [ -z "${completion_promise:-}" ]; then
if [ -z "${completion_promise:-}" ]; then
completion_promise="<promise>DO_COMPLETE</promise>"
fi
fi
phases_done=0
if [ "$current_phase" -ge "$max_phases" ]; then
local phases_done=0
if [ "$current_phase" -ge "$max_phases" ]; then
phases_done=1
fi
fi
promise_met=0
if [ -n "$completion_promise" ]; then
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 }
@@ -107,17 +117,35 @@ if [ -n "$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

View File

@@ -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"