@walwal-harness/cli 6.0.5 → 6.1.0

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 (43) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/commands/harness-solo.md +20 -20
  3. package/commands/harness-team.md +1 -1
  4. package/package.json +1 -1
  5. package/scripts/conductor-tick.sh +382 -0
  6. package/scripts/harness-archive.sh +123 -0
  7. package/scripts/harness-dashboard-up.sh +72 -0
  8. package/scripts/harness-dashboard.sh +509 -0
  9. package/scripts/harness-goal-init.sh +72 -0
  10. package/scripts/harness-goal-show.sh +37 -0
  11. package/scripts/harness-gotcha-memory.sh +348 -0
  12. package/scripts/harness-gotcha-register.sh +393 -0
  13. package/scripts/harness-meeting-doc.sh +153 -0
  14. package/scripts/harness-monitor.sh +398 -0
  15. package/scripts/harness-next.sh +577 -0
  16. package/scripts/harness-progress-set.sh +41 -0
  17. package/scripts/harness-prompt-history.sh +164 -0
  18. package/scripts/harness-queue-manager.sh +694 -0
  19. package/scripts/harness-session-start.sh +140 -0
  20. package/scripts/harness-statusline.sh +139 -0
  21. package/scripts/harness-task-session.sh +48 -0
  22. package/scripts/harness-tmux.sh +372 -0
  23. package/scripts/harness-user-prompt-submit.sh +123 -0
  24. package/scripts/init-agents-md.sh +337 -0
  25. package/scripts/init-ref-docs.sh +262 -0
  26. package/scripts/lib/harness-audit.sh +113 -0
  27. package/scripts/lib/harness-guardrail.sh +148 -0
  28. package/scripts/lib/harness-keywait.sh +57 -0
  29. package/scripts/lib/harness-render-progress.sh +346 -0
  30. package/scripts/scan-project.sh +367 -0
  31. package/skills/brainstorming/SKILL.md +4 -4
  32. package/skills/conductor/SKILL.md +12 -16
  33. package/skills/cqo/SKILL.md +7 -0
  34. package/skills/cto/SKILL.md +7 -0
  35. package/skills/dispatcher/SKILL.md +17 -28
  36. package/skills/evaluator-code-quality/SKILL.md +1 -1
  37. package/skills/evaluator-functional/SKILL.md +1 -1
  38. package/skills/evaluator-visual/SKILL.md +1 -1
  39. package/skills/generator-backend/SKILL.md +1 -1
  40. package/skills/generator-frontend/SKILL.md +1 -1
  41. package/skills/meeting-manager/SKILL.md +33 -1
  42. package/skills/planner/SKILL.md +23 -9
  43. package/skills/service-ops/SKILL.md +19 -4
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # walwal-harness — Brick Office dashboard launcher
3
+ #
4
+ # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
+ # walwal-harness CLI 패키지에는 R3F + Next.js 무게 때문에 dashboard 가 미포함이고,
6
+ # 본 스크립트가 git sparse-checkout 으로 apps/harness-dashboard/ 만 가져와서
7
+ # 사용자 프로젝트 외부 (~/.walwal-harness/dashboard) 에 격리 설치 후 dev 실행한다.
8
+ #
9
+ # Usage:
10
+ # bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
11
+ # bash scripts/harness-dashboard-up.sh --port 3050 # override port
12
+ # HARNESS_ROOT=/path/to/project bash ... # explicit harness root
13
+ set -euo pipefail
14
+
15
+ REPO_URL="${WALWAL_HARNESS_REPO:-https://github.com/30eggis/walwal-harness.git}"
16
+ DASHBOARD_PATH="apps/harness-dashboard"
17
+ LOCAL_DIR="${HOME}/.walwal-harness/dashboard"
18
+ PORT="${PORT:-3001}"
19
+
20
+ while [[ $# -gt 0 ]]; do
21
+ case "$1" in
22
+ --port) PORT="$2"; shift 2 ;;
23
+ --reinstall) rm -rf "$LOCAL_DIR"; shift ;;
24
+ --help|-h)
25
+ sed -n '2,16p' "$0"
26
+ exit 0 ;;
27
+ *) echo "[brick-office] unknown arg: $1" >&2; exit 2 ;;
28
+ esac
29
+ done
30
+
31
+ if [[ -z "${HARNESS_ROOT:-}" ]]; then
32
+ HARNESS_ROOT="$(pwd)"
33
+ fi
34
+ if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
35
+ echo "[brick-office] FAIL: ${HARNESS_ROOT} 에 .harness/ 가 없습니다."
36
+ echo " 먼저 'npx walwal-harness' 로 초기화하세요."
37
+ exit 3
38
+ fi
39
+
40
+ echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
41
+ echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
42
+
43
+ if [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
44
+ echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
45
+ mkdir -p "${LOCAL_DIR}"
46
+ cd "${LOCAL_DIR}"
47
+ git init -q
48
+ git remote add origin "${REPO_URL}" 2>/dev/null || true
49
+ git config core.sparseCheckout true
50
+ echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
51
+ git fetch --depth=1 origin main -q
52
+ git checkout main -q
53
+ echo "[brick-office] 다운로드 완료."
54
+ fi
55
+
56
+ cd "${LOCAL_DIR}/${DASHBOARD_PATH}"
57
+
58
+ if [[ ! -d node_modules ]]; then
59
+ echo "[brick-office] npm install (한 번만 실행, ~30s)..."
60
+ npm install --no-audit --no-fund --loglevel=error
61
+ fi
62
+
63
+ echo ""
64
+ echo "╔══════════════════════════════════════════════════════════╗"
65
+ echo "║ Brick Office — http://localhost:${PORT}"
66
+ echo "║ ║"
67
+ echo "║ HARNESS_ROOT 이 가리키는 프로젝트의 .harness/ 상태를 ║"
68
+ echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료. ║"
69
+ echo "╚══════════════════════════════════════════════════════════╝"
70
+ echo ""
71
+
72
+ HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -p "${PORT}"
@@ -0,0 +1,509 @@
1
+ #!/bin/bash
2
+ # harness-dashboard.sh — Unified Dashboard (Solo + Team mode)
3
+ #
4
+ # Reads progress.json.mode to decide rendering:
5
+ # solo: Sequential pipeline progress (sprint map, agent bar, prompt history)
6
+ # team: Queue summary, team status, feature list, bottleneck alerts
7
+ #
8
+ # Usage: bash scripts/harness-dashboard.sh [project-root]
9
+
10
+ set -uo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
14
+ source "$SCRIPT_DIR/lib/harness-keywait.sh"
15
+
16
+ PROJECT_ROOT="${1:-}"
17
+ if [ -z "$PROJECT_ROOT" ]; then
18
+ PROJECT_ROOT="$(resolve_harness_root ".")" || {
19
+ echo "[dashboard] .harness/ not found. Pass project root as argument."
20
+ exit 1
21
+ }
22
+ fi
23
+
24
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
+ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
26
+ QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
27
+ PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
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
+ MAGENTA="\033[35m"
37
+ RESET="\033[0m"
38
+
39
+ strip_ansi() {
40
+ sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
41
+ }
42
+
43
+ get_term_width() {
44
+ echo "${_COLS:-$(tput cols 2>/dev/null || echo 80)}"
45
+ }
46
+
47
+ get_mode() {
48
+ if [ -f "$PROGRESS" ] && command -v jq &>/dev/null; then
49
+ local raw_mode queue active
50
+ raw_mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
51
+ queue="$(dirname "$PROGRESS")/actions/feature-queue.json"
52
+ if [ "$raw_mode" != "team" ] && [ -f "$queue" ]; then
53
+ active=$(jq -r '(.queue.in_progress | length) // 0' "$queue" 2>/dev/null || echo 0)
54
+ if [ "${active:-0}" -gt 0 ]; then
55
+ local heal_teams=$active
56
+ [ "$heal_teams" -gt 3 ] && heal_teams=3
57
+ local SCRIPT_DIR_LOCAL
58
+ SCRIPT_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
59
+ bash "$SCRIPT_DIR_LOCAL/harness-progress-set.sh" "$(dirname "$(dirname "$PROGRESS")")" \
60
+ ".mode = \"team\" | .team_state.active_teams = ${heal_teams} | .team_state.paused_at = null" \
61
+ 2>/dev/null
62
+ if [ -f "$(dirname "$PROGRESS")/progress.log" ]; then
63
+ echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | dashboard reset ${raw_mode}→team (queue had ${active} in_progress)" >> "$(dirname "$PROGRESS")/progress.log"
64
+ fi
65
+ echo "team"
66
+ return
67
+ fi
68
+ fi
69
+ echo "$raw_mode"
70
+ else
71
+ echo "solo"
72
+ fi
73
+ }
74
+
75
+ # ══════════════════════════════════════════
76
+ # Common header
77
+ # ══════════════════════════════════════════
78
+ render_header() {
79
+ local now project_name mode
80
+ now=$(date +"%H:%M:%S")
81
+ project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
82
+ mode=$(get_mode)
83
+
84
+ local mode_label
85
+ case "$mode" in
86
+ team) mode_label="${GREEN}TEAM${RESET}" ;;
87
+ paused) mode_label="${YELLOW}PAUSED${RESET}" ;;
88
+ *) mode_label="${CYAN}SOLO${RESET}" ;;
89
+ esac
90
+
91
+ echo -e "${BOLD}HARNESS DASHBOARD${RESET} ${mode_label} ${DIM}${project_name} | ${now}${RESET}"
92
+ echo ""
93
+ }
94
+
95
+ # ══════════════════════════════════════════
96
+ # Solo Mode rendering
97
+ # ══════════════════════════════════════════
98
+ render_solo_sprint_overview() {
99
+ if [ ! -f "$PROGRESS" ]; then
100
+ echo -e " ${DIM}(waiting for harness init...)${RESET}"
101
+ return
102
+ fi
103
+
104
+ local pipeline sprint_num sprint_status current_agent agent_status retry_count
105
+ # progress.json 이 깨졌을 때 jq 가 stderr 로 에러를 뿜고 stdout 은 빈 문자열을 반환.
106
+ # 모든 jq 호출에 2>/dev/null + bash fallback 으로 변수 기본값 보장.
107
+ if ! jq empty "$PROGRESS" 2>/dev/null; then
108
+ echo -e " ${RED}⚠ .harness/progress.json 이 손상되었습니다 (invalid JSON).${RESET}"
109
+ echo -e " ${DIM}복구: cp .harness/progress.json{,.bak} && jq . .harness/progress.json.bak${RESET}"
110
+ echo -e " ${DIM}또는: npx @walwal-harness/cli@latest --force (전체 재초기화)${RESET}"
111
+ echo ""
112
+ return
113
+ fi
114
+ pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS" 2>/dev/null)
115
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
116
+ sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
117
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
118
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
119
+ retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS" 2>/dev/null)
120
+ # 빈 문자열 → 기본값 (jq 가 silently fail 한 잔여 케이스 대비)
121
+ pipeline="${pipeline:-?}"
122
+ sprint_num="${sprint_num:-0}"
123
+ sprint_status="${sprint_status:-init}"
124
+ current_agent="${current_agent:-none}"
125
+ agent_status="${agent_status:-pending}"
126
+ retry_count="${retry_count:-0}"
127
+
128
+ local status_color="$RESET"
129
+ case "$agent_status" in
130
+ running) status_color="$GREEN" ;;
131
+ completed) status_color="$CYAN" ;;
132
+ failed) status_color="$RED" ;;
133
+ blocked) status_color="$RED" ;;
134
+ *) status_color="$YELLOW" ;;
135
+ esac
136
+
137
+ echo -e " ${BOLD}Pipeline${RESET} ${pipeline} ${BOLD}Sprint${RESET} ${sprint_num} (${sprint_status}) ${BOLD}Agent${RESET} ${status_color}${current_agent} [${agent_status}]${RESET}$([ "$retry_count" -gt 0 ] && echo -e " ${RED}R${retry_count}${RESET}")"
138
+ echo ""
139
+ }
140
+
141
+ render_solo_failure_info() {
142
+ if [ ! -f "$PROGRESS" ]; then return; fi
143
+
144
+ local failure_agent
145
+ failure_agent=$(jq -r '.failure.agent // empty' "$PROGRESS" 2>/dev/null)
146
+
147
+ if [ -n "$failure_agent" ] && [ "$failure_agent" != "null" ]; then
148
+ local failure_loc failure_msg
149
+ failure_loc=$(jq -r '.failure.location // ""' "$PROGRESS")
150
+ failure_msg=$(jq -r '.failure.message // ""' "$PROGRESS" | strip_ansi | tr '\n' ' ' | sed 's/ */ /g')
151
+ if [ ${#failure_msg} -gt 80 ]; then failure_msg="${failure_msg:0:78}.."; fi
152
+
153
+ echo -e " ${RED}${BOLD}FAIL${RESET} ${RED}${failure_agent} → ${failure_loc}${RESET}"
154
+ if [ -n "$failure_msg" ]; then
155
+ echo -e " ${DIM}${failure_msg}${RESET}"
156
+ fi
157
+ echo ""
158
+ fi
159
+ }
160
+
161
+ render_solo_agent_info() {
162
+ if [ ! -f "$PROGRESS" ]; then return; fi
163
+
164
+ local next_agent agent_status
165
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
166
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
167
+
168
+ if [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ] && [ "$agent_status" != "blocked" ]; then
169
+ echo -e " ${CYAN}Next → /harness-${next_agent}${RESET}"
170
+ fi
171
+
172
+ render_agent_bar "$PROJECT_ROOT" 2>/dev/null
173
+ echo ""
174
+ }
175
+
176
+ render_solo_prompt_history() {
177
+ echo -e "${BOLD}Recent Activity${RESET}"
178
+
179
+ if [ -f "$PROGRESS_LOG" ]; then
180
+ grep -v '^#' "$PROGRESS_LOG" 2>/dev/null | grep -v '^$' | tail -10 | while IFS= read -r line; do
181
+ local ts agent action detail
182
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
183
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
184
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
185
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
186
+
187
+ local short_ts icon color
188
+ short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
189
+
190
+ # action 기반으로 먼저 분류 (team 모드에서 agent=team-N이므로)
191
+ case "$action" in
192
+ eval-start|eval-check|eval-done|eval)
193
+ icon="✦" ; color="$MAGENTA" ;;
194
+ gen-start|gen-read|gen-write|gen-test|gen-done|gen)
195
+ icon="▶" ; color="$GREEN" ;;
196
+ result|pass)
197
+ icon="✓" ; color="$GREEN" ;;
198
+ fail)
199
+ icon="✗" ; color="$RED" ;;
200
+ *)
201
+ # fallback: agent 기반
202
+ case "$agent" in
203
+ dispatcher*) icon="▸" ; color="$MAGENTA" ;;
204
+ planner*) icon="□" ; color="$YELLOW" ;;
205
+ generator*) icon="▶" ; color="$GREEN" ;;
206
+ eval*) icon="✦" ; color="$MAGENTA" ;;
207
+ team-*) icon="◆" ; color="$CYAN" ;;
208
+ user*) icon="★" ; color="$BOLD" ;;
209
+ *) icon="·" ; color="$DIM" ;;
210
+ esac
211
+ ;;
212
+ esac
213
+
214
+ if [ ${#detail} -gt 40 ]; then detail="${detail:0:38}.."; fi
215
+
216
+ echo -e "${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
217
+ done
218
+ else
219
+ echo -e "${DIM}(no progress.log yet)${RESET}"
220
+ fi
221
+ }
222
+
223
+ # ══════════════════════════════════════════
224
+ # Team Mode rendering
225
+ # ══════════════════════════════════════════
226
+ render_team_queue_summary() {
227
+ if [ ! -f "$QUEUE" ]; then
228
+ echo -e " ${DIM}(queue not initialized — run /harness-team)${RESET}"
229
+ return
230
+ fi
231
+
232
+ local ready blocked in_prog passed failed total
233
+ ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
234
+ blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
235
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
236
+ failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
237
+
238
+ # passed 는 queue.passed ∪ feature-list.json 의 self-passed 를 dedup 합집합으로 계산
239
+ # total 도 feature-list IDs ∪ queue 전체 IDs 합집합으로 계산 — 새 sprint feature 가
240
+ # queue 에만 추가되고 feature-list 에 누락되어도 누락 없이 카운트 (예: F-800+ Sprint 8)
241
+ if [ -f "$FEATURES" ]; then
242
+ passed=$(jq -r --slurpfile q "$QUEUE" '
243
+ ($q[0].queue.passed // []) as $qp |
244
+ ([.features[] | select((.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) | .id]) as $sp |
245
+ ($qp + $sp | unique | length)
246
+ ' "$FEATURES" 2>/dev/null || echo 0)
247
+ total=$(jq -r --slurpfile q "$QUEUE" '
248
+ ([.features[].id]) as $fl |
249
+ (($q[0].queue.ready // []) +
250
+ ($q[0].queue.passed // []) +
251
+ (($q[0].queue.failed // []) | if type == "object" then keys else . end) +
252
+ (($q[0].queue.in_progress // {}) | keys) +
253
+ (($q[0].queue.blocked // {}) | keys)) as $qids |
254
+ ($fl + $qids | unique | length)
255
+ ' "$FEATURES" 2>/dev/null || echo 0)
256
+ else
257
+ passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
258
+ total=$((ready + blocked + in_prog + passed + failed))
259
+ fi
260
+ ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}; total=${total:-0}
261
+
262
+ local pct=0
263
+ if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
264
+ local bar_w=16
265
+ local filled=$(( pct * bar_w / 100 ))
266
+ local empty=$(( bar_w - filled ))
267
+ local bar=""
268
+ for ((i=0; i<filled; i++)); do bar+="█"; done
269
+ for ((i=0; i<empty; i++)); do bar+="░"; done
270
+
271
+ echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
272
+
273
+ # Integrity warn — 큐에 있지만 feature-list 에 없는 orphan feature 가 있으면 경고.
274
+ # 발생 원인: Lead 가 jq 로 큐 직접 편집 (canonical path = enqueue 명령 사용).
275
+ if [ -f "$FEATURES" ]; then
276
+ local orphan_count
277
+ orphan_count=$(jq -r --slurpfile q "$QUEUE" '
278
+ ([.features[].id]) as $fl |
279
+ (($q[0].queue.ready // []) +
280
+ ($q[0].queue.passed // []) +
281
+ (($q[0].queue.failed // []) | if type == "object" then keys else . end) +
282
+ (($q[0].queue.in_progress // {}) | keys) +
283
+ (($q[0].queue.blocked // {}) | keys)) as $qids |
284
+ ($qids - $fl) | unique | length
285
+ ' "$FEATURES" 2>/dev/null || echo 0)
286
+ if [ "${orphan_count:-0}" -gt 0 ]; then
287
+ echo -e " ${YELLOW}⚠ ${orphan_count} feature(s) in queue but not in feature-list.json${RESET} ${DIM}(use: queue-manager enqueue)${RESET}"
288
+ fi
289
+ fi
290
+ }
291
+
292
+ render_team_status() {
293
+ if [ ! -f "$QUEUE" ]; then return; fi
294
+
295
+ local team_count
296
+ team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
297
+ if [ "${team_count:-0}" -eq 0 ]; then return; fi
298
+
299
+ echo ""
300
+ echo -e "${BOLD}Teams${RESET}"
301
+
302
+ for i in $(seq 1 "$team_count"); do
303
+ local t_status t_feature t_phase t_attempt
304
+ t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
305
+ t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
306
+
307
+ if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
308
+ t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
309
+ t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
310
+ else
311
+ t_phase="—"; t_attempt=""
312
+ fi
313
+
314
+ local icon color
315
+ case "$t_status" in
316
+ busy) icon="▶"; color="$GREEN" ;;
317
+ idle) icon="○"; color="$DIM" ;;
318
+ *) icon="?"; color="$RESET" ;;
319
+ esac
320
+
321
+ local phase_short=""
322
+ case "$t_phase" in
323
+ gen) phase_short="${CYAN}Gen${RESET}" ;;
324
+ gate) phase_short="${YELLOW}Gate${RESET}" ;;
325
+ eval) phase_short="${MAGENTA}Eval${RESET}" ;;
326
+ *) phase_short="${DIM}-${RESET}" ;;
327
+ esac
328
+
329
+ printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
330
+ if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
331
+ printf " #%s/5" "$t_attempt"
332
+ fi
333
+ echo ""
334
+ done
335
+ }
336
+
337
+ render_team_features() {
338
+ if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
339
+
340
+ echo ""
341
+ echo -e "${BOLD}Features${RESET}"
342
+
343
+ # feature-list IDs ∪ queue 전체 IDs 합집합을 iterate.
344
+ # feature-list 에 등록 안 된 queue-only feature 는 name="(queue)" 로 표시.
345
+ jq -r --slurpfile q "$QUEUE" '
346
+ ($q[0].queue.passed // []) as $passed |
347
+ ($q[0].queue.failed // []) as $failed_raw |
348
+ (if ($failed_raw | type) == "object" then ($failed_raw | keys) else $failed_raw end) as $failed |
349
+ ($q[0].queue.ready // []) as $ready |
350
+ ($q[0].queue.in_progress // {}) as $prog |
351
+ ($q[0].queue.blocked // {}) as $blocked |
352
+ ([.features[] | {id: .id, name: (.name // .title // .description // "?"), passes: (.passes // [])}]) as $fl |
353
+ ([$fl[] | {(.id): .}] | add // {}) as $fl_map |
354
+ (([$fl[].id]) + $ready + $passed + $failed + ($prog | keys) + ($blocked | keys) | unique) as $all_ids |
355
+ $all_ids[] |
356
+ . as $fid |
357
+ ($fl_map[$fid] // {name: "(queue)", passes: []}) as $f |
358
+ ($f.name | if length > 18 then .[0:16] + ".." else . end) as $fname |
359
+ (($f.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) as $self_passed |
360
+ (if ($fid | IN($passed[])) or $self_passed then "P"
361
+ elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
362
+ elif ($fid | IN($failed[])) then "F"
363
+ elif ($fid | IN($ready[])) then "R"
364
+ elif $blocked[$fid] then "BLK|\($blocked[$fid] | length)"
365
+ else "U" end) as $st |
366
+ "\($st)\t\($fid)\t\($fname)"
367
+ ' "$FEATURES" 2>/dev/null | while IFS=$'\t' read -r st fid fname; do
368
+ case "$st" in
369
+ P) printf " ${GREEN}●${RESET} %-6s %s\n" "$fid" "$fname" ;;
370
+ F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
371
+ R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
372
+ BLK\|*) deps=$(echo "$st" | cut -d'|' -f2)
373
+ printf " ${MAGENTA}◍${RESET} %-6s %-22s ${DIM}blocked (deps:%s)${RESET}\n" "$fid" "$fname" "$deps" ;;
374
+ U) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
375
+ I\|*) team=$(echo "$st" | cut -d'|' -f2)
376
+ phase=$(echo "$st" | cut -d'|' -f3)
377
+ printf " ${CYAN}◐${RESET} %-6s %-22s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
378
+ *) printf " ? %-6s %s\n" "$fid" "$fname" ;;
379
+ esac
380
+ done
381
+ }
382
+
383
+ render_team_bottleneck() {
384
+ if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
385
+
386
+ local failed_list
387
+ failed_list=$(jq -r '.queue.failed[]' "$QUEUE" 2>/dev/null)
388
+ if [ -z "$failed_list" ]; then return; fi
389
+
390
+ local team_count idle_teams=0 blocked_count in_prog
391
+ team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 0)
392
+ blocked_count=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
393
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
394
+
395
+ for i in $(seq 1 "$team_count"); do
396
+ local ts
397
+ ts=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
398
+ if [ "$ts" = "idle" ]; then idle_teams=$((idle_teams + 1)); fi
399
+ done
400
+
401
+ while IFS= read -r fid; do
402
+ [ -z "$fid" ] && continue
403
+ local deps_on_this
404
+ deps_on_this=$(jq --arg fid "$fid" '[.queue.blocked | to_entries[] | select(.value | index($fid))] | length' "$QUEUE" 2>/dev/null || echo 0)
405
+
406
+ if [ "$deps_on_this" -gt 0 ]; then
407
+ echo ""
408
+ echo -e " ${RED}${BOLD}⚠ BOTTLENECK${RESET} ${RED}${fid}${RESET} failed → ${YELLOW}${deps_on_this} features blocked${RESET}"
409
+ if [ "$idle_teams" -gt 0 ]; then
410
+ echo -e " ${BOLD}→ requeue:${RESET} bash scripts/harness-queue-manager.sh requeue ${fid} ."
411
+ fi
412
+ fi
413
+ done <<< "$failed_list"
414
+
415
+ if [ "$idle_teams" -eq "$team_count" ] && [ "$in_prog" -eq 0 ] && [ "$blocked_count" -gt 0 ]; then
416
+ echo ""
417
+ echo -e " ${RED}${BOLD}!! STALLED${RESET} — All teams idle, ${blocked_count} features blocked"
418
+ fi
419
+ }
420
+
421
+ # ══════════════════════════════════════════
422
+ # Archive Prompt — 처리 완료된 프롬프트 목록
423
+ # ══════════════════════════════════════════
424
+ render_archive_prompts() {
425
+ if [ ! -f "$PROGRESS_LOG" ]; then return; fi
426
+
427
+ # user-prompt 엔트리 중 이후에 result/pass/completed 액션이 있는 것 = 처리 완료
428
+ local all_prompts
429
+ all_prompts=$(grep '| user-prompt |' "$PROGRESS_LOG" 2>/dev/null)
430
+ if [ -z "$all_prompts" ]; then return; fi
431
+
432
+ local total_prompts
433
+ total_prompts=$(echo "$all_prompts" | wc -l | tr -d ' ')
434
+
435
+ # 마지막 프롬프트를 제외한 모든 프롬프트 = archived (처리 완료)
436
+ # newest first (tac), 전체 출력 (제한 없음)
437
+ local archived
438
+ if [ "$total_prompts" -le 1 ]; then return; fi
439
+ archived=$(echo "$all_prompts" | head -n $((total_prompts - 1)) | tac)
440
+
441
+ echo ""
442
+ echo -e "${BOLD}Archive Prompt${RESET} ${DIM}(처리 완료)${RESET}"
443
+
444
+ echo "$archived" | while IFS= read -r line; do
445
+ local ts detail
446
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
447
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
448
+
449
+ local short_ts
450
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
451
+
452
+ if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
453
+
454
+ echo -e " ${GREEN}✓${RESET} ${DIM}${short_ts}${RESET} ${detail}"
455
+ done
456
+ }
457
+
458
+ # ══════════════════════════════════════════
459
+ # Render dispatch
460
+ # ══════════════════════════════════════════
461
+ render_dashboard() {
462
+ local mode
463
+ mode=$(get_mode)
464
+
465
+ render_header
466
+
467
+ case "$mode" in
468
+ team|paused)
469
+ render_team_queue_summary
470
+ render_team_status
471
+ render_team_features
472
+ render_team_bottleneck
473
+ # Archive Prompt는 별도 패널(harness-prompt-history.sh)에서 렌더링 — Dashboard 모니터링 영역 보호
474
+ ;;
475
+ *)
476
+ render_solo_sprint_overview
477
+ render_solo_failure_info
478
+
479
+ if [ -f "$FEATURES" ]; then
480
+ render_progress "$PROJECT_ROOT" 2>/dev/null
481
+ fi
482
+
483
+ render_solo_agent_info
484
+ # Archive Prompt는 별도 패널(harness-prompt-history.sh)에서 렌더링
485
+ ;;
486
+ esac
487
+ }
488
+
489
+ # ══════════════════════════════════════════
490
+ # Main — Auto-refresh loop
491
+ # ══════════════════════════════════════════
492
+ tput civis 2>/dev/null
493
+ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
494
+
495
+ clear
496
+
497
+ while true; do
498
+ _ROWS=$(tput lines 2>/dev/null || echo 30)
499
+ _COLS=$(tput cols 2>/dev/null || echo 80)
500
+ export _ROWS _COLS
501
+ buf=$(render_dashboard 2>&1)
502
+ tput cup 0 0 2>/dev/null
503
+ # Append ESC[K after every line so each row clears its right-side residue
504
+ # from any previous frame (fixes wrapped shell-prompt bleed-through).
505
+ printf '%s\n' "$buf" | awk '{printf "%s\033[K\n", $0}'
506
+ tput ed 2>/dev/null
507
+ printf "${DIM} [r] refresh [q] quit${RESET}\033[K\n"
508
+ wait_or_refresh 3 || true
509
+ done
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # harness-goal-init.sh — 새 GOAL 발급 (CEO=Dispatcher 전용)
3
+ # 사용법: bash scripts/harness-goal-init.sh "<title>"
4
+ #
5
+ # 동작:
6
+ # 1. .harness/actions/goals.md 가 없으면 template로 생성
7
+ # 2. 다음 GOAL ID(G-N) 발급 → progress.json.goals.list 에 등록
8
+ # 3. progress.json.goals.active_id 갱신
9
+ # 4. CTO 협의 미완료 상태(cto_feasibility=null, owner_confirmed=false)
10
+ #
11
+ # 후속:
12
+ # - CTO는 cto_feasibility 의견을 협의 후 같은 항목에 채워줌
13
+ # - Owner 최종 확인 시 dispatcher 가 owner_confirmed=true 로 갱신
14
+ set -euo pipefail
15
+
16
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
17
+ GOAL_FILE="$ROOT/.harness/actions/goals.md"
18
+ TEMPLATE="$ROOT/.harness/actions/goals.md.template"
19
+ PROGRESS="$ROOT/.harness/progress.json"
20
+
21
+ TITLE="${1:-}"
22
+ if [ -z "$TITLE" ]; then
23
+ echo "usage: $0 \"<title>\"" >&2
24
+ exit 1
25
+ fi
26
+
27
+ mkdir -p "$ROOT/.harness/actions"
28
+
29
+ # 1. 파일이 없으면 template 복사 후 ISO 시각 채움
30
+ if [ ! -f "$GOAL_FILE" ]; then
31
+ if [ ! -f "$TEMPLATE" ]; then
32
+ echo "template missing: $TEMPLATE" >&2; exit 1
33
+ fi
34
+ ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
35
+ sed "s|<ISO>|$ISO|g" "$TEMPLATE" > "$GOAL_FILE"
36
+ fi
37
+
38
+ # 2. 다음 GOAL ID 계산
39
+ LAST_N=$(jq -r '.goals.list | map(.id // "G-0") | map(sub("G-"; "")) | map(tonumber? // 0) | max // 0' "$PROGRESS" 2>/dev/null || echo 0)
40
+ NEXT_N=$((LAST_N + 1))
41
+ GOAL_ID="G-${NEXT_N}"
42
+ ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
43
+
44
+ # 3. progress.json 에 등록 (partial update)
45
+ jq --arg id "$GOAL_ID" --arg title "$TITLE" --arg iso "$ISO" '
46
+ .goals.list += [{
47
+ "id": $id,
48
+ "title": $title,
49
+ "status": "draft",
50
+ "owner_confirmed": false,
51
+ "cto_feasibility": null,
52
+ "created_at": $iso
53
+ }] |
54
+ .goals.active_id = $id |
55
+ .updated_at = $iso
56
+ ' "$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
57
+
58
+ # 4. goals.md 에 GOAL 섹션 append (CEO가 이후 본문 채움)
59
+ cat >> "$GOAL_FILE" <<EOF
60
+
61
+ ## $GOAL_ID — $TITLE
62
+ status: draft
63
+ owner_confirmed: false
64
+ cto_feasibility: null
65
+ created_at: $ISO
66
+
67
+ (CEO가 success_metrics·deadline·kpis·runbook 채움)
68
+ EOF
69
+
70
+ echo "$GOAL_ID 발급 완료: $TITLE"
71
+ echo "→ $GOAL_FILE 에서 본문 작성"
72
+ echo "→ CTO 협의 후 owner 확정 시 owner_confirmed=true"