336 lines
11 KiB
Bash
Executable File
336 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# wm-dispatch: Execute action plan tasks via agent sessions.
|
|
#
|
|
# Usage:
|
|
# wm-dispatch <task-id> # dispatch one task (foreground)
|
|
# wm-dispatch <task-id> <task-id>... # dispatch multiple in parallel
|
|
# wm-dispatch --wave <N> # dispatch all unblocked tasks in wave N
|
|
# wm-dispatch --next [N] # dispatch next N unblocked tasks (default 1)
|
|
#
|
|
# Options:
|
|
# --dry-run Print the prompt instead of dispatching
|
|
# --backend B Override backend (default: openrouter)
|
|
# --model M Override model (default: qwen/qwen3-coder)
|
|
# --keep Keep session on success (failures always kept)
|
|
# --bg Submit and return immediately (use wm-collect to harvest)
|
|
#
|
|
# See also: wm-gen (ad-hoc tasks from templates)
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
TOOLS="$ROOT/tools/wm-port"
|
|
MOUNT="${OLLIE:-$HOME/mnt/ollie}"
|
|
DONE_FILE="$TOOLS/done.txt"
|
|
LOG_DIR="$TOOLS/logs"
|
|
|
|
source "$TOOLS/task-map.sh"
|
|
|
|
# Defaults
|
|
backend="openrouter"
|
|
model="qwen/qwen3-coder"
|
|
dry_run=0
|
|
keep=0
|
|
bg=0
|
|
wave=""
|
|
next=0
|
|
task_ids=()
|
|
|
|
die() { printf 'wm-dispatch: %s\n' "$*" >&2; exit 1; }
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run) dry_run=1 ;;
|
|
--backend) backend="$2"; shift ;;
|
|
--model) model="$2"; shift ;;
|
|
--keep) keep=1 ;;
|
|
--bg) bg=1 ;;
|
|
--wave) wave="$2"; shift ;;
|
|
--next) next="${2:-1}"; shift ;;
|
|
--help|-h) sed -n '2,/^$/{ s/^# //; s/^#//; p; }' "$0"; exit 0 ;;
|
|
-*) die "unknown flag: $1" ;;
|
|
*) task_ids+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
mkdir -p "$LOG_DIR"
|
|
touch "$DONE_FILE"
|
|
|
|
# Resolve task list from --wave or --next
|
|
if [ -n "$wave" ]; then
|
|
while IFS= read -r tid; do
|
|
[ "${TASK_WAVE[$tid]:-}" = "$wave" ] || continue
|
|
task_is_done "$tid" "$DONE_FILE" && continue
|
|
task_is_unblocked "$tid" "$DONE_FILE" || continue
|
|
task_ids+=("$tid")
|
|
done < <(task_all_ids)
|
|
[ ${#task_ids[@]} -eq 0 ] && die "no unblocked tasks in wave $wave"
|
|
elif [ "$next" -gt 0 ]; then
|
|
count=0
|
|
while IFS= read -r tid; do
|
|
task_is_done "$tid" "$DONE_FILE" && continue
|
|
task_is_unblocked "$tid" "$DONE_FILE" || continue
|
|
task_ids+=("$tid")
|
|
(( ++count >= next )) && break
|
|
done < <(task_all_ids)
|
|
[ ${#task_ids[@]} -eq 0 ] && die "no unblocked tasks remaining"
|
|
fi
|
|
|
|
[ ${#task_ids[@]} -eq 0 ] && die "no task IDs specified. Use: wm-dispatch <task-id> or --wave N or --next N"
|
|
|
|
# Validate all task IDs
|
|
for tid in "${task_ids[@]}"; do
|
|
[ -n "${TASK_TEMPLATE[$tid]:-}" ] || die "unknown task ID: $tid"
|
|
task_is_done "$tid" "$DONE_FILE" && die "task $tid is already done"
|
|
task_is_unblocked "$tid" "$DONE_FILE" || {
|
|
blockers="${TASK_BLOCKERS[$tid]}"
|
|
pending=""
|
|
for dep in $blockers; do
|
|
task_is_done "$dep" "$DONE_FILE" || pending="$pending $dep"
|
|
done
|
|
die "task $tid is blocked by:$pending"
|
|
}
|
|
done
|
|
|
|
# ── Extract task block from ACTION_PLAN.md ──
|
|
# Finds the section for a given task ID (everything from its header to the next
|
|
# header of equal or higher level, or next --- separator).
|
|
extract_task_block() {
|
|
local tid="$1"
|
|
local plan="$ROOT/ACTION_PLAN.md"
|
|
|
|
awk -v tid="$tid" '
|
|
BEGIN { found=0; level=0 }
|
|
/^#{1,6} / {
|
|
if (found) {
|
|
match($0, /^#+/)
|
|
if (RLENGTH <= level) exit
|
|
}
|
|
if (index($0, tid) > 0) {
|
|
match($0, /^#+/)
|
|
level = RLENGTH
|
|
found = 1
|
|
}
|
|
}
|
|
/^---/ { if (found) exit }
|
|
found { print }
|
|
' "$plan"
|
|
}
|
|
|
|
# ── Extract template block from ACTION_PLAN_PROMPTS.md ──
|
|
extract_template() {
|
|
local tnum="$1"
|
|
local prompts="$ROOT/ACTION_PLAN_PROMPTS.md"
|
|
|
|
awk -v tnum="$tnum" '
|
|
BEGIN { found=0; in_code=0 }
|
|
/^## Template / {
|
|
if (found && !in_code) exit
|
|
if ($0 ~ "Template " tnum ":" || $0 ~ "Template " tnum " ") {
|
|
found=1
|
|
next
|
|
}
|
|
}
|
|
/^## Template [0-9]/ { if (found) exit }
|
|
/^---$/ { if (found && !in_code) exit }
|
|
found { print }
|
|
' "$prompts"
|
|
}
|
|
|
|
# ── Extract reference tables ──
|
|
extract_reference_tables() {
|
|
local prompts="$ROOT/ACTION_PLAN_PROMPTS.md"
|
|
awk '
|
|
/^## Reference Tables/,/^## Template 1/ {
|
|
if (/^## Template 1/) exit
|
|
print
|
|
}
|
|
' "$prompts"
|
|
}
|
|
|
|
# ── Build the full prompt for a task ──
|
|
build_prompt() {
|
|
local tid="$1"
|
|
local tnum="${TASK_TEMPLATE[$tid]}"
|
|
local tname="${TEMPLATE_NAME[$tnum]}"
|
|
|
|
local task_block
|
|
task_block=$(extract_task_block "$tid")
|
|
[ -n "$task_block" ] || die "could not extract task block for $tid from ACTION_PLAN.md"
|
|
|
|
local template
|
|
template=$(extract_template "$tnum")
|
|
|
|
local ref_tables
|
|
ref_tables=$(extract_reference_tables)
|
|
|
|
if [ "$tnum" -eq 9 ]; then
|
|
# Verification tasks: read-only audit, report findings
|
|
cat <<PROMPT
|
|
You are executing a verification task for the WindowMaker Wayland port.
|
|
This is a READ-ONLY audit. Do NOT modify any files. Do NOT create branches.
|
|
|
|
# Task: $tid
|
|
|
|
## Task Description (from ACTION_PLAN.md)
|
|
|
|
$task_block
|
|
|
|
## Procedure (Template $tnum)
|
|
|
|
$template
|
|
|
|
## Important Rules
|
|
|
|
1. Work in the git repository at $ROOT.
|
|
2. Run the verification checks described in the procedure.
|
|
3. Report findings using the REPORT FORMAT from the procedure.
|
|
4. End with exactly one of:
|
|
- DONE: verified clean, no issues found.
|
|
- STOP: found issues that need human decision. List each issue with file:line and a one-line description.
|
|
PROMPT
|
|
else
|
|
# Code modification tasks: branch, edit, compile
|
|
cat <<PROMPT
|
|
You are executing a mechanical Wayland porting task for WindowMaker.
|
|
|
|
# Task: $tid ($tname)
|
|
|
|
## Task Description (from ACTION_PLAN.md)
|
|
|
|
$task_block
|
|
|
|
## Procedure (Template $tnum)
|
|
|
|
$template
|
|
|
|
$ref_tables
|
|
|
|
## Important Rules
|
|
|
|
1. Work in the git repository at $ROOT. Create a git branch named "port/$tid" before making changes.
|
|
2. Make exactly the changes described. No more, no less.
|
|
3. After making changes, verify with the grep commands in the template.
|
|
4. Attempt to compile with: cd $ROOT && make -j\$(nproc) 2>&1 | tail -30
|
|
5. If compilation fails, fix the errors. If you cannot fix them, revert and report.
|
|
6. If you hit a STOP condition (missing vtable slot, missing API), revert changes and report exactly what is missing.
|
|
7. When done, run: git add -A && git diff --cached --stat
|
|
8. Report the final status as one of:
|
|
- DONE: task completed, compiles clean
|
|
- STOP: blocked on <reason>
|
|
- FAIL: could not complete because <reason>
|
|
|
|
Line numbers in ACTION_PLAN.md may have drifted. Use grep to find current line numbers before editing.
|
|
PROMPT
|
|
fi
|
|
}
|
|
|
|
# ── Dispatch a single task ──
|
|
dispatch_one() {
|
|
local tid="$1"
|
|
local prompt
|
|
prompt=$(build_prompt "$tid")
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
echo "════════════════════════════════════════"
|
|
echo "TASK: $tid (Template ${TASK_TEMPLATE[$tid]}: ${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]})"
|
|
echo "════════════════════════════════════════"
|
|
echo "$prompt"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
local session_name="wm-port-${tid}"
|
|
local session_path="$MOUNT/s/$session_name"
|
|
local flags=(
|
|
-name "$session_name"
|
|
-cwd "$ROOT"
|
|
-agent coding
|
|
-backend "$backend"
|
|
-model "$model"
|
|
-keep
|
|
)
|
|
[ "$bg" -eq 1 ] && flags+=(-bg)
|
|
|
|
printf '\033[1;36m▶ Dispatching %s\033[0m (%s)\n' \
|
|
"$tid" "${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]}" >&2
|
|
|
|
if [ "$bg" -eq 1 ]; then
|
|
local sp
|
|
sp=$(printf '%s' "$prompt" | "$MOUNT/s/b" "${flags[@]}")
|
|
printf '%s\t%s\n' "$tid" "$sp"
|
|
printf '\033[1;33m → %s\033[0m\n' "$sp" >&2
|
|
else
|
|
local result
|
|
result=$(printf '%s' "$prompt" | "$MOUNT/s/b" "${flags[@]}" 2>/dev/null) || true
|
|
|
|
# Save log
|
|
printf '%s\n' "$result" > "$LOG_DIR/${tid}.log"
|
|
|
|
# Parse result status — search broadly since agents format varies
|
|
local status="UNKNOWN"
|
|
local should_keep="$keep"
|
|
if [ -z "$result" ] || echo "$result" | head -1 | grep -q '^user:'; then
|
|
status="FAIL"
|
|
should_keep=1
|
|
printf '\033[1;31m✗ %s: FAIL (agent produced no response)\033[0m\n' "$tid" >&2
|
|
elif echo "$result" | grep -qiE '\*\*DONE\*\*|^DONE:|^DONE\b|Status:.*DONE|DONE: task completed|DONE: verified'; then
|
|
status="DONE"
|
|
echo "$tid" >> "$DONE_FILE"
|
|
printf '\033[1;32m✓ %s: DONE\033[0m\n' "$tid" >&2
|
|
elif echo "$result" | grep -qiE '\*\*STOP\*\*|^STOP:|^STOP\b|Status:.*STOP|STOP: blocked'; then
|
|
status="STOP"
|
|
should_keep=1
|
|
printf '\033[1;33m⚠ %s: STOP\033[0m\n' "$tid" >&2
|
|
echo "$result" | grep -iE 'STOP' | head -3 | sed 's/^/ /' >&2
|
|
elif echo "$result" | grep -qiE '\*\*FAIL\*\*|^FAIL:|^FAIL\b|Status:.*FAIL|FAIL: could not'; then
|
|
status="FAIL"
|
|
should_keep=1
|
|
printf '\033[1;31m✗ %s: FAIL\033[0m\n' "$tid" >&2
|
|
echo "$result" | grep -iE 'FAIL' | head -3 | sed 's/^/ /' >&2
|
|
else
|
|
if echo "$result" | grep -qiE 'compiles clean|no action needed'; then
|
|
status="DONE"
|
|
echo "$tid" >> "$DONE_FILE"
|
|
printf '\033[1;32m✓ %s: DONE (inferred from output)\033[0m\n' "$tid" >&2
|
|
else
|
|
status="UNKNOWN"
|
|
should_keep=1
|
|
printf '\033[1;35m? %s: status unclear — review log\033[0m\n' "$tid" >&2
|
|
echo "$result" | grep -v '^$' | tail -5 | sed 's/^/ /' >&2
|
|
fi
|
|
fi
|
|
|
|
printf ' Log: %s\n' "$LOG_DIR/${tid}.log" >&2
|
|
|
|
# Clean up session: keep on failure/unclear for inspection, kill on success
|
|
if [ "$should_keep" -eq 1 ]; then
|
|
printf ' Session: %s\n' "$session_path" >&2
|
|
elif [ -d "$session_path" ]; then
|
|
echo kill > "$session_path/ctl" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ── Main ──
|
|
if [ ${#task_ids[@]} -eq 1 ]; then
|
|
dispatch_one "${task_ids[0]}"
|
|
else
|
|
# Multiple tasks: dispatch in parallel (background mode)
|
|
printf '\033[1;36m▶ Dispatching %d tasks in parallel\033[0m\n' ${#task_ids[@]} >&2
|
|
|
|
pids=()
|
|
for tid in "${task_ids[@]}"; do
|
|
dispatch_one "$tid" &
|
|
pids+=($!)
|
|
done
|
|
|
|
# Wait for all
|
|
rc=0
|
|
for pid in "${pids[@]}"; do
|
|
wait "$pid" || rc=1
|
|
done
|
|
exit $rc
|
|
fi
|