@walwal-harness/cli 6.0.3 → 6.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,129 +0,0 @@
1
- #!/bin/bash
2
- # harness-session-start.sh — SessionStart 훅
3
- # 새 세션 시작 시 자동으로:
4
- # 1) 이전 에이전트가 completed이면 harness-next.sh 실행 (게이트 + handoff)
5
- # 2) Planner/Dispatcher 사이클이면 audit 리셋
6
- # 3) 모드별 안내 출력
7
-
8
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
- LIB="$SCRIPT_DIR/lib/harness-render-progress.sh"
10
- AUDIT_LIB="$SCRIPT_DIR/lib/harness-audit.sh"
11
-
12
- if [ ! -f "$LIB" ]; then exit 0; fi
13
- source "$LIB"
14
- [ -f "$AUDIT_LIB" ] && source "$AUDIT_LIB"
15
- command -v jq &>/dev/null || exit 0
16
-
17
- PROJECT_ROOT="$(resolve_harness_root "." 2>/dev/null)" || exit 0
18
- PROGRESS="$PROJECT_ROOT/.harness/progress.json"
19
- CONFIG="$PROJECT_ROOT/.harness/config.json"
20
- HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
21
- [ -f "$PROGRESS" ] || exit 0
22
-
23
- sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
24
- sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
25
- current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
26
- next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
27
- agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
28
- mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
29
-
30
- # ─────────────────────────────────────────
31
- # Auto-heal mode drift — Team 상태 유실 복구
32
- # feature-queue.json 에 활성 작업(in_progress)이 있는데 mode 가 solo/paused 면
33
- # 누군가 progress.json 을 통째로 덮어써서 team_state 를 날린 것. mode=team 으로
34
- # 자동 복원하고 경고 로그 남김.
35
- # ─────────────────────────────────────────
36
- FEATURE_QUEUE_HEAL="$PROJECT_ROOT/.harness/actions/feature-queue.json"
37
- if [ -f "$FEATURE_QUEUE_HEAL" ] && [ "$mode" != "team" ]; then
38
- active_count=$(jq -r '(.queue.in_progress | length) // 0' "$FEATURE_QUEUE_HEAL" 2>/dev/null || echo 0)
39
- if [ "${active_count:-0}" -gt 0 ]; then
40
- heal_teams=$active_count
41
- [ "$heal_teams" -gt 3 ] && heal_teams=3
42
- bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
43
- ".mode = \"team\" | .team_state.active_teams = ${heal_teams} | .team_state.paused_at = null" \
44
- 2>/dev/null
45
- mode="team"
46
- echo "# Harness: mode auto-healed to 'team' (feature-queue has ${active_count} active, but mode was drifted)" >&2
47
- if [ -f "$PROJECT_ROOT/.harness/progress.log" ]; then
48
- echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | reset solo→team (queue had ${active_count} in_progress)" >> "$PROJECT_ROOT/.harness/progress.log"
49
- fi
50
- fi
51
- fi
52
-
53
- # ─────────────────────────────────────────
54
- # Team Mode — 팀이 자율 실행 중이면 오케스트레이터 안내
55
- # ─────────────────────────────────────────
56
- if [ "$mode" = "team" ]; then
57
- FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
58
- passed=0; total=0; in_prog=0; failed=0
59
- if [ -f "$FEATURE_QUEUE" ]; then
60
- passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
61
- total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
62
- in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
63
- failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
64
- fi
65
-
66
- echo "# Harness Team Mode active"
67
- echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
68
- echo "# Teams run autonomously (Gen-Eval loop, max 5 retries)."
69
- echo "# Stop: /harness-stop | Switch to solo: /harness-solo"
70
- exit 0
71
- fi
72
-
73
- # ─────────────────────────────────────────
74
- # Paused Mode — 중단 상태 안내
75
- # ─────────────────────────────────────────
76
- if [ "$mode" = "paused" ]; then
77
- echo "# Harness paused — resume with /harness-team or /harness-solo"
78
- exit 0
79
- fi
80
-
81
- # ─────────────────────────────────────────
82
- # init 상태: 첫 안내
83
- # ─────────────────────────────────────────
84
- if [ "$sprint_status" = "init" ]; then
85
- echo "# Harness ready — say \"하네스 엔지니어링 시작\" or /harness-dispatcher"
86
- echo "# 기본은 Solo 모드. 병렬 3팀 실행을 원하면 Planner 완료 후 /harness-team."
87
- exit 0
88
- fi
89
-
90
- # ─────────────────────────────────────────
91
- # Audit lifecycle: Planner/Dispatcher 시작 시 리셋
92
- # ─────────────────────────────────────────
93
- init_audit "$PROJECT_ROOT"
94
- if [ "$next_agent" = "planner" ] || [ "$next_agent" = "dispatcher" ]; then
95
- # 새 사이클 — 이전 audit을 archive로 이동하고 새로 시작
96
- reset_audit "$PROJECT_ROOT" "$sprint_num"
97
- audit_log "system" "cycle" "start" "sprint-${sprint_num}" "new plan/dispatch cycle"
98
- fi
99
-
100
- # ─────────────────────────────────────────
101
- # 이전 에이전트가 completed/failed → 자동 전환
102
- # ─────────────────────────────────────────
103
- if [ "$agent_status" = "completed" ] || [ "$agent_status" = "failed" ]; then
104
- bash "$SCRIPT_DIR/harness-next.sh" "$PROJECT_ROOT" 2>/dev/null
105
-
106
- next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
107
- agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
108
- fi
109
-
110
- # ─────────────────────────────────────────
111
- # 상태별 안내 출력
112
- # ─────────────────────────────────────────
113
- if [ "$agent_status" = "blocked" ]; then
114
- echo "# Harness BLOCKED — retry limit reached, user intervention required"
115
-
116
- elif [ -f "$HANDOFF" ] && [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ]; then
117
- handoff_model=$(jq -r '.model // "opus"' "$HANDOFF" 2>/dev/null)
118
- handoff_thinking=$(jq -r '.thinking_mode // empty' "$HANDOFF" 2>/dev/null)
119
-
120
- mode_str=""
121
- if [ -n "$handoff_thinking" ] && [ "$handoff_thinking" != "null" ]; then
122
- mode_str=" /${handoff_thinking}"
123
- fi
124
-
125
- echo "# Harness: next → /harness-${next_agent} (${handoff_model}${mode_str})"
126
-
127
- elif [ "$current_agent" != "none" ] && [ "$current_agent" != "null" ]; then
128
- echo "# Harness: ${current_agent} [${agent_status}]"
129
- fi
@@ -1,139 +0,0 @@
1
- #!/bin/bash
2
- # harness-statusline.sh — Claude Code statusline hook (v5 unified)
3
- # 터미널 하단에 항상 고정되는 1줄 compact 상태 표시.
4
- # Solo: [S1] FULL | solo | >backend | sonnet | 2/5 feat | ctx 45% | $1.23
5
- # Team: [S1] FULL | team | T1:gen T2:eval T3:idle | 2/5 feat | ctx 45% | $1.23
6
-
7
- # Read Claude Code session data from stdin
8
- input=$(cat)
9
-
10
- # Extract Claude Code built-in data
11
- context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null | cut -d. -f1)
12
- cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' 2>/dev/null)
13
-
14
- # Resolve project root (walk up from cwd)
15
- CWD=$(echo "$input" | jq -r '.workspace.current_dir // empty' 2>/dev/null)
16
- if [ -z "$CWD" ]; then CWD="$PWD"; fi
17
-
18
- PROJECT_ROOT="$CWD"
19
- while [ "$PROJECT_ROOT" != "/" ]; do
20
- if [ -d "$PROJECT_ROOT/.harness" ]; then break; fi
21
- PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
22
- done
23
-
24
- PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
- FEATURE_LIST="$PROJECT_ROOT/.harness/actions/feature-list.json"
26
- PIPELINE_JSON="$PROJECT_ROOT/.harness/actions/pipeline.json"
27
-
28
- # No harness → minimal status
29
- if [ ! -f "$PROGRESS" ]; then
30
- echo "harness: not initialized | ctx ${context_pct}%"
31
- exit 0
32
- fi
33
-
34
- # Read harness state
35
- sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
36
- sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
37
- pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS" 2>/dev/null)
38
- current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
39
- agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
40
- next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
41
- retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS" 2>/dev/null)
42
- harness_mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
43
-
44
- # Pipeline short name
45
- case "$pipeline" in
46
- FULLSTACK) pl="FULL" ;;
47
- FE-ONLY) pl="FE" ;;
48
- BE-ONLY) pl="BE" ;;
49
- null|"?") pl="?" ;;
50
- *) pl="$pipeline" ;;
51
- esac
52
-
53
- # Agent short name (strip harness prefix)
54
- agent_short="${current_agent#generator-}"
55
- agent_short="${agent_short#evaluator-}"
56
- if [ "$current_agent" = "none" ] || [ "$current_agent" = "null" ]; then
57
- agent_short="$next_agent"
58
- if [ "$agent_short" = "none" ] || [ "$agent_short" = "null" ]; then
59
- agent_short="idle"
60
- fi
61
- fi
62
-
63
- # Feature progress
64
- total_features=0
65
- completed_features=0
66
- if [ -f "$FEATURE_LIST" ]; then
67
- total_features=$(jq '.features | length' "$FEATURE_LIST" 2>/dev/null || echo 0)
68
- completed_features=$(jq '[.features[]? | select(
69
- (.passes // []) | (
70
- (map(select(. == "evaluator-code-quality")) | length > 0) and
71
- (map(select(. == "evaluator-functional")) | length > 0) and
72
- (map(select(. == "evaluator-visual")) | length > 0)
73
- )
74
- )] | length' "$FEATURE_LIST" 2>/dev/null || echo 0)
75
- fi
76
-
77
- # Status indicator
78
- status_icon=""
79
- case "$agent_status" in
80
- running) status_icon=">" ;;
81
- completed) status_icon="v" ;;
82
- failed) status_icon="x" ;;
83
- blocked) status_icon="!" ;;
84
- *) status_icon="-" ;;
85
- esac
86
-
87
- # Retry indicator
88
- retry_str=""
89
- if [ "$retry_count" -gt 0 ]; then
90
- retry_str=" R${retry_count}"
91
- fi
92
-
93
- # Model/mode for current or next agent
94
- CONFIG="$PROJECT_ROOT/.harness/config.json"
95
- active_agent="$current_agent"
96
- if [ "$active_agent" = "none" ] || [ "$active_agent" = "null" ]; then
97
- active_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
98
- fi
99
- model_short=""
100
- if [ -f "$CONFIG" ] && [ "$active_agent" != "none" ] && [ "$active_agent" != "null" ]; then
101
- am=$(jq -r ".agents[\"${active_agent}\"].model // empty" "$CONFIG" 2>/dev/null)
102
- at=$(jq -r ".agents[\"${active_agent}\"].thinking_mode // empty" "$CONFIG" 2>/dev/null)
103
- if [ -n "$am" ]; then
104
- model_short="${am}"
105
- if [ -n "$at" ] && [ "$at" != "null" ]; then
106
- model_short+="/${at}"
107
- fi
108
- fi
109
- fi
110
-
111
- # Build compact status line based on mode
112
- FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
113
-
114
- if [ "$harness_mode" = "team" ] && [ -f "$FEATURE_QUEUE" ]; then
115
- # Team mode: show team status instead of single agent
116
- team_info=""
117
- for i in 1 2 3; do
118
- t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$FEATURE_QUEUE" 2>/dev/null)
119
- t_feature=$(jq -r ".teams[\"$i\"].feature // \"\"" "$FEATURE_QUEUE" 2>/dev/null)
120
- if [ "$t_status" = "busy" ] && [ -n "$t_feature" ] && [ "$t_feature" != "null" ]; then
121
- t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$FEATURE_QUEUE" 2>/dev/null)
122
- team_info+="T${i}:${t_phase} "
123
- else
124
- team_info+="T${i}:- "
125
- fi
126
- done
127
- t_passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
128
- t_total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
129
-
130
- echo "[S${sprint_num}] ${pl} | team | ${team_info}| ${t_passed}/${t_total} feat | ctx ${context_pct}% | \$${cost}"
131
- else
132
- # Solo/paused mode: show single agent status
133
- mode_tag="${harness_mode}"
134
- if [ -n "$model_short" ]; then
135
- echo "[S${sprint_num}] ${pl} | ${mode_tag} | ${status_icon}${agent_short}${retry_str} | ${model_short} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
136
- else
137
- echo "[S${sprint_num}] ${pl} | ${mode_tag} | ${status_icon}${agent_short}${retry_str} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
138
- fi
139
- fi
@@ -1,372 +0,0 @@
1
- #!/bin/bash
2
- # harness-tmux.sh — Unified Harness Studio layout
3
- #
4
- # Team Mode (--team):
5
- # ┌──────────────┬──────────────┬──────────────┐
6
- # │ │ │ TEAM 1 │
7
- # │ Dashboard │ │ │
8
- # │ (queue + │ Gotcha & ├──────────────┤
9
- # │ status + │ Memory │ TEAM 2 │
10
- # │ features) │ │ │
11
- # ├──────────────┤ ├──────────────┤
12
- # │ Archive │ │ TEAM 3 │
13
- # │ Prompt │ │ │
14
- # └──────────────┴──────────────┴──────────────┘
15
- # (Archive Prompt는 별도 패널 — Dashboard 모니터링 영역 보호)
16
- #
17
- # Rendering strategy:
18
- # 1. iTerm2 detected → native split panes (no tmux needed)
19
- # 2. tmux available → tmux session
20
- # 3. Fallback → open new terminal windows
21
- #
22
- # Usage:
23
- # bash scripts/harness-tmux.sh [project-root] --team
24
- # bash scripts/harness-tmux.sh [project-root] --solo
25
- # bash scripts/harness-tmux.sh [project-root] --team --force-tmux # skip iTerm2, use tmux
26
- # bash scripts/harness-tmux.sh --kill
27
-
28
- set -euo pipefail
29
-
30
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
31
- SESSION_NAME="harness-studio"
32
-
33
- PROJECT_ROOT=""
34
- MODE=""
35
- DETACH=false
36
- FORCE_TMUX=false
37
-
38
- for arg in "$@"; do
39
- case "$arg" in
40
- --team) MODE="team" ;;
41
- --solo) MODE="solo" ;;
42
- --detach) DETACH=true ;;
43
- --force-tmux) FORCE_TMUX=true ;;
44
- --kill)
45
- tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "tmux session killed." || true
46
- # Also try to close iTerm2 studio tab if exists
47
- if [ "${TERM_PROGRAM:-}" = "iTerm.app" ]; then
48
- osascript -e '
49
- tell application "iTerm2"
50
- repeat with w in windows
51
- repeat with t in tabs of w
52
- if name of current session of t contains "harness-studio" then
53
- close t
54
- end if
55
- end repeat
56
- end repeat
57
- end tell
58
- ' 2>/dev/null || true
59
- fi
60
- echo "Killed."
61
- exit 0
62
- ;;
63
- *)
64
- if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
65
- ;;
66
- esac
67
- done
68
-
69
- if [ -z "$PROJECT_ROOT" ]; then
70
- dir="$(pwd)"
71
- while [ "$dir" != "/" ]; do
72
- if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
73
- dir="$(dirname "$dir")"
74
- done
75
- fi
76
-
77
- if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
78
- echo "Error: .harness/ not found."
79
- exit 1
80
- fi
81
-
82
- # Auto-detect mode from progress.json if not specified.
83
- # Self-heal: feature-queue.json 에 in_progress 가 있으면 mode 와 무관하게 team 으로 간주.
84
- # (SessionStart 훅 외 경로에서 dashboard/tmux 가 stale solo 를 표시하던 버그)
85
- if [ -z "$MODE" ]; then
86
- if command -v jq &>/dev/null && [ -f "$PROJECT_ROOT/.harness/progress.json" ]; then
87
- MODE=$(jq -r '.mode // "solo"' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
88
- QUEUE_FILE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
89
- if [ "$MODE" != "team" ] && [ -f "$QUEUE_FILE" ]; then
90
- ACTIVE_COUNT=$(jq -r '(.queue.in_progress | length) // 0' "$QUEUE_FILE" 2>/dev/null || echo 0)
91
- if [ "${ACTIVE_COUNT:-0}" -gt 0 ]; then
92
- HEAL_TEAMS=$ACTIVE_COUNT
93
- [ "$HEAL_TEAMS" -gt 3 ] && HEAL_TEAMS=3
94
- bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
95
- ".mode = \"team\" | .team_state.active_teams = ${HEAL_TEAMS} | .team_state.paused_at = null" \
96
- 2>/dev/null
97
- MODE="team"
98
- echo "# tmux: mode auto-healed to 'team' (queue had ${ACTIVE_COUNT} in_progress)"
99
- if [ -f "$PROJECT_ROOT/.harness/progress.log" ]; then
100
- echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | tmux reset solo→team (queue had ${ACTIVE_COUNT} in_progress)" >> "$PROJECT_ROOT/.harness/progress.log"
101
- fi
102
- fi
103
- fi
104
- fi
105
- MODE="${MODE:-solo}"
106
- fi
107
-
108
- echo "Project: $PROJECT_ROOT"
109
- echo "Mode: $MODE"
110
-
111
- # ══════════════════════════════════════════
112
- # Strategy 1: iTerm2 native split panes
113
- # ══════════════════════════════════════════
114
- launch_iterm2_team() {
115
- osascript <<APPLESCRIPT
116
- tell application "iTerm2"
117
- activate
118
-
119
- -- Create new window with first session (Prompt History)
120
- set studioWindow to (create window with default profile)
121
-
122
- tell studioWindow
123
- tell current session of current tab
124
- set name to "harness-studio"
125
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
126
-
127
- -- Split down (bottom of Dashboard column) → Archive Prompt
128
- set archivePane to (split horizontally with default profile)
129
- tell archivePane
130
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-prompt-history.sh' '${PROJECT_ROOT}'"
131
- end tell
132
-
133
- -- Split right → Gotchas (top of Rules column, full-height initially)
134
- set gotchaPane to (split vertically with default profile)
135
- tell gotchaPane
136
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode gotcha"
137
-
138
- -- IMPORTANT: Team 컬럼을 먼저 분리 (전체 높이 유지). 그 후에 gotchaPane 을 horizontal 분할.
139
- set t1Pane to (split vertically with default profile)
140
- tell t1Pane
141
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 1"
142
-
143
- set t2Pane to (split horizontally with default profile)
144
- tell t2Pane
145
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 2"
146
-
147
- set t3Pane to (split horizontally with default profile)
148
- tell t3Pane
149
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}' --team 3"
150
- end tell
151
- end tell
152
- end tell
153
-
154
- -- Team 컬럼 분리 후, gotchaPane (Rules 컬럼) 을 horizontal 두 번 split → Conventions / Memory
155
- set convPane to (split horizontally with default profile)
156
- tell convPane
157
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode conventions"
158
-
159
- set memPane to (split horizontally with default profile)
160
- tell memPane
161
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode memory"
162
- end tell
163
- end tell
164
- end tell
165
- end tell
166
- end tell
167
- end tell
168
- APPLESCRIPT
169
- }
170
-
171
- launch_iterm2_solo() {
172
- # Solo 레이아웃 (Team 모드와 동일 구조, 우측만 단일 Monitor 패널):
173
- # Left col : Dashboard (top) + Prompt History (bottom)
174
- # Middle : Gotcha · Memory · Conventions
175
- # Right : Monitor (lifecycle stream, 단일 에이전트)
176
- osascript <<APPLESCRIPT
177
- tell application "iTerm2"
178
- activate
179
-
180
- set studioWindow to (create window with default profile)
181
-
182
- tell studioWindow
183
- tell current session of current tab
184
- set name to "harness-studio"
185
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
186
-
187
- -- Split down (bottom of Dashboard column) → Prompt History
188
- set archivePane to (split horizontally with default profile)
189
- tell archivePane
190
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-prompt-history.sh' '${PROJECT_ROOT}'"
191
- end tell
192
-
193
- -- Split right → Gotchas (top of Rules column, full-height initially)
194
- set gotchaPane to (split vertically with default profile)
195
- tell gotchaPane
196
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode gotcha"
197
-
198
- -- Monitor 컬럼을 먼저 분리 (전체 높이 유지)
199
- set monPane to (split vertically with default profile)
200
- tell monPane
201
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-monitor.sh' '${PROJECT_ROOT}'"
202
- end tell
203
-
204
- -- Monitor 분리 후, gotchaPane (Rules 컬럼) 을 horizontal 두 번 split → Conventions / Memory
205
- set convPane to (split horizontally with default profile)
206
- tell convPane
207
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode conventions"
208
-
209
- set memPane to (split horizontally with default profile)
210
- tell memPane
211
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}' --mode memory"
212
- end tell
213
- end tell
214
- end tell
215
- end tell
216
- end tell
217
- end tell
218
- APPLESCRIPT
219
- }
220
-
221
- # ══════════════════════════════════════════
222
- # Strategy 2: tmux
223
- # ══════════════════════════════════════════
224
- launch_tmux_team() {
225
- tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
226
-
227
- PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 -P -F '#{pane_id}')
228
- PANE_GOTCHA=$(tmux split-window -h -p 70 -t "$PANE_DASH" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
229
- PANE_T1=$(tmux split-window -h -p 60 -t "$PANE_GOTCHA" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
230
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 1'")
231
- PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
232
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 2'")
233
- PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
234
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 3'")
235
- # Dashboard 열을 상(Dashboard)/하(Archive Prompt)로 분할 — archive가 길어져도 모니터링 유지
236
- PANE_ARCHIVE=$(tmux split-window -v -p 35 -t "$PANE_DASH" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
237
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
238
-
239
- # Gotcha 패널을 3 개 sub-pane 으로 수직 분할: Gotchas / Conventions / Memory
240
- # 각 pane 은 tmux copy-mode (prefix + [) 로 독립 스크롤 가능
241
- PANE_CONV=$(tmux split-window -v -p 60 -t "$PANE_GOTCHA" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
242
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode conventions'")
243
- PANE_MEM=$(tmux split-window -v -p 50 -t "$PANE_CONV" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
244
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode memory'")
245
-
246
- tmux send-keys -t "$PANE_DASH" "bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"" Enter
247
- tmux send-keys -t "$PANE_GOTCHA" "bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode gotcha" Enter
248
-
249
- tmux select-pane -t "$PANE_DASH" -T "Dashboard"
250
- tmux select-pane -t "$PANE_ARCHIVE" -T "Archive Prompt"
251
- tmux select-pane -t "$PANE_GOTCHA" -T "Gotchas (scrollable: prefix + [ )"
252
- tmux select-pane -t "$PANE_CONV" -T "Conventions (scrollable: prefix + [ )"
253
- tmux select-pane -t "$PANE_MEM" -T "Shared Memory (scrollable: prefix + [ )"
254
- tmux select-pane -t "$PANE_T1" -T "TEAM 1"
255
- tmux select-pane -t "$PANE_T2" -T "TEAM 2"
256
- tmux select-pane -t "$PANE_T3" -T "TEAM 3"
257
- tmux select-pane -t "$PANE_DASH"
258
-
259
- tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
260
- tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
261
- }
262
-
263
- launch_tmux_solo() {
264
- # Solo 레이아웃 (Team 모드와 동일 구조, 우측만 단일 Monitor 패널).
265
- tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
266
-
267
- PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50 -P -F '#{pane_id}')
268
- PANE_GOTCHA=$(tmux split-window -h -p 70 -t "$PANE_DASH" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
269
- PANE_MONITOR=$(tmux split-window -h -p 50 -t "$PANE_GOTCHA" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
270
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
271
- PANE_HISTORY=$(tmux split-window -v -p 35 -t "$PANE_DASH" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
272
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
273
-
274
- # Gotcha 패널을 3 개 sub-pane 으로 수직 분할: Gotchas / Conventions / Memory
275
- PANE_CONV=$(tmux split-window -v -p 60 -t "$PANE_GOTCHA" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
276
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode conventions'")
277
- PANE_MEM=$(tmux split-window -v -p 50 -t "$PANE_CONV" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
278
- "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode memory'")
279
-
280
- tmux send-keys -t "$PANE_DASH" "bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"" Enter
281
- tmux send-keys -t "$PANE_GOTCHA" "bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\" --mode gotcha" Enter
282
-
283
- tmux select-pane -t "$PANE_DASH" -T "Dashboard"
284
- tmux select-pane -t "$PANE_HISTORY" -T "Prompt History"
285
- tmux select-pane -t "$PANE_GOTCHA" -T "Gotchas (scrollable: prefix + [ )"
286
- tmux select-pane -t "$PANE_CONV" -T "Conventions (scrollable: prefix + [ )"
287
- tmux select-pane -t "$PANE_MEM" -T "Shared Memory (scrollable: prefix + [ )"
288
- tmux select-pane -t "$PANE_MONITOR" -T "Monitor"
289
- tmux select-pane -t "$PANE_DASH"
290
-
291
- tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
292
- tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
293
- }
294
-
295
- # ══════════════════════════════════════════
296
- # Dispatch: pick best strategy
297
- # ══════════════════════════════════════════
298
-
299
- # Detect iTerm2 — works even from Claude Code's non-TTY Bash
300
- IS_ITERM=false
301
- if [ "${TERM_PROGRAM:-}" = "iTerm.app" ]; then
302
- IS_ITERM=true
303
- elif [ "$(uname)" = "Darwin" ]; then
304
- # Check if iTerm2 is running (for non-TTY environments like Claude Code)
305
- if pgrep -q "iTerm2" 2>/dev/null; then
306
- IS_ITERM=true
307
- fi
308
- fi
309
-
310
- HAS_TMUX=false
311
- if command -v tmux &>/dev/null; then
312
- HAS_TMUX=true
313
- fi
314
-
315
- # --force-tmux overrides iTerm2 detection
316
- if [ "$FORCE_TMUX" = true ]; then
317
- if [ "$HAS_TMUX" != true ]; then
318
- echo "ERROR: --force-tmux specified but tmux not installed. Run: brew install tmux"
319
- exit 1
320
- fi
321
- IS_ITERM=false
322
- fi
323
-
324
- if [ "$IS_ITERM" = true ]; then
325
- echo "Strategy: iTerm2 native split panes"
326
- if [ "$MODE" = "team" ]; then
327
- launch_iterm2_team
328
- else
329
- launch_iterm2_solo
330
- fi
331
- echo "Layout ready"
332
-
333
- elif [ "$HAS_TMUX" = true ]; then
334
- echo "Strategy: tmux"
335
- if [ "$MODE" = "team" ]; then
336
- launch_tmux_team
337
- else
338
- launch_tmux_solo
339
- fi
340
-
341
- # Attach to tmux
342
- if [ "$DETACH" = true ]; then
343
- echo "Session created. Attach: tmux attach -t $SESSION_NAME"
344
- echo "Layout ready"
345
- elif ! tty -s 2>/dev/null; then
346
- # Non-TTY (e.g., Claude Code Bash tool) — open in new terminal
347
- if [ "$(uname)" = "Darwin" ]; then
348
- osascript -e "
349
- tell application \"Terminal\"
350
- do script \"tmux attach -t $SESSION_NAME\"
351
- activate
352
- end tell
353
- " 2>/dev/null && echo "OPENED_TERMINAL=true" || echo "Layout ready"
354
- else
355
- echo "Session created. Attach: tmux attach -t $SESSION_NAME"
356
- echo "Layout ready"
357
- fi
358
- else
359
- if [ -n "${TMUX:-}" ]; then
360
- tmux switch-client -t "$SESSION_NAME"
361
- else
362
- echo "Launching Harness Studio ($MODE mode)..."
363
- tmux attach -t "$SESSION_NAME"
364
- fi
365
- fi
366
-
367
- else
368
- echo "ERROR: Neither iTerm2 nor tmux found."
369
- echo "Install tmux: brew install tmux"
370
- echo "Or use iTerm2: https://iterm2.com"
371
- exit 1
372
- fi