@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.21

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.
@@ -1,18 +1,11 @@
1
1
  #!/bin/bash
2
- # harness-studio-v4.sh — Harness Studio v4: Parallel Agent Teams
2
+ # harness-studio-v4.sh — Harness Studio v4
3
3
  #
4
- # ┌──────────────────────┬─────────────────────────┐
5
- # │ Dashboard Team 1 (worker log)
6
- # │ (Queue + Teams + ├─────────────────────────┤
7
- # │ Feature status) │ Team 2 (worker log)
8
- # ├──────────────────────┤ │
9
- # │ Control ├─────────────────────────┤
10
- # │ harness> _ │ Team 3 (worker log) │
11
- # └──────────────────────┴─────────────────────────┘
12
- #
13
- # Usage:
14
- # bash scripts/harness-studio-v4.sh [project-root]
15
- # bash scripts/harness-studio-v4.sh --kill
4
+ # ┌──────────────┬──────────────┬──────────────┐
5
+ # │ Progress Team 1
6
+ # │ Main ├──────────────┤ Team 2 │
7
+ # │ (claude) │ Prompts Team 3
8
+ # └──────────────┴──────────────┴──────────────┘
16
9
 
17
10
  set -euo pipefail
18
11
 
@@ -20,17 +13,10 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
13
  SESSION_NAME="harness-v4"
21
14
 
22
15
  PROJECT_ROOT=""
23
- KILL=false
24
-
25
16
  for arg in "$@"; do
26
17
  case "$arg" in
27
- --kill)
28
- tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Killed." || echo "No session."
29
- exit 0
30
- ;;
31
- *)
32
- if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
33
- ;;
18
+ --kill) tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Killed." || echo "No session."; exit 0 ;;
19
+ *) if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi ;;
34
20
  esac
35
21
  done
36
22
 
@@ -43,73 +29,87 @@ if [ -z "$PROJECT_ROOT" ]; then
43
29
  fi
44
30
 
45
31
  if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
46
- echo "Error: .harness/ not found."
47
- exit 1
32
+ echo "Error: .harness/ not found."; exit 1
48
33
  fi
49
34
 
35
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
50
36
  echo "Project: $PROJECT_ROOT"
51
- echo "Session: $SESSION_NAME"
52
37
 
38
+ tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
39
+ sleep 1
40
+ # Ensure no leftover session
53
41
  tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
54
42
 
55
- # ── Initialize queue if not exists ──
43
+ # ── Queue ──
56
44
  QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
57
45
  if [ ! -f "$QUEUE" ]; then
58
- echo "Initializing feature queue..."
59
46
  bash "$SCRIPT_DIR/harness-queue-manager.sh" init "$PROJECT_ROOT"
47
+ else
48
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT"
60
49
  fi
61
50
 
62
51
  # ══════════════════════════════════════════
63
- # Build 5-pane layout using explicit pane IDs
52
+ # Layout: use PANE IDs (not indices!) to avoid renumbering issues
53
+ #
54
+ # 1. MAIN
55
+ # 2. split-h MAIN → MAIN | RIGHT (RIGHT = pane ID captured)
56
+ # 3. split-h RIGHT → MID | RIGHT (use -P to capture MID ID, RIGHT stays)
57
+ # 4. split-v RIGHT → T1 | T2_AREA (RIGHT becomes T1, T2_AREA captured)
58
+ # 5. split-v T2_AREA → T2 | T3 (T2_AREA becomes T2, T3 captured)
59
+ # 6. split-v MID → PROGRESS | PROMPTS (MID becomes PROGRESS, PROMPTS captured)
64
60
  # ══════════════════════════════════════════
65
61
 
66
- # 1. Dashboard (top-left)
67
- PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50 \
68
- -P -F '#{pane_id}' \
69
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
70
-
71
- # 2. Team 1 (top-right)
72
- PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
73
- -P -F '#{pane_id}' \
74
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'")
75
-
76
- # 3. Control (bottom-left, 25% of left)
77
- PANE_CTRL=$(tmux split-window -v -p 25 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
78
- -P -F '#{pane_id}' \
79
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-control-v4.sh\" \"${PROJECT_ROOT}\"'")
80
-
81
- # 4. Team 2 (middle-right, split from Team 1)
82
- PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
83
- -P -F '#{pane_id}' \
84
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
85
-
86
- # 5. Team 3 (bottom-right, split from Team 2)
87
- PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
88
- -P -F '#{pane_id}' \
89
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
90
-
91
- # ── Pane titles ──
92
- tmux select-pane -t "$PANE_DASH" -T "Dashboard"
93
- tmux select-pane -t "$PANE_CTRL" -T "Control"
94
- tmux select-pane -t "$PANE_T1" -T "Team 1"
95
- tmux select-pane -t "$PANE_T2" -T "Team 2"
96
- tmux select-pane -t "$PANE_T3" -T "Team 3"
62
+ # 1. MAIN
63
+ P_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
64
+ -P -F '#{pane_id}')
65
+
66
+ # 2. RIGHT column (66% of total)
67
+ P_RIGHT=$(tmux split-window -h -p 66 -t "$P_MAIN" -c "$PROJECT_ROOT" \
68
+ -P -F '#{pane_id}')
69
+
70
+ # 3. MID column split RIGHT, new pane goes LEFT of RIGHT (using -b flag)
71
+ P_MID=$(tmux split-window -hb -p 50 -t "$P_RIGHT" -c "$PROJECT_ROOT" \
72
+ -P -F '#{pane_id}')
73
+ # P_MID = left half (mid column), P_RIGHT = right half (teams column)
74
+
75
+ # 4. T1/T2 area split RIGHT vertically (66% goes to new pane below)
76
+ P_T2_AREA=$(tmux split-window -v -p 66 -t "$P_RIGHT" -c "$PROJECT_ROOT" \
77
+ -P -F '#{pane_id}')
78
+ P_T1="$P_RIGHT"
79
+ # P_T1 = top 34%, P_T2_AREA = bottom 66%
80
+
81
+ # 5. T2/T3 — split T2_AREA (50/50)
82
+ P_T3=$(tmux split-window -v -p 50 -t "$P_T2_AREA" -c "$PROJECT_ROOT" \
83
+ -P -F '#{pane_id}')
84
+ P_T2="$P_T2_AREA"
85
+
86
+ # 6. Progress/Prompts — split MID vertically
87
+ P_PROMPTS=$(tmux split-window -v -p 40 -t "$P_MID" -c "$PROJECT_ROOT" \
88
+ -P -F '#{pane_id}')
89
+ P_PROGRESS="$P_MID"
90
+
91
+ # ── Commands (using pane IDs stable regardless of index renumbering) ──
92
+ tmux send-keys -t "$P_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
93
+ tmux send-keys -t "$P_PROGRESS" "exec bash '${SCRIPT_DIR}/harness-dashboard-v4.sh' '${PROJECT_ROOT}'" Enter
94
+ tmux send-keys -t "$P_PROMPTS" "exec bash '${SCRIPT_DIR}/harness-prompts-v4.sh' '${PROJECT_ROOT}'" Enter
95
+ tmux send-keys -t "$P_T1" "exec bash '${SCRIPT_DIR}/harness-team-worker.sh' 1 '${PROJECT_ROOT}'" Enter
96
+ tmux send-keys -t "$P_T2" "exec bash '${SCRIPT_DIR}/harness-team-worker.sh' 2 '${PROJECT_ROOT}'" Enter
97
+ tmux send-keys -t "$P_T3" "exec bash '${SCRIPT_DIR}/harness-team-worker.sh' 3 '${PROJECT_ROOT}'" Enter
98
+
99
+ # ── Titles ──
100
+ tmux select-pane -t "$P_MAIN" -T "Main"
101
+ tmux select-pane -t "$P_PROGRESS" -T "Progress"
102
+ tmux select-pane -t "$P_PROMPTS" -T "Prompts"
103
+ tmux select-pane -t "$P_T1" -T "Team 1"
104
+ tmux select-pane -t "$P_T2" -T "Team 2"
105
+ tmux select-pane -t "$P_T3" -T "Team 3"
97
106
 
98
107
  tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
99
108
  tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
109
+ tmux select-pane -t "$P_MAIN"
100
110
 
101
- # Focus Control
102
- tmux select-pane -t "$PANE_CTRL"
103
-
104
- # Attach
105
111
  if [ -n "${TMUX:-}" ]; then
106
112
  tmux switch-client -t "$SESSION_NAME"
107
113
  else
108
- echo ""
109
- echo "Launching Harness Studio v4..."
110
- echo " Dashboard (left↑) : Feature Queue + Team status"
111
- echo " Control (left↓) : start/pause/assign/requeue"
112
- echo " Team 1-3 (right) : Parallel worker logs"
113
- echo ""
114
114
  tmux attach -t "$SESSION_NAME"
115
115
  fi
@@ -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,111 +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
- gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" --model "$GEN_MODEL" --output-format text 2>&1) || true
328
+ gen_output=$(cd "$WORKTREE_DIR" && claude -p "$gen_prompt" \
329
+ --dangerously-skip-permissions \
330
+ --model "$GEN_MODEL" \
331
+ --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
284
332
  gen_elapsed=$(( $(date +%s) - gen_start ))
285
333
 
286
- 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 ' ')
287
335
  log "Gen done (${gen_elapsed}s) — ${files_changed} files"
288
- 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"
289
337
 
290
- # Auto-commit
291
- (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
292
340
 
293
- # ── Pre-eval gate ──
294
- log "Pre-eval gate..."
341
+ # ── Pre-eval gate (in worktree) ──
342
+ log "Gate..."
295
343
  bash "$QUEUE_MGR" update_phase "$feature_id" "gate" "$attempt" "$PROJECT_ROOT" 2>/dev/null
296
344
 
297
- if ! run_pre_eval_gate "$feature_id"; then
298
- log "${RED}Gate FAIL — retrying gen${RESET}"
299
- 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."
300
348
  attempt=$((attempt + 1))
301
349
  continue
302
350
  fi
303
351
 
304
- # ── Evaluate ──
305
- log "Eval ${feature_id} (${EVAL_MODEL})..."
352
+ # ── Evaluate (in worktree) ──
353
+ log "Eval (${EVAL_MODEL})..."
306
354
  bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
307
355
 
308
356
  eval_prompt=$(build_eval_prompt "$feature_id")
309
357
 
310
358
  eval_start=$(date +%s)
311
- eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" --model "$EVAL_MODEL" --output-format text 2>&1) || true
359
+ eval_output=$(cd "$WORKTREE_DIR" && claude -p "$eval_prompt" \
360
+ --dangerously-skip-permissions \
361
+ --model "$EVAL_MODEL" \
362
+ --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
312
363
  eval_elapsed=$(( $(date +%s) - eval_start ))
313
364
 
314
- # Parse result
315
365
  result_line=$(parse_eval_result "$eval_output")
316
366
  verdict=$(echo "$result_line" | cut -d'|' -f1)
317
367
  score=$(echo "$result_line" | cut -d'|' -f2)
318
368
  feedback=$(echo "$result_line" | cut -d'|' -f3-)
319
369
 
320
- log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
370
+ log_progress "eval" "${feature_id} #${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
321
371
 
322
372
  if [ "$verdict" = "PASS" ]; then
323
- 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)"
324
374
  passed=true
325
375
  break
326
376
  else
327
- log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
377
+ log "${RED}✗ FAIL ${score}/3.00${RESET} (${eval_elapsed}s)"
328
378
  log "${DIM} ${feedback}${RESET}"
329
379
  eval_feedback="$feedback"
330
380
  attempt=$((attempt + 1))
331
381
  fi
332
382
  done
333
383
 
334
- # ══════════════════════════════════════════
335
- # Phase 3: Branch merge with conflict handling
336
- # ══════════════════════════════════════════
384
+ # ── Result ──
337
385
  if [ "$passed" = true ]; then
338
- log "Merging ${branch} → main..."
339
- acquire_git_lock
340
-
341
- merge_ok=false
342
-
343
- # Attempt 1: straight merge
344
- if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
345
- merge_ok=true
346
- else
347
- # Attempt 2: abort failed merge, rebase, re-eval gate, then merge
348
- log "${YELLOW}Conflict detected — rebasing ${branch} onto main...${RESET}"
349
- (cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
350
- (cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
351
-
352
- if (cd "$PROJECT_ROOT" && git rebase main 2>/dev/null); then
353
- log "Rebase OK. Re-running gate..."
354
-
355
- if run_pre_eval_gate "$feature_id"; then
356
- log "Gate still PASS after rebase."
357
- 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
358
- merge_ok=true
359
- fi
360
- else
361
- log "${RED}Gate FAIL after rebase — needs re-gen${RESET}"
362
- fi
363
- else
364
- log "${RED}Rebase failed — conflicts too complex${RESET}"
365
- (cd "$PROJECT_ROOT" && git rebase --abort 2>/dev/null) || true
366
- fi
367
- fi
368
-
369
- release_git_lock
370
-
371
- if [ "$merge_ok" = true ]; then
372
- # Clean up feature branch
373
- (cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
386
+ log "Merging → main..."
374
387
 
388
+ if merge_to_main "$branch"; then
389
+ # Cleanup worktree after successful merge
390
+ cleanup_worktree
375
391
  bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
376
- log_progress "pass" "${feature_id} merged to main"
392
+ log_progress "pass" "${feature_id} merged & cleaned"
377
393
 
378
- # Update feature-list.json passes
394
+ # Update feature-list.json
379
395
  if [ -f "$FEATURES" ]; then
380
396
  jq --arg fid "$feature_id" '
381
397
  .features |= map(
@@ -388,18 +404,15 @@ while true; do
388
404
 
389
405
  log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
390
406
  else
391
- log "${RED}${BOLD}Merge failed — ${feature_id} marked as failed${RESET}"
392
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
407
+ cleanup_worktree
393
408
  bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
409
+ log "${RED}Merge failed → ${feature_id} FAILED${RESET}"
394
410
  log_progress "merge-fail" "${feature_id}"
395
411
  fi
396
-
397
412
  else
398
- log "${RED}${BOLD}✗ ${feature_id} FAILED after ${MAX_ATTEMPTS} attempts${RESET}"
399
- acquire_git_lock
400
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
401
- release_git_lock
413
+ cleanup_worktree
402
414
  bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
415
+ log "${RED}${BOLD}✗ ${feature_id} FAILED (${MAX_ATTEMPTS} attempts)${RESET}"
403
416
  log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
404
417
  fi
405
418
 
@@ -51,7 +51,37 @@ current_agent=${CURRENT_AGENT} (running) 인데 /harness-${REQUESTED_SKILL} 호
51
51
  fi
52
52
  fi
53
53
 
54
- # ── Compact context 주입 ──
54
+ # ── v4 Parallel Mode 감지 ──
55
+ FEATURE_QUEUE="$CWD/.harness/actions/feature-queue.json"
56
+ if [ -f "$FEATURE_QUEUE" ]; then
57
+ V4_PASSED=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
58
+ V4_TOTAL=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
59
+ V4_FAILED=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
60
+
61
+ # Log user prompt to progress.log (truncated to 80 chars)
62
+ PROGRESS_LOG="$CWD/.harness/progress.log"
63
+ if [ -n "$PROMPT" ] && [ -f "$PROGRESS_LOG" ]; then
64
+ PROMPT_SHORT=$(echo "$PROMPT" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-80)
65
+ # Skip logging for empty or very short prompts
66
+ if [ ${#PROMPT_SHORT} -gt 2 ]; then
67
+ echo "$(date +"%Y-%m-%d %H:%M") | user-prompt | input | ${PROMPT_SHORT}" >> "$PROGRESS_LOG"
68
+ fi
69
+ fi
70
+
71
+ cat <<EOF
72
+ [harness-v4] ${V4_PASSED}/${V4_TOTAL} features passed | ${V4_FAILED} failed
73
+
74
+ ## v4 Parallel Mode Active
75
+ - 3 Agent Teams이 feature-queue에서 자율적으로 Gen→Eval 루프 실행 중
76
+ - 당신은 **오케스트레이터** 역할: 대시보드 모니터링, 실패 대응, 수동 개입
77
+ - /harness-generator-*, /harness-evaluator-* 스킬 호출 금지 (Teams가 처리)
78
+ - 할 수 있는 것: 코드 리뷰, 아키텍처 결정, failed feature 분석, requeue 판단
79
+ - skip: "harness skip" 시 일반 대화
80
+ EOF
81
+ exit 0
82
+ fi
83
+
84
+ # ── Compact context 주입 (v3 mode) ──
55
85
  cat <<EOF
56
86
  [harness] S${SPRINT_NUM} | ${PIPELINE} | agent=${CURRENT_AGENT} (${AGENT_STATUS}) | next=${NEXT_AGENT}
57
87
  ${CONTEXT_WARNING}