@walwal-harness/cli 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/init.js +37 -6
- package/package.json +1 -1
- package/scripts/harness-dashboard.sh +179 -0
- package/scripts/harness-eval-watcher.sh +230 -0
- package/scripts/harness-monitor.sh +193 -0
- package/scripts/harness-tmux.sh +121 -0
package/bin/init.js
CHANGED
|
@@ -14,9 +14,11 @@ const path = require('path');
|
|
|
14
14
|
const { execSync } = require('child_process');
|
|
15
15
|
|
|
16
16
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const subcommand = args.find(a => !a.startsWith('-')) || null;
|
|
19
|
+
const isAuto = args.includes('--auto');
|
|
20
|
+
const isForce = args.includes('--force');
|
|
21
|
+
const isHelp = args.includes('--help') || args.includes('-h');
|
|
20
22
|
|
|
21
23
|
// ─────────────────────────────────────────
|
|
22
24
|
// Resolve project root
|
|
@@ -551,9 +553,11 @@ function showHelp() {
|
|
|
551
553
|
╚══════════════════════════════════════╝
|
|
552
554
|
|
|
553
555
|
Usage:
|
|
554
|
-
npx walwal-harness
|
|
555
|
-
npx walwal-harness --force
|
|
556
|
-
npx walwal-harness
|
|
556
|
+
npx walwal-harness Initialize project for harness engineering
|
|
557
|
+
npx walwal-harness --force Re-initialize (overwrites existing files)
|
|
558
|
+
npx walwal-harness studio Launch Harness Studio (tmux 4-pane monitor)
|
|
559
|
+
npx walwal-harness studio --ai Studio + AI eval summary (API cost)
|
|
560
|
+
npx walwal-harness --help Show this help
|
|
557
561
|
|
|
558
562
|
What it does:
|
|
559
563
|
1. Scaffolds .harness/ directory (actions, archive, gotchas, config)
|
|
@@ -580,12 +584,39 @@ After init:
|
|
|
580
584
|
// ─────────────────────────────────────────
|
|
581
585
|
// Main
|
|
582
586
|
// ─────────────────────────────────────────
|
|
587
|
+
function runStudio() {
|
|
588
|
+
const useAi = args.includes('--ai') ? '--ai' : '';
|
|
589
|
+
const scriptsDir = path.join(PKG_ROOT, 'scripts');
|
|
590
|
+
const tmuxScript = path.join(scriptsDir, 'harness-tmux.sh');
|
|
591
|
+
|
|
592
|
+
if (!fs.existsSync(tmuxScript)) {
|
|
593
|
+
log('ERROR: harness-tmux.sh not found. Update @walwal-harness/cli to >= 3.6.0');
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
execSync('which tmux', { stdio: 'ignore' });
|
|
599
|
+
} catch {
|
|
600
|
+
log('ERROR: tmux is required. Install with: brew install tmux');
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const cmd = `bash "${tmuxScript}" "${PROJECT_ROOT}" ${useAi}`.trim();
|
|
605
|
+
log(`Launching Harness Studio...`);
|
|
606
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
607
|
+
}
|
|
608
|
+
|
|
583
609
|
function main() {
|
|
584
610
|
if (isHelp) {
|
|
585
611
|
showHelp();
|
|
586
612
|
return;
|
|
587
613
|
}
|
|
588
614
|
|
|
615
|
+
if (subcommand === 'studio') {
|
|
616
|
+
runStudio();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
589
620
|
const pkg = require(path.join(PKG_ROOT, 'package.json'));
|
|
590
621
|
console.log('');
|
|
591
622
|
console.log('╔══════════════════════════════════════╗');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-dashboard.sh — Panel 1: 실시간 대시보드
|
|
3
|
+
# progress.json + feature-list.json 을 2초 간격으로 시각화
|
|
4
|
+
# Usage: bash scripts/harness-dashboard.sh [project-root]
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
10
|
+
|
|
11
|
+
PROJECT_ROOT="${1:-}"
|
|
12
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
14
|
+
echo "[dashboard] .harness/ not found. Pass project root as argument."
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
20
|
+
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
21
|
+
|
|
22
|
+
# ── ANSI helpers ──
|
|
23
|
+
BOLD="\033[1m"
|
|
24
|
+
DIM="\033[2m"
|
|
25
|
+
GREEN="\033[32m"
|
|
26
|
+
YELLOW="\033[33m"
|
|
27
|
+
RED="\033[31m"
|
|
28
|
+
CYAN="\033[36m"
|
|
29
|
+
RESET="\033[0m"
|
|
30
|
+
|
|
31
|
+
render_header() {
|
|
32
|
+
local now
|
|
33
|
+
now=$(date +"%H:%M:%S")
|
|
34
|
+
local project_name
|
|
35
|
+
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
38
|
+
echo -e "${BOLD}║ HARNESS DASHBOARD ║${RESET}"
|
|
39
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
40
|
+
echo -e " ${DIM}${project_name} | Updated: ${now}${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render_sprint_overview() {
|
|
45
|
+
if [ ! -f "$PROGRESS" ]; then
|
|
46
|
+
echo -e " ${DIM}(waiting for harness init...)${RESET}"
|
|
47
|
+
return
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
local pipeline sprint_num sprint_status current_agent agent_status retry_count
|
|
51
|
+
pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS")
|
|
52
|
+
sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
|
|
53
|
+
sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS")
|
|
54
|
+
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
|
|
55
|
+
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
|
|
56
|
+
retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
|
|
57
|
+
|
|
58
|
+
# Status color
|
|
59
|
+
local status_color="$RESET"
|
|
60
|
+
case "$agent_status" in
|
|
61
|
+
running) status_color="$GREEN" ;;
|
|
62
|
+
completed) status_color="$CYAN" ;;
|
|
63
|
+
failed) status_color="$RED" ;;
|
|
64
|
+
blocked) status_color="$RED" ;;
|
|
65
|
+
*) status_color="$YELLOW" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
echo -e " ${BOLD}Pipeline${RESET} ${pipeline}"
|
|
69
|
+
echo -e " ${BOLD}Sprint${RESET} ${sprint_num} (${sprint_status})"
|
|
70
|
+
echo -e " ${BOLD}Agent${RESET} ${status_color}${current_agent} [${agent_status}]${RESET}"
|
|
71
|
+
if [ "$retry_count" -gt 0 ]; then
|
|
72
|
+
echo -e " ${BOLD}Retries${RESET} ${RED}${retry_count}${RESET}"
|
|
73
|
+
fi
|
|
74
|
+
echo ""
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render_sprint_map() {
|
|
78
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
79
|
+
|
|
80
|
+
local sprint_keys
|
|
81
|
+
sprint_keys=$(jq -r '.sprint_progress // {} | keys[]' "$PROGRESS" 2>/dev/null | sort -n)
|
|
82
|
+
if [ -z "$sprint_keys" ]; then return; fi
|
|
83
|
+
|
|
84
|
+
echo -e " ${BOLD}Sprint Map${RESET}"
|
|
85
|
+
|
|
86
|
+
while IFS= read -r s; do
|
|
87
|
+
local status features_csv notes
|
|
88
|
+
status=$(jq -r ".sprint_progress[\"${s}\"].status // \"pending\"" "$PROGRESS")
|
|
89
|
+
features_csv=$(jq -r ".sprint_progress[\"${s}\"].features // [] | join(\",\")" "$PROGRESS")
|
|
90
|
+
notes=$(jq -r ".sprint_progress[\"${s}\"].notes // \"\"" "$PROGRESS")
|
|
91
|
+
|
|
92
|
+
# Status icon
|
|
93
|
+
local icon="○"
|
|
94
|
+
case "$status" in
|
|
95
|
+
completed) icon="●" ;;
|
|
96
|
+
in_progress) icon="◐" ;;
|
|
97
|
+
scaffolded) icon="◑" ;;
|
|
98
|
+
pending) icon="○" ;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
# Count features
|
|
102
|
+
local feat_count
|
|
103
|
+
feat_count=$(jq ".sprint_progress[\"${s}\"].features // [] | length" "$PROGRESS")
|
|
104
|
+
|
|
105
|
+
# Truncate notes
|
|
106
|
+
if [ ${#notes} -gt 40 ]; then
|
|
107
|
+
notes="${notes:0:38}.."
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
printf " %s S%-2s %2d feat %-12s %s\n" "$icon" "$s" "$feat_count" "$status" "$notes"
|
|
111
|
+
done <<< "$sprint_keys"
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
render_build_status() {
|
|
117
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
118
|
+
|
|
119
|
+
local build_status tsc_status routes_total last_build
|
|
120
|
+
build_status=$(jq -r '.build.build_status // "unknown"' "$PROGRESS" 2>/dev/null)
|
|
121
|
+
tsc_status=$(jq -r '.build.tsc_status // "unknown"' "$PROGRESS" 2>/dev/null)
|
|
122
|
+
routes_total=$(jq -r '.build.routes_total // 0' "$PROGRESS" 2>/dev/null)
|
|
123
|
+
last_build=$(jq -r '.build.last_build // "n/a"' "$PROGRESS" 2>/dev/null)
|
|
124
|
+
|
|
125
|
+
if [ "$build_status" = "null" ] || [ "$build_status" = "unknown" ]; then return; fi
|
|
126
|
+
|
|
127
|
+
local build_color="$GREEN"
|
|
128
|
+
if [ "$build_status" != "passing" ]; then build_color="$RED"; fi
|
|
129
|
+
local tsc_color="$GREEN"
|
|
130
|
+
if [ "$tsc_status" != "clean" ]; then tsc_color="$RED"; fi
|
|
131
|
+
|
|
132
|
+
echo -e " ${BOLD}Build${RESET}"
|
|
133
|
+
echo -e " Build: ${build_color}${build_status}${RESET} | TSC: ${tsc_color}${tsc_status}${RESET} | Routes: ${routes_total}"
|
|
134
|
+
echo ""
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
render_failure_info() {
|
|
138
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
139
|
+
|
|
140
|
+
local failure_agent
|
|
141
|
+
failure_agent=$(jq -r '.failure.agent // empty' "$PROGRESS" 2>/dev/null)
|
|
142
|
+
|
|
143
|
+
if [ -n "$failure_agent" ] && [ "$failure_agent" != "null" ]; then
|
|
144
|
+
local failure_loc failure_msg
|
|
145
|
+
failure_loc=$(jq -r '.failure.location // ""' "$PROGRESS")
|
|
146
|
+
failure_msg=$(jq -r '.failure.message // ""' "$PROGRESS")
|
|
147
|
+
|
|
148
|
+
echo -e " ${RED}${BOLD}╔═ FAILURE ═══════════════════════════╗${RESET}"
|
|
149
|
+
echo -e " ${RED} Agent: ${failure_agent}${RESET}"
|
|
150
|
+
echo -e " ${RED} Where: ${failure_loc}${RESET}"
|
|
151
|
+
if [ -n "$failure_msg" ] && [ "$failure_msg" != "null" ]; then
|
|
152
|
+
echo -e " ${RED} ${failure_msg}${RESET}"
|
|
153
|
+
fi
|
|
154
|
+
echo -e " ${RED}${BOLD}╚════════════════════════════════════╝${RESET}"
|
|
155
|
+
echo ""
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ── Main Loop ──
|
|
160
|
+
while true; do
|
|
161
|
+
clear
|
|
162
|
+
render_header
|
|
163
|
+
render_sprint_overview
|
|
164
|
+
render_build_status
|
|
165
|
+
render_failure_info
|
|
166
|
+
render_sprint_map
|
|
167
|
+
|
|
168
|
+
# Feature progress (reuse existing lib)
|
|
169
|
+
if [ -f "$FEATURES" ]; then
|
|
170
|
+
render_progress "$PROJECT_ROOT" 2>/dev/null
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Agent sequence bar
|
|
174
|
+
render_agent_bar "$PROJECT_ROOT" 2>/dev/null
|
|
175
|
+
echo ""
|
|
176
|
+
|
|
177
|
+
echo -e " ${DIM}Refreshing every 2s | Ctrl+C to exit${RESET}"
|
|
178
|
+
sleep 2
|
|
179
|
+
done
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-eval-watcher.sh — Panel 3: Evaluation 결과 Summary 리뷰
|
|
3
|
+
# evaluation-*.md 파일 변경 감지 → 요약 출력 (또는 claude -p 호출)
|
|
4
|
+
# Usage: bash scripts/harness-eval-watcher.sh [project-root] [--ai]
|
|
5
|
+
# --ai evaluation 완료 시 claude -p 로 AI 요약 생성 (API 비용 발생)
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
|
|
11
|
+
PROJECT_ROOT="${1:-}"
|
|
12
|
+
USE_AI=false
|
|
13
|
+
for arg in "$@"; do
|
|
14
|
+
if [ "$arg" = "--ai" ]; then USE_AI=true; fi
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
if [ -z "$PROJECT_ROOT" ] || [ "$PROJECT_ROOT" = "--ai" ]; then
|
|
18
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
19
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
20
|
+
echo "[eval-watcher] .harness/ not found. Pass project root as argument."
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
26
|
+
EVAL_FUNC="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
|
|
27
|
+
EVAL_VISUAL="$PROJECT_ROOT/.harness/actions/evaluation-visual.md"
|
|
28
|
+
|
|
29
|
+
# ── ANSI helpers ──
|
|
30
|
+
BOLD="\033[1m"
|
|
31
|
+
DIM="\033[2m"
|
|
32
|
+
GREEN="\033[32m"
|
|
33
|
+
YELLOW="\033[33m"
|
|
34
|
+
RED="\033[31m"
|
|
35
|
+
CYAN="\033[36m"
|
|
36
|
+
RESET="\033[0m"
|
|
37
|
+
|
|
38
|
+
LAST_FUNC_HASH=""
|
|
39
|
+
LAST_VISUAL_HASH=""
|
|
40
|
+
EVAL_COUNT=0
|
|
41
|
+
|
|
42
|
+
print_header() {
|
|
43
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
44
|
+
echo -e "${BOLD}║ EVALUATION REVIEW ║${RESET}"
|
|
45
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
46
|
+
echo ""
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
file_hash() {
|
|
50
|
+
if [ -f "$1" ]; then
|
|
51
|
+
md5 -q "$1" 2>/dev/null || md5sum "$1" 2>/dev/null | awk '{print $1}' || echo ""
|
|
52
|
+
else
|
|
53
|
+
echo ""
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── Extract eval summary from markdown ──
|
|
58
|
+
extract_eval_summary() {
|
|
59
|
+
local file="$1" type="$2"
|
|
60
|
+
|
|
61
|
+
if [ ! -f "$file" ]; then return; fi
|
|
62
|
+
|
|
63
|
+
EVAL_COUNT=$((EVAL_COUNT + 1))
|
|
64
|
+
local now
|
|
65
|
+
now=$(date +"%H:%M:%S")
|
|
66
|
+
|
|
67
|
+
echo -e " ${BOLD}═══ ${type} Evaluation #${EVAL_COUNT} ═══${RESET} ${DIM}(${now})${RESET}"
|
|
68
|
+
echo ""
|
|
69
|
+
|
|
70
|
+
# Extract Verdict (PASS/FAIL)
|
|
71
|
+
local verdict
|
|
72
|
+
verdict=$(grep -i "verdict\|result\|판정" "$file" | head -3)
|
|
73
|
+
if [ -n "$verdict" ]; then
|
|
74
|
+
if echo "$verdict" | grep -qi "PASS"; then
|
|
75
|
+
echo -e " ${GREEN}${BOLD}VERDICT: PASS${RESET}"
|
|
76
|
+
elif echo "$verdict" | grep -qi "FAIL"; then
|
|
77
|
+
echo -e " ${RED}${BOLD}VERDICT: FAIL${RESET}"
|
|
78
|
+
else
|
|
79
|
+
echo -e " ${YELLOW}VERDICT: ${verdict}${RESET}"
|
|
80
|
+
fi
|
|
81
|
+
echo ""
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Extract Score
|
|
85
|
+
local score_line
|
|
86
|
+
score_line=$(grep -iE "score|점수|weighted|가중" "$file" | head -3)
|
|
87
|
+
if [ -n "$score_line" ]; then
|
|
88
|
+
echo -e " ${BOLD}Score${RESET}"
|
|
89
|
+
echo "$score_line" | while IFS= read -r line; do
|
|
90
|
+
# Color by score value
|
|
91
|
+
local num
|
|
92
|
+
num=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+' | head -1)
|
|
93
|
+
if [ -n "$num" ]; then
|
|
94
|
+
local int_part
|
|
95
|
+
int_part=$(echo "$num" | cut -d. -f1)
|
|
96
|
+
if [ "${int_part:-0}" -ge 3 ] || ([ "${int_part:-0}" -eq 2 ] && [ "$(echo "$num >= 2.80" | bc 2>/dev/null)" = "1" ]); then
|
|
97
|
+
echo -e " ${GREEN}${line}${RESET}"
|
|
98
|
+
else
|
|
99
|
+
echo -e " ${RED}${line}${RESET}"
|
|
100
|
+
fi
|
|
101
|
+
else
|
|
102
|
+
echo " $line"
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
echo ""
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Extract individual rubric items (R1-R5 or V1-V5)
|
|
109
|
+
local rubric_lines
|
|
110
|
+
rubric_lines=$(grep -E "^[|#].*[RV][1-5]" "$file" | head -10)
|
|
111
|
+
if [ -n "$rubric_lines" ]; then
|
|
112
|
+
echo -e " ${BOLD}Rubric${RESET}"
|
|
113
|
+
echo "$rubric_lines" | while IFS= read -r line; do
|
|
114
|
+
echo " $line"
|
|
115
|
+
done
|
|
116
|
+
echo ""
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Extract FAIL reasons
|
|
120
|
+
local fail_lines
|
|
121
|
+
fail_lines=$(grep -iE "fail|실패|regression|불일치|위반" "$file" | head -5)
|
|
122
|
+
if [ -n "$fail_lines" ]; then
|
|
123
|
+
echo -e " ${RED}${BOLD}Issues${RESET}"
|
|
124
|
+
echo "$fail_lines" | while IFS= read -r line; do
|
|
125
|
+
echo -e " ${RED}• ${line}${RESET}"
|
|
126
|
+
done
|
|
127
|
+
echo ""
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Extract action items / recommendations
|
|
131
|
+
local action_lines
|
|
132
|
+
action_lines=$(grep -iE "recommend|action|수정|개선|필요|re-generate" "$file" | head -5)
|
|
133
|
+
if [ -n "$action_lines" ]; then
|
|
134
|
+
echo -e " ${YELLOW}${BOLD}Actions${RESET}"
|
|
135
|
+
echo "$action_lines" | while IFS= read -r line; do
|
|
136
|
+
echo -e " ${YELLOW}→ ${line}${RESET}"
|
|
137
|
+
done
|
|
138
|
+
echo ""
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo -e " ${DIM}─────────────────────────────────────${RESET}"
|
|
142
|
+
echo ""
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ── AI Summary (optional) ──
|
|
146
|
+
generate_ai_summary() {
|
|
147
|
+
local file="$1" type="$2"
|
|
148
|
+
|
|
149
|
+
if [ "$USE_AI" != true ]; then return; fi
|
|
150
|
+
if ! command -v claude &>/dev/null; then
|
|
151
|
+
echo -e " ${DIM}(claude CLI not found — skipping AI summary)${RESET}"
|
|
152
|
+
return
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo -e " ${CYAN}Generating AI summary...${RESET}"
|
|
156
|
+
|
|
157
|
+
local summary
|
|
158
|
+
summary=$(claude -p "다음 ${type} Evaluation 문서를 3줄 이내로 요약해줘. 핵심 결과(PASS/FAIL), 주요 문제점, 다음 액션을 포함:
|
|
159
|
+
|
|
160
|
+
$(cat "$file")" --output-format text 2>/dev/null || echo "(AI summary failed)")
|
|
161
|
+
|
|
162
|
+
echo -e " ${CYAN}${BOLD}AI Summary:${RESET}"
|
|
163
|
+
echo "$summary" | while IFS= read -r line; do
|
|
164
|
+
echo -e " ${CYAN}${line}${RESET}"
|
|
165
|
+
done
|
|
166
|
+
echo ""
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── Main ──
|
|
170
|
+
print_header
|
|
171
|
+
|
|
172
|
+
# Show existing evals on startup
|
|
173
|
+
if [ -f "$EVAL_FUNC" ]; then
|
|
174
|
+
echo -e " ${DIM}Found existing functional evaluation${RESET}"
|
|
175
|
+
extract_eval_summary "$EVAL_FUNC" "Functional"
|
|
176
|
+
LAST_FUNC_HASH=$(file_hash "$EVAL_FUNC")
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ -f "$EVAL_VISUAL" ]; then
|
|
180
|
+
echo -e " ${DIM}Found existing visual evaluation${RESET}"
|
|
181
|
+
extract_eval_summary "$EVAL_VISUAL" "Visual"
|
|
182
|
+
LAST_VISUAL_HASH=$(file_hash "$EVAL_VISUAL")
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [ -z "$LAST_FUNC_HASH" ] && [ -z "$LAST_VISUAL_HASH" ]; then
|
|
186
|
+
echo -e " ${DIM}Waiting for evaluation files...${RESET}"
|
|
187
|
+
echo -e " ${DIM} ${EVAL_FUNC}${RESET}"
|
|
188
|
+
echo -e " ${DIM} ${EVAL_VISUAL}${RESET}"
|
|
189
|
+
echo ""
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
echo -e " ${DIM}Watching for changes every 3s | Ctrl+C to exit${RESET}"
|
|
193
|
+
if [ "$USE_AI" = true ]; then
|
|
194
|
+
echo -e " ${CYAN}AI summary enabled (claude -p)${RESET}"
|
|
195
|
+
fi
|
|
196
|
+
echo ""
|
|
197
|
+
|
|
198
|
+
# ── Poll for changes ──
|
|
199
|
+
while true; do
|
|
200
|
+
# Check functional eval
|
|
201
|
+
local_func_hash=$(file_hash "$EVAL_FUNC")
|
|
202
|
+
if [ "$local_func_hash" != "$LAST_FUNC_HASH" ] && [ -n "$local_func_hash" ]; then
|
|
203
|
+
LAST_FUNC_HASH="$local_func_hash"
|
|
204
|
+
extract_eval_summary "$EVAL_FUNC" "Functional"
|
|
205
|
+
generate_ai_summary "$EVAL_FUNC" "Functional"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Check visual eval
|
|
209
|
+
local_visual_hash=$(file_hash "$EVAL_VISUAL")
|
|
210
|
+
if [ "$local_visual_hash" != "$LAST_VISUAL_HASH" ] && [ -n "$local_visual_hash" ]; then
|
|
211
|
+
LAST_VISUAL_HASH="$local_visual_hash"
|
|
212
|
+
extract_eval_summary "$EVAL_VISUAL" "Visual"
|
|
213
|
+
generate_ai_summary "$EVAL_VISUAL" "Visual"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Check agent status for eval-related transitions
|
|
217
|
+
if [ -f "$PROGRESS" ]; then
|
|
218
|
+
local agent
|
|
219
|
+
agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
220
|
+
local status
|
|
221
|
+
status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
222
|
+
|
|
223
|
+
if [[ "$agent" == evaluator-* ]] && [ "$status" = "running" ]; then
|
|
224
|
+
# Show that evaluator is active
|
|
225
|
+
:
|
|
226
|
+
fi
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
sleep 3
|
|
230
|
+
done
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-monitor.sh — Panel 2: Task & Agent Lifecycle 모니터링
|
|
3
|
+
# audit.log 스트리밍 + progress.json 변경 감지
|
|
4
|
+
# Usage: bash scripts/harness-monitor.sh [project-root]
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="${1:-}"
|
|
11
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
12
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
14
|
+
echo "[monitor] .harness/ not found. Pass project root as argument."
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
20
|
+
AUDIT_LOG="$PROJECT_ROOT/.harness/actions/audit.log"
|
|
21
|
+
HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
|
|
22
|
+
|
|
23
|
+
# ── ANSI helpers ──
|
|
24
|
+
BOLD="\033[1m"
|
|
25
|
+
DIM="\033[2m"
|
|
26
|
+
GREEN="\033[32m"
|
|
27
|
+
YELLOW="\033[33m"
|
|
28
|
+
RED="\033[31m"
|
|
29
|
+
CYAN="\033[36m"
|
|
30
|
+
MAGENTA="\033[35m"
|
|
31
|
+
RESET="\033[0m"
|
|
32
|
+
|
|
33
|
+
LAST_AGENT=""
|
|
34
|
+
LAST_STATUS=""
|
|
35
|
+
LAST_SPRINT=""
|
|
36
|
+
EVENT_COUNT=0
|
|
37
|
+
|
|
38
|
+
print_event() {
|
|
39
|
+
local ts="$1" icon="$2" color="$3" msg="$4"
|
|
40
|
+
EVENT_COUNT=$((EVENT_COUNT + 1))
|
|
41
|
+
echo -e " ${DIM}${ts}${RESET} ${color}${icon}${RESET} ${msg}"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── Header ──
|
|
45
|
+
print_header() {
|
|
46
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
47
|
+
echo -e "${BOLD}║ AGENT LIFECYCLE MONITOR ║${RESET}"
|
|
48
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
49
|
+
echo ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ── Detect agent transitions from progress.json ──
|
|
53
|
+
check_transitions() {
|
|
54
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
55
|
+
|
|
56
|
+
local agent status sprint
|
|
57
|
+
agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
58
|
+
status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
59
|
+
sprint=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
60
|
+
|
|
61
|
+
local now
|
|
62
|
+
now=$(date +"%H:%M:%S")
|
|
63
|
+
|
|
64
|
+
# Sprint change
|
|
65
|
+
if [ "$sprint" != "$LAST_SPRINT" ] && [ -n "$LAST_SPRINT" ]; then
|
|
66
|
+
print_event "$now" ">>>" "$MAGENTA" "${BOLD}Sprint ${sprint} started${RESET}"
|
|
67
|
+
echo ""
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Agent change
|
|
71
|
+
if [ "$agent" != "$LAST_AGENT" ] && [ -n "$LAST_AGENT" ]; then
|
|
72
|
+
if [ "$LAST_AGENT" != "none" ] && [ "$LAST_AGENT" != "null" ]; then
|
|
73
|
+
print_event "$now" " ✓ " "$GREEN" "${LAST_AGENT} → ${BOLD}done${RESET}"
|
|
74
|
+
fi
|
|
75
|
+
if [ "$agent" != "none" ] && [ "$agent" != "null" ]; then
|
|
76
|
+
print_event "$now" " ▶ " "$CYAN" "${BOLD}${agent}${RESET} started"
|
|
77
|
+
|
|
78
|
+
# Show handoff context if available
|
|
79
|
+
if [ -f "$HANDOFF" ]; then
|
|
80
|
+
local from focus
|
|
81
|
+
from=$(jq -r '.from // empty' "$HANDOFF" 2>/dev/null)
|
|
82
|
+
focus=$(jq -r '.focus_features // [] | join(", ")' "$HANDOFF" 2>/dev/null)
|
|
83
|
+
if [ -n "$from" ] && [ "$from" != "null" ]; then
|
|
84
|
+
echo -e " ${DIM}handoff from: ${from}${RESET}"
|
|
85
|
+
fi
|
|
86
|
+
if [ -n "$focus" ] && [ "$focus" != "null" ]; then
|
|
87
|
+
echo -e " ${DIM}focus: ${focus}${RESET}"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Status change (same agent)
|
|
94
|
+
if [ "$agent" = "$LAST_AGENT" ] && [ "$status" != "$LAST_STATUS" ] && [ -n "$LAST_STATUS" ]; then
|
|
95
|
+
local status_color="$RESET"
|
|
96
|
+
local status_icon="•"
|
|
97
|
+
case "$status" in
|
|
98
|
+
running) status_color="$GREEN"; status_icon=" ▶ " ;;
|
|
99
|
+
completed) status_color="$CYAN"; status_icon=" ✓ " ;;
|
|
100
|
+
failed) status_color="$RED"; status_icon=" ✗ " ;;
|
|
101
|
+
blocked) status_color="$RED"; status_icon=" ! " ;;
|
|
102
|
+
pending) status_color="$YELLOW"; status_icon=" … " ;;
|
|
103
|
+
esac
|
|
104
|
+
print_event "$now" "$status_icon" "$status_color" "${agent} → ${BOLD}${status}${RESET}"
|
|
105
|
+
|
|
106
|
+
# Show failure detail
|
|
107
|
+
if [ "$status" = "failed" ]; then
|
|
108
|
+
local fail_msg
|
|
109
|
+
fail_msg=$(jq -r '.failure.message // empty' "$PROGRESS" 2>/dev/null)
|
|
110
|
+
if [ -n "$fail_msg" ] && [ "$fail_msg" != "null" ]; then
|
|
111
|
+
echo -e " ${RED}${fail_msg}${RESET}"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
LAST_AGENT="$agent"
|
|
117
|
+
LAST_STATUS="$status"
|
|
118
|
+
LAST_SPRINT="$sprint"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ── Stream audit.log new lines ──
|
|
122
|
+
stream_audit() {
|
|
123
|
+
if [ ! -f "$AUDIT_LOG" ]; then return; fi
|
|
124
|
+
|
|
125
|
+
# Use tail -f in background, parse each new line
|
|
126
|
+
tail -n 0 -f "$AUDIT_LOG" 2>/dev/null | while IFS= read -r line; do
|
|
127
|
+
# Skip comments/headers
|
|
128
|
+
if [[ "$line" == "#"* ]] || [[ -z "$line" ]]; then continue; fi
|
|
129
|
+
|
|
130
|
+
# Parse: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
|
|
131
|
+
local ts agent action status target detail
|
|
132
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
133
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
134
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
135
|
+
status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
136
|
+
target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
|
137
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$6); print $6}')
|
|
138
|
+
|
|
139
|
+
# Color by status
|
|
140
|
+
local color="$RESET" icon="•"
|
|
141
|
+
case "$status" in
|
|
142
|
+
start) color="$CYAN"; icon="▶" ;;
|
|
143
|
+
complete) color="$GREEN"; icon="✓" ;;
|
|
144
|
+
fail) color="$RED"; icon="✗" ;;
|
|
145
|
+
pass) color="$GREEN"; icon="✓" ;;
|
|
146
|
+
skip) color="$DIM"; icon="–" ;;
|
|
147
|
+
esac
|
|
148
|
+
|
|
149
|
+
# Short timestamp (HH:MM:SS from ISO)
|
|
150
|
+
local short_ts
|
|
151
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}:[0-9]{2}' || echo "$ts")
|
|
152
|
+
|
|
153
|
+
echo -e " ${DIM}${short_ts}${RESET} ${color}${icon}${RESET} ${BOLD}${agent}${RESET} ${action} ${target} ${DIM}${detail}${RESET}"
|
|
154
|
+
done &
|
|
155
|
+
AUDIT_TAIL_PID=$!
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ── Cleanup ──
|
|
159
|
+
cleanup() {
|
|
160
|
+
if [ -n "${AUDIT_TAIL_PID:-}" ]; then
|
|
161
|
+
kill "$AUDIT_TAIL_PID" 2>/dev/null || true
|
|
162
|
+
fi
|
|
163
|
+
exit 0
|
|
164
|
+
}
|
|
165
|
+
trap cleanup EXIT INT TERM
|
|
166
|
+
|
|
167
|
+
# ── Main ──
|
|
168
|
+
print_header
|
|
169
|
+
|
|
170
|
+
# Initialize state
|
|
171
|
+
if [ -f "$PROGRESS" ]; then
|
|
172
|
+
LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
173
|
+
LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
174
|
+
LAST_SPRINT=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Show existing history
|
|
178
|
+
if [ -f "$PROGRESS" ]; then
|
|
179
|
+
echo -e " ${BOLD}History${RESET}"
|
|
180
|
+
jq -r '.history // [] | .[] | " \(.timestamp) \(.agent) — \(.action): \(.detail)"' "$PROGRESS" 2>/dev/null
|
|
181
|
+
echo ""
|
|
182
|
+
echo -e " ${DIM}── Live events below ──${RESET}"
|
|
183
|
+
echo ""
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Start audit log streaming
|
|
187
|
+
stream_audit
|
|
188
|
+
|
|
189
|
+
# Poll progress.json for transitions
|
|
190
|
+
while true; do
|
|
191
|
+
check_transitions
|
|
192
|
+
sleep 2
|
|
193
|
+
done
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-tmux.sh — tmux 4-pane 레이아웃 자동 구성
|
|
3
|
+
#
|
|
4
|
+
# ┌──────────────────────┬──────────────────────┐
|
|
5
|
+
# │ │ Panel 1: Dashboard │
|
|
6
|
+
# │ Main Terminal ├──────────────────────┤
|
|
7
|
+
# │ (claude 대화형) │ Panel 2: Monitor │
|
|
8
|
+
# │ ├──────────────────────┤
|
|
9
|
+
# │ │ Panel 3: Eval Review │
|
|
10
|
+
# └──────────────────────┴──────────────────────┘
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# bash scripts/harness-tmux.sh [project-root]
|
|
14
|
+
# bash scripts/harness-tmux.sh /path/to/project --ai (AI eval summary 활성화)
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
# --ai Panel 3에서 claude -p 로 AI 요약 생성 (API 비용 발생)
|
|
18
|
+
# --detach 세션 생성 후 attach 하지 않음
|
|
19
|
+
# --kill 기존 harness-studio 세션 종료
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
SESSION_NAME="harness-studio"
|
|
25
|
+
|
|
26
|
+
PROJECT_ROOT=""
|
|
27
|
+
USE_AI=""
|
|
28
|
+
DETACH=false
|
|
29
|
+
KILL=false
|
|
30
|
+
|
|
31
|
+
for arg in "$@"; do
|
|
32
|
+
case "$arg" in
|
|
33
|
+
--ai) USE_AI="--ai" ;;
|
|
34
|
+
--detach) DETACH=true ;;
|
|
35
|
+
--kill)
|
|
36
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Session '$SESSION_NAME' killed." || echo "No session to kill."
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# Auto-detect project root
|
|
46
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
47
|
+
dir="$(pwd)"
|
|
48
|
+
while [ "$dir" != "/" ]; do
|
|
49
|
+
if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
|
|
50
|
+
dir="$(dirname "$dir")"
|
|
51
|
+
done
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
55
|
+
echo "Error: .harness/ directory not found."
|
|
56
|
+
echo "Usage: bash scripts/harness-tmux.sh [project-root]"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "Project: $PROJECT_ROOT"
|
|
61
|
+
echo "Session: $SESSION_NAME"
|
|
62
|
+
|
|
63
|
+
# Kill existing session if any
|
|
64
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
65
|
+
|
|
66
|
+
# ── Create tmux session ──
|
|
67
|
+
# Pane 0 (left, Main): interactive shell at project root
|
|
68
|
+
tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50
|
|
69
|
+
|
|
70
|
+
# Split right column (40% width)
|
|
71
|
+
tmux split-window -h -l 80 -t "$SESSION_NAME" -c "$PROJECT_ROOT"
|
|
72
|
+
|
|
73
|
+
# Pane 1 (top-right): Dashboard
|
|
74
|
+
tmux send-keys -t "${SESSION_NAME}.1" "bash ${SCRIPT_DIR}/harness-dashboard.sh '${PROJECT_ROOT}'" Enter
|
|
75
|
+
|
|
76
|
+
# Split pane 1 vertically for Monitor
|
|
77
|
+
tmux split-window -v -t "${SESSION_NAME}.1" -c "$PROJECT_ROOT"
|
|
78
|
+
|
|
79
|
+
# Pane 2 (mid-right): Monitor
|
|
80
|
+
tmux send-keys -t "${SESSION_NAME}.2" "bash ${SCRIPT_DIR}/harness-monitor.sh '${PROJECT_ROOT}'" Enter
|
|
81
|
+
|
|
82
|
+
# Split pane 2 vertically for Eval
|
|
83
|
+
tmux split-window -v -t "${SESSION_NAME}.2" -c "$PROJECT_ROOT"
|
|
84
|
+
|
|
85
|
+
# Pane 3 (bottom-right): Eval Watcher
|
|
86
|
+
tmux send-keys -t "${SESSION_NAME}.3" "bash ${SCRIPT_DIR}/harness-eval-watcher.sh '${PROJECT_ROOT}' ${USE_AI}" Enter
|
|
87
|
+
|
|
88
|
+
# ── Layout adjustments ──
|
|
89
|
+
# Select left pane (Main) — this is where the user works
|
|
90
|
+
tmux select-pane -t "${SESSION_NAME}.0"
|
|
91
|
+
|
|
92
|
+
# Set pane titles (requires tmux >= 3.2 with pane-border-format)
|
|
93
|
+
tmux select-pane -t "${SESSION_NAME}.0" -T "Main"
|
|
94
|
+
tmux select-pane -t "${SESSION_NAME}.1" -T "Dashboard"
|
|
95
|
+
tmux select-pane -t "${SESSION_NAME}.2" -T "Monitor"
|
|
96
|
+
tmux select-pane -t "${SESSION_NAME}.3" -T "Eval Review"
|
|
97
|
+
|
|
98
|
+
# Enable pane border titles
|
|
99
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
100
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
101
|
+
|
|
102
|
+
# ── Attach or print info ──
|
|
103
|
+
if [ "$DETACH" = true ]; then
|
|
104
|
+
echo ""
|
|
105
|
+
echo "Session created. Attach with:"
|
|
106
|
+
echo " tmux attach -t $SESSION_NAME"
|
|
107
|
+
else
|
|
108
|
+
# If already in tmux, switch; otherwise attach
|
|
109
|
+
if [ -n "${TMUX:-}" ]; then
|
|
110
|
+
tmux switch-client -t "$SESSION_NAME"
|
|
111
|
+
else
|
|
112
|
+
echo ""
|
|
113
|
+
echo "Launching harness-studio..."
|
|
114
|
+
echo " Main (left) : Interactive terminal — run 'claude' here"
|
|
115
|
+
echo " Dashboard (right↑) : Real-time progress"
|
|
116
|
+
echo " Monitor (right↔) : Agent lifecycle events"
|
|
117
|
+
echo " Eval (right↓) : Evaluation summaries"
|
|
118
|
+
echo ""
|
|
119
|
+
tmux attach -t "$SESSION_NAME"
|
|
120
|
+
fi
|
|
121
|
+
fi
|