@walwal-harness/cli 3.6.6 → 3.7.0
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/package.json +1 -1
- package/scripts/harness-dashboard.sh +250 -79
- package/scripts/harness-tmux.sh +47 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
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 —
|
|
3
|
-
# progress.json + feature-list.json + progress.log 를 2초 간격으로 시각화
|
|
2
|
+
# harness-dashboard.sh — Control Center: Dashboard + Manual Prompt
|
|
4
3
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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 -
|
|
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
|
-
# ──
|
|
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}
|
|
53
|
-
echo -e "${BOLD}║ HARNESS
|
|
54
|
-
echo -e "${BOLD}
|
|
55
|
-
echo -e " ${DIM}${project_name} |
|
|
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}
|
|
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 -
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
164
|
+
col_width=$(( (term_width - 7) / 2 ))
|
|
166
165
|
if [ "$col_width" -lt 30 ]; then col_width=30; fi
|
|
167
|
-
if [ "$col_width" -gt
|
|
166
|
+
if [ "$col_width" -gt 55 ]; then col_width=55; fi
|
|
168
167
|
|
|
169
|
-
|
|
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" |
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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
|
-
|
|
365
|
+
tmux send-keys -t "$tmux_target" "/harness-${current_agent}" Enter
|
|
284
366
|
}
|
|
285
367
|
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
package/scripts/harness-tmux.sh
CHANGED
|
@@ -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
|
-
# │
|
|
6
|
-
# │ Main
|
|
7
|
-
# │ (
|
|
8
|
-
# │
|
|
9
|
-
# │
|
|
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
|
|
16
|
+
# bash scripts/harness-tmux.sh /path/to/project --ai
|
|
15
17
|
#
|
|
16
18
|
# Options:
|
|
17
|
-
# --ai
|
|
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
|
-
# ──
|
|
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
|
-
|
|
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
|
-
# ──
|
|
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
|
-
#
|
|
102
|
-
tmux select-pane -t "${SESSION_NAME}.0" -T "
|
|
103
|
-
tmux select-pane -t "${SESSION_NAME}.1" -T "
|
|
104
|
-
tmux select-pane -t "${SESSION_NAME}.2" -T "
|
|
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
|
|
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 "
|
|
124
|
-
echo "
|
|
125
|
-
echo "
|
|
126
|
-
echo " Eval (right↓)
|
|
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
|