@walwal-harness/cli 3.6.6 → 3.7.1

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.
package/bin/init.js CHANGED
@@ -214,6 +214,19 @@ function installScripts() {
214
214
  const scriptsSrc = path.join(PKG_ROOT, 'scripts');
215
215
  const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
216
216
 
217
+ // Core scripts are ALWAYS overwritten on update (not user-editable)
218
+ // These contain harness logic that must stay in sync with the CLI version
219
+ const coreScripts = new Set([
220
+ 'harness-next.sh',
221
+ 'harness-session-start.sh',
222
+ 'harness-statusline.sh',
223
+ 'harness-user-prompt-submit.sh',
224
+ 'harness-dashboard.sh',
225
+ 'harness-monitor.sh',
226
+ 'harness-eval-watcher.sh',
227
+ 'harness-tmux.sh',
228
+ ]);
229
+
217
230
  if (fs.existsSync(scriptsSrc)) {
218
231
  ensureDir(scriptsDest);
219
232
  const entries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
@@ -221,21 +234,20 @@ function installScripts() {
221
234
  const srcPath = path.join(scriptsSrc, entry.name);
222
235
  const destPath = path.join(scriptsDest, entry.name);
223
236
  if (entry.isDirectory()) {
224
- // Copy subdirectories (e.g., lib/)
225
- if (!fileExists(destPath) || isForce) {
226
- copyDir(srcPath, destPath);
227
- // Make scripts in subdirs executable
228
- try {
229
- const subFiles = fs.readdirSync(destPath);
230
- for (const f of subFiles) {
231
- if (f.endsWith('.sh')) {
232
- fs.chmodSync(path.join(destPath, f), '755');
233
- }
237
+ // lib/ and other subdirectories — always overwrite
238
+ copyDir(srcPath, destPath);
239
+ try {
240
+ const subFiles = fs.readdirSync(destPath);
241
+ for (const f of subFiles) {
242
+ if (f.endsWith('.sh')) {
243
+ fs.chmodSync(path.join(destPath, f), '755');
234
244
  }
235
- } catch (e) {}
236
- }
245
+ }
246
+ } catch (e) {}
237
247
  } else {
238
- if (!fileExists(destPath) || isForce) {
248
+ // Core scripts: always overwrite. Others: skip if exists (unless --force)
249
+ const isCore = coreScripts.has(entry.name);
250
+ if (isCore || !fileExists(destPath) || isForce) {
239
251
  copyFile(srcPath, destPath);
240
252
  try { fs.chmodSync(destPath, '755'); } catch (e) {}
241
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "3.6.6",
3
+ "version": "3.7.1",
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,17 +1,21 @@
1
1
  #!/bin/bash
2
- # harness-dashboard.sh — Panel 1: 실시간 대시보드
3
- # progress.json + feature-list.json + progress.log 를 2초 간격으로 시각화
2
+ # harness-dashboard.sh — Control Center: Dashboard + Manual Prompt
4
3
  #
5
- # Layout:
6
- # ┌─────────────────────┬──────────────────────┐
7
- # │ Sprint Map │ Prompt History │
8
- # ├─────────────────────┴──────────────────────┤
9
- # │ Processing Status (features, agent bar) │
10
- # └────────────────────────────────────────────┘
4
+ # 상단: 실시간 대시보드 (Sprint Map | Prompt History + Processing Status)
5
+ # 하단: 사용자 명령 입력 (오케스트레이션)
6
+ #
7
+ # Commands:
8
+ # status / s 대시보드 즉시 갱신
9
+ # next / n 다음 에이전트 세션 시작 (Agent Session pane으로 전달)
10
+ # retry / r 현재 에이전트 재실행
11
+ # stop 에이전트 세션 중지
12
+ # log <message> progress.log에 수동 메모 추가
13
+ # clear-fail failure 상태 클리어
14
+ # quit / q 대시보드 종료
11
15
  #
12
16
  # Usage: bash scripts/harness-dashboard.sh [project-root]
13
17
 
14
- set -euo pipefail
18
+ set -uo pipefail
15
19
 
16
20
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
21
  source "$SCRIPT_DIR/lib/harness-render-progress.sh"
@@ -38,21 +42,28 @@ CYAN="\033[36m"
38
42
  MAGENTA="\033[35m"
39
43
  RESET="\033[0m"
40
44
 
41
- # ── Get terminal width for column layout ──
45
+ # ── Strip ANSI escape sequences ──
46
+ strip_ansi() {
47
+ sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
48
+ }
49
+
42
50
  get_term_width() {
43
51
  tput cols 2>/dev/null || echo 80
44
52
  }
45
53
 
54
+ # ══════════════════════════════════════════
55
+ # Render functions
56
+ # ══════════════════════════════════════════
57
+
46
58
  render_header() {
47
- local now
59
+ local now project_name
48
60
  now=$(date +"%H:%M:%S")
49
- local project_name
50
61
  project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
51
62
 
52
- echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
53
- echo -e "${BOLD}║ HARNESS DASHBOARD ║${RESET}"
54
- echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
55
- echo -e " ${DIM}${project_name} | Updated: ${now}${RESET}"
63
+ echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
64
+ echo -e "${BOLD}║ HARNESS CONTROL CENTER ║${RESET}"
65
+ echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
66
+ echo -e " ${DIM}${project_name} | ${now}${RESET}"
56
67
  echo ""
57
68
  }
58
69
 
@@ -79,11 +90,10 @@ render_sprint_overview() {
79
90
  *) status_color="$YELLOW" ;;
80
91
  esac
81
92
 
82
- echo -e " ${BOLD}Pipeline${RESET} ${pipeline} | ${BOLD}Sprint${RESET} ${sprint_num} (${sprint_status}) | ${BOLD}Agent${RESET} ${status_color}${current_agent} [${agent_status}]${RESET}$([ "$retry_count" -gt 0 ] && echo -e " | ${RED}R${retry_count}${RESET}")"
93
+ echo -e " ${BOLD}Pipeline${RESET} ${pipeline} ${BOLD}Sprint${RESET} ${sprint_num} (${sprint_status}) ${BOLD}Agent${RESET} ${status_color}${current_agent} [${agent_status}]${RESET}$([ "$retry_count" -gt 0 ] && echo -e " ${RED}R${retry_count}${RESET}")"
83
94
  echo ""
84
95
  }
85
96
 
86
- # ── Sprint Map column (left) ──
87
97
  render_sprint_map_lines() {
88
98
  if [ ! -f "$PROGRESS" ]; then return; fi
89
99
 
@@ -106,47 +116,37 @@ render_sprint_map_lines() {
106
116
  *) icon="○" ;;
107
117
  esac
108
118
 
109
- if [ ${#notes} -gt 28 ]; then
110
- notes="${notes:0:26}.."
111
- fi
119
+ if [ ${#notes} -gt 28 ]; then notes="${notes:0:26}.."; fi
112
120
 
113
121
  printf "%s S%-2s %2df %-11s %s\n" "$icon" "$s" "$feat_count" "$status" "$notes"
114
122
  done <<< "$sprint_keys"
115
123
  }
116
124
 
117
- # ── Prompt History column (right) ──
118
125
  render_prompt_history_lines() {
119
126
  echo -e "${BOLD}Prompt History${RESET}"
120
127
 
121
- # Source 1: progress.log (user/dispatcher interventions)
122
128
  if [ -f "$PROGRESS_LOG" ]; then
123
- grep -v '^#' "$PROGRESS_LOG" | grep -v '^$' | tail -15 | while IFS= read -r line; do
124
- # Format: 2026-04-13 | agent | action | detail
129
+ grep -v '^#' "$PROGRESS_LOG" 2>/dev/null | grep -v '^$' | tail -12 | while IFS= read -r line; do
125
130
  local ts agent action detail
126
131
  ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
127
132
  agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
128
133
  action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
129
134
  detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
130
135
 
131
- # Short date (MM-DD)
132
- local short_ts
136
+ local short_ts icon color
133
137
  short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
134
138
 
135
- # Icon by agent/action
136
- local icon color
137
139
  case "$agent" in
138
140
  dispatcher*) icon="▸" ; color="$MAGENTA" ;;
139
141
  brainstormer) icon="◇" ; color="$CYAN" ;;
140
142
  planner*) icon="□" ; color="$YELLOW" ;;
141
143
  generator*) icon="▶" ; color="$GREEN" ;;
142
144
  eval*) icon="✦" ; color="$RED" ;;
145
+ user|manual) icon="★" ; color="$BOLD" ;;
143
146
  *) icon="·" ; color="$DIM" ;;
144
147
  esac
145
148
 
146
- # Truncate detail
147
- if [ ${#detail} -gt 35 ]; then
148
- detail="${detail:0:33}.."
149
- fi
149
+ if [ ${#detail} -gt 30 ]; then detail="${detail:0:28}.."; fi
150
150
 
151
151
  echo -e "${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET}"
152
152
  if [ -n "$detail" ]; then
@@ -158,17 +158,14 @@ render_prompt_history_lines() {
158
158
  fi
159
159
  }
160
160
 
161
- # ── Side-by-side rendering ──
162
161
  render_two_columns() {
163
- local term_width
162
+ local term_width col_width
164
163
  term_width=$(get_term_width)
165
- local col_width=$(( (term_width - 7) / 2 )) # 7 = indent(4) + separator(3)
164
+ col_width=$(( (term_width - 7) / 2 ))
166
165
  if [ "$col_width" -lt 30 ]; then col_width=30; fi
167
- if [ "$col_width" -gt 60 ]; then col_width=60; fi
166
+ if [ "$col_width" -gt 55 ]; then col_width=55; fi
168
167
 
169
- # Capture each column into temp arrays
170
- local left_lines=()
171
- local right_lines=()
168
+ local left_lines=() right_lines=()
172
169
 
173
170
  while IFS= read -r line; do
174
171
  left_lines+=("$line")
@@ -178,28 +175,22 @@ render_two_columns() {
178
175
  right_lines+=("$line")
179
176
  done < <(render_prompt_history_lines 2>/dev/null)
180
177
 
181
- # Determine max rows
182
178
  local left_count=${#left_lines[@]}
183
179
  local right_count=${#right_lines[@]}
184
180
  local max_rows=$left_count
185
181
  if [ "$right_count" -gt "$max_rows" ]; then max_rows=$right_count; fi
186
182
 
187
- # Print separator
188
183
  local sep=""
189
184
  for ((i=0; i<term_width-4; i++)); do sep+="─"; done
190
185
  echo -e " ${DIM}${sep}${RESET}"
191
186
 
192
- # Print rows side by side
193
187
  for ((i=0; i<max_rows; i++)); do
194
188
  local left_text="${left_lines[$i]:-}"
195
189
  local right_text="${right_lines[$i]:-}"
196
190
 
197
- # Strip ANSI for length calculation
198
191
  local left_plain
199
- left_plain=$(echo -e "$left_text" | sed 's/\x1b\[[0-9;]*m//g')
192
+ left_plain=$(echo -e "$left_text" | strip_ansi)
200
193
  local left_len=${#left_plain}
201
-
202
- # Pad left column
203
194
  local pad_needed=$(( col_width - left_len ))
204
195
  if [ "$pad_needed" -lt 0 ]; then pad_needed=0; fi
205
196
  local padding=""
@@ -231,11 +222,6 @@ render_build_status() {
231
222
  echo ""
232
223
  }
233
224
 
234
- # Strip all ANSI escape sequences from a string
235
- strip_ansi() {
236
- sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
237
- }
238
-
239
225
  render_failure_info() {
240
226
  if [ ! -f "$PROGRESS" ]; then return; fi
241
227
 
@@ -245,13 +231,8 @@ render_failure_info() {
245
231
  if [ -n "$failure_agent" ] && [ "$failure_agent" != "null" ]; then
246
232
  local failure_loc failure_msg
247
233
  failure_loc=$(jq -r '.failure.location // ""' "$PROGRESS")
248
- # Get raw message, strip ANSI codes, take first meaningful line
249
234
  failure_msg=$(jq -r '.failure.message // ""' "$PROGRESS" | strip_ansi | tr '\n' ' ' | sed 's/ */ /g')
250
-
251
- # Truncate to 80 chars
252
- if [ ${#failure_msg} -gt 80 ]; then
253
- failure_msg="${failure_msg:0:78}.."
254
- fi
235
+ if [ ${#failure_msg} -gt 80 ]; then failure_msg="${failure_msg:0:78}.."; fi
255
236
 
256
237
  echo -e " ${RED}${BOLD}FAIL${RESET} ${RED}${failure_agent} → ${failure_loc}${RESET}"
257
238
  if [ -n "$failure_msg" ]; then
@@ -261,38 +242,228 @@ render_failure_info() {
261
242
  fi
262
243
  }
263
244
 
264
- # ── Render all sections ──
265
- render_all() {
266
- render_header
267
- render_sprint_overview
268
- render_build_status
269
- render_failure_info
245
+ render_agent_info() {
246
+ # Next action hint
247
+ if [ ! -f "$PROGRESS" ]; then return; fi
270
248
 
271
- # Two-column: Sprint Map | Prompt History
272
- render_two_columns
249
+ local next_agent agent_status
250
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
251
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
273
252
 
274
- # Feature progress (reuse existing lib)
275
- if [ -f "$FEATURES" ]; then
276
- render_progress "$PROJECT_ROOT" 2>/dev/null
253
+ if [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ] && [ "$agent_status" != "blocked" ]; then
254
+ echo -e " ${CYAN}Next → ${next_agent}${RESET} ${DIM}(type 'next' to start)${RESET}"
277
255
  fi
278
256
 
279
- # Agent sequence bar
257
+ # Agent bar
280
258
  render_agent_bar "$PROJECT_ROOT" 2>/dev/null
281
259
  echo ""
260
+ }
261
+
262
+ # ══════════════════════════════════════════
263
+ # Full dashboard render
264
+ # ══════════════════════════════════════════
265
+ render_dashboard() {
266
+ tput cup 0 0 2>/dev/null
267
+ tput civis 2>/dev/null
268
+
269
+ local buf
270
+ buf=$(
271
+ render_header
272
+ render_sprint_overview
273
+ render_build_status
274
+ render_failure_info
275
+ render_two_columns
276
+
277
+ if [ -f "$FEATURES" ]; then
278
+ render_progress "$PROJECT_ROOT" 2>/dev/null
279
+ fi
280
+
281
+ render_agent_info
282
+ )
283
+ echo "$buf"
284
+ tput ed 2>/dev/null
285
+ tput cnorm 2>/dev/null
286
+ }
287
+
288
+ # ══════════════════════════════════════════
289
+ # Command handlers
290
+ # ══════════════════════════════════════════
291
+
292
+ cmd_log() {
293
+ local msg="$1"
294
+ if [ -z "$msg" ]; then
295
+ echo -e " ${RED}Usage: log <message>${RESET}"
296
+ return
297
+ fi
298
+
299
+ local ts
300
+ ts=$(date +"%Y-%m-%d")
301
+ echo "${ts} | manual | note | ${msg}" >> "$PROGRESS_LOG"
302
+ echo -e " ${GREEN}Logged:${RESET} ${msg}"
303
+ }
304
+
305
+ cmd_clear_fail() {
306
+ if [ ! -f "$PROGRESS" ]; then return; fi
307
+
308
+ jq '.failure = {agent: null, location: null, message: null, retry_target: null} |
309
+ .sprint.status = "in_progress" |
310
+ .sprint.retry_count = 0 |
311
+ .agent_status = "completed"' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
312
+ echo -e " ${GREEN}Failure state cleared.${RESET}"
313
+ }
314
+
315
+ cmd_next() {
316
+ # Find the Agent Session pane and send the next agent command
317
+ local tmux_target="harness-studio.2" # Agent Session pane
318
+
319
+ if ! tmux list-panes -t harness-studio 2>/dev/null | grep -q .; then
320
+ echo -e " ${RED}tmux session not found. Run inside harness-studio.${RESET}"
321
+ return
322
+ fi
323
+
324
+ local next_agent
325
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
326
+
327
+ if [ "$next_agent" = "none" ] || [ "$next_agent" = "null" ]; then
328
+ echo -e " ${YELLOW}No next agent in queue.${RESET}"
329
+ return
330
+ fi
331
+
332
+ echo -e " ${CYAN}Starting ${next_agent} in Agent Session pane...${RESET}"
333
+
334
+ # Log the manual intervention
335
+ local ts
336
+ ts=$(date +"%Y-%m-%d")
337
+ echo "${ts} | manual | start-agent | ${next_agent}" >> "$PROGRESS_LOG"
338
+
339
+ # Send command to Agent Session pane
340
+ tmux send-keys -t "$tmux_target" "/harness-${next_agent}" Enter
341
+ }
342
+
343
+ cmd_retry() {
344
+ local tmux_target="harness-studio.2"
345
+
346
+ if ! tmux list-panes -t harness-studio 2>/dev/null | grep -q .; then
347
+ echo -e " ${RED}tmux session not found.${RESET}"
348
+ return
349
+ fi
350
+
351
+ local current_agent
352
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
353
+
354
+ if [ "$current_agent" = "none" ] || [ "$current_agent" = "null" ]; then
355
+ echo -e " ${YELLOW}No current agent to retry.${RESET}"
356
+ return
357
+ fi
358
+
359
+ echo -e " ${CYAN}Retrying ${current_agent} in Agent Session pane...${RESET}"
360
+
361
+ local ts
362
+ ts=$(date +"%Y-%m-%d")
363
+ echo "${ts} | manual | retry | ${current_agent}" >> "$PROGRESS_LOG"
282
364
 
283
- echo -e " ${DIM}Refreshing every 2s | Ctrl+C to exit${RESET}"
365
+ tmux send-keys -t "$tmux_target" "/harness-${current_agent}" Enter
284
366
  }
285
367
 
286
- # ── Main Loop ──
287
- tput civis 2>/dev/null
288
- trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
368
+ cmd_stop() {
369
+ local tmux_target="harness-studio.2"
289
370
 
371
+ if ! tmux list-panes -t harness-studio 2>/dev/null | grep -q .; then
372
+ echo -e " ${RED}tmux session not found.${RESET}"
373
+ return
374
+ fi
375
+
376
+ echo -e " ${YELLOW}Sending Ctrl+C to Agent Session...${RESET}"
377
+ tmux send-keys -t "$tmux_target" C-c
378
+
379
+ local ts
380
+ ts=$(date +"%Y-%m-%d")
381
+ echo "${ts} | manual | stop | user-initiated" >> "$PROGRESS_LOG"
382
+ }
383
+
384
+ show_help() {
385
+ echo ""
386
+ echo -e " ${BOLD}Commands${RESET}"
387
+ echo -e " ${CYAN}status${RESET} / ${CYAN}s${RESET} Refresh dashboard"
388
+ echo -e " ${CYAN}next${RESET} / ${CYAN}n${RESET} Start next agent in Agent Session"
389
+ echo -e " ${CYAN}retry${RESET} / ${CYAN}r${RESET} Retry current agent"
390
+ echo -e " ${CYAN}stop${RESET} Send Ctrl+C to Agent Session"
391
+ echo -e " ${CYAN}log${RESET} <message> Add note to progress.log"
392
+ echo -e " ${CYAN}clear-fail${RESET} Clear failure state"
393
+ echo -e " ${CYAN}help${RESET} / ${CYAN}h${RESET} Show this help"
394
+ echo -e " ${CYAN}quit${RESET} / ${CYAN}q${RESET} Exit dashboard"
395
+ echo ""
396
+ }
397
+
398
+ # ══════════════════════════════════════════
399
+ # Main — Interactive loop
400
+ # ══════════════════════════════════════════
290
401
  clear
402
+ render_dashboard
403
+
404
+ echo ""
405
+ echo -e " ${DIM}Type 'help' for commands. Dashboard auto-refreshes every 5s.${RESET}"
406
+ echo ""
407
+
408
+ # Background auto-refresh
409
+ (
410
+ while true; do
411
+ sleep 5
412
+ # Signal main process to refresh
413
+ kill -USR1 $$ 2>/dev/null || exit 0
414
+ done
415
+ ) &
416
+ REFRESH_PID=$!
417
+
418
+ # Trap USR1 to refresh dashboard
419
+ trap 'render_dashboard' USR1
420
+ trap 'kill $REFRESH_PID 2>/dev/null; exit 0' EXIT INT TERM
291
421
 
292
422
  while true; do
293
- buf=$(render_all 2>/dev/null)
294
- tput cup 0 0 2>/dev/null
295
- echo "$buf"
296
- tput ed 2>/dev/null
297
- sleep 2
423
+ echo -ne " ${BOLD}harness>${RESET} "
424
+ # Read with timeout so USR1 can interrupt
425
+ if read -t 5 -r input; then
426
+ # Trim whitespace
427
+ input=$(echo "$input" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
428
+
429
+ case "$input" in
430
+ status|s)
431
+ render_dashboard
432
+ ;;
433
+ next|n)
434
+ cmd_next
435
+ ;;
436
+ retry|r)
437
+ cmd_retry
438
+ ;;
439
+ stop)
440
+ cmd_stop
441
+ ;;
442
+ log\ *)
443
+ cmd_log "${input#log }"
444
+ ;;
445
+ clear-fail)
446
+ cmd_clear_fail
447
+ ;;
448
+ help|h)
449
+ show_help
450
+ ;;
451
+ quit|q)
452
+ echo -e " ${DIM}Goodbye.${RESET}"
453
+ exit 0
454
+ ;;
455
+ "")
456
+ # Empty input — just refresh
457
+ render_dashboard
458
+ ;;
459
+ *)
460
+ # Unknown command — treat as manual note
461
+ echo -e " ${DIM}Unknown command. Logging as note.${RESET}"
462
+ cmd_log "$input"
463
+ ;;
464
+ esac
465
+ else
466
+ # Timeout — auto-refresh
467
+ render_dashboard
468
+ fi
298
469
  done
@@ -97,12 +97,38 @@ run_pre_eval_gate() {
97
97
  timeout=$(jq -r '.flow.pre_eval_gate.timeout_seconds // 120' "$CONFIG" 2>/dev/null)
98
98
 
99
99
  # 실패 위치 결정 (backend or frontend)
100
+ # current_agent가 실패로 변경된 경우, completed_agents에서 직전 Generator를 찾는다
100
101
  local location="backend"
101
102
  local checks_key="backend_checks"
102
- if [ "$current_agent" = "generator-frontend" ] || [ "$current_agent" = "generator-frontend-flutter" ]; then
103
- location="frontend"
104
- checks_key="frontend_checks"
105
- fi
103
+ local source_agent="$current_agent"
104
+
105
+ # current_agent가 generator가 아닌 경우 (예: dispatcher로 리라우팅된 상태),
106
+ # completed_agents에서 마지막 generator를 찾는다
107
+ case "$source_agent" in
108
+ generator-frontend|generator-frontend-flutter)
109
+ location="frontend"
110
+ checks_key="frontend_checks"
111
+ ;;
112
+ generator-backend)
113
+ location="backend"
114
+ checks_key="backend_checks"
115
+ ;;
116
+ *)
117
+ # completed_agents에서 마지막 generator를 역순으로 찾기
118
+ local last_gen
119
+ last_gen=$(jq -r '.completed_agents // [] | map(select(startswith("generator-"))) | last // empty' "$PROGRESS" 2>/dev/null)
120
+ case "$last_gen" in
121
+ generator-frontend|generator-frontend-flutter)
122
+ location="frontend"
123
+ checks_key="frontend_checks"
124
+ ;;
125
+ generator-backend)
126
+ location="backend"
127
+ checks_key="backend_checks"
128
+ ;;
129
+ esac
130
+ ;;
131
+ esac
106
132
 
107
133
  local -a checks
108
134
  mapfile -t checks < <(jq -r ".flow.pre_eval_gate.${checks_key}[]" "$CONFIG" 2>/dev/null)
@@ -1,20 +1,22 @@
1
1
  #!/bin/bash
2
- # harness-tmux.sh — tmux 4-pane 레이아웃 자동 구성
2
+ # harness-tmux.sh — Harness Studio tmux 4-pane 레이아웃
3
3
  #
4
- # ┌──────────────────────┬──────────────────────┐
5
- # │ │ Panel 1: Dashboard │
6
- # │ Main Terminal ├──────────────────────┤
7
- # │ (claude 대화형) │ Panel 2: Monitor │
8
- # │ ├──────────────────────┤
9
- # │ │ Panel 3: Eval Review │
10
- # └──────────────────────┴──────────────────────┘
4
+ # ┌───────────────────────────┬─────────────────────────┐
5
+ # │ │ Monitor (compact) │
6
+ # │ Main: Control Center ├─────────────────────────┤
7
+ # │ (Dashboard + Manual │ Agent Session │
8
+ # │ Prompt + Orchestration) │ (claude --skip-perms) │
9
+ # │ ├─────────────────────────┤
10
+ # │ │ Eval Review │
11
+ # │ │ (eval 결과 요약) │
12
+ # └───────────────────────────┴─────────────────────────┘
11
13
  #
12
14
  # Usage:
13
15
  # bash scripts/harness-tmux.sh [project-root]
14
- # bash scripts/harness-tmux.sh /path/to/project --ai (AI eval summary 활성화)
16
+ # bash scripts/harness-tmux.sh /path/to/project --ai
15
17
  #
16
18
  # Options:
17
- # --ai Panel 3에서 claude -p 로 AI 요약 생성 (API 비용 발생)
19
+ # --ai Eval Review에서 claude -p 로 AI 요약 생성
18
20
  # --detach 세션 생성 후 attach 하지 않음
19
21
  # --kill 기존 harness-studio 세션 종료
20
22
 
@@ -26,7 +28,6 @@ SESSION_NAME="harness-studio"
26
28
  PROJECT_ROOT=""
27
29
  USE_AI=""
28
30
  DETACH=false
29
- KILL=false
30
31
 
31
32
  for arg in "$@"; do
32
33
  case "$arg" in
@@ -63,28 +64,9 @@ echo "Session: $SESSION_NAME"
63
64
  # Kill existing session if any
64
65
  tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
65
66
 
66
- # ── Create tmux session ──
67
- # Pane 0 (left, Main): interactive shell at project root
68
- tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50
69
-
70
- # Split right column (40% width)
71
- # Use direct command execution (not send-keys) to avoid shell init noise (nvm warnings etc.)
72
- tmux split-window -h -l 80 -t "$SESSION_NAME" -c "$PROJECT_ROOT" \
73
- "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"'"
74
-
75
- # Split pane 1 vertically for Monitor
76
- tmux split-window -v -t "${SESSION_NAME}.1" -c "$PROJECT_ROOT" \
77
- "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'"
78
-
79
- # Split pane 2 vertically for Eval
80
- tmux split-window -v -t "${SESSION_NAME}.2" -c "$PROJECT_ROOT" \
81
- "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-eval-watcher.sh\" \"${PROJECT_ROOT}\" ${USE_AI}'"
82
-
83
- # ── Launch Claude in Main pane ──
67
+ # ── Resolve Claude command for Agent Session ──
84
68
  HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
85
69
  CLAUDE_CMD="claude --dangerously-skip-permissions"
86
-
87
- # If handoff.json exists, pick up model from it
88
70
  if [ -f "$HANDOFF" ]; then
89
71
  _model=$(jq -r '.model // empty' "$HANDOFF" 2>/dev/null)
90
72
  if [ -n "$_model" ] && [ "$_model" != "null" ]; then
@@ -92,38 +74,58 @@ if [ -f "$HANDOFF" ]; then
92
74
  fi
93
75
  fi
94
76
 
95
- tmux send-keys -t "${SESSION_NAME}.0" "$CLAUDE_CMD" Enter
77
+ # ══════════════════════════════════════════
78
+ # Layout Construction
79
+ # ══════════════════════════════════════════
80
+ # Step 1: Create session → Pane 0 = Main (Control Center)
81
+ tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50
82
+
83
+ # Step 2: Split right column (45% width) → Pane 1 = Monitor (top-right, compact)
84
+ tmux split-window -h -p 45 -t "$SESSION_NAME" -c "$PROJECT_ROOT" \
85
+ "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'"
86
+
87
+ # Step 3: Split Pane 1 vertically → Pane 2 = Agent Session (mid-right)
88
+ # Monitor gets ~20% of right column height, remaining 80% split below
89
+ tmux split-window -v -p 80 -t "${SESSION_NAME}.1" -c "$PROJECT_ROOT"
90
+
91
+ # Pane 2 = Agent Session (interactive shell, will run claude)
92
+ tmux send-keys -t "${SESSION_NAME}.2" "$CLAUDE_CMD" Enter
93
+
94
+ # Step 4: Split Pane 2 vertically → Pane 3 = Eval Review (bottom-right)
95
+ # Agent Session and Eval Review split 1:1
96
+ tmux split-window -v -p 50 -t "${SESSION_NAME}.2" -c "$PROJECT_ROOT" \
97
+ "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-eval-watcher.sh\" \"${PROJECT_ROOT}\" ${USE_AI}'"
98
+
99
+ # ── Launch Dashboard in Main pane ──
100
+ tmux send-keys -t "${SESSION_NAME}.0" "bash ${SCRIPT_DIR}/harness-dashboard.sh '${PROJECT_ROOT}'" Enter
96
101
 
97
- # ── Layout adjustments ──
98
- # Select left pane (Main) — this is where the user works
102
+ # ── Select Main pane as active ──
99
103
  tmux select-pane -t "${SESSION_NAME}.0"
100
104
 
101
- # Set pane titles (requires tmux >= 3.2 with pane-border-format)
102
- tmux select-pane -t "${SESSION_NAME}.0" -T "Main"
103
- tmux select-pane -t "${SESSION_NAME}.1" -T "Dashboard"
104
- tmux select-pane -t "${SESSION_NAME}.2" -T "Monitor"
105
+ # ── Pane titles ──
106
+ tmux select-pane -t "${SESSION_NAME}.0" -T "Control Center"
107
+ tmux select-pane -t "${SESSION_NAME}.1" -T "Monitor"
108
+ tmux select-pane -t "${SESSION_NAME}.2" -T "Agent Session"
105
109
  tmux select-pane -t "${SESSION_NAME}.3" -T "Eval Review"
106
110
 
107
- # Enable pane border titles
108
111
  tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
109
112
  tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
110
113
 
111
- # ── Attach or print info ──
114
+ # ── Attach ──
112
115
  if [ "$DETACH" = true ]; then
113
116
  echo ""
114
117
  echo "Session created. Attach with:"
115
118
  echo " tmux attach -t $SESSION_NAME"
116
119
  else
117
- # If already in tmux, switch; otherwise attach
118
120
  if [ -n "${TMUX:-}" ]; then
119
121
  tmux switch-client -t "$SESSION_NAME"
120
122
  else
121
123
  echo ""
122
124
  echo "Launching harness-studio..."
123
- echo " Main (left) : Interactive terminal — run 'claude' here"
124
- echo " Dashboard (right↑) : Real-time progress"
125
- echo " Monitor (right↔) : Agent lifecycle events"
126
- echo " Eval (right↓) : Evaluation summaries"
125
+ echo " Control Center (left) : Dashboard + manual prompting"
126
+ echo " Monitor (right↑) : Agent lifecycle (compact)"
127
+ echo " Agent Session (right↔) : claude --dangerously-skip-permissions"
128
+ echo " Eval Review (right↓) : Evaluation summaries"
127
129
  echo ""
128
130
  tmux attach -t "$SESSION_NAME"
129
131
  fi