@walwal-harness/cli 4.0.0-beta.1 → 4.0.0-beta.11

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 CHANGED
@@ -129,17 +129,56 @@ function scaffoldHarness() {
129
129
  }
130
130
  }
131
131
 
132
- // Copy config.json
132
+ // config.json — ALWAYS update (harness system file, not user data)
133
+ // But preserve user's custom settings (pre_eval_gate.frontend_cwd, behavior, etc.)
133
134
  const configSrc = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
134
135
  const configDest = path.join(HARNESS_DIR, 'config.json');
135
- if (fs.existsSync(configSrc) && (!fileExists(configDest) || isForce)) {
136
- copyFile(configSrc, configDest);
136
+ if (fs.existsSync(configSrc)) {
137
+ if (fileExists(configDest) && !isForce) {
138
+ // Merge: keep user's customizations, update harness structure
139
+ try {
140
+ const existing = JSON.parse(fs.readFileSync(configDest, 'utf8'));
141
+ const template = JSON.parse(fs.readFileSync(configSrc, 'utf8'));
142
+ // Preserve user customizations
143
+ const userPreserve = {
144
+ behavior: existing.behavior,
145
+ 'flow.pre_eval_gate.frontend_cwd': existing?.flow?.pre_eval_gate?.frontend_cwd,
146
+ 'flow.pre_eval_gate.backend_cwd': existing?.flow?.pre_eval_gate?.backend_cwd,
147
+ 'flow.pre_eval_gate.frontend_checks': existing?.flow?.pre_eval_gate?.frontend_checks,
148
+ 'flow.pre_eval_gate.backend_checks': existing?.flow?.pre_eval_gate?.backend_checks,
149
+ };
150
+ // Write template, then re-apply user settings
151
+ fs.writeFileSync(configDest, JSON.stringify(template, null, 2) + '\n');
152
+ // Re-apply preserved user settings
153
+ const merged = JSON.parse(fs.readFileSync(configDest, 'utf8'));
154
+ if (userPreserve.behavior) merged.behavior = userPreserve.behavior;
155
+ if (userPreserve['flow.pre_eval_gate.frontend_cwd']) {
156
+ merged.flow.pre_eval_gate.frontend_cwd = userPreserve['flow.pre_eval_gate.frontend_cwd'];
157
+ }
158
+ if (userPreserve['flow.pre_eval_gate.backend_cwd']) {
159
+ merged.flow.pre_eval_gate.backend_cwd = userPreserve['flow.pre_eval_gate.backend_cwd'];
160
+ }
161
+ if (userPreserve['flow.pre_eval_gate.frontend_checks']) {
162
+ merged.flow.pre_eval_gate.frontend_checks = userPreserve['flow.pre_eval_gate.frontend_checks'];
163
+ }
164
+ if (userPreserve['flow.pre_eval_gate.backend_checks']) {
165
+ merged.flow.pre_eval_gate.backend_checks = userPreserve['flow.pre_eval_gate.backend_checks'];
166
+ }
167
+ fs.writeFileSync(configDest, JSON.stringify(merged, null, 2) + '\n');
168
+ log('config.json updated (user settings preserved)');
169
+ } catch (e) {
170
+ copyFile(configSrc, configDest);
171
+ log('config.json replaced (merge failed)');
172
+ }
173
+ } else {
174
+ copyFile(configSrc, configDest);
175
+ }
137
176
  }
138
177
 
139
- // Copy HARNESS.md
178
+ // HARNESS.md — ALWAYS update
140
179
  const harnessMdSrc = path.join(PKG_ROOT, 'assets', 'templates', 'HARNESS.md');
141
180
  const harnessMdDest = path.join(HARNESS_DIR, 'HARNESS.md');
142
- if (fs.existsSync(harnessMdSrc) && (!fileExists(harnessMdDest) || isForce)) {
181
+ if (fs.existsSync(harnessMdSrc)) {
143
182
  copyFile(harnessMdSrc, harnessMdDest);
144
183
  }
145
184
 
@@ -186,6 +225,17 @@ function installSkills() {
186
225
  return;
187
226
  }
188
227
 
228
+ // harness- 프리픽스 스킬 전체 삭제 후 재복사 — 잔류 방지
229
+ if (fs.existsSync(CLAUDE_SKILLS_DIR)) {
230
+ const existing = fs.readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true });
231
+ for (const entry of existing) {
232
+ if (entry.isDirectory() && entry.name.startsWith('harness-')) {
233
+ fs.rmSync(path.join(CLAUDE_SKILLS_DIR, entry.name), { recursive: true, force: true });
234
+ }
235
+ }
236
+ log(' Cleared existing harness-* skills');
237
+ }
238
+
189
239
  const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
190
240
  .filter(d => d.isDirectory())
191
241
  .map(d => d.name);
@@ -193,13 +243,8 @@ function installSkills() {
193
243
  for (const skill of skills) {
194
244
  const src = path.join(skillsSrc, skill);
195
245
  const dest = path.join(CLAUDE_SKILLS_DIR, `harness-${skill}`);
196
-
197
- if (!fileExists(dest) || isForce) {
198
- copyDir(src, dest);
199
- log(` Installed: harness-${skill}`);
200
- } else {
201
- log(` Skipped (exists): harness-${skill}`);
202
- }
246
+ copyDir(src, dest);
247
+ log(` Installed: harness-${skill}`);
203
248
  }
204
249
 
205
250
  log('Skills installation complete');
@@ -214,48 +259,28 @@ function installScripts() {
214
259
  const scriptsSrc = path.join(PKG_ROOT, 'scripts');
215
260
  const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
216
261
 
217
- // Core scripts are ALWAYS overwritten on update (not user-editable)
218
- // These contain harness logic that must stay in sync with the CLI version
219
- const coreScripts = new Set([
220
- 'harness-next.sh',
221
- 'harness-session-start.sh',
222
- 'harness-statusline.sh',
223
- 'harness-user-prompt-submit.sh',
224
- 'harness-dashboard.sh',
225
- 'harness-monitor.sh',
226
- 'harness-eval-watcher.sh',
227
- 'harness-tmux.sh',
228
- 'harness-control.sh',
229
- 'harness-dashboard-v4.sh',
230
- 'harness-queue-manager.sh',
231
- ]);
262
+ // 전체 삭제 재복사 버전 잔류 파일 방지
263
+ if (fs.existsSync(scriptsDest)) {
264
+ fs.rmSync(scriptsDest, { recursive: true, force: true });
265
+ log(' Cleared existing scripts/');
266
+ }
232
267
 
233
268
  if (fs.existsSync(scriptsSrc)) {
234
- ensureDir(scriptsDest);
235
- const entries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
236
- for (const entry of entries) {
237
- const srcPath = path.join(scriptsSrc, entry.name);
238
- const destPath = path.join(scriptsDest, entry.name);
239
- if (entry.isDirectory()) {
240
- // lib/ and other subdirectories — always overwrite
241
- copyDir(srcPath, destPath);
242
- try {
243
- const subFiles = fs.readdirSync(destPath);
244
- for (const f of subFiles) {
245
- if (f.endsWith('.sh')) {
246
- fs.chmodSync(path.join(destPath, f), '755');
247
- }
248
- }
249
- } catch (e) {}
250
- } else {
251
- // Core scripts: always overwrite. Others: skip if exists (unless --force)
252
- const isCore = coreScripts.has(entry.name);
253
- if (isCore || !fileExists(destPath) || isForce) {
254
- copyFile(srcPath, destPath);
255
- try { fs.chmodSync(destPath, '755'); } catch (e) {}
269
+ copyDir(scriptsSrc, scriptsDest);
270
+
271
+ // chmod +x for all .sh files (recursive)
272
+ function chmodRecursive(dir) {
273
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
274
+ for (const entry of entries) {
275
+ const full = path.join(dir, entry.name);
276
+ if (entry.isDirectory()) {
277
+ chmodRecursive(full);
278
+ } else if (entry.name.endsWith('.sh')) {
279
+ try { fs.chmodSync(full, '755'); } catch (e) {}
256
280
  }
257
281
  }
258
282
  }
283
+ chmodRecursive(scriptsDest);
259
284
  }
260
285
 
261
286
  log('Scripts installation complete');
@@ -421,6 +446,28 @@ function installUserPromptSubmitHook() {
421
446
  // ─────────────────────────────────────────
422
447
  // 4. AGENTS.md + CLAUDE.md
423
448
  // ─────────────────────────────────────────
449
+ // ─────────────────────────────────────────
450
+ // 3d. Agent Teams env var
451
+ // ─────────────────────────────────────────
452
+ function installAgentTeamsEnv() {
453
+ const settingsPath = path.join(PROJECT_ROOT, '.claude', 'settings.json');
454
+ let settings = {};
455
+ if (fileExists(settingsPath)) {
456
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
457
+ }
458
+
459
+ if (!settings.env) settings.env = {};
460
+
461
+ if (settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] !== '1') {
462
+ settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
463
+ ensureDir(path.dirname(settingsPath));
464
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
465
+ log('Agent Teams enabled (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)');
466
+ } else {
467
+ log('Agent Teams already enabled');
468
+ }
469
+ }
470
+
424
471
  function setupAgentsMd() {
425
472
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
426
473
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
@@ -703,6 +750,7 @@ function main() {
703
750
  installSessionHook();
704
751
  installStatusline();
705
752
  installUserPromptSubmitHook();
753
+ installAgentTeamsEnv();
706
754
  setupAgentsMd();
707
755
  checkPlaywrightMcp();
708
756
  checkRecommendedSkills();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "4.0.0-beta.1",
3
+ "version": "4.0.0-beta.11",
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"
@@ -9,7 +9,9 @@
9
9
  "postinstall": "node bin/init.js --auto",
10
10
  "init": "node bin/init.js",
11
11
  "scan": "bash scripts/scan-project.sh .",
12
- "reset": "node bin/init.js --force"
12
+ "reset": "node bin/init.js --force",
13
+ "studio": "bash scripts/harness-studio.sh",
14
+ "studio:kill": "bash scripts/harness-studio.sh --kill"
13
15
  },
14
16
  "keywords": [
15
17
  "harness-engineering",
@@ -133,10 +133,55 @@ render_features() {
133
133
  done
134
134
  }
135
135
 
136
+ render_bottleneck_alert() {
137
+ if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
138
+
139
+ local failed_list blocked_count idle_teams in_prog
140
+ failed_list=$(jq -r '.queue.failed[]' "$QUEUE" 2>/dev/null)
141
+ blocked_count=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
142
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
143
+ idle_teams=0
144
+
145
+ # Count idle teams
146
+ local team_count
147
+ team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 0)
148
+ for i in $(seq 1 "$team_count"); do
149
+ local ts
150
+ ts=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
151
+ if [ "$ts" = "idle" ]; then idle_teams=$((idle_teams + 1)); fi
152
+ done
153
+
154
+ if [ -z "$failed_list" ]; then return; fi
155
+
156
+ # Check each failed feature for blocking impact
157
+ while IFS= read -r fid; do
158
+ [ -z "$fid" ] && continue
159
+ # Count how many blocked features depend on this fid
160
+ local deps_on_this
161
+ deps_on_this=$(jq --arg fid "$fid" '[.queue.blocked | to_entries[] | select(.value | index($fid))] | length' "$QUEUE" 2>/dev/null || echo 0)
162
+
163
+ if [ "$deps_on_this" -gt 0 ]; then
164
+ echo ""
165
+ echo -e " ${RED}${BOLD}⚠ BOTTLENECK${RESET} ${RED}${fid}${RESET} failed → ${YELLOW}${deps_on_this} features blocked${RESET}"
166
+ if [ "$idle_teams" -gt 0 ]; then
167
+ echo -e " ${BOLD}→ requeue:${RESET} bash scripts/harness-queue-manager.sh requeue ${fid} ."
168
+ fi
169
+ fi
170
+ done <<< "$failed_list"
171
+
172
+ # All teams idle + nothing in progress = stalled
173
+ if [ "$idle_teams" -eq "$team_count" ] && [ "$in_prog" -eq 0 ] && [ "$blocked_count" -gt 0 ]; then
174
+ echo ""
175
+ echo -e " ${RED}${BOLD}!! STALLED${RESET} — All teams idle, ${blocked_count} features blocked"
176
+ echo -e " ${BOLD}→ requeue failed features or check dependency graph${RESET}"
177
+ fi
178
+ }
179
+
136
180
  render_all() {
137
181
  render_header
138
182
  render_queue_summary
139
183
  render_teams
184
+ render_bottleneck_alert
140
185
  echo ""
141
186
  render_features
142
187
  }
@@ -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
+