@walwal-harness/cli 5.0.2 → 5.0.4
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 +42 -2
- package/scripts/harness-prompt-history.sh +112 -66
- package/scripts/harness-tmux.sh +189 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Solo/Team mode, Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer. Supports React, Next.js, and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -316,6 +316,46 @@ render_team_bottleneck() {
|
|
|
316
316
|
fi
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
# ══════════════════════════════════════════
|
|
320
|
+
# Archive Prompt — 처리 완료된 프롬프트 목록
|
|
321
|
+
# ══════════════════════════════════════════
|
|
322
|
+
render_archive_prompts() {
|
|
323
|
+
if [ ! -f "$PROGRESS_LOG" ]; then return; fi
|
|
324
|
+
|
|
325
|
+
# user-prompt 엔트리 중 이후에 result/pass/completed 액션이 있는 것 = 처리 완료
|
|
326
|
+
local all_prompts
|
|
327
|
+
all_prompts=$(grep '| user-prompt |' "$PROGRESS_LOG" 2>/dev/null)
|
|
328
|
+
if [ -z "$all_prompts" ]; then return; fi
|
|
329
|
+
|
|
330
|
+
local total_prompts
|
|
331
|
+
total_prompts=$(echo "$all_prompts" | wc -l | tr -d ' ')
|
|
332
|
+
|
|
333
|
+
# 마지막 프롬프트를 제외한 모든 프롬프트 = archived (처리 완료)
|
|
334
|
+
local archived
|
|
335
|
+
if [ "$total_prompts" -le 1 ]; then return; fi
|
|
336
|
+
archived=$(echo "$all_prompts" | head -n $((total_prompts - 1)) | tail -8)
|
|
337
|
+
|
|
338
|
+
echo ""
|
|
339
|
+
echo -e "${BOLD}Archive Prompt${RESET} ${DIM}(처리 완료)${RESET}"
|
|
340
|
+
|
|
341
|
+
echo "$archived" | while IFS= read -r line; do
|
|
342
|
+
local ts detail
|
|
343
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
344
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
345
|
+
|
|
346
|
+
local short_ts
|
|
347
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
|
|
348
|
+
|
|
349
|
+
if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
|
|
350
|
+
|
|
351
|
+
echo -e " ${GREEN}✓${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
352
|
+
done
|
|
353
|
+
|
|
354
|
+
if [ "$total_prompts" -gt 9 ]; then
|
|
355
|
+
echo -e " ${DIM}… +$((total_prompts - 9)) more${RESET}"
|
|
356
|
+
fi
|
|
357
|
+
}
|
|
358
|
+
|
|
319
359
|
# ══════════════════════════════════════════
|
|
320
360
|
# Render dispatch
|
|
321
361
|
# ══════════════════════════════════════════
|
|
@@ -331,6 +371,7 @@ render_dashboard() {
|
|
|
331
371
|
render_team_status
|
|
332
372
|
render_team_features
|
|
333
373
|
render_team_bottleneck
|
|
374
|
+
render_archive_prompts
|
|
334
375
|
;;
|
|
335
376
|
*)
|
|
336
377
|
render_solo_sprint_overview
|
|
@@ -341,8 +382,7 @@ render_dashboard() {
|
|
|
341
382
|
fi
|
|
342
383
|
|
|
343
384
|
render_solo_agent_info
|
|
344
|
-
|
|
345
|
-
render_solo_prompt_history
|
|
385
|
+
render_archive_prompts
|
|
346
386
|
;;
|
|
347
387
|
esac
|
|
348
388
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-prompt-history.sh — Prompt History pane
|
|
3
|
-
#
|
|
2
|
+
# harness-prompt-history.sh — User Prompt History pane
|
|
3
|
+
# 사용자가 입력한 프롬프트만 표시 + 현재 처리 단계(Planner/Team) 표시
|
|
4
4
|
# Usage: bash scripts/harness-prompt-history.sh [project-root]
|
|
5
5
|
|
|
6
6
|
set -uo pipefail
|
|
@@ -14,7 +14,8 @@ if [ -z "$PROJECT_ROOT" ]; then
|
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
16
|
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
17
|
-
|
|
17
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
18
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
18
19
|
|
|
19
20
|
BOLD="\033[1m"
|
|
20
21
|
DIM="\033[2m"
|
|
@@ -23,93 +24,138 @@ YELLOW="\033[33m"
|
|
|
23
24
|
RED="\033[31m"
|
|
24
25
|
CYAN="\033[36m"
|
|
25
26
|
MAGENTA="\033[35m"
|
|
27
|
+
WHITE="\033[37m"
|
|
26
28
|
RESET="\033[0m"
|
|
29
|
+
BG_BLUE="\033[44m"
|
|
30
|
+
BG_GREEN="\033[42m"
|
|
31
|
+
BG_YELLOW="\033[43m"
|
|
27
32
|
|
|
28
33
|
render_header() {
|
|
29
|
-
echo -e "${BOLD}
|
|
34
|
+
echo -e "${BOLD}USER PROMPTS${RESET} ${DIM}$(date +%H:%M:%S)${RESET}"
|
|
30
35
|
echo ""
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
# 현재 처리 상태 배지
|
|
39
|
+
render_processing_status() {
|
|
40
|
+
local mode="solo" current_agent="none" agent_status="pending"
|
|
41
|
+
|
|
42
|
+
if [ -f "$PROGRESS" ] && command -v jq &>/dev/null; then
|
|
43
|
+
mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
|
|
44
|
+
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
45
|
+
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
local badge=""
|
|
49
|
+
case "$mode" in
|
|
50
|
+
team)
|
|
51
|
+
# Team 모드: 각 팀의 현재 상태 표시
|
|
52
|
+
badge="${BG_BLUE}${WHITE}${BOLD} TEAM MODE ${RESET}"
|
|
53
|
+
if [ -f "$QUEUE" ]; then
|
|
54
|
+
local team_info=""
|
|
55
|
+
for i in 1 2 3; do
|
|
56
|
+
local t_status t_feature t_phase
|
|
57
|
+
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
58
|
+
t_feature=$(jq -r ".teams[\"$i\"].feature // \"\"" "$QUEUE" 2>/dev/null)
|
|
59
|
+
if [ "$t_status" = "busy" ] && [ -n "$t_feature" ] && [ "$t_feature" != "null" ]; then
|
|
60
|
+
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
61
|
+
team_info+=" ${CYAN}T${i}${RESET}:${t_feature}(${t_phase})"
|
|
62
|
+
else
|
|
63
|
+
team_info+=" ${DIM}T${i}:idle${RESET}"
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
echo -e " ${badge}${team_info}"
|
|
67
|
+
else
|
|
68
|
+
echo -e " ${badge}"
|
|
69
|
+
fi
|
|
70
|
+
;;
|
|
71
|
+
paused)
|
|
72
|
+
echo -e " ${BG_YELLOW}${WHITE}${BOLD} PAUSED ${RESET} ${DIM}/harness-team 또는 /harness-solo${RESET}"
|
|
73
|
+
;;
|
|
74
|
+
*)
|
|
75
|
+
# Solo 모드: 현재 에이전트 표시
|
|
76
|
+
if [ "$current_agent" != "none" ] && [ "$current_agent" != "null" ]; then
|
|
77
|
+
local agent_color="$RESET"
|
|
78
|
+
case "$current_agent" in
|
|
79
|
+
planner*) agent_color="$YELLOW" ;;
|
|
80
|
+
generator*) agent_color="$GREEN" ;;
|
|
81
|
+
eval*) agent_color="$RED" ;;
|
|
82
|
+
dispatcher*) agent_color="$MAGENTA" ;;
|
|
83
|
+
esac
|
|
84
|
+
local status_icon="·"
|
|
85
|
+
case "$agent_status" in
|
|
86
|
+
running) status_icon="▶" ;;
|
|
87
|
+
completed) status_icon="✓" ;;
|
|
88
|
+
failed) status_icon="✗" ;;
|
|
89
|
+
esac
|
|
90
|
+
echo -e " ${BG_GREEN}${WHITE}${BOLD} SOLO ${RESET} ${agent_color}${status_icon} ${current_agent}${RESET}"
|
|
91
|
+
else
|
|
92
|
+
echo -e " ${BG_GREEN}${WHITE}${BOLD} SOLO ${RESET} ${DIM}대기 중${RESET}"
|
|
93
|
+
fi
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
echo ""
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# 사용자 프롬프트만 필터하여 표시
|
|
100
|
+
render_user_prompts() {
|
|
34
101
|
if [ ! -f "$PROGRESS_LOG" ]; then
|
|
35
|
-
echo -e " ${DIM}(
|
|
102
|
+
echo -e " ${DIM}(프롬프트 기록 없음)${RESET}"
|
|
36
103
|
return
|
|
37
104
|
fi
|
|
38
105
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
44
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
106
|
+
local term_height
|
|
107
|
+
term_height=$(tput lines 2>/dev/null || echo 30)
|
|
108
|
+
local max_lines=$((term_height - 10))
|
|
109
|
+
if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
|
|
45
110
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
case "$agent" in
|
|
50
|
-
dispatcher*) icon="▸" ; color="$MAGENTA" ;;
|
|
51
|
-
brainstormer) icon="◇" ; color="$CYAN" ;;
|
|
52
|
-
planner*) icon="□" ; color="$YELLOW" ;;
|
|
53
|
-
generator*) icon="▶" ; color="$GREEN" ;;
|
|
54
|
-
eval*) icon="✦" ; color="$RED" ;;
|
|
55
|
-
user|manual) icon="★" ; color="$BOLD" ;;
|
|
56
|
-
team*) icon="⊕" ; color="$CYAN" ;;
|
|
57
|
-
*) icon="·" ; color="$DIM" ;;
|
|
58
|
-
esac
|
|
59
|
-
|
|
60
|
-
if [ ${#detail} -gt 40 ]; then detail="${detail:0:38}.."; fi
|
|
61
|
-
|
|
62
|
-
printf " %b%b%b %b%-8s%b %b%s%b %b%s%b\n" \
|
|
63
|
-
"$color" "$icon" "$RESET" \
|
|
64
|
-
"$DIM" "$short_ts" "$RESET" \
|
|
65
|
-
"$RESET" "$agent" "$RESET" \
|
|
66
|
-
"$DIM" "$action" "$RESET"
|
|
67
|
-
if [ -n "$detail" ] && [ "$detail" != " " ]; then
|
|
68
|
-
echo -e " ${DIM}${detail}${RESET}"
|
|
69
|
-
fi
|
|
70
|
-
done
|
|
71
|
-
}
|
|
111
|
+
# user-prompt 행만 필터
|
|
112
|
+
local prompts
|
|
113
|
+
prompts=$(grep '| user-prompt |' "$PROGRESS_LOG" 2>/dev/null | tail -"$max_lines")
|
|
72
114
|
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
if [ -z "$prompts" ]; then
|
|
116
|
+
echo -e " ${DIM}(사용자 프롬프트 없음)${RESET}"
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
75
119
|
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
local line_num=0
|
|
121
|
+
local total_prompts
|
|
122
|
+
total_prompts=$(grep -c '| user-prompt |' "$PROGRESS_LOG" 2>/dev/null || echo 0)
|
|
123
|
+
|
|
124
|
+
echo "$prompts" | while IFS= read -r line; do
|
|
125
|
+
line_num=$((line_num + 1))
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
local ts agent action status target
|
|
127
|
+
local ts detail
|
|
81
128
|
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
82
|
-
|
|
83
|
-
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
84
|
-
status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
85
|
-
target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
|
86
|
-
|
|
87
|
-
local color="$RESET" icon="·"
|
|
88
|
-
case "$status" in
|
|
89
|
-
start) color="$CYAN"; icon="▶" ;;
|
|
90
|
-
complete) color="$GREEN"; icon="✓" ;;
|
|
91
|
-
fail) color="$RED"; icon="✗" ;;
|
|
92
|
-
pass) color="$GREEN"; icon="✓" ;;
|
|
93
|
-
skip) color="$DIM"; icon="–" ;;
|
|
94
|
-
esac
|
|
129
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
95
130
|
|
|
96
131
|
local short_ts
|
|
97
|
-
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}
|
|
132
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
|
|
133
|
+
|
|
134
|
+
# 프롬프트 내용 truncate
|
|
135
|
+
local max_width
|
|
136
|
+
max_width=$(( $(tput cols 2>/dev/null || echo 40) - 12 ))
|
|
137
|
+
if [ "$max_width" -lt 20 ]; then max_width=20; fi
|
|
138
|
+
if [ ${#detail} -gt "$max_width" ]; then
|
|
139
|
+
detail="${detail:0:$((max_width - 2))}.."
|
|
140
|
+
fi
|
|
98
141
|
|
|
99
|
-
|
|
142
|
+
# 처리 상태 찾기: 이 프롬프트 이후의 첫 번째 에이전트 액션
|
|
143
|
+
local processed_by=""
|
|
144
|
+
local prompt_ts_epoch
|
|
145
|
+
prompt_ts_epoch=$(echo "$ts" | sed 's/[^0-9:]//g')
|
|
100
146
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"$DIM" "$short_ts" "$RESET" \
|
|
104
|
-
"$agent" \
|
|
105
|
-
"$DIM" "$target" "$RESET"
|
|
147
|
+
# 간단히: 마지막 프롬프트인지 확인 → 현재 처리 중 표시
|
|
148
|
+
echo -e " ${DIM}${short_ts}${RESET} ${WHITE}${detail}${RESET}"
|
|
106
149
|
done
|
|
150
|
+
|
|
151
|
+
echo ""
|
|
152
|
+
echo -e " ${DIM}── ${total_prompts} prompts total ──${RESET}"
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
render_all() {
|
|
110
156
|
render_header
|
|
111
|
-
|
|
112
|
-
|
|
157
|
+
render_processing_status
|
|
158
|
+
render_user_prompts
|
|
113
159
|
}
|
|
114
160
|
|
|
115
161
|
# ── Main loop ──
|
package/scripts/harness-tmux.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-tmux.sh — Unified Harness Studio
|
|
2
|
+
# harness-tmux.sh — Unified Harness Studio layout
|
|
3
3
|
#
|
|
4
4
|
# Team Mode (--team):
|
|
5
5
|
# ┌──────────────┬──────────────┬──────────────┐
|
|
@@ -12,14 +12,10 @@
|
|
|
12
12
|
# │ Codex) │ │ TEAM 3 │
|
|
13
13
|
# └──────────────┴──────────────┴──────────────┘
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# ├──────────────┤ │ │
|
|
20
|
-
# │ Prompt │ │ │
|
|
21
|
-
# │ History │ │ │
|
|
22
|
-
# └──────────────┴──────────────┴──────────────┘
|
|
15
|
+
# Rendering strategy:
|
|
16
|
+
# 1. iTerm2 detected → native split panes (no tmux needed)
|
|
17
|
+
# 2. tmux available → tmux session
|
|
18
|
+
# 3. Fallback → open new terminal windows
|
|
23
19
|
#
|
|
24
20
|
# Usage:
|
|
25
21
|
# bash scripts/harness-tmux.sh [project-root] --team
|
|
@@ -41,7 +37,22 @@ for arg in "$@"; do
|
|
|
41
37
|
--solo) MODE="solo" ;;
|
|
42
38
|
--detach) DETACH=true ;;
|
|
43
39
|
--kill)
|
|
44
|
-
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "
|
|
40
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "tmux session killed." || true
|
|
41
|
+
# Also try to close iTerm2 studio tab if exists
|
|
42
|
+
if [ "${TERM_PROGRAM:-}" = "iTerm.app" ]; then
|
|
43
|
+
osascript -e '
|
|
44
|
+
tell application "iTerm2"
|
|
45
|
+
repeat with w in windows
|
|
46
|
+
repeat with t in tabs of w
|
|
47
|
+
if name of current session of t contains "harness-studio" then
|
|
48
|
+
close t
|
|
49
|
+
end if
|
|
50
|
+
end repeat
|
|
51
|
+
end repeat
|
|
52
|
+
end tell
|
|
53
|
+
' 2>/dev/null || true
|
|
54
|
+
fi
|
|
55
|
+
echo "Killed."
|
|
45
56
|
exit 0
|
|
46
57
|
;;
|
|
47
58
|
*)
|
|
@@ -72,134 +83,215 @@ if [ -z "$MODE" ]; then
|
|
|
72
83
|
fi
|
|
73
84
|
|
|
74
85
|
echo "Project: $PROJECT_ROOT"
|
|
75
|
-
echo "Session: $SESSION_NAME"
|
|
76
86
|
echo "Mode: $MODE"
|
|
77
87
|
|
|
78
|
-
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
79
|
-
|
|
80
88
|
# ══════════════════════════════════════════
|
|
81
|
-
#
|
|
89
|
+
# Strategy 1: iTerm2 native split panes
|
|
82
90
|
# ══════════════════════════════════════════
|
|
83
|
-
|
|
91
|
+
launch_iterm2_team() {
|
|
92
|
+
osascript <<APPLESCRIPT
|
|
93
|
+
tell application "iTerm2"
|
|
94
|
+
activate
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-P -F '#{pane_id}')
|
|
96
|
+
-- Create new window with first session (Prompt History)
|
|
97
|
+
set studioWindow to (create window with default profile)
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
tell studioWindow
|
|
100
|
+
tell current session of current tab
|
|
101
|
+
set name to "harness-studio"
|
|
102
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-prompt-history.sh' '${PROJECT_ROOT}'"
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
-- Split right → Dashboard
|
|
105
|
+
set dashPane to (split vertically with default profile)
|
|
106
|
+
tell dashPane
|
|
107
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
-- Split right → Team 1
|
|
110
|
+
set t1Pane to (split vertically with default profile)
|
|
111
|
+
tell t1Pane
|
|
112
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 1"
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
-- Split down → Team 2
|
|
115
|
+
set t2Pane to (split horizontally with default profile)
|
|
116
|
+
tell t2Pane
|
|
117
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 2"
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
-- Split down → Team 3
|
|
120
|
+
set t3Pane to (split horizontally with default profile)
|
|
121
|
+
tell t3Pane
|
|
122
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 3"
|
|
123
|
+
end tell
|
|
124
|
+
end tell
|
|
125
|
+
end tell
|
|
126
|
+
end tell
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
-- Split down → Controller (bottom-left)
|
|
129
|
+
set ctrlPane to (split horizontally with default profile)
|
|
130
|
+
tell ctrlPane
|
|
131
|
+
write text "cd '${PROJECT_ROOT}' && clear"
|
|
132
|
+
end tell
|
|
133
|
+
end tell
|
|
134
|
+
end tell
|
|
135
|
+
end tell
|
|
136
|
+
APPLESCRIPT
|
|
137
|
+
}
|
|
114
138
|
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
launch_iterm2_solo() {
|
|
140
|
+
osascript <<APPLESCRIPT
|
|
141
|
+
tell application "iTerm2"
|
|
142
|
+
activate
|
|
143
|
+
|
|
144
|
+
set studioWindow to (create window with default profile)
|
|
145
|
+
|
|
146
|
+
tell studioWindow
|
|
147
|
+
tell current session of current tab
|
|
148
|
+
set name to "harness-studio"
|
|
149
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
|
|
150
|
+
|
|
151
|
+
-- Split right → Monitor
|
|
152
|
+
set monPane to (split vertically with default profile)
|
|
153
|
+
tell monPane
|
|
154
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}'"
|
|
155
|
+
|
|
156
|
+
-- Split right → Agent Session
|
|
157
|
+
set agentPane to (split vertically with default profile)
|
|
158
|
+
tell agentPane
|
|
159
|
+
write text "cd '${PROJECT_ROOT}' && clear"
|
|
160
|
+
end tell
|
|
161
|
+
end tell
|
|
117
162
|
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
-- Split down → Prompt History (bottom-left)
|
|
164
|
+
set histPane to (split horizontally with default profile)
|
|
165
|
+
tell histPane
|
|
166
|
+
write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-prompt-history.sh' '${PROJECT_ROOT}'"
|
|
167
|
+
end tell
|
|
168
|
+
end tell
|
|
169
|
+
end tell
|
|
170
|
+
end tell
|
|
171
|
+
APPLESCRIPT
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ══════════════════════════════════════════
|
|
175
|
+
# Strategy 2: tmux
|
|
176
|
+
# ══════════════════════════════════════════
|
|
177
|
+
launch_tmux_team() {
|
|
178
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
179
|
+
|
|
180
|
+
PANE_LEFT=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 -P -F '#{pane_id}')
|
|
181
|
+
PANE_MID=$(tmux split-window -h -p 70 -t "$PANE_LEFT" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
|
|
182
|
+
PANE_T1=$(tmux split-window -h -p 60 -t "$PANE_MID" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
|
|
183
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 1'")
|
|
184
|
+
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
|
|
185
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 2'")
|
|
186
|
+
PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
|
|
187
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 3'")
|
|
188
|
+
PANE_CTRL=$(tmux split-window -v -p 55 -t "$PANE_LEFT" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
|
|
189
|
+
|
|
190
|
+
tmux send-keys -t "$PANE_LEFT" "bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"" Enter
|
|
191
|
+
tmux send-keys -t "$PANE_MID" "bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"" Enter
|
|
120
192
|
tmux send-keys -t "$PANE_CTRL" "clear" Enter
|
|
121
193
|
|
|
122
|
-
# Pane titles
|
|
123
194
|
tmux select-pane -t "$PANE_LEFT" -T "Prompt History"
|
|
124
195
|
tmux select-pane -t "$PANE_CTRL" -T "Controller"
|
|
125
196
|
tmux select-pane -t "$PANE_MID" -T "Dashboard"
|
|
126
197
|
tmux select-pane -t "$PANE_T1" -T "TEAM 1"
|
|
127
198
|
tmux select-pane -t "$PANE_T2" -T "TEAM 2"
|
|
128
199
|
tmux select-pane -t "$PANE_T3" -T "TEAM 3"
|
|
129
|
-
|
|
130
|
-
# Focus on Controller
|
|
131
200
|
tmux select-pane -t "$PANE_CTRL"
|
|
132
201
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else
|
|
202
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
203
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
204
|
+
}
|
|
137
205
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-P -F '#{pane_id}' \
|
|
141
|
-
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"'")
|
|
206
|
+
launch_tmux_solo() {
|
|
207
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
142
208
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
PANE_DASHBOARD=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50 -P -F '#{pane_id}' \
|
|
210
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"'")
|
|
211
|
+
PANE_MONITOR=$(tmux split-window -h -p 60 -t "$PANE_DASHBOARD" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
|
|
146
212
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
|
|
147
|
-
|
|
148
|
-
# 3. Split Dashboard vertically: Dashboard (top 70%) | Prompt History (bottom 30%)
|
|
149
|
-
PANE_HISTORY=$(tmux split-window -v -p 30 -t "$PANE_DASHBOARD" -c "$PROJECT_ROOT" \
|
|
150
|
-
-P -F '#{pane_id}' \
|
|
213
|
+
PANE_HISTORY=$(tmux split-window -v -p 30 -t "$PANE_DASHBOARD" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
|
|
151
214
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
|
|
215
|
+
PANE_AGENT=$(tmux split-window -v -p 85 -t "$PANE_MONITOR" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
|
|
152
216
|
|
|
153
|
-
# 4. Split Monitor: Monitor (top 15%) | Agent Session (bottom 85%)
|
|
154
|
-
PANE_AGENT=$(tmux split-window -v -p 85 -t "$PANE_MONITOR" -c "$PROJECT_ROOT" \
|
|
155
|
-
-P -F '#{pane_id}')
|
|
156
|
-
|
|
157
|
-
# 5. Agent Session — empty shell ready for claude
|
|
158
|
-
tmux send-keys -t "$PANE_AGENT" "unset npm_config_prefix 2>/dev/null" Enter
|
|
159
217
|
tmux send-keys -t "$PANE_AGENT" "clear" Enter
|
|
160
218
|
|
|
161
|
-
# Pane titles
|
|
162
219
|
tmux select-pane -t "$PANE_DASHBOARD" -T "Dashboard"
|
|
163
220
|
tmux select-pane -t "$PANE_MONITOR" -T "Monitor"
|
|
164
221
|
tmux select-pane -t "$PANE_HISTORY" -T "Prompt History"
|
|
165
222
|
tmux select-pane -t "$PANE_AGENT" -T "Agent Session"
|
|
166
|
-
|
|
167
|
-
# Focus on Agent Session
|
|
168
223
|
tmux select-pane -t "$PANE_AGENT"
|
|
224
|
+
|
|
225
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
226
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# ══════════════════════════════════════════
|
|
230
|
+
# Dispatch: pick best strategy
|
|
231
|
+
# ══════════════════════════════════════════
|
|
232
|
+
|
|
233
|
+
# Detect iTerm2 — works even from Claude Code's non-TTY Bash
|
|
234
|
+
IS_ITERM=false
|
|
235
|
+
if [ "${TERM_PROGRAM:-}" = "iTerm.app" ]; then
|
|
236
|
+
IS_ITERM=true
|
|
237
|
+
elif [ "$(uname)" = "Darwin" ]; then
|
|
238
|
+
# Check if iTerm2 is running (for non-TTY environments like Claude Code)
|
|
239
|
+
if pgrep -q "iTerm2" 2>/dev/null; then
|
|
240
|
+
IS_ITERM=true
|
|
241
|
+
fi
|
|
169
242
|
fi
|
|
170
243
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
244
|
+
HAS_TMUX=false
|
|
245
|
+
if command -v tmux &>/dev/null; then
|
|
246
|
+
HAS_TMUX=true
|
|
247
|
+
fi
|
|
174
248
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
249
|
+
if [ "$IS_ITERM" = true ]; then
|
|
250
|
+
echo "Strategy: iTerm2 native split panes"
|
|
251
|
+
if [ "$MODE" = "team" ]; then
|
|
252
|
+
launch_iterm2_team
|
|
253
|
+
else
|
|
254
|
+
launch_iterm2_solo
|
|
255
|
+
fi
|
|
179
256
|
echo "Layout ready"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if [ "$
|
|
184
|
-
|
|
185
|
-
tell application \"Terminal\"
|
|
186
|
-
do script \"tmux attach -t $SESSION_NAME\"
|
|
187
|
-
activate
|
|
188
|
-
end tell
|
|
189
|
-
" 2>/dev/null && \
|
|
190
|
-
echo "OPENED_TERMINAL=true" || echo "Layout ready"
|
|
257
|
+
|
|
258
|
+
elif [ "$HAS_TMUX" = true ]; then
|
|
259
|
+
echo "Strategy: tmux"
|
|
260
|
+
if [ "$MODE" = "team" ]; then
|
|
261
|
+
launch_tmux_team
|
|
191
262
|
else
|
|
192
|
-
|
|
193
|
-
echo "Layout ready"
|
|
263
|
+
launch_tmux_solo
|
|
194
264
|
fi
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
265
|
+
|
|
266
|
+
# Attach to tmux
|
|
267
|
+
if [ "$DETACH" = true ]; then
|
|
268
|
+
echo "Session created. Attach: tmux attach -t $SESSION_NAME"
|
|
269
|
+
echo "Layout ready"
|
|
270
|
+
elif ! tty -s 2>/dev/null; then
|
|
271
|
+
# Non-TTY (e.g., Claude Code Bash tool) — open in new terminal
|
|
272
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
273
|
+
osascript -e "
|
|
274
|
+
tell application \"Terminal\"
|
|
275
|
+
do script \"tmux attach -t $SESSION_NAME\"
|
|
276
|
+
activate
|
|
277
|
+
end tell
|
|
278
|
+
" 2>/dev/null && echo "OPENED_TERMINAL=true" || echo "Layout ready"
|
|
279
|
+
else
|
|
280
|
+
echo "Session created. Attach: tmux attach -t $SESSION_NAME"
|
|
281
|
+
echo "Layout ready"
|
|
282
|
+
fi
|
|
198
283
|
else
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
284
|
+
if [ -n "${TMUX:-}" ]; then
|
|
285
|
+
tmux switch-client -t "$SESSION_NAME"
|
|
286
|
+
else
|
|
287
|
+
echo "Launching Harness Studio ($MODE mode)..."
|
|
288
|
+
tmux attach -t "$SESSION_NAME"
|
|
289
|
+
fi
|
|
204
290
|
fi
|
|
291
|
+
|
|
292
|
+
else
|
|
293
|
+
echo "ERROR: Neither iTerm2 nor tmux found."
|
|
294
|
+
echo "Install tmux: brew install tmux"
|
|
295
|
+
echo "Or use iTerm2: https://iterm2.com"
|
|
296
|
+
exit 1
|
|
205
297
|
fi
|