@walwal-harness/cli 4.0.0-alpha.9 → 4.0.0-beta.10

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.
Files changed (29) hide show
  1. package/README.md +235 -273
  2. package/assets/templates/config.json +1 -48
  3. package/assets/templates/gitignore-append.txt +1 -0
  4. package/bin/init.js +139 -67
  5. package/package.json +4 -2
  6. package/scripts/harness-dashboard-v4.sh +66 -82
  7. package/scripts/harness-monitor.sh +202 -67
  8. package/scripts/harness-next.sh +4 -15
  9. package/scripts/harness-prompt-history.sh +126 -0
  10. package/scripts/harness-studio-setup.sh +143 -0
  11. package/scripts/harness-studio.sh +66 -0
  12. package/scripts/harness-tmux-v4.sh +136 -0
  13. package/scripts/harness-user-prompt-submit.sh +13 -0
  14. package/skills/dispatcher/SKILL.md +7 -2
  15. package/skills/team-action/SKILL.md +217 -0
  16. package/skills/team-stop/SKILL.md +19 -0
  17. package/scripts/harness-control-v4.sh +0 -97
  18. package/scripts/harness-studio-v4.sh +0 -122
  19. package/scripts/harness-team-worker.sh +0 -415
  20. package/skills/evaluator-functional-flutter/SKILL.md +0 -206
  21. package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
  22. package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
  23. package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
  24. package/skills/generator-frontend-flutter/SKILL.md +0 -173
  25. package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
  26. package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
  27. package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
  28. package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
  29. package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
@@ -1,6 +1,9 @@
1
1
  #!/bin/bash
2
- # harness-monitor.sh — Panel 2: Task & Agent Lifecycle 모니터링
3
- # audit.log 스트리밍 + progress.json 변경 감지
2
+ # harness-monitor.sh — Agent Lifecycle Monitor
3
+ #
4
+ # v3 모드: 단일 이벤트 스트림 (progress.json 변경 감지 + audit.log)
5
+ # v4 모드: 팀별 섹션 분리 (feature-queue.json + progress.log 기반)
6
+ #
4
7
  # Usage: bash scripts/harness-monitor.sh [project-root]
5
8
 
6
9
  set -uo pipefail
@@ -17,8 +20,9 @@ if [ -z "$PROJECT_ROOT" ]; then
17
20
  fi
18
21
 
19
22
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
23
+ PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
20
24
  AUDIT_LOG="$PROJECT_ROOT/.harness/actions/audit.log"
21
- HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
25
+ QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
22
26
 
23
27
  # ── ANSI helpers ──
24
28
  BOLD="\033[1m"
@@ -28,28 +32,169 @@ YELLOW="\033[33m"
28
32
  RED="\033[31m"
29
33
  CYAN="\033[36m"
30
34
  MAGENTA="\033[35m"
35
+ BLUE="\033[34m"
31
36
  RESET="\033[0m"
32
37
 
38
+ # ── Team colors ──
39
+ T1_COLOR="$CYAN"
40
+ T2_COLOR="$MAGENTA"
41
+ T3_COLOR="$YELLOW"
42
+
43
+ team_color() {
44
+ case "$1" in
45
+ 1|team-1) echo "$T1_COLOR" ;;
46
+ 2|team-2) echo "$T2_COLOR" ;;
47
+ 3|team-3) echo "$T3_COLOR" ;;
48
+ *) echo "$DIM" ;;
49
+ esac
50
+ }
51
+
52
+ # ══════════════════════════════════════════
53
+ # v4 모드: 팀별 섹션 렌더링
54
+ # ══════════════════════════════════════════
55
+
56
+ render_v4_header() {
57
+ echo -e "${BOLD}TEAM MONITOR${RESET} ${DIM}$(date +%H:%M:%S)${RESET}"
58
+ echo ""
59
+ }
60
+
61
+ render_team_section() {
62
+ local team_num="$1"
63
+ local color
64
+ color=$(team_color "$team_num")
65
+
66
+ # 팀 상태 from queue
67
+ local t_status="idle" t_feature="—" t_phase="—" t_attempt=""
68
+ if [ -f "$QUEUE" ]; then
69
+ t_status=$(jq -r ".teams[\"$team_num\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
70
+ t_feature=$(jq -r ".teams[\"$team_num\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
71
+ if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
72
+ t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
73
+ t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
74
+ fi
75
+ fi
76
+
77
+ # 상태 아이콘
78
+ local icon="○"
79
+ case "$t_status" in
80
+ busy) icon="▶" ;;
81
+ idle) icon="○" ;;
82
+ esac
83
+
84
+ # Phase 표시
85
+ local phase_str=""
86
+ case "$t_phase" in
87
+ gen) phase_str="Gen" ;;
88
+ gate) phase_str="Gate" ;;
89
+ eval) phase_str="Eval" ;;
90
+ *) phase_str="" ;;
91
+ esac
92
+
93
+ # 헤더 라인
94
+ printf "%b%b T%s%b " "$color" "$icon" "$team_num" "$RESET"
95
+ if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
96
+ printf "%s " "$t_feature"
97
+ if [ -n "$phase_str" ]; then
98
+ printf "%b%s%b" "$color" "$phase_str" "$RESET"
99
+ fi
100
+ if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ] && [ "$t_attempt" != "1" ]; then
101
+ printf " %b#%s%b" "$DIM" "$t_attempt" "$RESET"
102
+ fi
103
+ else
104
+ printf "%bidle%b" "$DIM" "$RESET"
105
+ fi
106
+ echo ""
107
+
108
+ # 팀 로그 (progress.log에서 team-N 엔트리만 추출)
109
+ if [ -f "$PROGRESS_LOG" ]; then
110
+ grep -i "team-${team_num}\|team_${team_num}" "$PROGRESS_LOG" 2>/dev/null | tail -5 | while IFS= read -r line; do
111
+ local ts action detail
112
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
113
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
114
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
115
+
116
+ local short_ts
117
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
118
+
119
+ if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
120
+
121
+ # 액션별 아이콘
122
+ local a_icon="·" a_color="$DIM"
123
+ case "$action" in
124
+ gen) a_icon="▶"; a_color="$GREEN" ;;
125
+ eval) a_icon="✦"; a_color="$BLUE" ;;
126
+ pass) a_icon="✓"; a_color="$GREEN" ;;
127
+ fail) a_icon="✗"; a_color="$RED" ;;
128
+ dequeue) a_icon="→"; a_color="$CYAN" ;;
129
+ gate) a_icon="◆"; a_color="$YELLOW" ;;
130
+ *) a_icon="·"; a_color="$DIM" ;;
131
+ esac
132
+
133
+ printf " %b%s%b %b%s%b %b%s%b\n" \
134
+ "$DIM" "$short_ts" "$RESET" \
135
+ "$a_color" "$a_icon" "$RESET" \
136
+ "$DIM" "$detail" "$RESET"
137
+ done
138
+ fi
139
+ }
140
+
141
+ render_v4() {
142
+ render_v4_header
143
+
144
+ # 팀 수 확인
145
+ local team_count=3
146
+ if [ -f "$QUEUE" ]; then
147
+ team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 3)
148
+ fi
149
+
150
+ for i in $(seq 1 "$team_count"); do
151
+ render_team_section "$i"
152
+ echo ""
153
+ done
154
+
155
+ # Lead/시스템 이벤트 (team이 아닌 엔트리)
156
+ echo -e "${BOLD}SYSTEM${RESET}"
157
+ if [ -f "$PROGRESS_LOG" ]; then
158
+ grep -v 'team-[0-9]' "$PROGRESS_LOG" 2>/dev/null | grep -v '^#' | grep -v '^$' | tail -5 | while IFS= read -r line; do
159
+ local ts agent action detail
160
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
161
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
162
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
163
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
164
+
165
+ local short_ts
166
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
167
+
168
+ if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
169
+
170
+ printf " %b%s%b %s %b%s%b\n" \
171
+ "$DIM" "$short_ts" "$RESET" \
172
+ "$agent" \
173
+ "$DIM" "$detail" "$RESET"
174
+ done
175
+ fi
176
+ }
177
+
178
+ # ══════════════════════════════════════════
179
+ # v3 모드: 단일 스트림 (기존 동작)
180
+ # ══════════════════════════════════════════
181
+
33
182
  LAST_AGENT=""
34
183
  LAST_STATUS=""
35
184
  LAST_SPRINT=""
36
- EVENT_COUNT=0
37
185
 
38
186
  print_event() {
39
187
  local ts="$1" icon="$2" color="$3" msg="$4"
40
- EVENT_COUNT=$((EVENT_COUNT + 1))
41
188
  echo -e " ${DIM}${ts}${RESET} ${color}${icon}${RESET} ${msg}"
42
189
  }
43
190
 
44
- # ── Header ──
45
- print_header() {
191
+ print_v3_header() {
46
192
  echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
47
193
  echo -e "${BOLD}║ AGENT LIFECYCLE MONITOR ║${RESET}"
48
194
  echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
49
195
  echo ""
50
196
  }
51
197
 
52
- # ── Detect agent transitions from progress.json ──
53
198
  check_transitions() {
54
199
  if [ ! -f "$PROGRESS" ]; then return; fi
55
200
 
@@ -61,39 +206,22 @@ check_transitions() {
61
206
  local now
62
207
  now=$(date +"%H:%M:%S")
63
208
 
64
- # Sprint change
65
209
  if [ "$sprint" != "$LAST_SPRINT" ] && [ -n "$LAST_SPRINT" ]; then
66
210
  print_event "$now" ">>>" "$MAGENTA" "${BOLD}Sprint ${sprint} started${RESET}"
67
211
  echo ""
68
212
  fi
69
213
 
70
- # Agent change
71
214
  if [ "$agent" != "$LAST_AGENT" ] && [ -n "$LAST_AGENT" ]; then
72
215
  if [ "$LAST_AGENT" != "none" ] && [ "$LAST_AGENT" != "null" ]; then
73
216
  print_event "$now" " ✓ " "$GREEN" "${LAST_AGENT} → ${BOLD}done${RESET}"
74
217
  fi
75
218
  if [ "$agent" != "none" ] && [ "$agent" != "null" ]; then
76
219
  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
220
  fi
91
221
  fi
92
222
 
93
- # Status change (same agent)
94
223
  if [ "$agent" = "$LAST_AGENT" ] && [ "$status" != "$LAST_STATUS" ] && [ -n "$LAST_STATUS" ]; then
95
- local status_color="$RESET"
96
- local status_icon="•"
224
+ local status_color="$RESET" status_icon="•"
97
225
  case "$status" in
98
226
  running) status_color="$GREEN"; status_icon=" ▶ " ;;
99
227
  completed) status_color="$CYAN"; status_icon=" ✓ " ;;
@@ -103,7 +231,6 @@ check_transitions() {
103
231
  esac
104
232
  print_event "$now" "$status_icon" "$status_color" "${agent} → ${BOLD}${status}${RESET}"
105
233
 
106
- # Show failure detail
107
234
  if [ "$status" = "failed" ]; then
108
235
  local fail_msg
109
236
  fail_msg=$(jq -r '.failure.message // empty' "$PROGRESS" 2>/dev/null)
@@ -118,76 +245,84 @@ check_transitions() {
118
245
  LAST_SPRINT="$sprint"
119
246
  }
120
247
 
121
- # ── Stream audit.log new lines ──
122
248
  stream_audit() {
123
249
  if [ ! -f "$AUDIT_LOG" ]; then return; fi
124
-
125
- # Use tail -f in background, parse each new line
126
250
  tail -n 0 -f "$AUDIT_LOG" 2>/dev/null | while IFS= read -r line; do
127
- # Skip comments/headers
128
251
  if [[ "$line" == "#"* ]] || [[ -z "$line" ]]; then continue; fi
129
-
130
- # Parse: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
131
- local ts agent action status target detail
252
+ local ts agent action status target
132
253
  ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
133
254
  agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
134
255
  action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
135
256
  status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
136
257
  target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
137
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$6); print $6}')
138
258
 
139
- # Color by status
140
259
  local color="$RESET" icon="•"
141
260
  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="–" ;;
261
+ start) color="$CYAN"; icon="▶" ;;
262
+ complete) color="$GREEN"; icon="✓" ;;
263
+ fail) color="$RED"; icon="✗" ;;
264
+ pass) color="$GREEN"; icon="✓" ;;
265
+ skip) color="$DIM"; icon="–" ;;
147
266
  esac
148
267
 
149
- # Short timestamp (HH:MM:SS from ISO)
150
268
  local short_ts
151
269
  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}"
270
+ echo -e " ${DIM}${short_ts}${RESET} ${color}${icon}${RESET} ${BOLD}${agent}${RESET} ${action} ${target}"
154
271
  done &
155
272
  AUDIT_TAIL_PID=$!
156
273
  }
157
274
 
158
- # ── Cleanup ──
275
+ # ══════════════════════════════════════════
276
+ # Mode detection & main loop
277
+ # ══════════════════════════════════════════
278
+
159
279
  cleanup() {
160
280
  if [ -n "${AUDIT_TAIL_PID:-}" ]; then
161
281
  kill "$AUDIT_TAIL_PID" 2>/dev/null || true
162
282
  fi
283
+ tput cnorm 2>/dev/null
163
284
  exit 0
164
285
  }
165
286
  trap cleanup EXIT INT TERM
166
287
 
167
- # ── Main ──
168
- print_header
288
+ # 루프마다 v4/v3 동적 감지 — 시작 후에도 모드 전환 가능
289
+ tput civis 2>/dev/null
290
+ clear
169
291
 
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
292
+ CURRENT_MODE=""
176
293
 
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
294
+ while true; do
295
+ # 동적 모드 감지
296
+ if [ -f "$QUEUE" ]; then
297
+ NEW_MODE="v4"
298
+ else
299
+ NEW_MODE="v3"
300
+ fi
185
301
 
186
- # Start audit log streaming
187
- stream_audit
302
+ # 모드 전환 화면 초기화
303
+ if [ "$NEW_MODE" != "$CURRENT_MODE" ]; then
304
+ clear
305
+ CURRENT_MODE="$NEW_MODE"
306
+ if [ "$CURRENT_MODE" = "v3" ]; then
307
+ # v3 초기화
308
+ if [ -f "$PROGRESS" ]; then
309
+ LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
310
+ LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
311
+ LAST_SPRINT=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
312
+ fi
313
+ stream_audit
314
+ fi
315
+ fi
188
316
 
189
- # Poll progress.json for transitions
190
- while true; do
191
- check_transitions
192
- sleep 2
317
+ if [ "$CURRENT_MODE" = "v4" ]; then
318
+ buf=$(render_v4 2>&1)
319
+ tput cup 0 0 2>/dev/null
320
+ echo "$buf"
321
+ tput ed 2>/dev/null
322
+ else
323
+ check_transitions
324
+ fi
325
+
326
+ sleep 3
193
327
  done
328
+
@@ -59,21 +59,10 @@ if [ -f "$PIPELINE_JSON" ]; then
59
59
  fi
60
60
 
61
61
  # ─────────────────────────────────────────
62
- # fe_stack + fe_target 치환 헬퍼
63
- # pipeline_selection.pipelines 에서 읽은 에이전트명을 fe_stack/fe_target 에 따라 치환
64
- # - react: 그대로
65
- # - flutter+web: generator-frontend → generator-frontend-flutter 만 치환, eval 은 그대로 (Playwright 사용 가능)
66
- # - flutter+mobile/desktop: eval 도 정적 분석용으로 치환, evaluator-visual 은 __skip__
62
+ # fe_stack 치환 (no-op Flutter 지원 제거됨, 하위 호환용 stub)
67
63
  # ─────────────────────────────────────────
68
64
  substitute_fe_stack() {
69
- local agent="$1"
70
- if [ "$fe_stack" != "flutter" ]; then
71
- echo "$agent"
72
- return
73
- fi
74
- local sub
75
- sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
76
- echo "$sub"
65
+ echo "$1"
77
66
  }
78
67
 
79
68
  # ─────────────────────────────────────────
@@ -105,7 +94,7 @@ run_pre_eval_gate() {
105
94
  # current_agent가 generator가 아닌 경우 (예: dispatcher로 리라우팅된 상태),
106
95
  # completed_agents에서 마지막 generator를 찾는다
107
96
  case "$source_agent" in
108
- generator-frontend|generator-frontend-flutter)
97
+ generator-frontend)
109
98
  location="frontend"
110
99
  checks_key="frontend_checks"
111
100
  ;;
@@ -118,7 +107,7 @@ run_pre_eval_gate() {
118
107
  local last_gen
119
108
  last_gen=$(jq -r '.completed_agents // [] | map(select(startswith("generator-"))) | last // empty' "$PROGRESS" 2>/dev/null)
120
109
  case "$last_gen" in
121
- generator-frontend|generator-frontend-flutter)
110
+ generator-frontend)
122
111
  location="frontend"
123
112
  checks_key="frontend_checks"
124
113
  ;;
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # harness-prompt-history.sh — Prompt History pane for v4 tmux layout
3
+ # progress.log + audit.log에서 유저 프롬프트/에이전트 활동 히스토리를 표시
4
+ # Usage: bash scripts/harness-prompt-history.sh [project-root]
5
+
6
+ set -uo 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 ".")" || { echo "[history] .harness/ not found."; exit 1; }
14
+ fi
15
+
16
+ PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
17
+ AUDIT_LOG="$PROJECT_ROOT/.harness/actions/audit.log"
18
+
19
+ BOLD="\033[1m"
20
+ DIM="\033[2m"
21
+ GREEN="\033[32m"
22
+ YELLOW="\033[33m"
23
+ RED="\033[31m"
24
+ CYAN="\033[36m"
25
+ MAGENTA="\033[35m"
26
+ RESET="\033[0m"
27
+
28
+ render_header() {
29
+ echo -e "${BOLD}PROMPT HISTORY${RESET} ${DIM}$(date +%H:%M:%S)${RESET}"
30
+ echo ""
31
+ }
32
+
33
+ render_progress_log() {
34
+ if [ ! -f "$PROGRESS_LOG" ]; then
35
+ echo -e " ${DIM}(no progress.log yet)${RESET}"
36
+ return
37
+ fi
38
+
39
+ grep -v '^#' "$PROGRESS_LOG" 2>/dev/null | grep -v '^$' | tail -20 | while IFS= read -r line; do
40
+ local ts agent action detail
41
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
42
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
43
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
44
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
45
+
46
+ local short_ts icon color
47
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}:[0-9]{2}' || echo "$ts")
48
+
49
+ case "$agent" in
50
+ dispatcher*) icon="▸" ; color="$MAGENTA" ;;
51
+ brainstormer) icon="◇" ; color="$CYAN" ;;
52
+ planner*) icon="□" ; color="$YELLOW" ;;
53
+ generator*) icon="▶" ; color="$GREEN" ;;
54
+ eval*) icon="✦" ; color="$RED" ;;
55
+ user|manual) icon="★" ; color="$BOLD" ;;
56
+ team*) icon="⊕" ; color="$CYAN" ;;
57
+ *) icon="·" ; color="$DIM" ;;
58
+ esac
59
+
60
+ if [ ${#detail} -gt 40 ]; then detail="${detail:0:38}.."; fi
61
+
62
+ printf " %b%b%b %b%-8s%b %b%s%b %b%s%b\n" \
63
+ "$color" "$icon" "$RESET" \
64
+ "$DIM" "$short_ts" "$RESET" \
65
+ "$RESET" "$agent" "$RESET" \
66
+ "$DIM" "$action" "$RESET"
67
+ if [ -n "$detail" ] && [ "$detail" != " " ]; then
68
+ echo -e " ${DIM}${detail}${RESET}"
69
+ fi
70
+ done
71
+ }
72
+
73
+ render_audit_tail() {
74
+ if [ ! -f "$AUDIT_LOG" ]; then return; fi
75
+
76
+ echo ""
77
+ echo -e "${BOLD}AUDIT${RESET}"
78
+
79
+ grep -v '^#' "$AUDIT_LOG" 2>/dev/null | grep -v '^$' | tail -10 | while IFS= read -r line; do
80
+ local ts agent action status target
81
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
82
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
83
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
84
+ status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
85
+ target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
86
+
87
+ local color="$RESET" icon="·"
88
+ case "$status" in
89
+ start) color="$CYAN"; icon="▶" ;;
90
+ complete) color="$GREEN"; icon="✓" ;;
91
+ fail) color="$RED"; icon="✗" ;;
92
+ pass) color="$GREEN"; icon="✓" ;;
93
+ skip) color="$DIM"; icon="–" ;;
94
+ esac
95
+
96
+ local short_ts
97
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}:[0-9]{2}' || echo "$ts")
98
+
99
+ if [ ${#target} -gt 25 ]; then target="${target:0:23}.."; fi
100
+
101
+ printf " %b%b%b %b%s%b %-10s %b%s%b\n" \
102
+ "$color" "$icon" "$RESET" \
103
+ "$DIM" "$short_ts" "$RESET" \
104
+ "$agent" \
105
+ "$DIM" "$target" "$RESET"
106
+ done
107
+ }
108
+
109
+ render_all() {
110
+ render_header
111
+ render_progress_log
112
+ render_audit_tail
113
+ }
114
+
115
+ # ── Main loop ──
116
+ tput civis 2>/dev/null
117
+ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
118
+ clear
119
+
120
+ while true; do
121
+ buf=$(render_all 2>&1)
122
+ tput cup 0 0 2>/dev/null
123
+ echo "$buf"
124
+ tput ed 2>/dev/null
125
+ sleep 3
126
+ done
@@ -0,0 +1,143 @@
1
+ #!/bin/bash
2
+ # harness-studio-setup.sh — Claude 세션에서 3-column 레이아웃 자동 구축
3
+ #
4
+ # ┌──────────────┬──────────────┬──────────────┐
5
+ # │ │ Dashboard │ │
6
+ # │ Claude │ (v4 queue) │ Team Monitor│
7
+ # │ (Lead) ├──────────────┤ (lifecycle) │
8
+ # │ │ Command │ │
9
+ # │ │ History │ │
10
+ # └──────────────┴──────────────┴──────────────┘
11
+ #
12
+ # 두 가지 상황을 모두 처리:
13
+ # A) tmux 안에서 실행 → 현재 pane을 split하여 레이아웃 구축
14
+ # B) tmux 밖에서 실행 → 새 tmux 세션 생성, Claude를 좌측 pane에서 재실행
15
+ #
16
+ # Usage:
17
+ # bash scripts/harness-studio-setup.sh [project-root]
18
+ #
19
+ # 이미 구축됐으면 skip (멱등성 보장)
20
+
21
+ set -euo pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
24
+ SESSION_NAME="harness-studio"
25
+
26
+ PROJECT_ROOT="${1:-}"
27
+ if [ -z "$PROJECT_ROOT" ]; then
28
+ dir="$(pwd)"
29
+ while [ "$dir" != "/" ]; do
30
+ if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
31
+ dir="$(dirname "$dir")"
32
+ done
33
+ fi
34
+
35
+ if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
36
+ echo "[studio] .harness/ not found."
37
+ exit 1
38
+ fi
39
+
40
+ # ── Resolve Claude command ──
41
+ CLAUDE_CMD="claude --dangerously-skip-permissions"
42
+ if [ -f "$PROJECT_ROOT/.harness/handoff.json" ]; then
43
+ _model=$(jq -r '.model // empty' "$PROJECT_ROOT/.harness/handoff.json" 2>/dev/null)
44
+ if [ -n "$_model" ] && [ "$_model" != "null" ]; then
45
+ CLAUDE_CMD="$CLAUDE_CMD --model $_model"
46
+ fi
47
+ fi
48
+
49
+ # ══════════════════════════════════════════
50
+ # Case A: 이미 tmux 안에 있음 → pane split
51
+ # ══════════════════════════════════════════
52
+ if [ -n "${TMUX:-}" ]; then
53
+ # 멱등성: 이미 pane이 3개 이상이면 skip
54
+ PANE_COUNT=$(tmux list-panes | wc -l | tr -d ' ')
55
+ if [ "$PANE_COUNT" -ge 3 ]; then
56
+ echo "[studio] Layout already set up ($PANE_COUNT panes). Skipping."
57
+ exit 0
58
+ fi
59
+
60
+ PANE_CLAUDE=$(tmux display-message -p '#{pane_id}')
61
+ echo "[studio] Setting up 3-column layout (in-place split)..."
62
+
63
+ # Left 35% | Right 65%
64
+ PANE_MID=$(tmux split-window -h -p 65 -t "$PANE_CLAUDE" -c "$PROJECT_ROOT" \
65
+ -P -F '#{pane_id}' \
66
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
67
+
68
+ # Middle 45% | Right 55%
69
+ PANE_RIGHT=$(tmux split-window -h -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
70
+ -P -F '#{pane_id}' \
71
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
72
+
73
+ # Dashboard top 45% | History bottom 55%
74
+ PANE_HISTORY=$(tmux split-window -v -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
75
+ -P -F '#{pane_id}' \
76
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
77
+
78
+ # Pane titles
79
+ tmux select-pane -t "$PANE_CLAUDE" -T "Lead (Claude)"
80
+ tmux select-pane -t "$PANE_MID" -T "Dashboard"
81
+ tmux select-pane -t "$PANE_HISTORY" -T "Command History"
82
+ tmux select-pane -t "$PANE_RIGHT" -T "Team Monitor"
83
+
84
+ tmux set-option pane-border-status top 2>/dev/null || true
85
+ tmux set-option pane-border-format " #{pane_title} " 2>/dev/null || true
86
+
87
+ # 포커스를 Claude pane으로 복귀
88
+ tmux select-pane -t "$PANE_CLAUDE"
89
+
90
+ echo "[studio] Layout ready (in-place)."
91
+ exit 0
92
+ fi
93
+
94
+ # ══════════════════════════════════════════
95
+ # Case B: tmux 밖에 있음 → 새 세션 생성
96
+ # ══════════════════════════════════════════
97
+
98
+ # 이미 세션이 있으면 attach만
99
+ if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
100
+ echo "[studio] Session '$SESSION_NAME' already exists. Attaching..."
101
+ echo "[studio] ATTACH_TMUX=$SESSION_NAME"
102
+ exit 0
103
+ fi
104
+
105
+ echo "[studio] Creating new tmux session with 3-column layout..."
106
+
107
+ # 1. 새 세션 → Left pane (Claude 실행)
108
+ PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
109
+ -P -F '#{pane_id}')
110
+
111
+ # 2. Left 35% | Right 65%
112
+ PANE_MID=$(tmux split-window -h -p 65 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
113
+ -P -F '#{pane_id}' \
114
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
115
+
116
+ # 3. Middle 45% | Right 55%
117
+ PANE_RIGHT=$(tmux split-window -h -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
118
+ -P -F '#{pane_id}' \
119
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
120
+
121
+ # 4. Dashboard top 45% | History bottom 55%
122
+ PANE_HISTORY=$(tmux split-window -v -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
123
+ -P -F '#{pane_id}' \
124
+ "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
125
+
126
+ # 5. Left pane에서 Claude 자동 실행
127
+ tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null" Enter
128
+ tmux send-keys -t "$PANE_MAIN" "clear && $CLAUDE_CMD" Enter
129
+
130
+ # Pane titles
131
+ tmux select-pane -t "$PANE_MAIN" -T "Lead (Claude)"
132
+ tmux select-pane -t "$PANE_MID" -T "Dashboard"
133
+ tmux select-pane -t "$PANE_HISTORY" -T "Command History"
134
+ tmux select-pane -t "$PANE_RIGHT" -T "Team Monitor"
135
+
136
+ tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
137
+ tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
138
+
139
+ # Focus on Claude pane
140
+ tmux select-pane -t "$PANE_MAIN"
141
+
142
+ echo "[studio] Session '$SESSION_NAME' created."
143
+ echo "[studio] ATTACH_TMUX=$SESSION_NAME"