@walwal-harness/cli 4.0.0-alpha.1 → 4.0.0-alpha.10

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.
@@ -209,6 +209,17 @@
209
209
  "frontend_cwd": "",
210
210
  "timeout_seconds": 120,
211
211
  "on_fail": "reroute_to_generator"
212
+ },
213
+ "parallel": {
214
+ "comment": "v4 Parallel Agent Teams 설정. npx walwal-harness v4 로 실행.",
215
+ "enabled": false,
216
+ "concurrency": 3,
217
+ "max_attempts_per_feature": 3,
218
+ "gen_model": "sonnet",
219
+ "eval_model": "opus",
220
+ "branch_strategy": "feature-branch",
221
+ "merge_on_pass": true,
222
+ "rebase_on_conflict": true
212
223
  }
213
224
  },
214
225
  "evaluation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.10",
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"
@@ -44,18 +44,21 @@ render_queue_summary() {
44
44
  fi
45
45
 
46
46
  local ready blocked in_prog passed failed total concurrency
47
- ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null)
48
- blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null)
49
- in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
50
- passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null)
51
- failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null)
52
- concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null)
47
+ ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
48
+ blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
49
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
50
+ passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
51
+ failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
52
+ concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
53
+ ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
53
54
  total=$((ready + blocked + in_prog + passed + failed))
54
55
 
55
56
  # Progress bar
56
57
  local pct=0
57
58
  if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
58
- local bar_w=20 filled=$(( pct * bar_w / 100 )) empty=$(( bar_w - filled ))
59
+ local bar_w=20
60
+ local filled=$(( pct * bar_w / 100 ))
61
+ local empty=$(( bar_w - filled ))
59
62
  local bar=""
60
63
  for ((i=0; i<filled; i++)); do bar+="█"; done
61
64
  for ((i=0; i<empty; i++)); do bar+="░"; done
@@ -112,45 +115,106 @@ render_teams() {
112
115
  render_feature_list() {
113
116
  if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
114
117
 
115
- local total
116
- total=$(jq '.features | length' "$FEATURES" 2>/dev/null)
117
- if [ "${total:-0}" -eq 0 ]; then return; fi
118
-
119
118
  echo -e " ${BOLD}Features${RESET}"
120
119
 
121
- local i=0
122
- while [ "$i" -lt "$total" ]; do
123
- local fid fname status_icon
124
- fid=$(jq -r ".features[$i].id" "$FEATURES" 2>/dev/null)
125
- fname=$(jq -r ".features[$i].name // .features[$i].description // \"\"" "$FEATURES" 2>/dev/null)
126
- if [ ${#fname} -gt 22 ]; then fname="${fname:0:20}.."; fi
127
-
128
- # Determine status from queue
129
- local in_passed in_failed in_progress in_ready in_blocked
130
- in_passed=$(jq -r --arg f "$fid" '.queue.passed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
131
- in_failed=$(jq -r --arg f "$fid" '.queue.failed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
132
- in_progress=$(jq -r --arg f "$fid" '.queue.in_progress[$f] // empty' "$QUEUE" 2>/dev/null)
133
- in_ready=$(jq -r --arg f "$fid" '.queue.ready // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
134
-
135
- if [ "${in_passed:-0}" -gt 0 ]; then
136
- status_icon="${GREEN}●${RESET}"
137
- elif [ -n "$in_progress" ] && [ "$in_progress" != "" ]; then
138
- local team phase
139
- team=$(echo "$in_progress" | jq -r '.team // "?"' 2>/dev/null)
140
- phase=$(echo "$in_progress" | jq -r '.phase // "?"' 2>/dev/null)
141
- status_icon="${CYAN}◐${RESET} T${team}:${phase}"
142
- elif [ "${in_failed:-0}" -gt 0 ]; then
143
- status_icon="${RED}✗${RESET}"
144
- elif [ "${in_ready:-0}" -gt 0 ]; then
145
- status_icon="${YELLOW}○${RESET}"
146
- else
147
- status_icon="${DIM}◌${RESET}" # blocked
148
- fi
120
+ # Single jq call: merge feature-list + queue state → pre-formatted lines
121
+ jq -r --slurpfile q "$QUEUE" '
122
+ ($q[0].queue.passed // []) as $passed |
123
+ ($q[0].queue.failed // []) as $failed |
124
+ ($q[0].queue.ready // []) as $ready |
125
+ ($q[0].queue.in_progress // {}) as $prog |
126
+ .features[] |
127
+ .id as $fid |
128
+ (.name // .description // "?" | if length > 22 then .[0:20] + ".." else . end) as $fname |
129
+ (if ($fid | IN($passed[])) then "P"
130
+ elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
131
+ elif ($fid | IN($failed[])) then "F"
132
+ elif ($fid | IN($ready[])) then "R"
133
+ else "B" end) as $st |
134
+ "\($st)\t\($fid)\t\($fname)"
135
+ ' "$FEATURES" 2>/dev/null | while IFS=$'\t' read -r st fid fname; do
136
+ case "$st" in
137
+ P) printf " ${GREEN}●${RESET} %-6s %s\n" "$fid" "$fname" ;;
138
+ F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
139
+ R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
140
+ B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
141
+ I\|*) # in_progress: extract team and phase
142
+ team=$(echo "$st" | cut -d'|' -f2)
143
+ phase=$(echo "$st" | cut -d'|' -f3)
144
+ printf " ${CYAN}◐${RESET} %-6s %-18s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
145
+ *) printf " ? %-6s %s\n" "$fid" "$fname" ;;
146
+ esac
147
+ 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}"
149
163
 
150
- printf " %b %-6s %-24s\n" "$status_icon" "$fid" "$fname"
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}')
151
168
 
152
- i=$((i + 1))
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}"
153
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
+
154
218
  echo ""
155
219
  }
156
220
 
@@ -158,6 +222,8 @@ render_all() {
158
222
  render_header
159
223
  render_queue_summary
160
224
  render_teams
225
+ render_user_prompts
226
+ render_team_activity
161
227
  render_feature_list
162
228
  echo -e " ${DIM}Refreshing every 3s${RESET}"
163
229
  }
@@ -169,8 +235,7 @@ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
169
235
  clear
170
236
 
171
237
  while true; do
172
- local buf
173
- buf=$(render_all 2>/dev/null)
238
+ buf=$(render_all 2>&1)
174
239
  tput cup 0 0 2>/dev/null
175
240
  echo "$buf"
176
241
  tput ed 2>/dev/null
@@ -42,6 +42,25 @@ fi
42
42
  FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
43
43
  QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
44
44
  CONFIG="$PROJECT_ROOT/.harness/config.json"
45
+ QUEUE_LOCK="$PROJECT_ROOT/.harness/.queue-lock"
46
+
47
+ # ── Atomic queue lock — prevent race conditions between teams ──
48
+ acquire_queue_lock() {
49
+ local max_wait=30 waited=0
50
+ while ! mkdir "$QUEUE_LOCK" 2>/dev/null; do
51
+ sleep 0.1
52
+ waited=$((waited + 1))
53
+ if [ "$waited" -ge $((max_wait * 10)) ]; then
54
+ rm -rf "$QUEUE_LOCK"
55
+ mkdir "$QUEUE_LOCK" 2>/dev/null || true
56
+ break
57
+ fi
58
+ done
59
+ }
60
+
61
+ release_queue_lock() {
62
+ rm -rf "$QUEUE_LOCK" 2>/dev/null || true
63
+ }
45
64
 
46
65
  # ── Concurrency from config ──
47
66
  CONCURRENCY=3
@@ -122,12 +141,14 @@ cmd_dequeue() {
122
141
  if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
123
142
  if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
124
143
 
144
+ acquire_queue_lock
145
+
125
146
  local feature
126
147
  feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
127
148
 
128
149
  if [ -z "$feature" ]; then
150
+ release_queue_lock
129
151
  echo "[queue] No features in ready queue."
130
- # Check if all done
131
152
  local in_prog blocked
132
153
  in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
133
154
  blocked=$(jq '.queue.blocked | length' "$QUEUE")
@@ -137,13 +158,13 @@ cmd_dequeue() {
137
158
  return 1
138
159
  fi
139
160
 
140
- # Move feature from ready → in_progress, assign to team
141
161
  jq --arg fid "$feature" --arg tid "$team_id" '
142
162
  .queue.ready -= [$fid] |
143
163
  .queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
144
164
  .teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
145
165
  ' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
146
166
 
167
+ release_queue_lock
147
168
  echo "$feature"
148
169
  }
149
170
 
@@ -155,11 +176,11 @@ cmd_pass() {
155
176
  if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
156
177
  if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
157
178
 
158
- # Get team that was working on this feature
179
+ acquire_queue_lock
180
+
159
181
  local team_id
160
182
  team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
161
183
 
162
- # Move from in_progress → passed, free team, unblock dependents
163
184
  jq --arg fid "$fid" --arg tid "${team_id:-0}" '
164
185
  # Remove from in_progress
165
186
  del(.queue.in_progress[$fid]) |
@@ -188,6 +209,7 @@ cmd_pass() {
188
209
 
189
210
  local newly_ready
190
211
  newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
212
+ release_queue_lock
191
213
  echo "[queue] $fid PASSED. Ready: [$newly_ready]"
192
214
  }
193
215
 
@@ -199,6 +221,8 @@ cmd_fail() {
199
221
  if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
200
222
  if [ ! -f "$QUEUE" ]; then exit 1; fi
201
223
 
224
+ acquire_queue_lock
225
+
202
226
  local team_id
203
227
  team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
204
228
 
@@ -211,6 +235,7 @@ cmd_fail() {
211
235
  else . end)
212
236
  ' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
213
237
 
238
+ release_queue_lock
214
239
  echo "[queue] $fid FAILED."
215
240
  }
216
241
 
@@ -239,6 +264,8 @@ cmd_update_phase() {
239
264
  exit 1
240
265
  fi
241
266
 
267
+ acquire_queue_lock
268
+
242
269
  local jq_expr
243
270
  jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
244
271
  if [ -n "$attempt" ]; then
@@ -246,6 +273,7 @@ cmd_update_phase() {
246
273
  fi
247
274
 
248
275
  jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
276
+ release_queue_lock
249
277
  }
250
278
 
251
279
  # ══════════════════════════════════════════
@@ -297,6 +325,31 @@ cmd_status() {
297
325
  fi
298
326
  }
299
327
 
328
+ # ══════════════════════════════════════════
329
+ # recover — Move stale in_progress back to ready (after studio restart)
330
+ # ══════════════════════════════════════════
331
+ cmd_recover() {
332
+ if [ ! -f "$QUEUE" ]; then echo "[queue] Not initialized."; return; fi
333
+
334
+ local stale_count
335
+ stale_count=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
336
+
337
+ if [ "${stale_count:-0}" -eq 0 ]; then
338
+ echo "[queue] No stale in_progress entries."
339
+ return
340
+ fi
341
+
342
+ # Move all in_progress → ready, reset all teams to idle
343
+ jq '
344
+ .queue.ready += [.queue.in_progress | keys[]] |
345
+ .queue.ready |= unique |
346
+ .queue.in_progress = {} |
347
+ .teams |= with_entries(.value = { status: "idle", feature: null, branch: null, pid: null })
348
+ ' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
349
+
350
+ echo "[queue] Recovered ${stale_count} stale features back to ready queue."
351
+ }
352
+
300
353
  # ── Dispatch ──
301
354
  case "$CMD" in
302
355
  init) cmd_init ;;
@@ -305,9 +358,10 @@ case "$CMD" in
305
358
  fail) cmd_fail "$@" ;;
306
359
  requeue) cmd_requeue "$@" ;;
307
360
  update_phase) cmd_update_phase "$@" ;;
361
+ recover) cmd_recover ;;
308
362
  status) cmd_status ;;
309
363
  *)
310
- echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|update_phase|status> [args]"
364
+ echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|recover|update_phase|status> [args]"
311
365
  exit 1
312
366
  ;;
313
367
  esac
@@ -26,6 +26,24 @@ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
26
26
  next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
27
27
  agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
28
28
 
29
+ # ─────────────────────────────────────────
30
+ # v4 Parallel Mode — feature-queue.json이 존재하면 v4 안내
31
+ # ─────────────────────────────────────────
32
+ FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
33
+ if [ -f "$FEATURE_QUEUE" ]; then
34
+ passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
35
+ total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
36
+ in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
37
+ failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
38
+
39
+ echo "# Harness v4 — Parallel Agent Teams active"
40
+ echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
41
+ echo "# Teams are running autonomously. You are the orchestrator."
42
+ echo "# Role: Monitor dashboard, resolve failures, manual interventions only."
43
+ echo "# Do NOT run /harness-generator-* or /harness-evaluator-* — Teams handle Gen+Eval."
44
+ exit 0
45
+ fi
46
+
29
47
  # ─────────────────────────────────────────
30
48
  # init 상태: 첫 안내
31
49
  # ─────────────────────────────────────────
@@ -1,14 +1,17 @@
1
1
  #!/bin/bash
2
- # harness-studio-v4.sh — Harness Studio v4: Parallel Agent Teams
2
+ # harness-studio-v4.sh — Harness Studio v4: 3-Column Layout
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
- # └──────────────────────┴─────────────────────────┘
4
+ # ┌──────────────┬──────────────┬──────────────┐
5
+ # │ │ │ Team 1 │
6
+ # │ │ ├──────────────┤
7
+ # │ Main │ Dashboard │ Team 2 │
8
+ # │ (Claude) │ (read-only) ├──────────────┤
9
+ # │ │ │ Team 3 │
10
+ # └──────────────┴──────────────┴──────────────┘
11
+ #
12
+ # Main: 사용자가 직접 Claude Code를 실행하는 대화형 세션
13
+ # Dashboard: feature-queue + team status 자동 갱신 (입력 불가)
14
+ # Team 1~3: claude -p headless worker (입력 불가, 로그만 표시)
12
15
  #
13
16
  # Usage:
14
17
  # bash scripts/harness-studio-v4.sh [project-root]
@@ -20,7 +23,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
23
  SESSION_NAME="harness-v4"
21
24
 
22
25
  PROJECT_ROOT=""
23
- KILL=false
24
26
 
25
27
  for arg in "$@"; do
26
28
  case "$arg" in
@@ -52,45 +54,50 @@ echo "Session: $SESSION_NAME"
52
54
 
53
55
  tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
54
56
 
55
- # ── Initialize queue if not exists ──
57
+ # ── Initialize or recover queue ──
56
58
  QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
57
59
  if [ ! -f "$QUEUE" ]; then
58
60
  echo "Initializing feature queue..."
59
61
  bash "$SCRIPT_DIR/harness-queue-manager.sh" init "$PROJECT_ROOT"
62
+ else
63
+ echo "Recovering stale queue state..."
64
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT"
60
65
  fi
61
66
 
62
67
  # ══════════════════════════════════════════
63
- # Build 5-pane layout using explicit pane IDs
68
+ # 3-Column Layout (Main | Dashboard | Teams)
64
69
  # ══════════════════════════════════════════
65
70
 
66
- # 1. Dashboard (top-left)
67
- PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50 \
71
+ # Column 1: Main (interactive shell — user runs claude here)
72
+ PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
73
+ -P -F '#{pane_id}')
74
+
75
+ # Column 2: Dashboard (split right from Main, 66% remaining → 33% each of 3 cols)
76
+ PANE_DASH=$(tmux split-window -h -p 66 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
68
77
  -P -F '#{pane_id}' \
69
78
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
70
79
 
71
- # 2. Team 1 (top-right)
80
+ # Column 3: Team 1 (split right from Dashboard, 50% of remaining = 33% total)
72
81
  PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
73
82
  -P -F '#{pane_id}' \
74
83
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'")
75
84
 
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)
85
+ # Team 2 (split below Team 1)
82
86
  PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
83
87
  -P -F '#{pane_id}' \
84
88
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
85
89
 
86
- # 5. Team 3 (bottom-right, split from Team 2)
90
+ # Team 3 (split below Team 2)
87
91
  PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
88
92
  -P -F '#{pane_id}' \
89
93
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
90
94
 
95
+ # ── Launch Claude in Main pane ──
96
+ tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
97
+
91
98
  # ── Pane titles ──
99
+ tmux select-pane -t "$PANE_MAIN" -T "Main"
92
100
  tmux select-pane -t "$PANE_DASH" -T "Dashboard"
93
- tmux select-pane -t "$PANE_CTRL" -T "Control"
94
101
  tmux select-pane -t "$PANE_T1" -T "Team 1"
95
102
  tmux select-pane -t "$PANE_T2" -T "Team 2"
96
103
  tmux select-pane -t "$PANE_T3" -T "Team 3"
@@ -98,18 +105,18 @@ tmux select-pane -t "$PANE_T3" -T "Team 3"
98
105
  tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
99
106
  tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
100
107
 
101
- # Focus Control
102
- tmux select-pane -t "$PANE_CTRL"
108
+ # ── Focus Main pane ──
109
+ tmux select-pane -t "$PANE_MAIN"
103
110
 
104
- # Attach
111
+ # ── Attach ──
105
112
  if [ -n "${TMUX:-}" ]; then
106
113
  tmux switch-client -t "$SESSION_NAME"
107
114
  else
108
115
  echo ""
109
116
  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"
117
+ echo " Main (left) : Interactive — run 'claude' here"
118
+ echo " Dashboard (mid) : Feature Queue + Team status (auto-refresh)"
119
+ echo " Team 1-3 (right) : Parallel workers (headless, log only)"
113
120
  echo ""
114
121
  tmux attach -t "$SESSION_NAME"
115
122
  fi
@@ -40,11 +40,13 @@ CONFIG="$PROJECT_ROOT/.harness/config.json"
40
40
  PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
41
41
  QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
42
42
 
43
+ # ── Lock file for git operations (prevent race conditions between teams) ──
44
+ GIT_LOCK="$PROJECT_ROOT/.harness/.git-lock"
45
+
43
46
  MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
44
47
  GEN_MODEL="${GEN_MODEL:-sonnet}"
45
48
  EVAL_MODEL="${EVAL_MODEL:-opus}"
46
49
 
47
- # Read models from config if available
48
50
  if [ -f "$CONFIG" ]; then
49
51
  _gm=$(jq -r '.agents["generator-frontend"].model // empty' "$CONFIG" 2>/dev/null)
50
52
  _em=$(jq -r '.agents["evaluator-functional"].model // empty' "$CONFIG" 2>/dev/null)
@@ -68,20 +70,33 @@ log() {
68
70
  }
69
71
 
70
72
  log_progress() {
71
- local action="$1" detail="$2"
72
- local now
73
- now=$(date +"%Y-%m-%d")
74
- echo "${now} | team-${TEAM_ID} | ${action} | ${detail}" >> "$PROGRESS_LOG"
73
+ echo "$(date +"%Y-%m-%d") | team-${TEAM_ID} | ${1} | ${2}" >> "$PROGRESS_LOG"
74
+ }
75
+
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"
89
+ }
90
+
91
+ release_git_lock() {
92
+ rm -f "$GIT_LOCK"
75
93
  }
76
94
 
77
95
  # ── Pre-eval gate ──
78
96
  run_pre_eval_gate() {
79
- local feature_id="$1"
80
97
  local cwd="$PROJECT_ROOT"
81
98
 
82
- # Read frontend_cwd from config
83
99
  if [ -f "$CONFIG" ]; then
84
- local _cwd
85
100
  _cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
86
101
  if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
87
102
  cwd="$PROJECT_ROOT/$_cwd"
@@ -97,85 +112,86 @@ run_pre_eval_gate() {
97
112
  checks=("npx tsc --noEmit" "npx eslint src/")
98
113
  fi
99
114
 
100
- local all_pass=true
115
+ local all_pass=true fail_cmds=""
101
116
  for cmd in "${checks[@]}"; do
102
- if ! (cd "$cwd" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
103
- log "${RED}Gate FAIL:${RESET} $cmd"
117
+ if (cd "$cwd" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
118
+ log " ${GREEN}✓${RESET} $cmd"
119
+ else
120
+ log " ${RED}✗${RESET} $cmd"
104
121
  all_pass=false
122
+ fail_cmds+="$cmd; "
105
123
  fi
106
124
  done
107
125
 
108
- if [ "$all_pass" = true ]; then
109
- log "${GREEN}Gate: tsc ✓ eslint ✓${RESET}"
110
- return 0
111
- else
112
- return 1
113
- fi
126
+ [ "$all_pass" = true ]
114
127
  }
115
128
 
116
129
  # ── Build generator prompt ──
117
130
  build_gen_prompt() {
118
- local feature_id="$1"
119
- local attempt="$2"
120
- local eval_feedback="${3:-}"
131
+ local fid="$1" attempt="$2" feedback="${3:-}"
121
132
 
122
- local feature_json
123
- feature_json=$(jq --arg fid "$feature_id" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
133
+ local fobj
134
+ fobj=$(jq --arg fid "$fid" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
135
+ local fname fdesc ac_json deps_json
136
+ fname=$(echo "$fobj" | jq -r '.name // .description // ""')
137
+ fdesc=$(echo "$fobj" | jq -r '.description // ""')
138
+ ac_json=$(echo "$fobj" | jq -c '.ac // []')
139
+ deps_json=$(echo "$fobj" | jq -c '.depends_on // []')
124
140
 
125
- local feature_name feature_desc ac_json
126
- feature_name=$(echo "$feature_json" | jq -r '.name // .description // ""')
127
- feature_desc=$(echo "$feature_json" | jq -r '.description // ""')
128
- ac_json=$(echo "$feature_json" | jq -c '.ac // []')
141
+ local project_name
142
+ project_name=$(jq -r '.project_name // ""' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
129
143
 
130
- local prompt="You are Generator-Frontend for a harness engineering project.
144
+ cat <<PROMPT
145
+ You are Generator-Frontend for a harness engineering project.
131
146
 
132
- PROJECT: $(jq -r '.project_name // ""' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
147
+ PROJECT: ${project_name}
133
148
  CONVENTIONS: Read CONVENTIONS.md if it exists.
134
149
 
135
- YOUR TASK: Implement ONLY feature ${feature_id}: ${feature_name}
136
- Description: ${feature_desc}
150
+ YOUR TASK: Implement ONLY feature ${fid}: ${fname}
151
+ Description: ${fdesc}
152
+ Dependencies (already implemented): ${deps_json}
137
153
  Acceptance Criteria: ${ac_json}
138
154
 
139
155
  Read these files for context:
140
- - .harness/actions/feature-list.json (filter to ${feature_id})
156
+ - .harness/actions/feature-list.json (filter to ${fid})
141
157
  - .harness/actions/api-contract.json (relevant endpoints)
142
158
  - .harness/actions/plan.md (overall design)
143
159
 
144
160
  RULES:
145
- - Implement ONLY this feature, do not touch other features' code
161
+ - Implement ONLY this single feature
162
+ - Do NOT modify code belonging to other features
146
163
  - Follow existing code patterns and CONVENTIONS.md
147
- - Commit your changes with message: 'feat(${feature_id}): ${feature_name}'
148
- - Do NOT create tests (evaluator will handle that)"
164
+ - When done, stage and commit with: git add -A && git commit -m 'feat(${fid}): ${fname}'
165
+ PROMPT
149
166
 
150
- if [ "$attempt" -gt 1 ] && [ -n "$eval_feedback" ]; then
151
- prompt="$prompt
167
+ if [ "$attempt" -gt 1 ] && [ -n "$feedback" ]; then
168
+ cat <<RETRY
152
169
 
153
170
  PREVIOUS EVAL FEEDBACK (attempt ${attempt}):
154
- ${eval_feedback}
171
+ ${feedback}
155
172
 
156
- Fix the issues from the feedback above. Focus specifically on the failed criteria."
173
+ Fix the issues above. Focus specifically on the failed criteria.
174
+ RETRY
157
175
  fi
158
-
159
- echo "$prompt"
160
176
  }
161
177
 
162
178
  # ── Build evaluator prompt ──
163
179
  build_eval_prompt() {
164
- local feature_id="$1"
165
-
166
- local feature_json
167
- feature_json=$(jq --arg fid "$feature_id" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
180
+ local fid="$1"
168
181
 
169
- local feature_name ac_json
170
- feature_name=$(echo "$feature_json" | jq -r '.name // .description // ""')
171
- ac_json=$(echo "$feature_json" | jq -c '.ac // []')
182
+ local fobj
183
+ fobj=$(jq --arg fid "$fid" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
184
+ local fname ac_json
185
+ fname=$(echo "$fobj" | jq -r '.name // .description // ""')
186
+ ac_json=$(echo "$fobj" | jq -c '.ac // []')
172
187
 
173
188
  local passed_list
174
189
  passed_list=$(jq -r '.queue.passed // [] | join(", ")' "$QUEUE" 2>/dev/null)
175
190
 
176
- echo "You are Evaluator-Functional for a harness engineering project.
191
+ cat <<PROMPT
192
+ You are Evaluator-Functional for a harness engineering project.
177
193
 
178
- TASK: Evaluate feature ${feature_id}: ${feature_name}
194
+ TASK: Evaluate feature ${fid}: ${fname}
179
195
 
180
196
  Acceptance Criteria to verify:
181
197
  ${ac_json}
@@ -192,23 +208,24 @@ R5: Error handling & edge cases (15%)
192
208
  PASS threshold: 2.80 / 3.00
193
209
  FAIL: any AC not met, any regression failure
194
210
 
195
- OUTPUT FORMAT (must be parseable):
211
+ You MUST output this exact block (parseable by automation):
196
212
  ---EVAL-RESULT---
197
- FEATURE: ${feature_id}
213
+ FEATURE: ${fid}
198
214
  VERDICT: PASS or FAIL
199
215
  SCORE: X.XX
200
- FEEDBACK: <one paragraph summary of issues or confirmation>
201
- ---END-EVAL-RESULT---"
216
+ FEEDBACK: one paragraph summary
217
+ ---END-EVAL-RESULT---
218
+ PROMPT
202
219
  }
203
220
 
204
- # ── Parse eval result ──
221
+ # ── Parse eval result (macOS-compatible, no grep -P) ──
205
222
  parse_eval_result() {
206
223
  local output="$1"
207
224
 
208
225
  local verdict score feedback
209
- verdict=$(echo "$output" | grep -oP 'VERDICT:\s*\K\w+' | head -1)
210
- score=$(echo "$output" | grep -oP 'SCORE:\s*\K[0-9.]+' | head -1)
211
- feedback=$(echo "$output" | sed -n 's/^FEEDBACK:\s*//p' | head -1)
226
+ verdict=$(echo "$output" | grep -E '^VERDICT:' | sed 's/VERDICT:[[:space:]]*//' | head -1)
227
+ score=$(echo "$output" | grep -E '^SCORE:' | sed 's/SCORE:[[:space:]]*//' | head -1)
228
+ feedback=$(echo "$output" | grep -E '^FEEDBACK:' | sed 's/FEEDBACK:[[:space:]]*//' | head -1)
212
229
 
213
230
  echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
214
231
  }
@@ -223,14 +240,13 @@ while true; do
223
240
  # ── Dequeue next feature ──
224
241
  feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
225
242
 
226
- if [ -z "$feature_id" ] || [[ "$feature_id" == "[queue]"* ]]; then
227
- log "${DIM}No features in queue. Waiting 10s...${RESET}"
243
+ if [ -z "$feature_id" ] || [[ "$feature_id" == "["* ]]; then
244
+ log "${DIM}No features ready. Waiting 10s...${RESET}"
228
245
  sleep 10
229
246
 
230
247
  # Check if completely done
231
- local remaining
232
- remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null)
233
- if [ "${remaining:-1}" -eq 0 ]; then
248
+ remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null || echo "1")
249
+ if [ "${remaining}" -eq 0 ] 2>/dev/null; then
234
250
  log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
235
251
  log_progress "complete" "All features done"
236
252
  exit 0
@@ -241,16 +257,18 @@ while true; do
241
257
  log "${CYAN}▶ Dequeued ${feature_id}${RESET}"
242
258
  log_progress "dequeue" "${feature_id}"
243
259
 
244
- # ── Create feature branch ──
245
- local branch="feature/${feature_id}"
246
- (cd "$PROJECT_ROOT" && git checkout -b "$branch" main 2>/dev/null) || \
260
+ # ── Create feature branch (with lock) ──
261
+ 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) || \
247
264
  (cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
265
+ release_git_lock
248
266
  log "Branch: ${branch}"
249
267
 
250
268
  # ── Gen→Eval Loop ──
251
- local attempt=1
252
- local eval_feedback=""
253
- local passed=false
269
+ attempt=1
270
+ eval_feedback=""
271
+ passed=false
254
272
 
255
273
  while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
256
274
  log "${BOLD}── Attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
@@ -259,18 +277,21 @@ while true; do
259
277
  log "Gen ${feature_id} (${GEN_MODEL})..."
260
278
  bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
261
279
 
262
- local gen_prompt
263
280
  gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
264
281
 
265
- local gen_output
266
- gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" --model "$GEN_MODEL" --output-format text 2>&1) || true
282
+ 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" \
285
+ --dangerously-skip-permissions \
286
+ --model "$GEN_MODEL" \
287
+ --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
288
+ gen_elapsed=$(( $(date +%s) - gen_start ))
267
289
 
268
- local files_changed
269
- files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only | wc -l | tr -d ' ')
270
- log "Gen complete — ${files_changed} files changed"
271
- log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files"
290
+ files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
291
+ log "Gen done (${gen_elapsed}s) — ${files_changed} files"
292
+ log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files, ${gen_elapsed}s"
272
293
 
273
- # Auto-commit gen output
294
+ # Auto-commit
274
295
  (cd "$PROJECT_ROOT" && git add -A && git commit -m "feat(${feature_id}): gen attempt ${attempt}" --no-verify 2>/dev/null) || true
275
296
 
276
297
  # ── Pre-eval gate ──
@@ -288,72 +309,107 @@ while true; do
288
309
  log "Eval ${feature_id} (${EVAL_MODEL})..."
289
310
  bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
290
311
 
291
- local eval_prompt
292
312
  eval_prompt=$(build_eval_prompt "$feature_id")
293
313
 
294
- local eval_output
295
- eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" --model "$EVAL_MODEL" --output-format text 2>&1) || true
314
+ 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" \
317
+ --dangerously-skip-permissions \
318
+ --model "$EVAL_MODEL" \
319
+ --output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
320
+ eval_elapsed=$(( $(date +%s) - eval_start ))
296
321
 
297
322
  # Parse result
298
- local result_line verdict score feedback
299
323
  result_line=$(parse_eval_result "$eval_output")
300
324
  verdict=$(echo "$result_line" | cut -d'|' -f1)
301
325
  score=$(echo "$result_line" | cut -d'|' -f2)
302
326
  feedback=$(echo "$result_line" | cut -d'|' -f3-)
303
327
 
304
- log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score})"
328
+ log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
305
329
 
306
330
  if [ "$verdict" = "PASS" ]; then
307
- log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00"
331
+ log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
308
332
  passed=true
309
333
  break
310
334
  else
311
- log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00"
335
+ log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
312
336
  log "${DIM} ${feedback}${RESET}"
313
337
  eval_feedback="$feedback"
314
338
  attempt=$((attempt + 1))
315
339
  fi
316
340
  done
317
341
 
318
- # ── Result processing ──
342
+ # ══════════════════════════════════════════
343
+ # Phase 3: Branch merge with conflict handling
344
+ # ══════════════════════════════════════════
319
345
  if [ "$passed" = true ]; then
320
- # Merge to main
321
346
  log "Merging ${branch} → main..."
322
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null) || {
323
- log "${YELLOW}Merge conflict — rebasing...${RESET}"
324
- (cd "$PROJECT_ROOT" && git checkout "$branch" && git rebase main 2>/dev/null && \
325
- git checkout main && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null) || {
326
- log "${RED}Merge failed after rebase. Manual intervention needed.${RESET}"
327
- bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
328
- log_progress "merge-fail" "${feature_id}"
329
- continue
330
- }
331
- }
332
-
333
- # Mark passed in queue
334
- bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
335
- log_progress "pass" "${feature_id} merged to main"
336
-
337
- # Update feature-list.json passes
338
- if [ -f "$FEATURES" ]; then
339
- jq --arg fid "$feature_id" '
340
- .features |= map(
341
- if .id == $fid then
342
- .passes = ((.passes // []) + ["generator-frontend", "evaluator-functional"] | unique)
343
- else . end
344
- )
345
- ' "$FEATURES" > "${FEATURES}.tmp" && mv "${FEATURES}.tmp" "$FEATURES"
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
382
+
383
+ bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
384
+ log_progress "pass" "${feature_id} merged to main"
385
+
386
+ # Update feature-list.json passes
387
+ if [ -f "$FEATURES" ]; then
388
+ jq --arg fid "$feature_id" '
389
+ .features |= map(
390
+ if .id == $fid then
391
+ .passes = ((.passes // []) + ["generator-frontend", "evaluator-functional"] | unique)
392
+ else . end
393
+ )
394
+ ' "$FEATURES" > "${FEATURES}.tmp" && mv "${FEATURES}.tmp" "$FEATURES"
395
+ fi
396
+
397
+ log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
398
+ else
399
+ log "${RED}${BOLD}Merge failed — ${feature_id} marked as failed${RESET}"
400
+ (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
401
+ bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
402
+ log_progress "merge-fail" "${feature_id}"
346
403
  fi
347
404
 
348
405
  else
349
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
350
410
  bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
351
411
  log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
352
-
353
- # Return to main
354
- (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
355
412
  fi
356
413
 
357
- # Brief pause before next feature
358
414
  sleep 2
359
415
  done
@@ -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}
@@ -60,6 +60,32 @@ disable-model-invocation: true
60
60
  5. `actions/api-contract.json` — 기대 API 동작
61
61
  6. `.harness/progress.json`
62
62
 
63
+ ## v4 Feature-Level Mode (Parallel Agent Teams)
64
+
65
+ v4에서 Team Worker가 `claude -p`로 호출할 때, 프롬프트에 `FEATURE_ID`가 지정된다.
66
+
67
+ ### Feature-Level Rules
68
+ - `feature-list.json`에서 **지정된 FEATURE_ID의 AC만** 검증
69
+ - Regression: `feature-queue.json`의 `passed` 목록에 있는 Feature들의 AC 재검증
70
+ - Cross-Validation: Feature 단위에서는 skip (Sprint-End에서 수행)
71
+ - Visual Evaluation: Feature 단위에서는 skip (Sprint-End에서 수행)
72
+ - 출력 형식: `---EVAL-RESULT---` 블록 (Worker가 파싱 가능)
73
+
74
+ ### Feature-Level Scoring
75
+ - 동일한 R1-R5 루브릭 적용
76
+ - PASS 기준: 2.80/3.00 (변경 없음)
77
+ - 1건이라도 Regression 실패 시 FAIL (변경 없음)
78
+
79
+ ### Output Format (Machine-Parseable)
80
+ ```
81
+ ---EVAL-RESULT---
82
+ FEATURE: F-XXX
83
+ VERDICT: PASS or FAIL
84
+ SCORE: X.XX
85
+ FEEDBACK: one paragraph summary
86
+ ---END-EVAL-RESULT---
87
+ ```
88
+
63
89
  ## Evaluation Steps
64
90
 
65
91
  ### Step 0: IA Structure Compliance (GATE)
@@ -43,6 +43,23 @@ disable-model-invocation: true
43
43
 
44
44
  **Backend 통합 러너가 동작 중이어야 함.** Gateway 미응답 시 → STOP.
45
45
 
46
+ ## v4 Feature-Level Mode (Parallel Agent Teams)
47
+
48
+ v4에서 Team Worker가 `claude -p`로 호출할 때, 프롬프트에 `FEATURE_ID`가 지정된다.
49
+
50
+ ### Feature-Level Rules
51
+ - `feature-list.json`에서 **지정된 FEATURE_ID만** 필터하여 구현
52
+ - 다른 Feature의 코드를 수정하지 않음
53
+ - `depends_on`에 명시된 Feature는 이미 구현/머지 완료된 상태
54
+ - Feature branch (`feature/F-XXX`)에서 작업, 완료 시 commit
55
+ - Sprint Contract는 작성하지 않음 (v4에서는 Feature 단위로 관리)
56
+
57
+ ### Feature-Level Prompt Template
58
+ Worker가 전달하는 프롬프트에는 다음이 포함됨:
59
+ - `FEATURE_ID`, `feature_name`, `description`, `ac` (Acceptance Criteria)
60
+ - `depends_on` (이미 완료된 의존 Feature 목록)
61
+ - Eval 재시도 시: 이전 Eval의 피드백
62
+
46
63
  ## Sprint Workflow
47
64
 
48
65
  1. **Sprint Contract FE 섹션 추가** — 컴포넌트, API 연동, 성공 기준