|
5 | 5 | LOG_DIR="${1:-.gaai/project/contexts/backlog/.delivery-logs}" |
6 | 6 | PROJECT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" |
7 | 7 | BACKLOG="$PROJECT_DIR/.gaai/project/contexts/backlog/active.backlog.yaml" |
| 8 | +LOCK_DIR="${PROJECT_DIR}/.gaai/project/contexts/backlog/.delivery-locks" |
| 9 | +WORKTREE_BASE="${PROJECT_DIR}/../.gaai-worktrees/$(basename "$PROJECT_DIR")" |
8 | 10 |
|
9 | 11 | HAS_JQ=false |
10 | 12 | command -v jq &>/dev/null && HAS_JQ=true |
@@ -50,9 +52,157 @@ format_model() { |
50 | 52 |
|
51 | 53 | PHASE_CACHE_DIR="${PROJECT_DIR}/.gaai/project/contexts/backlog/.delivery-locks/.phase-cache" |
52 | 54 |
|
| 55 | +# Returns story IDs — one per line — for all currently active deliveries. |
| 56 | +# For legacy pipeline: active tmux sessions named gaai-deliver-{id}. |
| 57 | +# For 3phase pipeline: .lock files in LOCK_DIR where backlog status=in_progress. |
| 58 | +detect_active_stories() { |
| 59 | + local seen=() |
| 60 | + |
| 61 | + # Legacy: tmux sessions |
| 62 | + local tmux_ids |
| 63 | + tmux_ids=$(tmux list-sessions -F '#{session_name}' 2>/dev/null \ |
| 64 | + | grep '^gaai-deliver-' \ |
| 65 | + | sed 's/gaai-deliver-//' || true) |
| 66 | + for _id in $tmux_ids; do |
| 67 | + local _dp |
| 68 | + _dp=$(awk -v id="$_id" ' |
| 69 | + $0 == "- id: " id { found=1; next } |
| 70 | + found && /^- id:/ { exit } |
| 71 | + found && /^[[:space:]]+delivery_pipeline:/ { |
| 72 | + gsub(/^[[:space:]]+delivery_pipeline:[[:space:]]*/, "") |
| 73 | + gsub(/[[:space:]]*/, ""); print; exit |
| 74 | + } |
| 75 | + ' "$BACKLOG" 2>/dev/null || true) |
| 76 | + [[ "$_dp" != "3phase" ]] && echo "$_id" |
| 77 | + seen+=("$_id") |
| 78 | + done |
| 79 | + |
| 80 | + # 3phase: .lock files with in_progress status |
| 81 | + if [[ -d "$LOCK_DIR" ]]; then |
| 82 | + for _lf in "$LOCK_DIR"/*.lock; do |
| 83 | + [[ -f "$_lf" ]] || continue |
| 84 | + local _sid |
| 85 | + _sid=$(basename "$_lf" .lock) |
| 86 | + # Skip if already emitted via tmux path |
| 87 | + local _dup=0 |
| 88 | + for _s in "${seen[@]:-}"; do [[ "$_s" == "$_sid" ]] && _dup=1 && break; done |
| 89 | + [[ $_dup -eq 1 ]] && continue |
| 90 | + local _status _dp2 |
| 91 | + _status=$(awk -v id="$_sid" ' |
| 92 | + $0 == "- id: " id { found=1; next } |
| 93 | + found && /^- id:/ { exit } |
| 94 | + found && /^[[:space:]]+status:/ { |
| 95 | + gsub(/^[[:space:]]+status:[[:space:]]*/, "") |
| 96 | + gsub(/[[:space:]]*/, ""); print; exit |
| 97 | + } |
| 98 | + ' "$BACKLOG" 2>/dev/null || true) |
| 99 | + _dp2=$(awk -v id="$_sid" ' |
| 100 | + $0 == "- id: " id { found=1; next } |
| 101 | + found && /^- id:/ { exit } |
| 102 | + found && /^[[:space:]]+delivery_pipeline:/ { |
| 103 | + gsub(/^[[:space:]]+delivery_pipeline:[[:space:]]*/, "") |
| 104 | + gsub(/[[:space:]]*/, ""); print; exit |
| 105 | + } |
| 106 | + ' "$BACKLOG" 2>/dev/null || true) |
| 107 | + [[ "$_status" == "in_progress" && "$_dp2" == "3phase" ]] && echo "$_sid" |
| 108 | + done |
| 109 | + fi |
| 110 | +} |
| 111 | + |
| 112 | +# Returns the canonical log path for the current active phase of a 3phase story. |
| 113 | +# AC2: per-phase log at {worktree}/.delivery-logs/{id}.{phase}.log |
| 114 | +# Falls back to [no log yet] sentinel string when log does not exist. |
| 115 | +resolve_3phase_log() { |
| 116 | + local story_id="$1" |
| 117 | + local worktree="${WORKTREE_BASE}/${story_id}-workspace" |
| 118 | + |
| 119 | + # Determine active phase from markers (AC1 priority order) |
| 120 | + local active_phase="" |
| 121 | + for _ph in plan impl qa commit; do |
| 122 | + if [[ -f "${LOCK_DIR}/${story_id}.${_ph}.active" ]]; then |
| 123 | + active_phase="$_ph" |
| 124 | + break |
| 125 | + fi |
| 126 | + done |
| 127 | + |
| 128 | + if [[ -z "$active_phase" ]]; then |
| 129 | + # No active marker: derive last relevant phase from phase_status |
| 130 | + local ps |
| 131 | + ps=$(awk -v id="$story_id" ' |
| 132 | + $0 == "- id: " id { found=1; next } |
| 133 | + found && /^- id:/ { exit } |
| 134 | + found && /^[[:space:]]+phase_status:/ { |
| 135 | + gsub(/^[[:space:]]+phase_status:[[:space:]]*/, "") |
| 136 | + gsub(/[[:space:]]*/, ""); print; exit |
| 137 | + } |
| 138 | + ' "$BACKLOG" 2>/dev/null || true) |
| 139 | + case "$ps" in |
| 140 | + not_started) active_phase="plan" ;; |
| 141 | + planned) active_phase="plan" ;; |
| 142 | + implemented) active_phase="impl" ;; |
| 143 | + qa_passed) active_phase="qa" ;; |
| 144 | + done) active_phase="commit" ;; |
| 145 | + qa_failed|qa_escalated) active_phase="qa" ;; |
| 146 | + failed|escalated) active_phase="impl" ;; |
| 147 | + "") echo "[?]"; return ;; |
| 148 | + *) echo "[?]"; return ;; |
| 149 | + esac |
| 150 | + fi |
| 151 | + |
| 152 | + local log_path="${worktree}/.delivery-logs/${story_id}.${active_phase}.log" |
| 153 | + if [[ -f "$log_path" ]]; then |
| 154 | + echo "$log_path" |
| 155 | + else |
| 156 | + echo "[no log yet]" |
| 157 | + fi |
| 158 | +} |
| 159 | + |
| 160 | +# Returns the display phase label for a 3phase story using authoritative markers. |
| 161 | +# AC1: markers take priority over phase_status for in-progress display. |
| 162 | +detect_phase_3phase() { |
| 163 | + local story_id="$1" |
| 164 | + |
| 165 | + # Active marker check (highest priority) |
| 166 | + for _ph in plan impl qa commit; do |
| 167 | + if [[ -f "${LOCK_DIR}/${story_id}.${_ph}.active" ]]; then |
| 168 | + case "$_ph" in |
| 169 | + plan) echo "PLAN" ;; |
| 170 | + impl) echo "IMPL" ;; |
| 171 | + qa) echo "QA" ;; |
| 172 | + commit) echo "COMMIT" ;; |
| 173 | + esac |
| 174 | + return |
| 175 | + fi |
| 176 | + done |
| 177 | + |
| 178 | + # No active marker: read phase_status for terminal / idle display |
| 179 | + local ps |
| 180 | + ps=$(awk -v id="$story_id" ' |
| 181 | + $0 == "- id: " id { found=1; next } |
| 182 | + found && /^- id:/ { exit } |
| 183 | + found && /^[[:space:]]+phase_status:/ { |
| 184 | + gsub(/^[[:space:]]+phase_status:[[:space:]]*/, "") |
| 185 | + gsub(/[[:space:]]*/, ""); print; exit |
| 186 | + } |
| 187 | + ' "$BACKLOG" 2>/dev/null || true) |
| 188 | + |
| 189 | + case "$ps" in |
| 190 | + done) echo "DONE" ;; |
| 191 | + failed|escalated) echo "FAILED" ;; |
| 192 | + qa_failed) echo "QA_FAILED" ;; |
| 193 | + qa_escalated) echo "QA_ESCALATED" ;; |
| 194 | + not_started|planned|implemented|qa_passed) |
| 195 | + echo "IDLE @ ${ps}" ;; |
| 196 | + "") echo "[?]" ;; |
| 197 | + *) echo "[?]" ;; |
| 198 | + esac |
| 199 | +} |
| 200 | + |
53 | 201 | parse_log() { |
54 | 202 | local log_file="$1" |
55 | 203 | local story_id="$2" |
| 204 | + local pipeline="${3:-legacy}" # "3phase" or "legacy" |
| 205 | + local phase_override="${4:-}" # pre-computed phase label (3phase only) |
56 | 206 |
|
57 | 207 | if [[ ! -f "$log_file" ]]; then |
58 | 208 | echo -e " ${DIM}(log not yet created)${NC}" |
@@ -185,6 +335,12 @@ parse_log() { |
185 | 335 | fi |
186 | 336 |
|
187 | 337 | # ── Phase detection ── |
| 338 | + if [[ "$pipeline" == "3phase" && -n "$phase_override" ]]; then |
| 339 | + # 3phase: use authoritative marker-derived label (AC1) |
| 340 | + phase_label="$phase_override" |
| 341 | + phase_origin="" |
| 342 | + else |
| 343 | + # Legacy pipeline: existing log-content heuristic (unchanged) |
188 | 344 | # Walk recent events, classify each Bash command / Write target into a phase tag, |
189 | 345 | # keep the LAST non-empty classification — that's the current phase. |
190 | 346 | # Origin is captured from the same event so we can show e.g. "IMPL (sub)" when |
@@ -300,6 +456,7 @@ parse_log() { |
300 | 456 | # Current window advanced: write updated winner back to cache |
301 | 457 | printf '%s\t%s\t%s\n' "$cur_rank" "$phase_label" "$phase_origin" > "$cache_file" 2>/dev/null || true |
302 | 458 | fi |
| 459 | + fi # close: if [[ 3phase ]] ... else (legacy heuristic) ... fi |
303 | 460 | else |
304 | 461 | last_text=$(tail -200 "$log_file" 2>/dev/null \ |
305 | 462 | | grep -o '"type":"tool_use"[^}]*"name":"[^"]*"' \ |
@@ -382,30 +539,59 @@ parse_log() { |
382 | 539 | fi |
383 | 540 | } |
384 | 541 |
|
| 542 | +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then |
385 | 543 | while true; do |
386 | 544 | clear |
387 | 545 | # In tmux: clear scrollback left by `clear` so prior refresh doesn't ghost below |
388 | 546 | [[ -n "${TMUX:-}" ]] && tmux clear-history 2>/dev/null || true |
389 | 547 | echo "═══ Active Deliveries (refreshes every 5s) ═══" |
390 | 548 | echo "" |
391 | 549 |
|
392 | | - # Find active tmux delivery sessions |
393 | | - active_sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null \ |
394 | | - | grep '^gaai-deliver-' \ |
395 | | - | sed 's/gaai-deliver-//' || true) |
| 550 | + # Read active stories (3phase from locks + legacy from tmux) |
| 551 | + active_ids=() |
| 552 | + while IFS= read -r _id; do |
| 553 | + [[ -n "$_id" ]] && active_ids+=("$_id") |
| 554 | + done < <(detect_active_stories) |
396 | 555 |
|
397 | | - if [[ -z "$active_sessions" ]]; then |
| 556 | + if [[ ${#active_ids[@]} -eq 0 ]]; then |
398 | 557 | echo -e " ${DIM}No active deliveries. Use /gaai-discover to create stories for the backlog.${NC}" |
399 | 558 | sleep 5 |
400 | 559 | continue |
401 | 560 | fi |
402 | 561 |
|
403 | | - for story_id in $active_sessions; do |
404 | | - log_file="$LOG_DIR/${story_id}.log" |
405 | | - echo "── $story_id ──" |
406 | | - parse_log "$log_file" "$story_id" |
| 562 | + for story_id in "${active_ids[@]}"; do |
| 563 | + # Determine pipeline |
| 564 | + pipeline=$(awk -v id="$story_id" ' |
| 565 | + $0 == "- id: " id { found=1; next } |
| 566 | + found && /^- id:/ { exit } |
| 567 | + found && /^[[:space:]]+delivery_pipeline:/ { |
| 568 | + gsub(/^[[:space:]]+delivery_pipeline:[[:space:]]*/, "") |
| 569 | + gsub(/[[:space:]]*/, ""); print; exit |
| 570 | + } |
| 571 | + ' "$BACKLOG" 2>/dev/null || true) |
| 572 | + |
| 573 | + if [[ "$pipeline" == "3phase" ]]; then |
| 574 | + # AC1: marker-based phase detection |
| 575 | + phase_label=$(detect_phase_3phase "$story_id") |
| 576 | + # AC2: per-phase log path resolution |
| 577 | + log_path=$(resolve_3phase_log "$story_id") |
| 578 | + if [[ "$log_path" == "[no log yet]" || "$log_path" == "[?]" ]]; then |
| 579 | + echo "── $story_id ── [${phase_label}]" |
| 580 | + echo -e " ${DIM}${log_path}${NC}" |
| 581 | + echo "" |
| 582 | + continue |
| 583 | + fi |
| 584 | + echo "── $story_id ── [${phase_label}]" |
| 585 | + parse_log "$log_path" "$story_id" "3phase" "$phase_label" |
| 586 | + else |
| 587 | + # Legacy: unchanged path |
| 588 | + log_path="$LOG_DIR/${story_id}.log" |
| 589 | + echo "── $story_id ──" |
| 590 | + parse_log "$log_path" "$story_id" "legacy" "" |
| 591 | + fi |
407 | 592 | echo "" |
408 | 593 | done |
409 | 594 |
|
410 | 595 | sleep 5 |
411 | 596 | done |
| 597 | +fi |
0 commit comments