@walwal-harness/cli 4.0.0-alpha.12 → 4.0.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ # For consumer projects, add .worktrees/ to .gitignore
package/bin/init.js CHANGED
@@ -231,6 +231,7 @@ function installScripts() {
231
231
  'harness-control-v4.sh',
232
232
  'harness-queue-manager.sh',
233
233
  'harness-team-worker.sh',
234
+ 'harness-prompts-v4.sh',
234
235
  ]);
235
236
 
236
237
  if (fs.existsSync(scriptsSrc)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "4.0.0-alpha.12",
3
+ "version": "4.0.0-alpha.14",
4
4
  "description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # harness-dashboard-v4.sh — v4 Dashboard: Feature Queue + Team Status
3
- # Auto-refresh 3초 간격. feature-queue.json + feature-list.json 시각화.
2
+ # harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
3
+ # Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
4
4
 
5
5
  set -uo pipefail
6
6
 
@@ -30,42 +30,34 @@ render_header() {
30
30
  now=$(date +"%H:%M:%S")
31
31
  project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
32
32
 
33
- echo -e "${BOLD}╔════════════════════════════════════════════════╗${RESET}"
34
- echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
35
- echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
36
- echo -e " ${DIM}${project_name} | ${now}${RESET}"
37
- echo ""
33
+ echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
38
34
  }
39
35
 
40
36
  render_queue_summary() {
41
37
  if [ ! -f "$QUEUE" ]; then
42
- echo -e " ${DIM}(queue not initialized — run 'init' in Control)${RESET}"
38
+ echo -e " ${DIM}(queue not initialized)${RESET}"
43
39
  return
44
40
  fi
45
41
 
46
- local ready blocked in_prog passed failed total concurrency
42
+ local ready blocked in_prog passed failed total
47
43
  ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
48
44
  blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
49
45
  in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
50
46
  passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
51
47
  failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
52
- concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
53
48
  ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
54
49
  total=$((ready + blocked + in_prog + passed + failed))
55
50
 
56
- # Progress bar
57
51
  local pct=0
58
52
  if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
59
- local bar_w=20
53
+ local bar_w=16
60
54
  local filled=$(( pct * bar_w / 100 ))
61
55
  local empty=$(( bar_w - filled ))
62
56
  local bar=""
63
57
  for ((i=0; i<filled; i++)); do bar+="█"; done
64
58
  for ((i=0; i<empty; i++)); do bar+="░"; done
65
59
 
66
- echo -e " ${BOLD}Queue${RESET} ${bar} ${passed}/${total} (${pct}%) ${DIM}concurrency=${concurrency}${RESET}"
67
- echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
68
- echo ""
60
+ echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
69
61
  }
70
62
 
71
63
  render_teams() {
@@ -75,49 +67,44 @@ render_teams() {
75
67
  team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
76
68
  if [ "${team_count:-0}" -eq 0 ]; then return; fi
77
69
 
78
- echo -e " ${BOLD}Teams${RESET}"
79
-
80
70
  for i in $(seq 1 "$team_count"); do
81
71
  local t_status t_feature t_phase t_attempt
82
72
  t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
83
73
  t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
84
74
 
85
- # Get phase from in_progress
86
75
  if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
87
76
  t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
88
77
  t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
89
78
  else
90
- t_phase="—"
91
- t_attempt="—"
79
+ t_phase="—"; t_attempt=""
92
80
  fi
93
81
 
94
82
  local icon color
95
83
  case "$t_status" in
96
- busy) icon="▶" ; color="$GREEN" ;;
97
- idle) icon="○" ; color="$DIM" ;;
98
- paused) icon="" ; color="$YELLOW" ;;
99
- *) icon="?" ; color="$RESET" ;;
84
+ busy) icon="▶"; color="$GREEN" ;;
85
+ idle) icon="○"; color="$DIM" ;;
86
+ *) icon="?"; color="$RESET" ;;
100
87
  esac
101
88
 
102
- local phase_display=""
89
+ local phase_short=""
103
90
  case "$t_phase" in
104
- gen) phase_display="${CYAN}GEN${RESET}" ;;
105
- gate) phase_display="${YELLOW}GATE${RESET}" ;;
106
- eval) phase_display="${MAGENTA}EVAL${RESET}" ;;
107
- *) phase_display="${DIM}${t_phase}${RESET}" ;;
91
+ gen) phase_short="${CYAN}G${RESET}" ;;
92
+ gate) phase_short="${YELLOW}K${RESET}" ;;
93
+ eval) phase_short="${MAGENTA}E${RESET}" ;;
94
+ *) phase_short="${DIM}-${RESET}" ;;
108
95
  esac
109
96
 
110
- printf " %b %b Team %d %-8s %b attempt %s\n" "$color" "$icon" "$i" "$t_feature" "$phase_display" "$t_attempt"
97
+ printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
98
+ if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
99
+ printf " #%s" "$t_attempt"
100
+ fi
101
+ echo ""
111
102
  done
112
- echo ""
113
103
  }
114
104
 
115
- render_feature_list() {
105
+ render_features() {
116
106
  if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
117
107
 
118
- echo -e " ${BOLD}Features${RESET}"
119
-
120
- # Single jq call: merge feature-list + queue state → pre-formatted lines
121
108
  jq -r --slurpfile q "$QUEUE" '
122
109
  ($q[0].queue.passed // []) as $passed |
123
110
  ($q[0].queue.failed // []) as $failed |
@@ -125,7 +112,7 @@ render_feature_list() {
125
112
  ($q[0].queue.in_progress // {}) as $prog |
126
113
  .features[] |
127
114
  .id as $fid |
128
- (.name // .description // "?" | if length > 22 then .[0:20] + ".." else . end) as $fname |
115
+ (.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
129
116
  (if ($fid | IN($passed[])) then "P"
130
117
  elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
131
118
  elif ($fid | IN($failed[])) then "F"
@@ -138,100 +125,25 @@ render_feature_list() {
138
125
  F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
139
126
  R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
140
127
  B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
141
- I\|*) # in_progress: extract team and phase
142
- team=$(echo "$st" | cut -d'|' -f2)
128
+ I\|*) team=$(echo "$st" | cut -d'|' -f2)
143
129
  phase=$(echo "$st" | cut -d'|' -f3)
144
- printf " ${CYAN}◐${RESET} %-6s %-18s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
130
+ printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
145
131
  *) printf " ? %-6s %s\n" "$fid" "$fname" ;;
146
132
  esac
147
133
  done
148
-
149
- echo ""
150
- }
151
-
152
- render_user_prompts() {
153
- local log_file="$PROJECT_ROOT/.harness/progress.log"
154
- if [ ! -f "$log_file" ]; then return; fi
155
-
156
- # Extract user-prompt entries (newest first, max 5)
157
- local user_lines
158
- user_lines=$(grep 'user-prompt' "$log_file" 2>/dev/null | tail -r 2>/dev/null | head -5)
159
-
160
- if [ -z "$user_lines" ]; then return; fi
161
-
162
- echo -e " ${BOLD}Manual Prompts${RESET} ${DIM}(newest first)${RESET}"
163
-
164
- echo "$user_lines" | while IFS= read -r line; do
165
- local ts detail
166
- ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
167
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
168
-
169
- # Short timestamp (HH:MM or MM-DD HH:MM)
170
- local short_ts
171
- short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
172
-
173
- if [ ${#detail} -gt 55 ]; then detail="${detail:0:53}.."; fi
174
-
175
- echo -e " ${BOLD}★${RESET} ${DIM}${short_ts}${RESET} ${detail}"
176
- done
177
-
178
- echo ""
179
- }
180
-
181
- render_team_activity() {
182
- local log_file="$PROJECT_ROOT/.harness/progress.log"
183
- if [ ! -f "$log_file" ]; then return; fi
184
-
185
- local max_lines=8
186
-
187
- echo -e " ${BOLD}Activity${RESET} ${DIM}(newest first)${RESET}"
188
-
189
- # All non-user-prompt entries, newest first
190
- grep -v '^#' "$log_file" 2>/dev/null | grep -v '^$' | grep -v 'user-prompt' | \
191
- tail -r 2>/dev/null | head -"$max_lines" | \
192
- while IFS= read -r line; do
193
- local ts agent action detail
194
- ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
195
- agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
196
- action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
197
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
198
-
199
- local short_ts icon color
200
- short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
201
-
202
- case "$agent" in
203
- dispatcher*) icon="▸"; color="$MAGENTA" ;;
204
- team-*) icon="⚡"; color="$CYAN" ;;
205
- manual) icon="★"; color="$BOLD" ;;
206
- planner*) icon="□"; color="$YELLOW" ;;
207
- gen*) icon="▶"; color="$GREEN" ;;
208
- eval*) icon="✦"; color="$RED" ;;
209
- system) icon="⚙"; color="$DIM" ;;
210
- *) icon="·"; color="$DIM" ;;
211
- esac
212
-
213
- if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
214
-
215
- echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
216
- done
217
-
218
- echo ""
219
134
  }
220
135
 
221
136
  render_all() {
222
137
  render_header
223
138
  render_queue_summary
224
139
  render_teams
225
- render_user_prompts
226
- render_team_activity
227
- render_feature_list
228
- echo -e " ${DIM}Refreshing every 3s${RESET}"
140
+ echo ""
141
+ render_features
229
142
  }
230
143
 
231
144
  # ── Main loop ──
232
145
  tput civis 2>/dev/null
233
146
  trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
234
-
235
147
  clear
236
148
 
237
149
  while true; do
@@ -0,0 +1,106 @@
1
+ #!/bin/bash
2
+ # harness-prompts-v4.sh — v4 Dashboard 하단: Manual Prompts + Activity
3
+ # progress.log에서 user-prompt와 team 활동을 newest-first로 표시.
4
+ # 스크롤 가능 영역 — 프롬프트가 많아져도 상단 Progress를 가리지 않음.
5
+
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+
10
+ PROJECT_ROOT="${1:-}"
11
+ if [ -z "$PROJECT_ROOT" ]; then
12
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
13
+ PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[prompts] .harness/ not found."; exit 1; }
14
+ fi
15
+
16
+ LOG_FILE="$PROJECT_ROOT/.harness/progress.log"
17
+
18
+ BOLD="\033[1m"
19
+ DIM="\033[2m"
20
+ GREEN="\033[32m"
21
+ YELLOW="\033[33m"
22
+ RED="\033[31m"
23
+ CYAN="\033[36m"
24
+ MAGENTA="\033[35m"
25
+ RESET="\033[0m"
26
+
27
+ LAST_LINE_COUNT=0
28
+
29
+ render_prompts() {
30
+ if [ ! -f "$LOG_FILE" ]; then
31
+ echo -e " ${DIM}(progress.log not found)${RESET}"
32
+ return
33
+ fi
34
+
35
+ local term_h
36
+ term_h=$(tput lines 2>/dev/null || echo 20)
37
+ local max_lines=$((term_h - 3)) # Leave room for header
38
+ if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
39
+
40
+ echo -e "${BOLD}Prompts & Activity${RESET} ${DIM}(newest first)${RESET}"
41
+ echo ""
42
+
43
+ # Read all entries, reverse, limit to terminal height
44
+ grep -v '^#' "$LOG_FILE" 2>/dev/null | grep -v '^$' | \
45
+ tail -r 2>/dev/null | head -"$max_lines" | \
46
+ while IFS= read -r line; do
47
+ local ts agent action detail
48
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
49
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
50
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
51
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
52
+
53
+ local short_ts icon color
54
+ short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
55
+
56
+ case "$agent" in
57
+ user-prompt)
58
+ icon="★"; color="$BOLD"
59
+ # User prompts get full width, highlighted
60
+ if [ ${#detail} -gt 60 ]; then detail="${detail:0:58}.."; fi
61
+ echo -e " ${color}${icon} ${short_ts}${RESET} ${detail}"
62
+ ;;
63
+ dispatcher*)
64
+ icon="▸"; color="$MAGENTA"
65
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
66
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
67
+ ;;
68
+ team-*)
69
+ icon="⚡"; color="$CYAN"
70
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
71
+ # Color by action type
72
+ case "$action" in
73
+ *pass*) color="$GREEN"; icon="✓" ;;
74
+ *fail*) color="$RED"; icon="✗" ;;
75
+ *eval*) color="$MAGENTA"; icon="✦" ;;
76
+ *gen*) color="$CYAN"; icon="▶" ;;
77
+ *merge*) color="$GREEN"; icon="⊕" ;;
78
+ esac
79
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
80
+ ;;
81
+ manual)
82
+ icon="★"; color="$YELLOW"
83
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
84
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${detail}"
85
+ ;;
86
+ *)
87
+ icon="·"; color="$DIM"
88
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
89
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
90
+ ;;
91
+ esac
92
+ done
93
+ }
94
+
95
+ # ── Main loop ──
96
+ tput civis 2>/dev/null
97
+ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
98
+ clear
99
+
100
+ while true; do
101
+ buf=$(render_prompts 2>&1)
102
+ tput cup 0 0 2>/dev/null
103
+ echo "$buf"
104
+ tput ed 2>/dev/null
105
+ sleep 3
106
+ done
@@ -72,11 +72,16 @@ fi
72
72
  PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
73
73
  -P -F '#{pane_id}')
74
74
 
75
- # Column 2: Dashboard (split right from Main, 66% remaining → 33% each of 3 cols)
75
+ # Column 2: Dashboard Progress (split right from Main, 66% remaining)
76
76
  PANE_DASH=$(tmux split-window -h -p 66 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
77
77
  -P -F '#{pane_id}' \
78
78
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
79
79
 
80
+ # Dashboard bottom: Prompts & Activity (split below Dashboard, 40% bottom)
81
+ PANE_PROMPTS=$(tmux split-window -v -p 40 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
82
+ -P -F '#{pane_id}' \
83
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompts-v4.sh\" \"${PROJECT_ROOT}\"'")
84
+
80
85
  # Column 3: Team 1 (split right from Dashboard, 50% of remaining = 33% total)
81
86
  PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
82
87
  -P -F '#{pane_id}' \
@@ -96,11 +101,12 @@ PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
96
101
  tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
97
102
 
98
103
  # ── Pane titles ──
99
- tmux select-pane -t "$PANE_MAIN" -T "Main"
100
- tmux select-pane -t "$PANE_DASH" -T "Dashboard"
101
- tmux select-pane -t "$PANE_T1" -T "Team 1"
102
- tmux select-pane -t "$PANE_T2" -T "Team 2"
103
- tmux select-pane -t "$PANE_T3" -T "Team 3"
104
+ tmux select-pane -t "$PANE_MAIN" -T "Main"
105
+ tmux select-pane -t "$PANE_DASH" -T "Progress"
106
+ tmux select-pane -t "$PANE_PROMPTS" -T "Prompts"
107
+ tmux select-pane -t "$PANE_T1" -T "Team 1"
108
+ tmux select-pane -t "$PANE_T2" -T "Team 2"
109
+ tmux select-pane -t "$PANE_T3" -T "Team 3"
104
110
 
105
111
  tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
106
112
  tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
@@ -1,16 +1,10 @@
1
1
  #!/bin/bash
2
- # harness-team-worker.sh — Team Worker: Feature-level Gen→Eval loop (v4.0)
2
+ # harness-team-worker.sh — Team Worker v4: git worktree 격리 실행
3
3
  #
4
- # 1 Team = 1 프로세스. Feature Queue에서 feature를 꺼내
5
- # GenGate→Eval 루프를 claude -p 헤드리스로 자율 실행한다.
4
+ # Team 독립 worktree에서 작업하여 git 충돌 없이 병렬 실행.
5
+ # Feature PASS main merge worktree 정리.
6
6
  #
7
- # Usage:
8
- # bash scripts/harness-team-worker.sh <team_id> [project-root]
9
- #
10
- # Environment:
11
- # MAX_ATTEMPTS=3 Feature당 최대 Gen→Eval 시도 횟수
12
- # GEN_MODEL=sonnet Generator 모델
13
- # EVAL_MODEL=opus Evaluator 모델
7
+ # Usage: bash scripts/harness-team-worker.sh <team_id> [project-root]
14
8
 
15
9
  set -uo pipefail
16
10
 
@@ -39,10 +33,11 @@ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
39
33
  CONFIG="$PROJECT_ROOT/.harness/config.json"
40
34
  PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
41
35
  QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
42
-
43
- # ── Lock file for git operations (prevent race conditions between teams) ──
44
36
  GIT_LOCK="$PROJECT_ROOT/.harness/.git-lock"
45
37
 
38
+ # Worktree base directory
39
+ WORKTREE_DIR="$PROJECT_ROOT/.worktrees/team-${TEAM_ID}"
40
+
46
41
  MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
47
42
  GEN_MODEL="${GEN_MODEL:-sonnet}"
48
43
  EVAL_MODEL="${EVAL_MODEL:-opus}"
@@ -54,7 +49,6 @@ if [ -f "$CONFIG" ]; then
54
49
  if [ -n "$_em" ]; then EVAL_MODEL="$_em"; fi
55
50
  fi
56
51
 
57
- # ── ANSI helpers ──
58
52
  BOLD="\033[1m"
59
53
  DIM="\033[2m"
60
54
  GREEN="\033[32m"
@@ -64,42 +58,101 @@ CYAN="\033[36m"
64
58
  RESET="\033[0m"
65
59
 
66
60
  ts() { date +"%H:%M:%S"; }
61
+ log() { echo -e "[$(ts)] ${BOLD}T${TEAM_ID}${RESET} $*"; }
62
+ log_progress() { echo "$(date +"%Y-%m-%d %H:%M") | team-${TEAM_ID} | ${1} | ${2}" >> "$PROGRESS_LOG"; }
67
63
 
68
- log() {
69
- echo -e "[$(ts)] ${BOLD}T${TEAM_ID}${RESET} $*"
64
+ # ── Git lock ──
65
+ acquire_git_lock() {
66
+ local waited=0
67
+ while ! mkdir "$GIT_LOCK" 2>/dev/null; do
68
+ sleep 0.2
69
+ waited=$((waited + 1))
70
+ if [ "$waited" -ge 150 ]; then rm -rf "$GIT_LOCK"; mkdir "$GIT_LOCK" 2>/dev/null || true; break; fi
71
+ done
70
72
  }
73
+ release_git_lock() { rm -rf "$GIT_LOCK" 2>/dev/null || true; }
74
+
75
+ # ── Worktree management ──
76
+ setup_worktree() {
77
+ local branch="$1"
78
+
79
+ acquire_git_lock
80
+
81
+ # Clean previous worktree if exists
82
+ if [ -d "$WORKTREE_DIR" ]; then
83
+ (cd "$PROJECT_ROOT" && git worktree remove "$WORKTREE_DIR" --force 2>/dev/null) || rm -rf "$WORKTREE_DIR"
84
+ fi
85
+
86
+ # Create fresh worktree from main
87
+ (cd "$PROJECT_ROOT" && git worktree add "$WORKTREE_DIR" -b "$branch" main 2>/dev/null) || \
88
+ (cd "$PROJECT_ROOT" && git worktree add "$WORKTREE_DIR" "$branch" 2>/dev/null) || {
89
+ release_git_lock
90
+ log "${RED}Failed to create worktree${RESET}"
91
+ return 1
92
+ }
93
+
94
+ release_git_lock
71
95
 
72
- log_progress() {
73
- echo "$(date +"%Y-%m-%d") | team-${TEAM_ID} | ${1} | ${2}" >> "$PROGRESS_LOG"
96
+ # Copy .harness to worktree (symlink for shared state)
97
+ ln -sf "$PROJECT_ROOT/.harness" "$WORKTREE_DIR/.harness" 2>/dev/null || true
98
+
99
+ log "Worktree: ${WORKTREE_DIR}"
100
+ return 0
74
101
  }
75
102
 
76
- # ── Git lock — serialize git checkout/merge across teams ──
77
- acquire_git_lock() {
78
- local max_wait=60 waited=0
79
- while [ -f "$GIT_LOCK" ]; do
80
- sleep 1
81
- waited=$((waited + 1))
82
- if [ "$waited" -ge "$max_wait" ]; then
83
- log "${RED}Git lock timeout (${max_wait}s). Removing stale lock.${RESET}"
84
- rm -f "$GIT_LOCK"
85
- break
86
- fi
87
- done
88
- echo "T${TEAM_ID}" > "$GIT_LOCK"
103
+ cleanup_worktree() {
104
+ acquire_git_lock
105
+ if [ -d "$WORKTREE_DIR" ]; then
106
+ (cd "$PROJECT_ROOT" && git worktree remove "$WORKTREE_DIR" --force 2>/dev/null) || rm -rf "$WORKTREE_DIR"
107
+ fi
108
+ release_git_lock
89
109
  }
90
110
 
91
- release_git_lock() {
92
- rm -f "$GIT_LOCK"
111
+ merge_to_main() {
112
+ local branch="$1"
113
+
114
+ acquire_git_lock
115
+
116
+ local merge_ok=false
117
+
118
+ # Try merge
119
+ if (cd "$PROJECT_ROOT" && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
120
+ merge_ok=true
121
+ else
122
+ # Conflict → abort, then try rebase in worktree
123
+ (cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
124
+
125
+ log "${YELLOW}Merge conflict — rebasing in worktree...${RESET}"
126
+ if (cd "$WORKTREE_DIR" && git rebase main 2>/dev/null); then
127
+ # Retry merge after rebase
128
+ if (cd "$PROJECT_ROOT" && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null); then
129
+ merge_ok=true
130
+ fi
131
+ else
132
+ (cd "$WORKTREE_DIR" && git rebase --abort 2>/dev/null) || true
133
+ log "${RED}Rebase failed${RESET}"
134
+ fi
135
+ fi
136
+
137
+ # Clean up branch after merge
138
+ if [ "$merge_ok" = true ]; then
139
+ (cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
140
+ fi
141
+
142
+ release_git_lock
143
+
144
+ [ "$merge_ok" = true ]
93
145
  }
94
146
 
95
- # ── Pre-eval gate ──
147
+ # ── Pre-eval gate (runs in worktree) ──
96
148
  run_pre_eval_gate() {
97
- local cwd="$PROJECT_ROOT"
149
+ local work_dir="$WORKTREE_DIR"
98
150
 
151
+ # Resolve cwd within worktree
99
152
  if [ -f "$CONFIG" ]; then
100
153
  _cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
101
154
  if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
102
- cwd="$PROJECT_ROOT/$_cwd"
155
+ work_dir="$WORKTREE_DIR/$_cwd"
103
156
  fi
104
157
  fi
105
158
 
@@ -107,26 +160,24 @@ run_pre_eval_gate() {
107
160
  if [ -f "$CONFIG" ]; then
108
161
  mapfile -t checks < <(jq -r '.flow.pre_eval_gate.frontend_checks[]' "$CONFIG" 2>/dev/null)
109
162
  fi
110
-
111
163
  if [ ${#checks[@]} -eq 0 ]; then
112
164
  checks=("npx tsc --noEmit" "npx eslint src/")
113
165
  fi
114
166
 
115
- local all_pass=true fail_cmds=""
167
+ local all_pass=true
116
168
  for cmd in "${checks[@]}"; do
117
- if (cd "$cwd" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
169
+ if (cd "$work_dir" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
118
170
  log " ${GREEN}✓${RESET} $cmd"
119
171
  else
120
172
  log " ${RED}✗${RESET} $cmd"
121
173
  all_pass=false
122
- fail_cmds+="$cmd; "
123
174
  fi
124
175
  done
125
176
 
126
177
  [ "$all_pass" = true ]
127
178
  }
128
179
 
129
- # ── Build generator prompt ──
180
+ # ── Build prompts ──
130
181
  build_gen_prompt() {
131
182
  local fid="$1" attempt="$2" feedback="${3:-}"
132
183
 
@@ -161,7 +212,7 @@ RULES:
161
212
  - Implement ONLY this single feature
162
213
  - Do NOT modify code belonging to other features
163
214
  - Follow existing code patterns and CONVENTIONS.md
164
- - When done, stage and commit with: git add -A && git commit -m 'feat(${fid}): ${fname}'
215
+ - When done, stage and commit: git add -A && git commit -m 'feat(${fid}): ${fname}'
165
216
  PROMPT
166
217
 
167
218
  if [ "$attempt" -gt 1 ] && [ -n "$feedback" ]; then
@@ -175,7 +226,6 @@ RETRY
175
226
  fi
176
227
  }
177
228
 
178
- # ── Build evaluator prompt ──
179
229
  build_eval_prompt() {
180
230
  local fid="$1"
181
231
 
@@ -218,15 +268,12 @@ FEEDBACK: one paragraph summary
218
268
  PROMPT
219
269
  }
220
270
 
221
- # ── Parse eval result (macOS-compatible, no grep -P) ──
222
271
  parse_eval_result() {
223
272
  local output="$1"
224
-
225
273
  local verdict score feedback
226
274
  verdict=$(echo "$output" | grep -E '^VERDICT:' | sed 's/VERDICT:[[:space:]]*//' | head -1)
227
275
  score=$(echo "$output" | grep -E '^SCORE:' | sed 's/SCORE:[[:space:]]*//' | head -1)
228
276
  feedback=$(echo "$output" | grep -E '^FEEDBACK:' | sed 's/FEEDBACK:[[:space:]]*//' | head -1)
229
-
230
277
  echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
231
278
  }
232
279
 
@@ -237,14 +284,12 @@ log "${CYAN}Team ${TEAM_ID} started${RESET} (gen=${GEN_MODEL}, eval=${EVAL_MODEL
237
284
  log_progress "start" "Team ${TEAM_ID} worker started"
238
285
 
239
286
  while true; do
240
- # ── Dequeue next feature ──
287
+ # ── Dequeue ──
241
288
  feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
242
289
 
243
290
  if [ -z "$feature_id" ] || [[ "$feature_id" == "["* ]]; then
244
291
  log "${DIM}No features ready. Waiting 10s...${RESET}"
245
292
  sleep 10
246
-
247
- # Check if completely done
248
293
  remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null || echo "1")
249
294
  if [ "${remaining}" -eq 0 ] 2>/dev/null; then
250
295
  log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
@@ -254,16 +299,16 @@ while true; do
254
299
  continue
255
300
  fi
256
301
 
257
- log "${CYAN}▶ Dequeued ${feature_id}${RESET}"
302
+ log "${CYAN}▶ ${feature_id}${RESET}"
258
303
  log_progress "dequeue" "${feature_id}"
259
304
 
260
- # ── Create feature branch (with lock) ──
305
+ # ── Setup worktree ──
261
306
  branch="feature/${feature_id}"
262
- acquire_git_lock
263
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git checkout -b "$branch" 2>/dev/null) || \
264
- (cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
265
- release_git_lock
266
- log "Branch: ${branch}"
307
+ if ! setup_worktree "$branch"; then
308
+ bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
309
+ log_progress "fail" "${feature_id} worktree setup failed"
310
+ continue
311
+ fi
267
312
 
268
313
  # ── Gen→Eval Loop ──
269
314
  attempt=1
@@ -271,119 +316,82 @@ while true; do
271
316
  passed=false
272
317
 
273
318
  while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
274
- log "${BOLD}── Attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
319
+ log "${BOLD}── ${feature_id} attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
275
320
 
276
- # ── Generate ──
277
- log "Gen ${feature_id} (${GEN_MODEL})..."
321
+ # ── Generate (in worktree) ──
322
+ log "Gen (${GEN_MODEL})..."
278
323
  bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
279
324
 
280
325
  gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
281
326
 
282
327
  gen_start=$(date +%s)
283
- log "${DIM} claude -p --dangerously-skip-permissions --model ${GEN_MODEL}${RESET}"
284
- gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" \
328
+ gen_output=$(cd "$WORKTREE_DIR" && claude -p "$gen_prompt" \
285
329
  --dangerously-skip-permissions \
286
330
  --model "$GEN_MODEL" \
287
331
  --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
288
332
  gen_elapsed=$(( $(date +%s) - gen_start ))
289
333
 
290
- files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
334
+ files_changed=$(cd "$WORKTREE_DIR" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
291
335
  log "Gen done (${gen_elapsed}s) — ${files_changed} files"
292
- log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files, ${gen_elapsed}s"
336
+ log_progress "gen" "${feature_id} #${attempt}: ${files_changed} files, ${gen_elapsed}s"
293
337
 
294
- # Auto-commit
295
- (cd "$PROJECT_ROOT" && git add -A && git commit -m "feat(${feature_id}): gen attempt ${attempt}" --no-verify 2>/dev/null) || true
338
+ # Auto-commit in worktree
339
+ (cd "$WORKTREE_DIR" && git add -A && git commit -m "feat(${feature_id}): attempt ${attempt}" --no-verify 2>/dev/null) || true
296
340
 
297
- # ── Pre-eval gate ──
298
- log "Pre-eval gate..."
341
+ # ── Pre-eval gate (in worktree) ──
342
+ log "Gate..."
299
343
  bash "$QUEUE_MGR" update_phase "$feature_id" "gate" "$attempt" "$PROJECT_ROOT" 2>/dev/null
300
344
 
301
- if ! run_pre_eval_gate "$feature_id"; then
302
- log "${RED}Gate FAIL — retrying gen${RESET}"
303
- eval_feedback="Pre-eval gate failed: type check or lint errors. Fix compilation and lint issues."
345
+ if ! run_pre_eval_gate; then
346
+ log "${RED}Gate FAIL${RESET}"
347
+ eval_feedback="Pre-eval gate failed: type check or lint errors."
304
348
  attempt=$((attempt + 1))
305
349
  continue
306
350
  fi
307
351
 
308
- # ── Evaluate ──
309
- log "Eval ${feature_id} (${EVAL_MODEL})..."
352
+ # ── Evaluate (in worktree) ──
353
+ log "Eval (${EVAL_MODEL})..."
310
354
  bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
311
355
 
312
356
  eval_prompt=$(build_eval_prompt "$feature_id")
313
357
 
314
358
  eval_start=$(date +%s)
315
- log "${DIM} claude -p --dangerously-skip-permissions --model ${EVAL_MODEL}${RESET}"
316
- eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" \
359
+ eval_output=$(cd "$WORKTREE_DIR" && claude -p "$eval_prompt" \
317
360
  --dangerously-skip-permissions \
318
361
  --model "$EVAL_MODEL" \
319
362
  --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
320
363
  eval_elapsed=$(( $(date +%s) - eval_start ))
321
364
 
322
- # Parse result
323
365
  result_line=$(parse_eval_result "$eval_output")
324
366
  verdict=$(echo "$result_line" | cut -d'|' -f1)
325
367
  score=$(echo "$result_line" | cut -d'|' -f2)
326
368
  feedback=$(echo "$result_line" | cut -d'|' -f3-)
327
369
 
328
- log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
370
+ log_progress "eval" "${feature_id} #${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
329
371
 
330
372
  if [ "$verdict" = "PASS" ]; then
331
- log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
373
+ log "${GREEN}${BOLD}✓ PASS ${score}/3.00${RESET} (${eval_elapsed}s)"
332
374
  passed=true
333
375
  break
334
376
  else
335
- log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
377
+ log "${RED}✗ FAIL ${score}/3.00${RESET} (${eval_elapsed}s)"
336
378
  log "${DIM} ${feedback}${RESET}"
337
379
  eval_feedback="$feedback"
338
380
  attempt=$((attempt + 1))
339
381
  fi
340
382
  done
341
383
 
342
- # ══════════════════════════════════════════
343
- # Phase 3: Branch merge with conflict handling
344
- # ══════════════════════════════════════════
384
+ # ── Result ──
345
385
  if [ "$passed" = true ]; then
346
- log "Merging ${branch} → main..."
347
- acquire_git_lock
348
-
349
- merge_ok=false
350
-
351
- # Attempt 1: straight merge
352
- if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
353
- merge_ok=true
354
- else
355
- # Attempt 2: abort failed merge, rebase, re-eval gate, then merge
356
- log "${YELLOW}Conflict detected — rebasing ${branch} onto main...${RESET}"
357
- (cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
358
- (cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
359
-
360
- if (cd "$PROJECT_ROOT" && git rebase main 2>/dev/null); then
361
- log "Rebase OK. Re-running gate..."
362
-
363
- if run_pre_eval_gate "$feature_id"; then
364
- log "Gate still PASS after rebase."
365
- if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null); then
366
- merge_ok=true
367
- fi
368
- else
369
- log "${RED}Gate FAIL after rebase — needs re-gen${RESET}"
370
- fi
371
- else
372
- log "${RED}Rebase failed — conflicts too complex${RESET}"
373
- (cd "$PROJECT_ROOT" && git rebase --abort 2>/dev/null) || true
374
- fi
375
- fi
376
-
377
- release_git_lock
378
-
379
- if [ "$merge_ok" = true ]; then
380
- # Clean up feature branch
381
- (cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
386
+ log "Merging → main..."
382
387
 
388
+ if merge_to_main "$branch"; then
389
+ # Cleanup worktree after successful merge
390
+ cleanup_worktree
383
391
  bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
384
- log_progress "pass" "${feature_id} merged to main"
392
+ log_progress "pass" "${feature_id} merged & cleaned"
385
393
 
386
- # Update feature-list.json passes
394
+ # Update feature-list.json
387
395
  if [ -f "$FEATURES" ]; then
388
396
  jq --arg fid "$feature_id" '
389
397
  .features |= map(
@@ -396,18 +404,15 @@ while true; do
396
404
 
397
405
  log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
398
406
  else
399
- log "${RED}${BOLD}Merge failed — ${feature_id} marked as failed${RESET}"
400
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
407
+ cleanup_worktree
401
408
  bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
409
+ log "${RED}Merge failed → ${feature_id} FAILED${RESET}"
402
410
  log_progress "merge-fail" "${feature_id}"
403
411
  fi
404
-
405
412
  else
406
- log "${RED}${BOLD}✗ ${feature_id} FAILED after ${MAX_ATTEMPTS} attempts${RESET}"
407
- acquire_git_lock
408
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
409
- release_git_lock
413
+ cleanup_worktree
410
414
  bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
415
+ log "${RED}${BOLD}✗ ${feature_id} FAILED (${MAX_ATTEMPTS} attempts)${RESET}"
411
416
  log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
412
417
  fi
413
418