@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.
- package/CHANGELOG.md +43 -0
- package/bin/init.js +121 -4
- package/gotchas/conductor.md +15 -0
- package/gotchas/generator-frontend.md +27 -1
- package/gotchas/service-ops.md +27 -0
- package/package.json +1 -1
- package/skills/conductor/SKILL.md +28 -0
- package/skills/generator-frontend/SKILL.md +23 -3
- package/scripts/harness-archive.sh +0 -109
- package/scripts/harness-dashboard-up.sh +0 -72
- package/scripts/harness-dashboard.sh +0 -509
- package/scripts/harness-goal-init.sh +0 -72
- package/scripts/harness-goal-show.sh +0 -37
- package/scripts/harness-gotcha-memory.sh +0 -348
- package/scripts/harness-gotcha-register.sh +0 -393
- package/scripts/harness-monitor.sh +0 -398
- package/scripts/harness-next.sh +0 -546
- package/scripts/harness-progress-set.sh +0 -41
- package/scripts/harness-prompt-history.sh +0 -164
- package/scripts/harness-queue-manager.sh +0 -694
- package/scripts/harness-session-start.sh +0 -129
- package/scripts/harness-statusline.sh +0 -139
- package/scripts/harness-tmux.sh +0 -372
- package/scripts/harness-user-prompt-submit.sh +0 -105
- package/scripts/init-agents-md.sh +0 -337
- package/scripts/init-ref-docs.sh +0 -262
- package/scripts/lib/harness-audit.sh +0 -113
- package/scripts/lib/harness-guardrail.sh +0 -148
- package/scripts/lib/harness-keywait.sh +0 -57
- package/scripts/lib/harness-render-progress.sh +0 -346
- package/scripts/scan-project.sh +0 -367
|
@@ -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
|
package/scripts/harness-tmux.sh
DELETED
|
@@ -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
|