From 0000581d8e1c37c94cbbaf46c94e717951267ce2 Mon Sep 17 00:00:00 2001 From: Local User Date: Mon, 11 May 2026 10:42:45 -0500 Subject: [PATCH 1/2] feat: add coordinator agent with subagent-based worker dispatch Adds the autoship-coordinator skill and coordinator.sh that uses OpenCode's task tool to spawn general subagents per queued workspace. Supports task_id tracking, resume via existing task_ids, and configurable concurrency. Enable by calling coordinator.sh directly or via runner.sh with AUTOSHIP_COORDINATOR_MODE=true. --- hooks/opencode/coordinator.sh | 108 +++++++++++++++++++++++++++ skills/autoship-coordinator/SKILL.md | 43 +++++++++++ 2 files changed, 151 insertions(+) create mode 100644 hooks/opencode/coordinator.sh create mode 100644 skills/autoship-coordinator/SKILL.md diff --git a/hooks/opencode/coordinator.sh b/hooks/opencode/coordinator.sh new file mode 100644 index 000000000..a420515ec --- /dev/null +++ b/hooks/opencode/coordinator.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# AutoShip Coordinator — starts an OpenCode session that manages subagent workers +# Replaces fire-and-forget `opencode run` with task-tool subagent dispatch. +# +# Usage: +# bash hooks/opencode/coordinator.sh [--dry-run] [--once] +# +# Options: +# --dry-run Log what would be done but don't start coordinator +# --once Process one batch of workspaces then exit + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +AUTOSHIP_DIR="${AUTOSHIP_DIR:-.autoship}" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +WORKSPACES_DIR="$REPO_ROOT/$AUTOSHIP_DIR/workspaces" +STATE_FILE="$REPO_ROOT/$AUTOSHIP_DIR/state.json" +CONFIG_FILE="$REPO_ROOT/$AUTOSHIP_DIR/config.json" +LOG_FILE="$REPO_ROOT/$AUTOSHIP_DIR/coordinator.log" +COORDINATOR_PROMPT="$REPO_ROOT/$AUTOSHIP_DIR/COORDINATOR_PROMPT.md" +COORDINATOR_MODEL="${AUTOSHIP_COORDINATOR_MODEL:-opencode/big-pickle}" + +DRY_RUN=false +ONCE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --once) ONCE=true; shift ;; + -h|--help) + echo "Usage: $(basename "$0") [--dry-run] [--once]" + exit 0 ;; + *) echo "Unknown: $1"; exit 1 ;; + esac +done + +log() { echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $1" | tee -a "$LOG_FILE"; } + +# Resolve max concurrency +MAX_CONCURRENT=5 +if [[ -f "$CONFIG_FILE" ]]; then + config_max=$(jq -r '.max_workers // .maxConcurrent // .max_concurrent // 5' "$CONFIG_FILE" 2>/dev/null || echo 5) + [[ "$config_max" =~ ^[0-9]+$ && "$config_max" -gt 0 ]] && MAX_CONCURRENT=$config_max +fi + +log "Coordinator starting (model=$COORDINATOR_MODEL, max_concurrent=$MAX_CONCURRENT)" + +# Scan for QUEUED workspaces +queued=() +if [[ -d "$WORKSPACES_DIR" ]]; then + for ws_dir in "$WORKSPACES_DIR"/*/; do + [[ -d "$ws_dir" ]] || continue + s=$(tr -d '[:space:]' <"$ws_dir/status" 2>/dev/null || true) + [[ -f "$ws_dir/AUTOSHIP_PROMPT.md" && "$s" == "QUEUED" ]] && queued+=("$(basename "$ws_dir")") + done +fi + +if [[ ${#queued[@]} -eq 0 ]]; then + log "No QUEUED workspaces. Exiting." + exit 0 +fi + +log "Found ${#queued[@]} QUEUED: ${queued[*]}" + +batch=("${queued[@]:0:$MAX_CONCURRENT}") +[[ "$DRY_RUN" == "true" ]] && { log "DRY-RUN: would start for ${batch[*]}"; exit 0; } + +# Generate coordinator prompt +{ + cat < /status\` +3. Write started_at: \`date -u +%Y-%m-%dT%H:%M:%SZ > /started_at\` +4. Call \`task(subagent_type="general", prompt=)\` +5. On return, write task_id: \`echo "" > /task_id\` +6. Read workspace/status — COMPLETE or STUCK +7. Report summary when all done + +## Workspaces + +PROMPT_HEADER + + for issue_key in "${batch[@]}"; do + echo "- $WORKSPACES_DIR/$issue_key" + done + + echo "" + echo "Concurrency cap: $MAX_CONCURRENT" + echo "Spare multiple subagents concurrently by calling task multiple times in one message." +} > "$COORDINATOR_PROMPT" + +log "Starting coordinator with model=$COORDINATOR_MODEL" +cd "$REPO_ROOT" +opencode run --model "$COORDINATOR_MODEL" "$(cat "$COORDINATOR_PROMPT")" 2>&1 | tee -a "$LOG_FILE" + +coordinator_exit=$? +log "Coordinator exited with code $coordinator_exit" + +[[ "$ONCE" == "false" ]] && { log "Re-scanning..."; exec bash "$0" --once; } +exit $coordinator_exit diff --git a/skills/autoship-coordinator/SKILL.md b/skills/autoship-coordinator/SKILL.md new file mode 100644 index 000000000..0eada82f3 --- /dev/null +++ b/skills/autoship-coordinator/SKILL.md @@ -0,0 +1,43 @@ +--- +name: autoship-coordinator +description: Long-running AutoShip coordinator that spawns implementer subagents via OpenCode's task tool, tracks them by task_id, and handles lifecycle. +version: 1.0.0 +author: AutoShip +license: MIT +metadata: + hermes: + tags: [autoship, coordinator, orchestration, subagent, task-tool] +--- + +# AutoShip Coordinator + +Orchestrates AutoShip workers as OpenCode subagents. Scans for queued workspaces, spawns implementations via the `task` tool, tracks each worker by `task_id`, and handles completion, retry, and fallback. + +## When to Use + +Use this when running the coordinator to process queued workspaces, resume an interrupted coordinator session, or debug worker lifecycle. + +## Workflow + +1. Scan `.autoship/workspaces/*/` for status = `QUEUED` +2. Check concurrency cap from config (`max_workers` or `max_concurrent`) +3. For each eligible workspace up to the cap: + a. Read `workspace/model` and `workspace/AUTOSHIP_PROMPT.md` + b. Set status `RUNNING` + c. Call `task(subagent_type="general", prompt=)` + d. Write returned `task_id` to `workspace/task_id` +4. Wait for subagents to finish +5. For each: read `workspace/status` — if COMPLETE update state, if STUCK try fallback model +6. Repeat until no QUEUED workspaces remain + +## Spawning Subagents + +Use `task` with `subagent_type="general"` and the full prompt from `AUTOSHIP_PROMPT.md`. Record the returned `task_id` in `workspace/task_id` for tracking and resume. + +## Monitoring + +The coordinator relies on `workspace/status` files and `task` tool returns. On restart, it can resume subagents via stored `task_id`. + +## Boundaries + +Does NOT write implementation code, modify prompts, or change issue labels. Scope is limited to subagent lifecycle management. From e731b84f98dbed5ba55ce0d13778c5daca2b2f39 Mon Sep 17 00:00:00 2001 From: Local User Date: Mon, 11 May 2026 12:13:19 -0500 Subject: [PATCH 2/2] feat: update coordinator to use skill file, fix REPO_ROOT on Windows --- hooks/opencode/coordinator.sh | 95 +++------------------------- skills/autoship-coordinator/SKILL.md | 84 +++++++++++++----------- 2 files changed, 58 insertions(+), 121 deletions(-) diff --git a/hooks/opencode/coordinator.sh b/hooks/opencode/coordinator.sh index a420515ec..c4b580a6d 100644 --- a/hooks/opencode/coordinator.sh +++ b/hooks/opencode/coordinator.sh @@ -1,105 +1,30 @@ #!/usr/bin/env bash -# AutoShip Coordinator — starts an OpenCode session that manages subagent workers -# Replaces fire-and-forget `opencode run` with task-tool subagent dispatch. +# AutoShip Coordinator — entry point that runs the coordinator agent skill +# The coordinator agent handles scanning, dispatching, and monitoring. # # Usage: -# bash hooks/opencode/coordinator.sh [--dry-run] [--once] +# bash hooks/opencode/coordinator.sh [--once] # # Options: -# --dry-run Log what would be done but don't start coordinator -# --once Process one batch of workspaces then exit +# --once Process queued workspaces then exit (no re-scan loop) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" AUTOSHIP_DIR="${AUTOSHIP_DIR:-.autoship}" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -WORKSPACES_DIR="$REPO_ROOT/$AUTOSHIP_DIR/workspaces" -STATE_FILE="$REPO_ROOT/$AUTOSHIP_DIR/state.json" -CONFIG_FILE="$REPO_ROOT/$AUTOSHIP_DIR/config.json" +REPO_ROOT="${AUTOSHIP_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" LOG_FILE="$REPO_ROOT/$AUTOSHIP_DIR/coordinator.log" -COORDINATOR_PROMPT="$REPO_ROOT/$AUTOSHIP_DIR/COORDINATOR_PROMPT.md" -COORDINATOR_MODEL="${AUTOSHIP_COORDINATOR_MODEL:-opencode/big-pickle}" +SKILL_FILE="$SCRIPT_DIR/../../skills/autoship-coordinator/SKILL.md" +COORDINATOR_MODEL="${AUTOSHIP_COORDINATOR_MODEL:-kimi-for-coding/k2p6}" -DRY_RUN=false ONCE=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) DRY_RUN=true; shift ;; - --once) ONCE=true; shift ;; - -h|--help) - echo "Usage: $(basename "$0") [--dry-run] [--once]" - exit 0 ;; - *) echo "Unknown: $1"; exit 1 ;; - esac -done +[[ "${1:-}" == "--once" ]] && ONCE=true log() { echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $1" | tee -a "$LOG_FILE"; } -# Resolve max concurrency -MAX_CONCURRENT=5 -if [[ -f "$CONFIG_FILE" ]]; then - config_max=$(jq -r '.max_workers // .maxConcurrent // .max_concurrent // 5' "$CONFIG_FILE" 2>/dev/null || echo 5) - [[ "$config_max" =~ ^[0-9]+$ && "$config_max" -gt 0 ]] && MAX_CONCURRENT=$config_max -fi - -log "Coordinator starting (model=$COORDINATOR_MODEL, max_concurrent=$MAX_CONCURRENT)" - -# Scan for QUEUED workspaces -queued=() -if [[ -d "$WORKSPACES_DIR" ]]; then - for ws_dir in "$WORKSPACES_DIR"/*/; do - [[ -d "$ws_dir" ]] || continue - s=$(tr -d '[:space:]' <"$ws_dir/status" 2>/dev/null || true) - [[ -f "$ws_dir/AUTOSHIP_PROMPT.md" && "$s" == "QUEUED" ]] && queued+=("$(basename "$ws_dir")") - done -fi - -if [[ ${#queued[@]} -eq 0 ]]; then - log "No QUEUED workspaces. Exiting." - exit 0 -fi - -log "Found ${#queued[@]} QUEUED: ${queued[*]}" - -batch=("${queued[@]:0:$MAX_CONCURRENT}") -[[ "$DRY_RUN" == "true" ]] && { log "DRY-RUN: would start for ${batch[*]}"; exit 0; } - -# Generate coordinator prompt -{ - cat < /status\` -3. Write started_at: \`date -u +%Y-%m-%dT%H:%M:%SZ > /started_at\` -4. Call \`task(subagent_type="general", prompt=)\` -5. On return, write task_id: \`echo "" > /task_id\` -6. Read workspace/status — COMPLETE or STUCK -7. Report summary when all done - -## Workspaces - -PROMPT_HEADER - - for issue_key in "${batch[@]}"; do - echo "- $WORKSPACES_DIR/$issue_key" - done - - echo "" - echo "Concurrency cap: $MAX_CONCURRENT" - echo "Spare multiple subagents concurrently by calling task multiple times in one message." -} > "$COORDINATOR_PROMPT" - -log "Starting coordinator with model=$COORDINATOR_MODEL" +log "Coordinator starting (model=$COORDINATOR_MODEL)" cd "$REPO_ROOT" -opencode run --model "$COORDINATOR_MODEL" "$(cat "$COORDINATOR_PROMPT")" 2>&1 | tee -a "$LOG_FILE" +opencode run --model "$COORDINATOR_MODEL" "$SKILL_FILE" 2>&1 | tee -a "$LOG_FILE" coordinator_exit=$? log "Coordinator exited with code $coordinator_exit" diff --git a/skills/autoship-coordinator/SKILL.md b/skills/autoship-coordinator/SKILL.md index 0eada82f3..4683f443b 100644 --- a/skills/autoship-coordinator/SKILL.md +++ b/skills/autoship-coordinator/SKILL.md @@ -1,43 +1,55 @@ ---- -name: autoship-coordinator -description: Long-running AutoShip coordinator that spawns implementer subagents via OpenCode's task tool, tracks them by task_id, and handles lifecycle. -version: 1.0.0 -author: AutoShip -license: MIT -metadata: - hermes: - tags: [autoship, coordinator, orchestration, subagent, task-tool] ---- - # AutoShip Coordinator -Orchestrates AutoShip workers as OpenCode subagents. Scans for queued workspaces, spawns implementations via the `task` tool, tracks each worker by `task_id`, and handles completion, retry, and fallback. +You are the AutoShip Coordinator for this repository. You manage queued AutoShip issues by spawning implementer subagents. -## When to Use +## Mission -Use this when running the coordinator to process queued workspaces, resume an interrupted coordinator session, or debug worker lifecycle. +Process all workspaces in `.autoship/workspaces/` that have: +- `status` file containing exactly `QUEUED` +- `AUTOSHIP_PROMPT.md` file present ## Workflow -1. Scan `.autoship/workspaces/*/` for status = `QUEUED` -2. Check concurrency cap from config (`max_workers` or `max_concurrent`) -3. For each eligible workspace up to the cap: - a. Read `workspace/model` and `workspace/AUTOSHIP_PROMPT.md` - b. Set status `RUNNING` - c. Call `task(subagent_type="general", prompt=)` - d. Write returned `task_id` to `workspace/task_id` -4. Wait for subagents to finish -5. For each: read `workspace/status` — if COMPLETE update state, if STUCK try fallback model -6. Repeat until no QUEUED workspaces remain - -## Spawning Subagents - -Use `task` with `subagent_type="general"` and the full prompt from `AUTOSHIP_PROMPT.md`. Record the returned `task_id` in `workspace/task_id` for tracking and resume. - -## Monitoring - -The coordinator relies on `workspace/status` files and `task` tool returns. On restart, it can resume subagents via stored `task_id`. - -## Boundaries - -Does NOT write implementation code, modify prompts, or change issue labels. Scope is limited to subagent lifecycle management. +1. **Scan** — List all workspace directories, read their `status` and check for `AUTOSHIP_PROMPT.md` +2. **Filter** — Only process workspaces where status == "QUEUED" AND AUTOSHIP_PROMPT.md exists +3. **Dispatch** — For each qualifying workspace (up to concurrency limit): + a. Read `AUTOSHIP_PROMPT.md` for the task prompt + b. Read `model` file for target model (default: kimi-for-coding/k2p6) + c. Write "RUNNING" to `/status` + d. Write current UTC timestamp to `/started_at` + e. Call `task(subagent_type="general", prompt=)` + f. On task return, write the task_id to `/task_id` + g. Read final `/status` — expect COMPLETE, BLOCKED, or STUCK +4. **Report** — When all done, output a summary table: workspace, model, final_status + +## Concurrency + +Read `.autoship/config.json` for `max_workers` or `max_concurrent` field. Default: 5. +Spawn multiple subagents concurrently when possible by making multiple `task()` calls. + +## Status Tracking + +- RUNNING: subagent is active +- COMPLETE: task finished successfully +- BLOCKED: subagent reported it cannot proceed +- STUCK: subagent timed out or crashed +- QUEUED: ready for pickup (do not re-process RUNNING ones) + +## Error Handling + +If a workspace has no AUTOSHIP_PROMPT.md, log it and skip. +If a workspace status file is missing or unreadable, log it and skip. +If task() throws an error, write "STUCK" to status and continue. + +## Final Output Format + +``` +AutoShip Coordinator Summary +============================ +Processed: N workspaces +- issue-XXXX: COMPLETE (model: kimi-for-coding/k2p6) +- issue-YYYY: BLOCKED (model: kimi-for-coding/k2p6) +- issue-ZZZZ: STUCK (model: kimi-for-coding/k2p6) +``` + +Do not modify source code yourself — your only job is to scan, dispatch, and report.