@walwal-harness/cli 6.0.5 → 6.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/commands/harness-solo.md +20 -20
  3. package/commands/harness-team.md +1 -1
  4. package/gotchas/coo-developer.md +13 -0
  5. package/gotchas/documentationer.md +13 -0
  6. package/package.json +1 -1
  7. package/scripts/conductor-tick.sh +416 -0
  8. package/scripts/harness-archive.sh +123 -0
  9. package/scripts/harness-dashboard-up.sh +72 -0
  10. package/scripts/harness-dashboard.sh +509 -0
  11. package/scripts/harness-goal-init.sh +72 -0
  12. package/scripts/harness-goal-show.sh +37 -0
  13. package/scripts/harness-gotcha-memory.sh +348 -0
  14. package/scripts/harness-gotcha-register.sh +393 -0
  15. package/scripts/harness-meeting-doc.sh +153 -0
  16. package/scripts/harness-monitor.sh +398 -0
  17. package/scripts/harness-next.sh +577 -0
  18. package/scripts/harness-progress-set.sh +41 -0
  19. package/scripts/harness-prompt-history.sh +164 -0
  20. package/scripts/harness-queue-manager.sh +694 -0
  21. package/scripts/harness-session-start.sh +140 -0
  22. package/scripts/harness-statusline.sh +139 -0
  23. package/scripts/harness-task-session.sh +48 -0
  24. package/scripts/harness-tmux.sh +372 -0
  25. package/scripts/harness-user-prompt-submit.sh +123 -0
  26. package/scripts/init-agents-md.sh +337 -0
  27. package/scripts/init-ref-docs.sh +262 -0
  28. package/scripts/lib/harness-audit.sh +113 -0
  29. package/scripts/lib/harness-guardrail.sh +148 -0
  30. package/scripts/lib/harness-keywait.sh +57 -0
  31. package/scripts/lib/harness-render-progress.sh +346 -0
  32. package/scripts/scan-project.sh +367 -0
  33. package/skills/brainstorming/SKILL.md +4 -4
  34. package/skills/conductor/SKILL.md +19 -16
  35. package/skills/coo-developer/SKILL.md +42 -0
  36. package/skills/cqo/SKILL.md +7 -0
  37. package/skills/cto/SKILL.md +7 -0
  38. package/skills/dispatcher/SKILL.md +17 -28
  39. package/skills/dispatcher/persona-ceo.md +2 -1
  40. package/skills/documentationer/SKILL.md +46 -0
  41. package/skills/evaluator-code-quality/SKILL.md +1 -1
  42. package/skills/evaluator-functional/SKILL.md +1 -1
  43. package/skills/evaluator-visual/SKILL.md +1 -1
  44. package/skills/generator-backend/SKILL.md +1 -1
  45. package/skills/generator-frontend/SKILL.md +1 -1
  46. package/skills/meeting-manager/SKILL.md +34 -1
  47. package/skills/planner/SKILL.md +23 -9
  48. package/skills/planner/persona-coo-hr.md +17 -0
  49. package/skills/service-ops/SKILL.md +19 -4
@@ -0,0 +1,113 @@
1
+ #!/bin/bash
2
+ # harness-audit.sh — Audit log 헬퍼
3
+ #
4
+ # 라이프사이클: Planner 시작 → Eval 통과 (1 sprint cycle)
5
+ # 새 Planner/Dispatcher 사이클 시작 시 이전 로그는 archive로 이동.
6
+ #
7
+ # 사용법:
8
+ # source scripts/lib/harness-audit.sh
9
+ # audit_log "gen-backend" "develop" "start" "apps/service-user/" "User 서비스 CRUD 구현"
10
+ # audit_log "eval-func" "review" "complete" "POST /api/auth" "FAIL: 409→400 contract 불일치"
11
+ # audit_log "gen-backend" "handoff" "start" "→eval-functional" ""
12
+
13
+ AUDIT_LOG_FILE=""
14
+
15
+ # ─────────────────────────────────────────
16
+ # init_audit — audit.log 경로 설정 + 헤더 생성
17
+ # Args: $1 = project root
18
+ # ─────────────────────────────────────────
19
+ init_audit() {
20
+ local project_root="${1:-.}"
21
+ AUDIT_LOG_FILE="$project_root/.harness/actions/audit.log"
22
+
23
+ # 디렉토리 보장
24
+ mkdir -p "$(dirname "$AUDIT_LOG_FILE")"
25
+ }
26
+
27
+ # ─────────────────────────────────────────
28
+ # reset_audit — 새 사이클 시작 시 로그 초기화
29
+ # 이전 로그가 있으면 archive로 이동
30
+ # Args: $1 = project root, $2 = sprint number (optional)
31
+ # ─────────────────────────────────────────
32
+ reset_audit() {
33
+ local project_root="${1:-.}"
34
+ local sprint_num="${2:-0}"
35
+ init_audit "$project_root"
36
+
37
+ # 기존 로그가 있으면 archive로 이동
38
+ if [ -f "$AUDIT_LOG_FILE" ] && [ -s "$AUDIT_LOG_FILE" ]; then
39
+ local archive_dir="$project_root/.harness/archive"
40
+ mkdir -p "$archive_dir"
41
+ local ts
42
+ ts=$(date +%Y%m%d_%H%M%S)
43
+ mv "$AUDIT_LOG_FILE" "$archive_dir/audit_s${sprint_num}_${ts}.log"
44
+ fi
45
+
46
+ # 새 로그 헤더
47
+ cat > "$AUDIT_LOG_FILE" <<HEADER
48
+ # Harness Audit Log
49
+ # Cycle started: $(date -u +%Y-%m-%dT%H:%M:%SZ)
50
+ # Format: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
51
+ #─────────────────────────────────────────────────────────────────────────
52
+ HEADER
53
+ }
54
+
55
+ # ─────────────────────────────────────────
56
+ # audit_log — 로그 엔트리 추가
57
+ #
58
+ # Args:
59
+ # $1 = agent (planner, gen-backend, gen-frontend, eval-func, eval-visual)
60
+ # $2 = action (develop, review, handoff, plan, gate)
61
+ # $3 = status (start, complete, fail, skip)
62
+ # $4 = target (파일 경로, 엔드포인트, 에이전트명 등)
63
+ # $5 = detail (설명, 결과 등)
64
+ # ─────────────────────────────────────────
65
+ audit_log() {
66
+ if [ -z "$AUDIT_LOG_FILE" ] || [ ! -f "$AUDIT_LOG_FILE" ]; then
67
+ return 0 # audit 미초기화 시 silent pass
68
+ fi
69
+
70
+ local agent="${1:-unknown}"
71
+ local action="${2:-unknown}"
72
+ local log_status="${3:-unknown}"
73
+ local target="${4:-}"
74
+ local detail="${5:-}"
75
+ local ts
76
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
77
+
78
+ # 고정폭 포맷
79
+ printf "%-20s | %-14s | %-8s | %-8s | %-30s | %s\n" \
80
+ "$ts" "$agent" "$action" "$log_status" "$target" "$detail" >> "$AUDIT_LOG_FILE"
81
+ }
82
+
83
+ # ─────────────────────────────────────────
84
+ # audit_handoff — 핸즈오프 기록 (편의 함수)
85
+ # Args: $1=from_agent, $2=to_agent, $3=status(start|complete)
86
+ # ─────────────────────────────────────────
87
+ audit_handoff() {
88
+ audit_log "$1" "handoff" "$3" "→${2}" ""
89
+ }
90
+
91
+ # ─────────────────────────────────────────
92
+ # audit_gate — 게이트 체크 기록 (편의 함수)
93
+ # Args: $1=gate_name, $2=status(pass|fail), $3=detail
94
+ # ─────────────────────────────────────────
95
+ audit_gate() {
96
+ audit_log "system" "gate" "$2" "$1" "$3"
97
+ }
98
+
99
+ # ─────────────────────────────────────────
100
+ # render_audit — audit.log를 읽기 좋게 출력
101
+ # Args: $1 = project root (optional)
102
+ # ─────────────────────────────────────────
103
+ render_audit() {
104
+ local project_root="${1:-.}"
105
+ local log_file="$project_root/.harness/actions/audit.log"
106
+
107
+ if [ ! -f "$log_file" ]; then
108
+ echo " (audit log not yet created)"
109
+ return 0
110
+ fi
111
+
112
+ cat "$log_file"
113
+ }
@@ -0,0 +1,148 @@
1
+ #!/bin/bash
2
+ # harness-guardrail.sh — 런타임 파일 소유권 검증
3
+ # 에이전트 전환 시 이전 에이전트가 권한 밖 파일을 수정했는지 git diff로 검증.
4
+ #
5
+ # 사용법: source lib/harness-guardrail.sh && verify_file_ownership "$PROJECT_ROOT"
6
+ # 반환값: 0=통과, 1=위반 발견 (위반 목록을 stdout에 출력)
7
+
8
+ # ─────────────────────────────────────────
9
+ # Agent → 허용 경로 매핑
10
+ # ─────────────────────────────────────────
11
+ get_allowed_paths() {
12
+ local agent="$1"
13
+ case "$agent" in
14
+ planner)
15
+ echo "AGENTS.md"
16
+ echo ".harness/actions/"
17
+ echo ".harness/progress.json"
18
+ echo ".harness/progress.log"
19
+ ;;
20
+ generator-backend)
21
+ echo "apps/gateway/"
22
+ echo "apps/service-"
23
+ echo "libs/"
24
+ echo ".harness/actions/sprint-contract.md"
25
+ echo ".harness/actions/feature-list.json"
26
+ echo ".harness/progress.json"
27
+ echo "package.json"
28
+ echo "nest-cli.json"
29
+ echo "tsconfig"
30
+ echo "docker-compose"
31
+ ;;
32
+ generator-frontend)
33
+ echo "apps/web/"
34
+ echo "apps/flutter/"
35
+ echo "lib/"
36
+ echo ".harness/actions/sprint-contract.md"
37
+ echo ".harness/actions/feature-list.json"
38
+ echo ".harness/progress.json"
39
+ echo "package.json"
40
+ echo "tsconfig"
41
+ echo "pubspec"
42
+ ;;
43
+ evaluator-code-quality)
44
+ echo ".harness/actions/evaluation-code-quality.md"
45
+ echo ".harness/progress.json"
46
+ ;;
47
+ evaluator-functional)
48
+ echo ".harness/actions/evaluation-functional.md"
49
+ echo ".harness/progress.json"
50
+ echo "tests/"
51
+ echo "test/"
52
+ echo "e2e/"
53
+ echo "playwright"
54
+ ;;
55
+ evaluator-visual)
56
+ echo ".harness/actions/evaluation-visual.md"
57
+ echo ".harness/progress.json"
58
+ ;;
59
+ dispatcher)
60
+ echo ".harness/"
61
+ ;;
62
+ *)
63
+ echo "**" # unknown agent — allow all (safe fallback)
64
+ ;;
65
+ esac
66
+ }
67
+
68
+ # ─────────────────────────────────────────
69
+ # verify_file_ownership — git diff 기반 검증
70
+ #
71
+ # Args:
72
+ # $1 — project root
73
+ #
74
+ # Reads:
75
+ # .harness/progress.json (current_agent)
76
+ #
77
+ # Returns: 0=pass, 1=violation found
78
+ # ─────────────────────────────────────────
79
+ verify_file_ownership() {
80
+ local PROJECT_ROOT="$1"
81
+ local PROGRESS="$PROJECT_ROOT/.harness/progress.json"
82
+
83
+ if [ ! -f "$PROGRESS" ]; then return 0; fi
84
+
85
+ local agent
86
+ agent=$(jq -r '.current_agent // "null"' "$PROGRESS" 2>/dev/null)
87
+ if [ "$agent" = "null" ] || [ -z "$agent" ]; then return 0; fi
88
+
89
+ # git이 없으면 skip
90
+ if ! command -v git &>/dev/null; then return 0; fi
91
+
92
+ # 마지막 커밋 이후 변경된 파일 목록
93
+ local -a changed_files
94
+ mapfile -t changed_files < <(cd "$PROJECT_ROOT" && git diff --name-only HEAD 2>/dev/null)
95
+
96
+ # staged 파일도 포함
97
+ local -a staged_files
98
+ mapfile -t staged_files < <(cd "$PROJECT_ROOT" && git diff --name-only --cached 2>/dev/null)
99
+
100
+ # 합치기 (중복 제거)
101
+ local -a all_files
102
+ mapfile -t all_files < <(printf '%s\n' "${changed_files[@]}" "${staged_files[@]}" | sort -u | grep -v '^$')
103
+
104
+ if [ ${#all_files[@]} -eq 0 ]; then return 0; fi
105
+
106
+ # 허용 경로 목록
107
+ local -a allowed
108
+ mapfile -t allowed < <(get_allowed_paths "$agent")
109
+
110
+ # 위반 검사
111
+ local -a violations=()
112
+ for file in "${all_files[@]}"; do
113
+ local is_allowed=false
114
+ for pattern in "${allowed[@]}"; do
115
+ if [[ "$file" == $pattern* ]]; then
116
+ is_allowed=true
117
+ break
118
+ fi
119
+ done
120
+ if [ "$is_allowed" = false ]; then
121
+ violations+=("$file")
122
+ fi
123
+ done
124
+
125
+ if [ ${#violations[@]} -eq 0 ]; then
126
+ return 0
127
+ fi
128
+
129
+ # 위반 보고
130
+ echo ""
131
+ echo " ── Guardrail Violation ────────────────"
132
+ echo " Agent '$agent' modified files outside its allowed paths:"
133
+ echo ""
134
+ for v in "${violations[@]}"; do
135
+ echo " ✗ $v"
136
+ done
137
+ echo ""
138
+ echo " Allowed paths for $agent:"
139
+ for p in "${allowed[@]}"; do
140
+ echo " ✓ $p*"
141
+ done
142
+ echo ""
143
+ echo " Action: Review these changes before proceeding."
144
+ echo " ─────────────────────────────────────────"
145
+ echo ""
146
+
147
+ return 1
148
+ }
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # harness-keywait.sh — 패널 refresh/quit 키 대기 헬퍼
3
+ #
4
+ # 사용법:
5
+ # source scripts/lib/harness-keywait.sh
6
+ # while true; do
7
+ # render_panel
8
+ # echo " [r] refresh [q] quit"
9
+ # if wait_or_refresh 3; then
10
+ # # 타임아웃 → 일반 주기 리프레시
11
+ # :
12
+ # else
13
+ # # 'r' 키 즉시 리프레시 (반환값 != 0)
14
+ # :
15
+ # fi
16
+ # done
17
+ #
18
+ # 키 처리:
19
+ # 'r' / 'R' / Enter → 즉시 복귀 (return 1 — 강제 리프레시 신호)
20
+ # 'q' / 'Q' → exit 0 (패널 종료)
21
+ # 그 외 / 타임아웃 → return 0 (정상 주기 리프레시)
22
+ #
23
+ # TTY 가 아니면 plain sleep 으로 동작 (Claude Code Bash 도구 등에서도 안전).
24
+
25
+ wait_or_refresh() {
26
+ local secs="${1:-3}"
27
+
28
+ # Non-TTY fallback
29
+ if ! [ -t 0 ]; then
30
+ sleep "$secs"
31
+ return 0
32
+ fi
33
+
34
+ local key=""
35
+ # -t: timeout, -n 1: 1글자, -s: 에코 안함
36
+ # read 는 Enter 에서도 타임아웃 전에 즉시 복귀 (빈 키)
37
+ if IFS= read -rsn 1 -t "$secs" key 2>/dev/null; then
38
+ case "$key" in
39
+ q|Q)
40
+ # 커서 복원 후 종료
41
+ tput cnorm 2>/dev/null || true
42
+ clear
43
+ exit 0
44
+ ;;
45
+ r|R|"")
46
+ # 강제 리프레시 시그널
47
+ return 1
48
+ ;;
49
+ *)
50
+ # 기타 키는 무시하고 일반 복귀
51
+ return 0
52
+ ;;
53
+ esac
54
+ fi
55
+ # 타임아웃
56
+ return 0
57
+ }
@@ -0,0 +1,346 @@
1
+ #!/bin/bash
2
+ # harness-render-progress.sh — Feature-level 프로그래스 렌더링 (공통 함수)
3
+ # source로 포함하여 사용: source "$(dirname "$0")/lib/harness-render-progress.sh"
4
+
5
+ # ─────────────────────────────────────────
6
+ # Dependencies: jq
7
+ # ─────────────────────────────────────────
8
+ check_jq() {
9
+ if ! command -v jq &>/dev/null; then
10
+ echo "[harness] ERROR: jq is required. Install with: brew install jq" >&2
11
+ return 1
12
+ fi
13
+ }
14
+
15
+ # ─────────────────────────────────────────
16
+ # resolve_harness_root — .harness/ 디렉토리 찾기
17
+ # ─────────────────────────────────────────
18
+ resolve_harness_root() {
19
+ local dir="${1:-.}"
20
+ dir="$(cd "$dir" && pwd)"
21
+ while [ "$dir" != "/" ]; do
22
+ if [ -d "$dir/.harness" ]; then
23
+ echo "$dir"
24
+ return 0
25
+ fi
26
+ dir="$(dirname "$dir")"
27
+ done
28
+ return 1
29
+ }
30
+
31
+ # ─────────────────────────────────────────
32
+ # render_progress — Feature-level 프로그래스 출력
33
+ #
34
+ # Args:
35
+ # $1 — project root (optional, defaults to auto-detect)
36
+ #
37
+ # Reads:
38
+ # .harness/progress.json
39
+ # .harness/config.json
40
+ # .harness/actions/feature-list.json (if exists)
41
+ # ─────────────────────────────────────────
42
+ render_progress() {
43
+ check_jq || return 1
44
+
45
+ local PROJECT_ROOT
46
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || {
47
+ echo "[harness] .harness/ directory not found" >&2
48
+ return 1
49
+ }
50
+
51
+ local PROGRESS="$PROJECT_ROOT/.harness/progress.json"
52
+ local CONFIG="$PROJECT_ROOT/.harness/config.json"
53
+ local FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
54
+
55
+ if [ ! -f "$PROGRESS" ]; then
56
+ return 1
57
+ fi
58
+
59
+ # Read progress.json
60
+ local pipeline sprint_num sprint_status retry_count
61
+ local current_agent agent_status next_agent
62
+ local failure_agent failure_location failure_message
63
+
64
+ pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
65
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
66
+ sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS")
67
+ retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
68
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
69
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
70
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS")
71
+ failure_agent=$(jq -r '.failure.agent // empty' "$PROGRESS")
72
+ failure_location=$(jq -r '.failure.location // empty' "$PROGRESS")
73
+ failure_message=$(jq -r '.failure.message // empty' "$PROGRESS")
74
+
75
+ # Read max retries from config
76
+ local max_retries=10
77
+ if [ -f "$CONFIG" ]; then
78
+ max_retries=$(jq -r '.flow.max_retries_per_sprint // 10' "$CONFIG")
79
+ fi
80
+
81
+ # Pipeline agents for progress bar
82
+ local -a pipeline_agents
83
+ if [ -f "$CONFIG" ] && [ "$pipeline" != "null" ] && [ "$pipeline" != "unknown" ]; then
84
+ mapfile -t pipeline_agents < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null | sed 's/:.*//')
85
+ fi
86
+
87
+ # ── Header ──
88
+ local header="Sprint ${sprint_num} / ${pipeline}"
89
+ local pad_len=$(( 40 - ${#header} ))
90
+ local padding=""
91
+ for ((i=0; i<pad_len; i++)); do padding+="═"; done
92
+ echo ""
93
+ echo " ═══ ${header} ${padding}"
94
+ echo ""
95
+
96
+ # ── Feature Progress ──
97
+ if [ -f "$FEATURES" ]; then
98
+ local total_features completed_features=0 in_progress_features=0
99
+ total_features=$(jq '.features | length' "$FEATURES" 2>/dev/null || echo 0)
100
+
101
+ if [ "$total_features" -gt 0 ]; then
102
+ # Determine columns based on pipeline
103
+ local show_be=true show_fe=true
104
+ case "$pipeline" in
105
+ BE-ONLY) show_fe=false ;;
106
+ FE-ONLY) show_be=false ;;
107
+ esac
108
+
109
+ # Build feature lines
110
+ local i=0
111
+ while [ $i -lt "$total_features" ]; do
112
+ local fid fname passes be_pass fe_pass eval_func_pass eval_visual_pass
113
+ fid=$(jq -r ".features[$i].id // \"F-$((i+1))\"" "$FEATURES")
114
+ fname=$(jq -r ".features[$i].name // .features[$i].title // .features[$i].description // \"Feature $((i+1))\"" "$FEATURES")
115
+ passes=$(jq -r ".features[$i].passes // [] | .[]" "$FEATURES" 2>/dev/null)
116
+
117
+ # Truncate name to 20 chars
118
+ if [ ${#fname} -gt 20 ]; then
119
+ fname="${fname:0:18}.."
120
+ fi
121
+
122
+ # Check passes
123
+ be_pass="○"; fe_pass="○"; eval_cq_pass="○"; eval_func_pass="○"; eval_visual_pass="○"
124
+ local eval_done=false
125
+
126
+ while IFS= read -r p; do
127
+ case "$p" in
128
+ generator-backend) be_pass="✓" ;;
129
+ generator-frontend) fe_pass="✓" ;;
130
+ evaluator-code-quality) eval_cq_pass="✓" ;;
131
+ evaluator-functional) eval_func_pass="✓" ;;
132
+ evaluator-visual) eval_visual_pass="✓" ;;
133
+ esac
134
+ done <<< "$passes"
135
+
136
+ # Eval column: chain 내 존재하는 평가자 모두 PASS 여야 ✓
137
+ local eval_pass="○"
138
+ local eval_passed_count=0
139
+ local eval_total=3
140
+ [ "$eval_cq_pass" = "✓" ] && eval_passed_count=$((eval_passed_count + 1))
141
+ [ "$eval_func_pass" = "✓" ] && eval_passed_count=$((eval_passed_count + 1))
142
+ [ "$eval_visual_pass" = "✓" ] && eval_passed_count=$((eval_passed_count + 1))
143
+ if [ "$eval_passed_count" -eq "$eval_total" ]; then
144
+ eval_pass="✓"
145
+ elif [ "$eval_passed_count" -gt 0 ]; then
146
+ eval_pass="◐"
147
+ fi
148
+
149
+ # Feature status icon
150
+ local icon="○"
151
+ local is_complete=false
152
+ if [ "$show_be" = true ] && [ "$show_fe" = true ]; then
153
+ # FULLSTACK: all must pass
154
+ if [ "$be_pass" = "✓" ] && [ "$fe_pass" = "✓" ] && [ "$eval_pass" = "✓" ]; then
155
+ icon="✓"; is_complete=true
156
+ elif [ "$be_pass" = "✓" ] || [ "$fe_pass" = "✓" ]; then
157
+ icon="◐"
158
+ fi
159
+ elif [ "$show_be" = true ]; then
160
+ # BE-ONLY
161
+ if [ "$be_pass" = "✓" ] && [ "$eval_pass" = "✓" ]; then
162
+ icon="✓"; is_complete=true
163
+ elif [ "$be_pass" = "✓" ]; then
164
+ icon="◐"
165
+ fi
166
+ else
167
+ # FE-ONLY
168
+ if [ "$fe_pass" = "✓" ] && [ "$eval_pass" = "✓" ]; then
169
+ icon="✓"; is_complete=true
170
+ elif [ "$fe_pass" = "✓" ]; then
171
+ icon="◐"
172
+ fi
173
+ fi
174
+
175
+ if [ "$is_complete" = true ]; then
176
+ completed_features=$((completed_features + 1))
177
+ fi
178
+
179
+ # Build columns
180
+ local cols=""
181
+ if [ "$show_be" = true ]; then cols+="BE${be_pass} "; fi
182
+ if [ "$show_fe" = true ]; then cols+="FE${fe_pass} "; fi
183
+ cols+="Eval${eval_pass}"
184
+
185
+ printf " %s %-5s %-22s %s\n" "$icon" "$fid" "$fname" "$cols"
186
+
187
+ i=$((i + 1))
188
+ done
189
+
190
+ echo ""
191
+
192
+ # ── Progress Bar ──
193
+ local steps_per_feature=0
194
+ if [ "$show_be" = true ]; then steps_per_feature=$((steps_per_feature + 1)); fi
195
+ if [ "$show_fe" = true ]; then steps_per_feature=$((steps_per_feature + 1)); fi
196
+ steps_per_feature=$((steps_per_feature + 1)) # eval
197
+
198
+ local total_steps=$((total_features * steps_per_feature))
199
+ local done_steps=0
200
+
201
+ # Count individual passes across all features
202
+ i=0
203
+ while [ $i -lt "$total_features" ]; do
204
+ passes=$(jq -r ".features[$i].passes // [] | .[]" "$FEATURES" 2>/dev/null)
205
+ while IFS= read -r p; do
206
+ case "$p" in
207
+ generator-backend)
208
+ if [ "$show_be" = true ]; then done_steps=$((done_steps + 1)); fi
209
+ ;;
210
+ generator-frontend)
211
+ if [ "$show_fe" = true ]; then done_steps=$((done_steps + 1)); fi
212
+ ;;
213
+ evaluator-code-quality|evaluator-functional|evaluator-visual)
214
+ # Chain 내 평가자. 아래에서 통합 집계.
215
+ ;;
216
+ esac
217
+ done <<< "$passes"
218
+
219
+ # Check eval completion — 체인상의 3개 평가자 모두 통과 시에만 1 step 인정
220
+ local ecq ef ev
221
+ ecq=$(jq -r ".features[$i].passes // [] | map(select(. == \"evaluator-code-quality\")) | length" "$FEATURES" 2>/dev/null)
222
+ ef=$(jq -r ".features[$i].passes // [] | map(select(. == \"evaluator-functional\")) | length" "$FEATURES" 2>/dev/null)
223
+ ev=$(jq -r ".features[$i].passes // [] | map(select(. == \"evaluator-visual\")) | length" "$FEATURES" 2>/dev/null)
224
+ if [ "${ecq:-0}" -gt 0 ] && [ "${ef:-0}" -gt 0 ] && [ "${ev:-0}" -gt 0 ]; then
225
+ done_steps=$((done_steps + 1))
226
+ fi
227
+
228
+ i=$((i + 1))
229
+ done
230
+
231
+ local pct=0
232
+ if [ "$total_steps" -gt 0 ]; then
233
+ pct=$(( done_steps * 100 / total_steps ))
234
+ fi
235
+
236
+ local bar_width=16
237
+ local filled=$(( pct * bar_width / 100 ))
238
+ local empty=$(( bar_width - filled ))
239
+ local bar=""
240
+ for ((j=0; j<filled; j++)); do bar+="█"; done
241
+ for ((j=0; j<empty; j++)); do bar+="░"; done
242
+
243
+ echo " Features: ${completed_features}/${total_features} completed"
244
+ echo " Pipeline: ${bar} ${pct}%"
245
+ fi
246
+ else
247
+ echo " (feature-list.json not yet created — run Planner first)"
248
+ fi
249
+
250
+ echo ""
251
+
252
+ # ── Current Agent Status ──
253
+ if [ "$current_agent" != "none" ] && [ "$current_agent" != "null" ]; then
254
+ echo " Current: ${current_agent} [${agent_status}] (retry ${retry_count}/${max_retries})"
255
+ fi
256
+
257
+ # ── Failure Info ──
258
+ if [ -n "$failure_agent" ] && [ "$failure_agent" != "null" ]; then
259
+ echo ""
260
+ echo " ⚠ FAIL: ${failure_agent} → ${failure_location}"
261
+ if [ -n "$failure_message" ] && [ "$failure_message" != "null" ]; then
262
+ echo " ${failure_message}"
263
+ fi
264
+ fi
265
+
266
+ # ── Blocked ──
267
+ if [ "$agent_status" = "blocked" ]; then
268
+ echo ""
269
+ echo " ✖ BLOCKED: retry limit (${max_retries}) reached — user intervention required"
270
+ fi
271
+
272
+ # ── Next Action ──
273
+ if [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ] && [ "$agent_status" != "blocked" ]; then
274
+ # Read model & thinking mode from config
275
+ local next_model="opus"
276
+ local next_thinking="null"
277
+ if [ -f "$CONFIG" ]; then
278
+ next_model=$(jq -r ".agents[\"${next_agent}\"].model // \"opus\"" "$CONFIG" 2>/dev/null || echo "opus")
279
+ next_thinking=$(jq -r ".agents[\"${next_agent}\"].thinking_mode // \"null\"" "$CONFIG" 2>/dev/null || echo "null")
280
+ fi
281
+
282
+ local mode_str=""
283
+ if [ "$next_thinking" != "null" ]; then
284
+ mode_str=" [/${next_thinking}]"
285
+ fi
286
+
287
+ echo ""
288
+ echo " Next → /harness-${next_agent} (${next_model}${mode_str})"
289
+ echo " Tip → 새 세션을 시작하면 자동으로 안내됩니다"
290
+ fi
291
+
292
+ echo ""
293
+ }
294
+
295
+ # ─────────────────────────────────────────
296
+ # render_agent_bar — 에이전트 시퀀스 바 (compact)
297
+ # ─────────────────────────────────────────
298
+ render_agent_bar() {
299
+ local PROGRESS="${1:-.}/.harness/progress.json"
300
+ local CONFIG="${1:-.}/.harness/config.json"
301
+ local PIPELINE_JSON="${1:-.}/.harness/actions/pipeline.json"
302
+
303
+ if [ ! -f "$PROGRESS" ] || [ ! -f "$CONFIG" ]; then return 1; fi
304
+
305
+ local pipeline current_agent fe_stack fe_target
306
+ pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
307
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
308
+ fe_stack="react"
309
+ fe_target="web"
310
+ if [ -f "$PIPELINE_JSON" ]; then
311
+ fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
312
+ fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
313
+ if [ -z "$fe_target" ]; then
314
+ fe_target="web" # v5.6.5+: 치환 로직 제거. 기본 web.
315
+ fi
316
+ fi
317
+
318
+ local completed_agents
319
+ completed_agents=$(jq -r '.completed_agents // [] | .[]' "$PROGRESS")
320
+
321
+ if [ "$pipeline" = "null" ] || [ "$pipeline" = "unknown" ]; then return 1; fi
322
+
323
+ local agents_line=" Agents: "
324
+ local first=true
325
+
326
+ while IFS= read -r agent; do
327
+ agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
328
+ # v5.6.5+: fe_stack 에이전트 치환 제거. 스택별 동작은 ref-docs 로 조절.
329
+
330
+ if [ "$first" = true ]; then
331
+ first=false
332
+ else
333
+ agents_line+=" → "
334
+ fi
335
+
336
+ if echo "$completed_agents" | grep -q "^${agent}$"; then
337
+ agents_line+="${agent}✓"
338
+ elif [ "$agent" = "$current_agent" ]; then
339
+ agents_line+="[${agent}]"
340
+ else
341
+ agents_line+="${agent}"
342
+ fi
343
+ done < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null)
344
+
345
+ echo "$agents_line"
346
+ }