|
| 1 | +#!/usr/bin/env bash |
| 2 | +# daemon-dispatch.sh — 3-phase dispatch library for delivery-daemon.sh (E134S02) |
| 3 | +# |
| 4 | +# Sourceable library. No top-level execution code. |
| 5 | +# Caller must set before sourcing or calling any function: |
| 6 | +# BACKLOG_FILE — absolute path to active.backlog.yaml |
| 7 | +# SCHEDULER — absolute path to backlog-scheduler.sh |
| 8 | +# PROJECT_DIR — repo root (for runtime-routing-logger.js) |
| 9 | +# Optional: |
| 10 | +# GAAI_STUB_DELAY_S — seconds to sleep between stubs (default: 0) |
| 11 | +# ROUTING_LOG_PATH — test-only override for --log-path (default: empty, uses logger default) |
| 12 | + |
| 13 | +# ── Field extractors (AC1 — verbatim per story AC1 specification) ───────── |
| 14 | + |
| 15 | +get_phase_status() { |
| 16 | + local id="$1" |
| 17 | + awk -v id="$id" ' |
| 18 | + $0 == "- id: " id { found=1; next } |
| 19 | + found && /^- id:/ { exit } |
| 20 | + found && /^[[:space:]]+phase_status:/ { |
| 21 | + gsub(/^[[:space:]]+phase_status:[[:space:]]*/, "") |
| 22 | + gsub(/[[:space:]]*$/, "") |
| 23 | + gsub(/^"|"$/, "") |
| 24 | + print |
| 25 | + exit |
| 26 | + } |
| 27 | + ' "$BACKLOG_FILE" |
| 28 | +} |
| 29 | + |
| 30 | +get_delivery_pipeline() { |
| 31 | + local id="$1" |
| 32 | + awk -v id="$id" ' |
| 33 | + $0 == "- id: " id { found=1; next } |
| 34 | + found && /^- id:/ { exit } |
| 35 | + found && /^[[:space:]]+delivery_pipeline:/ { |
| 36 | + gsub(/^[[:space:]]+delivery_pipeline:[[:space:]]*/, "") |
| 37 | + gsub(/[[:space:]]*$/, "") |
| 38 | + gsub(/^"|"$/, "") |
| 39 | + print |
| 40 | + exit |
| 41 | + } |
| 42 | + ' "$BACKLOG_FILE" |
| 43 | +} |
| 44 | + |
| 45 | +# Helper: read impl_model_tag from backlog (returns "absent" if unset/missing) |
| 46 | +get_impl_model_tag() { |
| 47 | + local id="$1" |
| 48 | + local val |
| 49 | + val=$(awk -v id="$id" ' |
| 50 | + $0 == "- id: " id { found=1; next } |
| 51 | + found && /^- id:/ { exit } |
| 52 | + found && /^[[:space:]]+impl_model:/ { |
| 53 | + gsub(/^[[:space:]]+impl_model:[[:space:]]*/, "") |
| 54 | + gsub(/[[:space:]]*$/, "") |
| 55 | + gsub(/^"|"$/, "") |
| 56 | + print |
| 57 | + exit |
| 58 | + } |
| 59 | + ' "$BACKLOG_FILE" 2>/dev/null || true) |
| 60 | + echo "${val:-absent}" |
| 61 | +} |
| 62 | + |
| 63 | +# ── Routing record helper ───────────────────────────────────────────────── |
| 64 | +# Emits one JSONL record to runtime-routing.jsonl via runtime-routing-logger.js. |
| 65 | +# Arguments: story_id trace_id phase provider fallback_reason |
| 66 | +_emit_routing_record() { |
| 67 | + local story_id="$1" trace_id="$2" phase="$3" provider="$4" fallback_reason="$5" |
| 68 | + local impl_tag |
| 69 | + impl_tag=$(get_impl_model_tag "$story_id") |
| 70 | + |
| 71 | + local log_path_args=() |
| 72 | + if [[ -n "${ROUTING_LOG_PATH:-}" ]]; then |
| 73 | + log_path_args=(--log-path "$ROUTING_LOG_PATH") |
| 74 | + fi |
| 75 | + |
| 76 | + node "$PROJECT_DIR/.gaai/core/adapters/claude-code/runtime-routing-logger.js" \ |
| 77 | + --trace-id "$trace_id" \ |
| 78 | + --story-id "$story_id" \ |
| 79 | + --phase "$phase" \ |
| 80 | + --provider "$provider" \ |
| 81 | + --model "n/a" \ |
| 82 | + --duration-ms 0 \ |
| 83 | + --fallback-reason "$fallback_reason" \ |
| 84 | + --impl-model-tag "$impl_tag" \ |
| 85 | + "${log_path_args[@]}" \ |
| 86 | + 2>/dev/null || true |
| 87 | +} |
| 88 | + |
| 89 | +# ── Stub phase handlers (AC3 + AC4) ────────────────────────────────────── |
| 90 | + |
| 91 | +handle_plan_phase() { |
| 92 | + local story_id="$1" trace_id="$2" |
| 93 | + local ts |
| 94 | + ts=$(date '+%H:%M:%S') |
| 95 | + echo "[${ts}] ${story_id} phase=plan dispatched (stub)" |
| 96 | + |
| 97 | + # Emit routing record (AC4) |
| 98 | + _emit_routing_record "$story_id" "$trace_id" "plan" "stub" "" |
| 99 | + |
| 100 | + # Advance phase_status: not_started → planned (AC3) |
| 101 | + if ! "$SCHEDULER" --set-phase-status "$story_id" planned "$BACKLOG_FILE" 2>/dev/null; then |
| 102 | + echo "[ERROR] ${story_id} handle_plan_phase: --set-phase-status planned failed" |
| 103 | + _emit_routing_record "$story_id" "$trace_id" "plan" "error" "set-phase-status-failed" |
| 104 | + return 1 |
| 105 | + fi |
| 106 | + |
| 107 | + sleep "${GAAI_STUB_DELAY_S:-0}" |
| 108 | + return 0 |
| 109 | +} |
| 110 | + |
| 111 | +handle_impl_phase() { |
| 112 | + local story_id="$1" trace_id="$2" |
| 113 | + local ts |
| 114 | + ts=$(date '+%H:%M:%S') |
| 115 | + echo "[${ts}] ${story_id} phase=impl dispatched (stub)" |
| 116 | + |
| 117 | + # Emit routing record (AC4) |
| 118 | + _emit_routing_record "$story_id" "$trace_id" "impl" "stub" "" |
| 119 | + |
| 120 | + # Advance phase_status: planned → implemented (AC3) |
| 121 | + if ! "$SCHEDULER" --set-phase-status "$story_id" implemented "$BACKLOG_FILE" 2>/dev/null; then |
| 122 | + echo "[ERROR] ${story_id} handle_impl_phase: --set-phase-status implemented failed" |
| 123 | + _emit_routing_record "$story_id" "$trace_id" "impl" "error" "set-phase-status-failed" |
| 124 | + return 1 |
| 125 | + fi |
| 126 | + |
| 127 | + sleep "${GAAI_STUB_DELAY_S:-0}" |
| 128 | + return 0 |
| 129 | +} |
| 130 | + |
| 131 | +handle_qa_phase() { |
| 132 | + local story_id="$1" trace_id="$2" |
| 133 | + local ts |
| 134 | + ts=$(date '+%H:%M:%S') |
| 135 | + echo "[${ts}] ${story_id} phase=qa dispatched (stub)" |
| 136 | + |
| 137 | + # Emit routing record (AC4) |
| 138 | + _emit_routing_record "$story_id" "$trace_id" "qa" "stub" "" |
| 139 | + |
| 140 | + # Advance phase_status: implemented → qa_passed (AC3) |
| 141 | + if ! "$SCHEDULER" --set-phase-status "$story_id" qa_passed "$BACKLOG_FILE" 2>/dev/null; then |
| 142 | + echo "[ERROR] ${story_id} handle_qa_phase: --set-phase-status qa_passed failed" |
| 143 | + _emit_routing_record "$story_id" "$trace_id" "qa" "error" "set-phase-status-failed" |
| 144 | + return 1 |
| 145 | + fi |
| 146 | + |
| 147 | + sleep "${GAAI_STUB_DELAY_S:-0}" |
| 148 | + return 0 |
| 149 | +} |
| 150 | + |
| 151 | +handle_commit_phase() { |
| 152 | + local story_id="$1" trace_id="$2" |
| 153 | + local ts |
| 154 | + ts=$(date '+%H:%M:%S') |
| 155 | + echo "[${ts}] ${story_id} phase=commit dispatched (stub)" |
| 156 | + |
| 157 | + # Emit routing record (AC4) |
| 158 | + _emit_routing_record "$story_id" "$trace_id" "commit" "stub" "" |
| 159 | + |
| 160 | + # Advance phase_status: qa_passed → done (stub — real commit/PR/merge is E134S06) |
| 161 | + if ! "$SCHEDULER" --set-phase-status "$story_id" done "$BACKLOG_FILE" 2>/dev/null; then |
| 162 | + echo "[ERROR] ${story_id} handle_commit_phase: --set-phase-status done failed" |
| 163 | + _emit_routing_record "$story_id" "$trace_id" "commit" "error" "set-phase-status-failed" |
| 164 | + return 1 |
| 165 | + fi |
| 166 | + |
| 167 | + sleep "${GAAI_STUB_DELAY_S:-0}" |
| 168 | + return 0 |
| 169 | +} |
| 170 | + |
| 171 | +# ── Main dispatcher (AC1 + AC6) ─────────────────────────────────────────── |
| 172 | +# |
| 173 | +# Called by delivery-daemon.sh main loop for stories with delivery_pipeline=3phase. |
| 174 | +# Reads phase_status, routes to the appropriate handler for ONE phase, then returns. |
| 175 | +# The caller loops until phase_status is done/failed/escalated. |
| 176 | +# |
| 177 | +# Arguments: story_id [trace_id] |
| 178 | +# Returns: 0 on success, 1 on dispatch error (logs [ERROR] per AC6) |
| 179 | +dispatch_3phase_story() { |
| 180 | + local story_id="$1" |
| 181 | + local trace_id="${2:-$(python3 -c 'import uuid; print(str(uuid.uuid4()))' 2>/dev/null || echo "stub-$(date +%s)-$$")}" |
| 182 | + |
| 183 | + # Read phase_status (AC1 — awk extractor) |
| 184 | + local ps |
| 185 | + ps=$(get_phase_status "$story_id") |
| 186 | + |
| 187 | + if [[ -z "$ps" ]]; then |
| 188 | + # AC6(i): log ERROR |
| 189 | + echo "[ERROR] ${story_id} dispatch_3phase_story: phase_status field missing or empty" |
| 190 | + # AC6(iv): emit error routing record |
| 191 | + _emit_routing_record "$story_id" "$trace_id" "plan" "error" "phase_status_missing" |
| 192 | + # AC6(ii): return non-zero (caller loop will break) |
| 193 | + return 1 |
| 194 | + fi |
| 195 | + |
| 196 | + case "$ps" in |
| 197 | + not_started) |
| 198 | + handle_plan_phase "$story_id" "$trace_id" || return 1 |
| 199 | + ;; |
| 200 | + planned) |
| 201 | + handle_impl_phase "$story_id" "$trace_id" || return 1 |
| 202 | + ;; |
| 203 | + implemented) |
| 204 | + handle_qa_phase "$story_id" "$trace_id" || return 1 |
| 205 | + ;; |
| 206 | + qa_passed) |
| 207 | + handle_commit_phase "$story_id" "$trace_id" || return 1 |
| 208 | + ;; |
| 209 | + done|failed|escalated) |
| 210 | + # Terminal states — caller loop should stop. Not an error. |
| 211 | + return 0 |
| 212 | + ;; |
| 213 | + *) |
| 214 | + # AC6(i)(ii)(iii)(iv): invalid phase_status |
| 215 | + echo "[ERROR] ${story_id} dispatch_3phase_story: invalid phase_status='${ps}' — known values: not_started planned implemented qa_passed done failed escalated" |
| 216 | + _emit_routing_record "$story_id" "$trace_id" "plan" "error" "invalid_phase_status:${ps}" |
| 217 | + return 1 |
| 218 | + ;; |
| 219 | + esac |
| 220 | + |
| 221 | + return 0 |
| 222 | +} |
0 commit comments