@walwal-harness/cli 3.4.0 → 3.6.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.
- package/assets/templates/HARNESS.md +33 -2
- package/assets/templates/config.json +17 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard.sh +179 -0
- package/scripts/harness-eval-watcher.sh +230 -0
- package/scripts/harness-monitor.sh +193 -0
- package/scripts/harness-next.sh +21 -3
- package/scripts/harness-session-start.sh +17 -10
- package/scripts/harness-tmux.sh +121 -0
- package/scripts/lib/harness-audit.sh +113 -0
|
@@ -143,8 +143,39 @@ pending → draft → reviewed → approved
|
|
|
143
143
|
| 파일 | 역할 |
|
|
144
144
|
|------|------|
|
|
145
145
|
| `.harness/progress.json` | 기계 판독 상태 (현재 에이전트, 파이프라인, 실패 정보) |
|
|
146
|
-
| `.harness/progress.log` | 사람 판독 히스토리 (append-only) |
|
|
147
|
-
| `.harness/handoff.json` | 세션 전환 문서 (prompt, model, thinking_mode, artifacts
|
|
146
|
+
| `.harness/progress.log` | 사람 판독 히스토리 (append-only, 전체) |
|
|
147
|
+
| `.harness/handoff.json` | 세션 전환 문서 (prompt, model, thinking_mode, artifacts) |
|
|
148
|
+
| `.harness/actions/audit.log` | Sprint cycle 단위 실행 추적 (Planner→Eval 통과) |
|
|
149
|
+
|
|
150
|
+
### Audit Log
|
|
151
|
+
|
|
152
|
+
1 sprint cycle(Planner/Dispatcher 시작 → Eval 통과) 단위의 상세 실행 추적입니다.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
# 예시:
|
|
156
|
+
TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
|
|
157
|
+
2026-04-09T14:30:00Z | planner | plan | start | plan.md | Sprint 1 설계 시작
|
|
158
|
+
2026-04-09T14:35:00Z | planner | plan | complete | api-contract.json | 3 endpoints, 2 services
|
|
159
|
+
2026-04-09T14:35:01Z | planner | handoff | complete | →gen-backend |
|
|
160
|
+
2026-04-09T14:36:00Z | system | gate | pass | pre-eval | gen-backend
|
|
161
|
+
2026-04-09T14:40:00Z | gen-backend | develop | start | apps/service-user/ | User CRUD 구현
|
|
162
|
+
2026-04-09T14:50:00Z | gen-backend | develop | complete | apps/service-user/ | 4 endpoints
|
|
163
|
+
2026-04-09T14:50:01Z | gen-backend | handoff | complete | →eval-functional |
|
|
164
|
+
2026-04-09T14:55:00Z | eval-func | review | start | POST /api/auth/register | AC-001~003 검증
|
|
165
|
+
2026-04-09T14:58:00Z | eval-func | review | fail | POST /api/auth/register | 409→400 contract 불일치
|
|
166
|
+
2026-04-09T14:58:01Z | eval-func | handoff | complete | →gen-backend | Re-Generate
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**라이프사이클**: 새 Planner/Dispatcher 사이클 시작 시 이전 로그는 archive로 이동, 새 로그 시작.
|
|
170
|
+
|
|
171
|
+
**에이전트 기록 의무**: 모든 에이전트는 세션 중 주요 작업의 시작/완료를 audit에 기록해야 합니다.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# 에이전트 스킬에서 호출:
|
|
175
|
+
source scripts/lib/harness-audit.sh && init_audit .
|
|
176
|
+
audit_log "gen-backend" "develop" "start" "apps/service-user/" "User CRUD 구현"
|
|
177
|
+
audit_log "gen-backend" "develop" "complete" "apps/service-user/" "4 endpoints 완료"
|
|
178
|
+
```
|
|
148
179
|
|
|
149
180
|
### 실행 방법
|
|
150
181
|
|
|
@@ -337,6 +337,23 @@
|
|
|
337
337
|
"progress_state": ".harness/progress.json",
|
|
338
338
|
"progress_log": ".harness/progress.log"
|
|
339
339
|
},
|
|
340
|
+
"audit": {
|
|
341
|
+
"comment": "Audit log — 1 sprint cycle 단위의 실행 추적. Planner/Dispatcher 시작 시 리셋.",
|
|
342
|
+
"file": ".harness/actions/audit.log",
|
|
343
|
+
"lifecycle": "planner_to_eval_pass",
|
|
344
|
+
"reset_on": ["planner", "dispatcher"],
|
|
345
|
+
"archive_previous": true,
|
|
346
|
+
"format": "TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL",
|
|
347
|
+
"actions": {
|
|
348
|
+
"develop": "코드 생성/수정 (Generator)",
|
|
349
|
+
"review": "검수/평가 (Evaluator)",
|
|
350
|
+
"handoff": "에이전트 전환",
|
|
351
|
+
"plan": "설계/계획 (Planner)",
|
|
352
|
+
"gate": "자동 게이트 체크 (system)",
|
|
353
|
+
"escalate": "에스컬레이션 (system)"
|
|
354
|
+
},
|
|
355
|
+
"statuses": ["start", "complete", "fail", "pass", "skip", "warn"]
|
|
356
|
+
},
|
|
340
357
|
"conventions": {
|
|
341
358
|
"comment": "CONVENTIONS.md — 사용자가 자유 형식으로 작성하는 프로젝트 컨벤션. 모든 에이전트가 세션 시작 시 직접 읽음. Gotcha/AGENTS.md에 복사하지 않음.",
|
|
342
359
|
"file": "CONVENTIONS.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-dashboard.sh — Panel 1: 실시간 대시보드
|
|
3
|
+
# progress.json + feature-list.json 을 2초 간격으로 시각화
|
|
4
|
+
# Usage: bash scripts/harness-dashboard.sh [project-root]
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
10
|
+
|
|
11
|
+
PROJECT_ROOT="${1:-}"
|
|
12
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
14
|
+
echo "[dashboard] .harness/ not found. Pass project root as argument."
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
20
|
+
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
21
|
+
|
|
22
|
+
# ── ANSI helpers ──
|
|
23
|
+
BOLD="\033[1m"
|
|
24
|
+
DIM="\033[2m"
|
|
25
|
+
GREEN="\033[32m"
|
|
26
|
+
YELLOW="\033[33m"
|
|
27
|
+
RED="\033[31m"
|
|
28
|
+
CYAN="\033[36m"
|
|
29
|
+
RESET="\033[0m"
|
|
30
|
+
|
|
31
|
+
render_header() {
|
|
32
|
+
local now
|
|
33
|
+
now=$(date +"%H:%M:%S")
|
|
34
|
+
local project_name
|
|
35
|
+
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
38
|
+
echo -e "${BOLD}║ HARNESS DASHBOARD ║${RESET}"
|
|
39
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
40
|
+
echo -e " ${DIM}${project_name} | Updated: ${now}${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render_sprint_overview() {
|
|
45
|
+
if [ ! -f "$PROGRESS" ]; then
|
|
46
|
+
echo -e " ${DIM}(waiting for harness init...)${RESET}"
|
|
47
|
+
return
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
local pipeline sprint_num sprint_status current_agent agent_status retry_count
|
|
51
|
+
pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS")
|
|
52
|
+
sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
|
|
53
|
+
sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS")
|
|
54
|
+
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
|
|
55
|
+
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
|
|
56
|
+
retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
|
|
57
|
+
|
|
58
|
+
# Status color
|
|
59
|
+
local status_color="$RESET"
|
|
60
|
+
case "$agent_status" in
|
|
61
|
+
running) status_color="$GREEN" ;;
|
|
62
|
+
completed) status_color="$CYAN" ;;
|
|
63
|
+
failed) status_color="$RED" ;;
|
|
64
|
+
blocked) status_color="$RED" ;;
|
|
65
|
+
*) status_color="$YELLOW" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
echo -e " ${BOLD}Pipeline${RESET} ${pipeline}"
|
|
69
|
+
echo -e " ${BOLD}Sprint${RESET} ${sprint_num} (${sprint_status})"
|
|
70
|
+
echo -e " ${BOLD}Agent${RESET} ${status_color}${current_agent} [${agent_status}]${RESET}"
|
|
71
|
+
if [ "$retry_count" -gt 0 ]; then
|
|
72
|
+
echo -e " ${BOLD}Retries${RESET} ${RED}${retry_count}${RESET}"
|
|
73
|
+
fi
|
|
74
|
+
echo ""
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render_sprint_map() {
|
|
78
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
79
|
+
|
|
80
|
+
local sprint_keys
|
|
81
|
+
sprint_keys=$(jq -r '.sprint_progress // {} | keys[]' "$PROGRESS" 2>/dev/null | sort -n)
|
|
82
|
+
if [ -z "$sprint_keys" ]; then return; fi
|
|
83
|
+
|
|
84
|
+
echo -e " ${BOLD}Sprint Map${RESET}"
|
|
85
|
+
|
|
86
|
+
while IFS= read -r s; do
|
|
87
|
+
local status features_csv notes
|
|
88
|
+
status=$(jq -r ".sprint_progress[\"${s}\"].status // \"pending\"" "$PROGRESS")
|
|
89
|
+
features_csv=$(jq -r ".sprint_progress[\"${s}\"].features // [] | join(\",\")" "$PROGRESS")
|
|
90
|
+
notes=$(jq -r ".sprint_progress[\"${s}\"].notes // \"\"" "$PROGRESS")
|
|
91
|
+
|
|
92
|
+
# Status icon
|
|
93
|
+
local icon="○"
|
|
94
|
+
case "$status" in
|
|
95
|
+
completed) icon="●" ;;
|
|
96
|
+
in_progress) icon="◐" ;;
|
|
97
|
+
scaffolded) icon="◑" ;;
|
|
98
|
+
pending) icon="○" ;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
# Count features
|
|
102
|
+
local feat_count
|
|
103
|
+
feat_count=$(jq ".sprint_progress[\"${s}\"].features // [] | length" "$PROGRESS")
|
|
104
|
+
|
|
105
|
+
# Truncate notes
|
|
106
|
+
if [ ${#notes} -gt 40 ]; then
|
|
107
|
+
notes="${notes:0:38}.."
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
printf " %s S%-2s %2d feat %-12s %s\n" "$icon" "$s" "$feat_count" "$status" "$notes"
|
|
111
|
+
done <<< "$sprint_keys"
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
render_build_status() {
|
|
117
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
118
|
+
|
|
119
|
+
local build_status tsc_status routes_total last_build
|
|
120
|
+
build_status=$(jq -r '.build.build_status // "unknown"' "$PROGRESS" 2>/dev/null)
|
|
121
|
+
tsc_status=$(jq -r '.build.tsc_status // "unknown"' "$PROGRESS" 2>/dev/null)
|
|
122
|
+
routes_total=$(jq -r '.build.routes_total // 0' "$PROGRESS" 2>/dev/null)
|
|
123
|
+
last_build=$(jq -r '.build.last_build // "n/a"' "$PROGRESS" 2>/dev/null)
|
|
124
|
+
|
|
125
|
+
if [ "$build_status" = "null" ] || [ "$build_status" = "unknown" ]; then return; fi
|
|
126
|
+
|
|
127
|
+
local build_color="$GREEN"
|
|
128
|
+
if [ "$build_status" != "passing" ]; then build_color="$RED"; fi
|
|
129
|
+
local tsc_color="$GREEN"
|
|
130
|
+
if [ "$tsc_status" != "clean" ]; then tsc_color="$RED"; fi
|
|
131
|
+
|
|
132
|
+
echo -e " ${BOLD}Build${RESET}"
|
|
133
|
+
echo -e " Build: ${build_color}${build_status}${RESET} | TSC: ${tsc_color}${tsc_status}${RESET} | Routes: ${routes_total}"
|
|
134
|
+
echo ""
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
render_failure_info() {
|
|
138
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
139
|
+
|
|
140
|
+
local failure_agent
|
|
141
|
+
failure_agent=$(jq -r '.failure.agent // empty' "$PROGRESS" 2>/dev/null)
|
|
142
|
+
|
|
143
|
+
if [ -n "$failure_agent" ] && [ "$failure_agent" != "null" ]; then
|
|
144
|
+
local failure_loc failure_msg
|
|
145
|
+
failure_loc=$(jq -r '.failure.location // ""' "$PROGRESS")
|
|
146
|
+
failure_msg=$(jq -r '.failure.message // ""' "$PROGRESS")
|
|
147
|
+
|
|
148
|
+
echo -e " ${RED}${BOLD}╔═ FAILURE ═══════════════════════════╗${RESET}"
|
|
149
|
+
echo -e " ${RED} Agent: ${failure_agent}${RESET}"
|
|
150
|
+
echo -e " ${RED} Where: ${failure_loc}${RESET}"
|
|
151
|
+
if [ -n "$failure_msg" ] && [ "$failure_msg" != "null" ]; then
|
|
152
|
+
echo -e " ${RED} ${failure_msg}${RESET}"
|
|
153
|
+
fi
|
|
154
|
+
echo -e " ${RED}${BOLD}╚════════════════════════════════════╝${RESET}"
|
|
155
|
+
echo ""
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ── Main Loop ──
|
|
160
|
+
while true; do
|
|
161
|
+
clear
|
|
162
|
+
render_header
|
|
163
|
+
render_sprint_overview
|
|
164
|
+
render_build_status
|
|
165
|
+
render_failure_info
|
|
166
|
+
render_sprint_map
|
|
167
|
+
|
|
168
|
+
# Feature progress (reuse existing lib)
|
|
169
|
+
if [ -f "$FEATURES" ]; then
|
|
170
|
+
render_progress "$PROJECT_ROOT" 2>/dev/null
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Agent sequence bar
|
|
174
|
+
render_agent_bar "$PROJECT_ROOT" 2>/dev/null
|
|
175
|
+
echo ""
|
|
176
|
+
|
|
177
|
+
echo -e " ${DIM}Refreshing every 2s | Ctrl+C to exit${RESET}"
|
|
178
|
+
sleep 2
|
|
179
|
+
done
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-eval-watcher.sh — Panel 3: Evaluation 결과 Summary 리뷰
|
|
3
|
+
# evaluation-*.md 파일 변경 감지 → 요약 출력 (또는 claude -p 호출)
|
|
4
|
+
# Usage: bash scripts/harness-eval-watcher.sh [project-root] [--ai]
|
|
5
|
+
# --ai evaluation 완료 시 claude -p 로 AI 요약 생성 (API 비용 발생)
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
|
|
11
|
+
PROJECT_ROOT="${1:-}"
|
|
12
|
+
USE_AI=false
|
|
13
|
+
for arg in "$@"; do
|
|
14
|
+
if [ "$arg" = "--ai" ]; then USE_AI=true; fi
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
if [ -z "$PROJECT_ROOT" ] || [ "$PROJECT_ROOT" = "--ai" ]; then
|
|
18
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
19
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
20
|
+
echo "[eval-watcher] .harness/ not found. Pass project root as argument."
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
26
|
+
EVAL_FUNC="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
|
|
27
|
+
EVAL_VISUAL="$PROJECT_ROOT/.harness/actions/evaluation-visual.md"
|
|
28
|
+
|
|
29
|
+
# ── ANSI helpers ──
|
|
30
|
+
BOLD="\033[1m"
|
|
31
|
+
DIM="\033[2m"
|
|
32
|
+
GREEN="\033[32m"
|
|
33
|
+
YELLOW="\033[33m"
|
|
34
|
+
RED="\033[31m"
|
|
35
|
+
CYAN="\033[36m"
|
|
36
|
+
RESET="\033[0m"
|
|
37
|
+
|
|
38
|
+
LAST_FUNC_HASH=""
|
|
39
|
+
LAST_VISUAL_HASH=""
|
|
40
|
+
EVAL_COUNT=0
|
|
41
|
+
|
|
42
|
+
print_header() {
|
|
43
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
44
|
+
echo -e "${BOLD}║ EVALUATION REVIEW ║${RESET}"
|
|
45
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
46
|
+
echo ""
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
file_hash() {
|
|
50
|
+
if [ -f "$1" ]; then
|
|
51
|
+
md5 -q "$1" 2>/dev/null || md5sum "$1" 2>/dev/null | awk '{print $1}' || echo ""
|
|
52
|
+
else
|
|
53
|
+
echo ""
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── Extract eval summary from markdown ──
|
|
58
|
+
extract_eval_summary() {
|
|
59
|
+
local file="$1" type="$2"
|
|
60
|
+
|
|
61
|
+
if [ ! -f "$file" ]; then return; fi
|
|
62
|
+
|
|
63
|
+
EVAL_COUNT=$((EVAL_COUNT + 1))
|
|
64
|
+
local now
|
|
65
|
+
now=$(date +"%H:%M:%S")
|
|
66
|
+
|
|
67
|
+
echo -e " ${BOLD}═══ ${type} Evaluation #${EVAL_COUNT} ═══${RESET} ${DIM}(${now})${RESET}"
|
|
68
|
+
echo ""
|
|
69
|
+
|
|
70
|
+
# Extract Verdict (PASS/FAIL)
|
|
71
|
+
local verdict
|
|
72
|
+
verdict=$(grep -i "verdict\|result\|판정" "$file" | head -3)
|
|
73
|
+
if [ -n "$verdict" ]; then
|
|
74
|
+
if echo "$verdict" | grep -qi "PASS"; then
|
|
75
|
+
echo -e " ${GREEN}${BOLD}VERDICT: PASS${RESET}"
|
|
76
|
+
elif echo "$verdict" | grep -qi "FAIL"; then
|
|
77
|
+
echo -e " ${RED}${BOLD}VERDICT: FAIL${RESET}"
|
|
78
|
+
else
|
|
79
|
+
echo -e " ${YELLOW}VERDICT: ${verdict}${RESET}"
|
|
80
|
+
fi
|
|
81
|
+
echo ""
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Extract Score
|
|
85
|
+
local score_line
|
|
86
|
+
score_line=$(grep -iE "score|점수|weighted|가중" "$file" | head -3)
|
|
87
|
+
if [ -n "$score_line" ]; then
|
|
88
|
+
echo -e " ${BOLD}Score${RESET}"
|
|
89
|
+
echo "$score_line" | while IFS= read -r line; do
|
|
90
|
+
# Color by score value
|
|
91
|
+
local num
|
|
92
|
+
num=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+' | head -1)
|
|
93
|
+
if [ -n "$num" ]; then
|
|
94
|
+
local int_part
|
|
95
|
+
int_part=$(echo "$num" | cut -d. -f1)
|
|
96
|
+
if [ "${int_part:-0}" -ge 3 ] || ([ "${int_part:-0}" -eq 2 ] && [ "$(echo "$num >= 2.80" | bc 2>/dev/null)" = "1" ]); then
|
|
97
|
+
echo -e " ${GREEN}${line}${RESET}"
|
|
98
|
+
else
|
|
99
|
+
echo -e " ${RED}${line}${RESET}"
|
|
100
|
+
fi
|
|
101
|
+
else
|
|
102
|
+
echo " $line"
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
echo ""
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Extract individual rubric items (R1-R5 or V1-V5)
|
|
109
|
+
local rubric_lines
|
|
110
|
+
rubric_lines=$(grep -E "^[|#].*[RV][1-5]" "$file" | head -10)
|
|
111
|
+
if [ -n "$rubric_lines" ]; then
|
|
112
|
+
echo -e " ${BOLD}Rubric${RESET}"
|
|
113
|
+
echo "$rubric_lines" | while IFS= read -r line; do
|
|
114
|
+
echo " $line"
|
|
115
|
+
done
|
|
116
|
+
echo ""
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Extract FAIL reasons
|
|
120
|
+
local fail_lines
|
|
121
|
+
fail_lines=$(grep -iE "fail|실패|regression|불일치|위반" "$file" | head -5)
|
|
122
|
+
if [ -n "$fail_lines" ]; then
|
|
123
|
+
echo -e " ${RED}${BOLD}Issues${RESET}"
|
|
124
|
+
echo "$fail_lines" | while IFS= read -r line; do
|
|
125
|
+
echo -e " ${RED}• ${line}${RESET}"
|
|
126
|
+
done
|
|
127
|
+
echo ""
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Extract action items / recommendations
|
|
131
|
+
local action_lines
|
|
132
|
+
action_lines=$(grep -iE "recommend|action|수정|개선|필요|re-generate" "$file" | head -5)
|
|
133
|
+
if [ -n "$action_lines" ]; then
|
|
134
|
+
echo -e " ${YELLOW}${BOLD}Actions${RESET}"
|
|
135
|
+
echo "$action_lines" | while IFS= read -r line; do
|
|
136
|
+
echo -e " ${YELLOW}→ ${line}${RESET}"
|
|
137
|
+
done
|
|
138
|
+
echo ""
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo -e " ${DIM}─────────────────────────────────────${RESET}"
|
|
142
|
+
echo ""
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ── AI Summary (optional) ──
|
|
146
|
+
generate_ai_summary() {
|
|
147
|
+
local file="$1" type="$2"
|
|
148
|
+
|
|
149
|
+
if [ "$USE_AI" != true ]; then return; fi
|
|
150
|
+
if ! command -v claude &>/dev/null; then
|
|
151
|
+
echo -e " ${DIM}(claude CLI not found — skipping AI summary)${RESET}"
|
|
152
|
+
return
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo -e " ${CYAN}Generating AI summary...${RESET}"
|
|
156
|
+
|
|
157
|
+
local summary
|
|
158
|
+
summary=$(claude -p "다음 ${type} Evaluation 문서를 3줄 이내로 요약해줘. 핵심 결과(PASS/FAIL), 주요 문제점, 다음 액션을 포함:
|
|
159
|
+
|
|
160
|
+
$(cat "$file")" --output-format text 2>/dev/null || echo "(AI summary failed)")
|
|
161
|
+
|
|
162
|
+
echo -e " ${CYAN}${BOLD}AI Summary:${RESET}"
|
|
163
|
+
echo "$summary" | while IFS= read -r line; do
|
|
164
|
+
echo -e " ${CYAN}${line}${RESET}"
|
|
165
|
+
done
|
|
166
|
+
echo ""
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── Main ──
|
|
170
|
+
print_header
|
|
171
|
+
|
|
172
|
+
# Show existing evals on startup
|
|
173
|
+
if [ -f "$EVAL_FUNC" ]; then
|
|
174
|
+
echo -e " ${DIM}Found existing functional evaluation${RESET}"
|
|
175
|
+
extract_eval_summary "$EVAL_FUNC" "Functional"
|
|
176
|
+
LAST_FUNC_HASH=$(file_hash "$EVAL_FUNC")
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ -f "$EVAL_VISUAL" ]; then
|
|
180
|
+
echo -e " ${DIM}Found existing visual evaluation${RESET}"
|
|
181
|
+
extract_eval_summary "$EVAL_VISUAL" "Visual"
|
|
182
|
+
LAST_VISUAL_HASH=$(file_hash "$EVAL_VISUAL")
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [ -z "$LAST_FUNC_HASH" ] && [ -z "$LAST_VISUAL_HASH" ]; then
|
|
186
|
+
echo -e " ${DIM}Waiting for evaluation files...${RESET}"
|
|
187
|
+
echo -e " ${DIM} ${EVAL_FUNC}${RESET}"
|
|
188
|
+
echo -e " ${DIM} ${EVAL_VISUAL}${RESET}"
|
|
189
|
+
echo ""
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
echo -e " ${DIM}Watching for changes every 3s | Ctrl+C to exit${RESET}"
|
|
193
|
+
if [ "$USE_AI" = true ]; then
|
|
194
|
+
echo -e " ${CYAN}AI summary enabled (claude -p)${RESET}"
|
|
195
|
+
fi
|
|
196
|
+
echo ""
|
|
197
|
+
|
|
198
|
+
# ── Poll for changes ──
|
|
199
|
+
while true; do
|
|
200
|
+
# Check functional eval
|
|
201
|
+
local_func_hash=$(file_hash "$EVAL_FUNC")
|
|
202
|
+
if [ "$local_func_hash" != "$LAST_FUNC_HASH" ] && [ -n "$local_func_hash" ]; then
|
|
203
|
+
LAST_FUNC_HASH="$local_func_hash"
|
|
204
|
+
extract_eval_summary "$EVAL_FUNC" "Functional"
|
|
205
|
+
generate_ai_summary "$EVAL_FUNC" "Functional"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Check visual eval
|
|
209
|
+
local_visual_hash=$(file_hash "$EVAL_VISUAL")
|
|
210
|
+
if [ "$local_visual_hash" != "$LAST_VISUAL_HASH" ] && [ -n "$local_visual_hash" ]; then
|
|
211
|
+
LAST_VISUAL_HASH="$local_visual_hash"
|
|
212
|
+
extract_eval_summary "$EVAL_VISUAL" "Visual"
|
|
213
|
+
generate_ai_summary "$EVAL_VISUAL" "Visual"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Check agent status for eval-related transitions
|
|
217
|
+
if [ -f "$PROGRESS" ]; then
|
|
218
|
+
local agent
|
|
219
|
+
agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
220
|
+
local status
|
|
221
|
+
status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
222
|
+
|
|
223
|
+
if [[ "$agent" == evaluator-* ]] && [ "$status" = "running" ]; then
|
|
224
|
+
# Show that evaluator is active
|
|
225
|
+
:
|
|
226
|
+
fi
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
sleep 3
|
|
230
|
+
done
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-monitor.sh — Panel 2: Task & Agent Lifecycle 모니터링
|
|
3
|
+
# audit.log 스트리밍 + progress.json 변경 감지
|
|
4
|
+
# Usage: bash scripts/harness-monitor.sh [project-root]
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="${1:-}"
|
|
11
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
12
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || {
|
|
14
|
+
echo "[monitor] .harness/ not found. Pass project root as argument."
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
20
|
+
AUDIT_LOG="$PROJECT_ROOT/.harness/actions/audit.log"
|
|
21
|
+
HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
|
|
22
|
+
|
|
23
|
+
# ── ANSI helpers ──
|
|
24
|
+
BOLD="\033[1m"
|
|
25
|
+
DIM="\033[2m"
|
|
26
|
+
GREEN="\033[32m"
|
|
27
|
+
YELLOW="\033[33m"
|
|
28
|
+
RED="\033[31m"
|
|
29
|
+
CYAN="\033[36m"
|
|
30
|
+
MAGENTA="\033[35m"
|
|
31
|
+
RESET="\033[0m"
|
|
32
|
+
|
|
33
|
+
LAST_AGENT=""
|
|
34
|
+
LAST_STATUS=""
|
|
35
|
+
LAST_SPRINT=""
|
|
36
|
+
EVENT_COUNT=0
|
|
37
|
+
|
|
38
|
+
print_event() {
|
|
39
|
+
local ts="$1" icon="$2" color="$3" msg="$4"
|
|
40
|
+
EVENT_COUNT=$((EVENT_COUNT + 1))
|
|
41
|
+
echo -e " ${DIM}${ts}${RESET} ${color}${icon}${RESET} ${msg}"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── Header ──
|
|
45
|
+
print_header() {
|
|
46
|
+
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
47
|
+
echo -e "${BOLD}║ AGENT LIFECYCLE MONITOR ║${RESET}"
|
|
48
|
+
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
49
|
+
echo ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ── Detect agent transitions from progress.json ──
|
|
53
|
+
check_transitions() {
|
|
54
|
+
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
55
|
+
|
|
56
|
+
local agent status sprint
|
|
57
|
+
agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
58
|
+
status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
59
|
+
sprint=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
60
|
+
|
|
61
|
+
local now
|
|
62
|
+
now=$(date +"%H:%M:%S")
|
|
63
|
+
|
|
64
|
+
# Sprint change
|
|
65
|
+
if [ "$sprint" != "$LAST_SPRINT" ] && [ -n "$LAST_SPRINT" ]; then
|
|
66
|
+
print_event "$now" ">>>" "$MAGENTA" "${BOLD}Sprint ${sprint} started${RESET}"
|
|
67
|
+
echo ""
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Agent change
|
|
71
|
+
if [ "$agent" != "$LAST_AGENT" ] && [ -n "$LAST_AGENT" ]; then
|
|
72
|
+
if [ "$LAST_AGENT" != "none" ] && [ "$LAST_AGENT" != "null" ]; then
|
|
73
|
+
print_event "$now" " ✓ " "$GREEN" "${LAST_AGENT} → ${BOLD}done${RESET}"
|
|
74
|
+
fi
|
|
75
|
+
if [ "$agent" != "none" ] && [ "$agent" != "null" ]; then
|
|
76
|
+
print_event "$now" " ▶ " "$CYAN" "${BOLD}${agent}${RESET} started"
|
|
77
|
+
|
|
78
|
+
# Show handoff context if available
|
|
79
|
+
if [ -f "$HANDOFF" ]; then
|
|
80
|
+
local from focus
|
|
81
|
+
from=$(jq -r '.from // empty' "$HANDOFF" 2>/dev/null)
|
|
82
|
+
focus=$(jq -r '.focus_features // [] | join(", ")' "$HANDOFF" 2>/dev/null)
|
|
83
|
+
if [ -n "$from" ] && [ "$from" != "null" ]; then
|
|
84
|
+
echo -e " ${DIM}handoff from: ${from}${RESET}"
|
|
85
|
+
fi
|
|
86
|
+
if [ -n "$focus" ] && [ "$focus" != "null" ]; then
|
|
87
|
+
echo -e " ${DIM}focus: ${focus}${RESET}"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Status change (same agent)
|
|
94
|
+
if [ "$agent" = "$LAST_AGENT" ] && [ "$status" != "$LAST_STATUS" ] && [ -n "$LAST_STATUS" ]; then
|
|
95
|
+
local status_color="$RESET"
|
|
96
|
+
local status_icon="•"
|
|
97
|
+
case "$status" in
|
|
98
|
+
running) status_color="$GREEN"; status_icon=" ▶ " ;;
|
|
99
|
+
completed) status_color="$CYAN"; status_icon=" ✓ " ;;
|
|
100
|
+
failed) status_color="$RED"; status_icon=" ✗ " ;;
|
|
101
|
+
blocked) status_color="$RED"; status_icon=" ! " ;;
|
|
102
|
+
pending) status_color="$YELLOW"; status_icon=" … " ;;
|
|
103
|
+
esac
|
|
104
|
+
print_event "$now" "$status_icon" "$status_color" "${agent} → ${BOLD}${status}${RESET}"
|
|
105
|
+
|
|
106
|
+
# Show failure detail
|
|
107
|
+
if [ "$status" = "failed" ]; then
|
|
108
|
+
local fail_msg
|
|
109
|
+
fail_msg=$(jq -r '.failure.message // empty' "$PROGRESS" 2>/dev/null)
|
|
110
|
+
if [ -n "$fail_msg" ] && [ "$fail_msg" != "null" ]; then
|
|
111
|
+
echo -e " ${RED}${fail_msg}${RESET}"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
LAST_AGENT="$agent"
|
|
117
|
+
LAST_STATUS="$status"
|
|
118
|
+
LAST_SPRINT="$sprint"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ── Stream audit.log new lines ──
|
|
122
|
+
stream_audit() {
|
|
123
|
+
if [ ! -f "$AUDIT_LOG" ]; then return; fi
|
|
124
|
+
|
|
125
|
+
# Use tail -f in background, parse each new line
|
|
126
|
+
tail -n 0 -f "$AUDIT_LOG" 2>/dev/null | while IFS= read -r line; do
|
|
127
|
+
# Skip comments/headers
|
|
128
|
+
if [[ "$line" == "#"* ]] || [[ -z "$line" ]]; then continue; fi
|
|
129
|
+
|
|
130
|
+
# Parse: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
|
|
131
|
+
local ts agent action status target detail
|
|
132
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
133
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
134
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
135
|
+
status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
136
|
+
target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
|
137
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$6); print $6}')
|
|
138
|
+
|
|
139
|
+
# Color by status
|
|
140
|
+
local color="$RESET" icon="•"
|
|
141
|
+
case "$status" in
|
|
142
|
+
start) color="$CYAN"; icon="▶" ;;
|
|
143
|
+
complete) color="$GREEN"; icon="✓" ;;
|
|
144
|
+
fail) color="$RED"; icon="✗" ;;
|
|
145
|
+
pass) color="$GREEN"; icon="✓" ;;
|
|
146
|
+
skip) color="$DIM"; icon="–" ;;
|
|
147
|
+
esac
|
|
148
|
+
|
|
149
|
+
# Short timestamp (HH:MM:SS from ISO)
|
|
150
|
+
local short_ts
|
|
151
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}:[0-9]{2}' || echo "$ts")
|
|
152
|
+
|
|
153
|
+
echo -e " ${DIM}${short_ts}${RESET} ${color}${icon}${RESET} ${BOLD}${agent}${RESET} ${action} ${target} ${DIM}${detail}${RESET}"
|
|
154
|
+
done &
|
|
155
|
+
AUDIT_TAIL_PID=$!
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ── Cleanup ──
|
|
159
|
+
cleanup() {
|
|
160
|
+
if [ -n "${AUDIT_TAIL_PID:-}" ]; then
|
|
161
|
+
kill "$AUDIT_TAIL_PID" 2>/dev/null || true
|
|
162
|
+
fi
|
|
163
|
+
exit 0
|
|
164
|
+
}
|
|
165
|
+
trap cleanup EXIT INT TERM
|
|
166
|
+
|
|
167
|
+
# ── Main ──
|
|
168
|
+
print_header
|
|
169
|
+
|
|
170
|
+
# Initialize state
|
|
171
|
+
if [ -f "$PROGRESS" ]; then
|
|
172
|
+
LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
173
|
+
LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
174
|
+
LAST_SPRINT=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Show existing history
|
|
178
|
+
if [ -f "$PROGRESS" ]; then
|
|
179
|
+
echo -e " ${BOLD}History${RESET}"
|
|
180
|
+
jq -r '.history // [] | .[] | " \(.timestamp) \(.agent) — \(.action): \(.detail)"' "$PROGRESS" 2>/dev/null
|
|
181
|
+
echo ""
|
|
182
|
+
echo -e " ${DIM}── Live events below ──${RESET}"
|
|
183
|
+
echo ""
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Start audit log streaming
|
|
187
|
+
stream_audit
|
|
188
|
+
|
|
189
|
+
# Poll progress.json for transitions
|
|
190
|
+
while true; do
|
|
191
|
+
check_transitions
|
|
192
|
+
sleep 2
|
|
193
|
+
done
|
package/scripts/harness-next.sh
CHANGED
|
@@ -7,6 +7,7 @@ set -e
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
8
|
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
9
9
|
source "$SCRIPT_DIR/lib/harness-guardrail.sh"
|
|
10
|
+
source "$SCRIPT_DIR/lib/harness-audit.sh"
|
|
10
11
|
|
|
11
12
|
# ─────────────────────────────────────────
|
|
12
13
|
# Resolve project root
|
|
@@ -24,6 +25,9 @@ HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
|
|
|
24
25
|
|
|
25
26
|
check_jq || exit 1
|
|
26
27
|
|
|
28
|
+
# Initialize audit log
|
|
29
|
+
init_audit "$PROJECT_ROOT"
|
|
30
|
+
|
|
27
31
|
if [ ! -f "$PROGRESS" ]; then
|
|
28
32
|
echo "[harness] ERROR: progress.json not found" >&2
|
|
29
33
|
echo " Run 'npx walwal-harness --force' to reinitialize." >&2
|
|
@@ -277,15 +281,25 @@ verify_artifact_prerequisites() {
|
|
|
277
281
|
# Runtime Guardrail — 파일 소유권 검증
|
|
278
282
|
# ─────────────────────────────────────────
|
|
279
283
|
if [ "$agent_status" = "completed" ]; then
|
|
280
|
-
|
|
284
|
+
audit_log "$current_agent" "develop" "complete" "" "agent completed"
|
|
285
|
+
verify_file_ownership "$PROJECT_ROOT" || audit_gate "file-ownership" "warn" "boundary violation detected"
|
|
281
286
|
fi
|
|
282
287
|
|
|
283
288
|
# ─────────────────────────────────────────
|
|
284
289
|
# Run Pre-Eval Gate (if applicable)
|
|
285
290
|
# ─────────────────────────────────────────
|
|
286
291
|
if [ "$agent_status" = "completed" ]; then
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
audit_gate "pre-eval" "start" "$next_agent"
|
|
293
|
+
if run_pre_eval_gate "$next_agent"; then
|
|
294
|
+
audit_gate "pre-eval" "pass" "$next_agent"
|
|
295
|
+
else
|
|
296
|
+
audit_gate "pre-eval" "fail" "rerouted to $current_agent"
|
|
297
|
+
fi
|
|
298
|
+
if verify_artifact_prerequisites "$next_agent"; then
|
|
299
|
+
audit_gate "artifact-prereq" "pass" "$next_agent"
|
|
300
|
+
else
|
|
301
|
+
audit_gate "artifact-prereq" "fail" "$next_agent"
|
|
302
|
+
fi
|
|
289
303
|
fi
|
|
290
304
|
|
|
291
305
|
# ─────────────────────────────────────────
|
|
@@ -303,6 +317,7 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
|
|
|
303
317
|
escalate_after=$(jq -r '.flow.escalate_to_planner_after // 3' "$CONFIG" 2>/dev/null || echo 3)
|
|
304
318
|
if [ "$retry_count" -ge "$escalate_after" ] && [ "$sprint_status" = "failed" ] && [ "$next_agent" != "planner" ]; then
|
|
305
319
|
echo " ⚠ Escalation: ${retry_count}회 실패 — Planner에게 scope 축소/접근 변경 요청"
|
|
320
|
+
audit_log "system" "escalate" "start" "→planner" "retry ${retry_count}회 초과"
|
|
306
321
|
next_agent="planner"
|
|
307
322
|
jq --arg msg "Escalated after ${retry_count} failures. Planner must review scope or approach." \
|
|
308
323
|
'.next_agent = "planner" |
|
|
@@ -442,7 +457,10 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
|
|
|
442
457
|
timestamp: $timestamp
|
|
443
458
|
}' > "$HANDOFF"
|
|
444
459
|
|
|
460
|
+
audit_handoff "${current_agent:-dispatcher}" "$next_agent" "complete"
|
|
461
|
+
|
|
445
462
|
elif [ "$next_agent" = "archive" ]; then
|
|
463
|
+
audit_log "system" "archive" "start" "sprint-${sprint_num}" "sprint cycle complete"
|
|
446
464
|
jq -n \
|
|
447
465
|
--arg from "${current_agent:-evaluator}" \
|
|
448
466
|
--argjson sprint "$sprint_num" \
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# harness-session-start.sh — SessionStart 훅
|
|
3
3
|
# 새 세션 시작 시 자동으로:
|
|
4
|
-
# 1) 이전 에이전트가 completed이면 harness-next.sh
|
|
5
|
-
# 2)
|
|
6
|
-
# 3)
|
|
7
|
-
#
|
|
8
|
-
# 사용자가 수동으로 harness-next.sh를 실행할 필요가 없도록 자동화.
|
|
4
|
+
# 1) 이전 에이전트가 completed이면 harness-next.sh 실행 (게이트 + handoff)
|
|
5
|
+
# 2) Planner/Dispatcher 사이클이면 audit 리셋
|
|
6
|
+
# 3) 다음 에이전트 안내 출력
|
|
9
7
|
|
|
10
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
9
|
LIB="$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
10
|
+
AUDIT_LIB="$SCRIPT_DIR/lib/harness-audit.sh"
|
|
12
11
|
|
|
13
12
|
if [ ! -f "$LIB" ]; then exit 0; fi
|
|
14
13
|
source "$LIB"
|
|
14
|
+
[ -f "$AUDIT_LIB" ] && source "$AUDIT_LIB"
|
|
15
15
|
command -v jq &>/dev/null || exit 0
|
|
16
16
|
|
|
17
17
|
PROJECT_ROOT="$(resolve_harness_root "." 2>/dev/null)" || exit 0
|
|
@@ -21,6 +21,7 @@ HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
|
|
|
21
21
|
[ -f "$PROGRESS" ] || exit 0
|
|
22
22
|
|
|
23
23
|
sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
|
|
24
|
+
sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
24
25
|
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
25
26
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
26
27
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
@@ -34,14 +35,21 @@ if [ "$sprint_status" = "init" ]; then
|
|
|
34
35
|
fi
|
|
35
36
|
|
|
36
37
|
# ─────────────────────────────────────────
|
|
37
|
-
#
|
|
38
|
-
#
|
|
38
|
+
# Audit lifecycle: Planner/Dispatcher 시작 시 리셋
|
|
39
|
+
# ─────────────────────────────────────────
|
|
40
|
+
init_audit "$PROJECT_ROOT"
|
|
41
|
+
if [ "$next_agent" = "planner" ] || [ "$next_agent" = "dispatcher" ]; then
|
|
42
|
+
# 새 사이클 — 이전 audit을 archive로 이동하고 새로 시작
|
|
43
|
+
reset_audit "$PROJECT_ROOT" "$sprint_num"
|
|
44
|
+
audit_log "system" "cycle" "start" "sprint-${sprint_num}" "new plan/dispatch cycle"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# ─────────────────────────────────────────
|
|
48
|
+
# 이전 에이전트가 completed/failed → 자동 전환
|
|
39
49
|
# ─────────────────────────────────────────
|
|
40
50
|
if [ "$agent_status" = "completed" ] || [ "$agent_status" = "failed" ]; then
|
|
41
|
-
# harness-next.sh를 백그라운드로 실행하면 안 됨 — 결과가 필요
|
|
42
51
|
bash "$SCRIPT_DIR/harness-next.sh" "$PROJECT_ROOT" 2>/dev/null
|
|
43
52
|
|
|
44
|
-
# harness-next.sh가 progress.json과 handoff.json을 업데이트했으므로 다시 읽기
|
|
45
53
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
46
54
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
47
55
|
fi
|
|
@@ -53,7 +61,6 @@ if [ "$agent_status" = "blocked" ]; then
|
|
|
53
61
|
echo "# Harness BLOCKED — retry limit reached, user intervention required"
|
|
54
62
|
|
|
55
63
|
elif [ -f "$HANDOFF" ] && [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ]; then
|
|
56
|
-
# handoff.json에서 모델/모드 정보 읽기
|
|
57
64
|
handoff_model=$(jq -r '.model // "opus"' "$HANDOFF" 2>/dev/null)
|
|
58
65
|
handoff_thinking=$(jq -r '.thinking_mode // empty' "$HANDOFF" 2>/dev/null)
|
|
59
66
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-tmux.sh — tmux 4-pane 레이아웃 자동 구성
|
|
3
|
+
#
|
|
4
|
+
# ┌──────────────────────┬──────────────────────┐
|
|
5
|
+
# │ │ Panel 1: Dashboard │
|
|
6
|
+
# │ Main Terminal ├──────────────────────┤
|
|
7
|
+
# │ (claude 대화형) │ Panel 2: Monitor │
|
|
8
|
+
# │ ├──────────────────────┤
|
|
9
|
+
# │ │ Panel 3: Eval Review │
|
|
10
|
+
# └──────────────────────┴──────────────────────┘
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# bash scripts/harness-tmux.sh [project-root]
|
|
14
|
+
# bash scripts/harness-tmux.sh /path/to/project --ai (AI eval summary 활성화)
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
# --ai Panel 3에서 claude -p 로 AI 요약 생성 (API 비용 발생)
|
|
18
|
+
# --detach 세션 생성 후 attach 하지 않음
|
|
19
|
+
# --kill 기존 harness-studio 세션 종료
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
SESSION_NAME="harness-studio"
|
|
25
|
+
|
|
26
|
+
PROJECT_ROOT=""
|
|
27
|
+
USE_AI=""
|
|
28
|
+
DETACH=false
|
|
29
|
+
KILL=false
|
|
30
|
+
|
|
31
|
+
for arg in "$@"; do
|
|
32
|
+
case "$arg" in
|
|
33
|
+
--ai) USE_AI="--ai" ;;
|
|
34
|
+
--detach) DETACH=true ;;
|
|
35
|
+
--kill)
|
|
36
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Session '$SESSION_NAME' killed." || echo "No session to kill."
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# Auto-detect project root
|
|
46
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
47
|
+
dir="$(pwd)"
|
|
48
|
+
while [ "$dir" != "/" ]; do
|
|
49
|
+
if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
|
|
50
|
+
dir="$(dirname "$dir")"
|
|
51
|
+
done
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
55
|
+
echo "Error: .harness/ directory not found."
|
|
56
|
+
echo "Usage: bash scripts/harness-tmux.sh [project-root]"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "Project: $PROJECT_ROOT"
|
|
61
|
+
echo "Session: $SESSION_NAME"
|
|
62
|
+
|
|
63
|
+
# Kill existing session if any
|
|
64
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
65
|
+
|
|
66
|
+
# ── Create tmux session ──
|
|
67
|
+
# Pane 0 (left, Main): interactive shell at project root
|
|
68
|
+
tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50
|
|
69
|
+
|
|
70
|
+
# Split right column (40% width)
|
|
71
|
+
tmux split-window -h -l 80 -t "$SESSION_NAME" -c "$PROJECT_ROOT"
|
|
72
|
+
|
|
73
|
+
# Pane 1 (top-right): Dashboard
|
|
74
|
+
tmux send-keys -t "${SESSION_NAME}.1" "bash ${SCRIPT_DIR}/harness-dashboard.sh '${PROJECT_ROOT}'" Enter
|
|
75
|
+
|
|
76
|
+
# Split pane 1 vertically for Monitor
|
|
77
|
+
tmux split-window -v -t "${SESSION_NAME}.1" -c "$PROJECT_ROOT"
|
|
78
|
+
|
|
79
|
+
# Pane 2 (mid-right): Monitor
|
|
80
|
+
tmux send-keys -t "${SESSION_NAME}.2" "bash ${SCRIPT_DIR}/harness-monitor.sh '${PROJECT_ROOT}'" Enter
|
|
81
|
+
|
|
82
|
+
# Split pane 2 vertically for Eval
|
|
83
|
+
tmux split-window -v -t "${SESSION_NAME}.2" -c "$PROJECT_ROOT"
|
|
84
|
+
|
|
85
|
+
# Pane 3 (bottom-right): Eval Watcher
|
|
86
|
+
tmux send-keys -t "${SESSION_NAME}.3" "bash ${SCRIPT_DIR}/harness-eval-watcher.sh '${PROJECT_ROOT}' ${USE_AI}" Enter
|
|
87
|
+
|
|
88
|
+
# ── Layout adjustments ──
|
|
89
|
+
# Select left pane (Main) — this is where the user works
|
|
90
|
+
tmux select-pane -t "${SESSION_NAME}.0"
|
|
91
|
+
|
|
92
|
+
# Set pane titles (requires tmux >= 3.2 with pane-border-format)
|
|
93
|
+
tmux select-pane -t "${SESSION_NAME}.0" -T "Main"
|
|
94
|
+
tmux select-pane -t "${SESSION_NAME}.1" -T "Dashboard"
|
|
95
|
+
tmux select-pane -t "${SESSION_NAME}.2" -T "Monitor"
|
|
96
|
+
tmux select-pane -t "${SESSION_NAME}.3" -T "Eval Review"
|
|
97
|
+
|
|
98
|
+
# Enable pane border titles
|
|
99
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
100
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
101
|
+
|
|
102
|
+
# ── Attach or print info ──
|
|
103
|
+
if [ "$DETACH" = true ]; then
|
|
104
|
+
echo ""
|
|
105
|
+
echo "Session created. Attach with:"
|
|
106
|
+
echo " tmux attach -t $SESSION_NAME"
|
|
107
|
+
else
|
|
108
|
+
# If already in tmux, switch; otherwise attach
|
|
109
|
+
if [ -n "${TMUX:-}" ]; then
|
|
110
|
+
tmux switch-client -t "$SESSION_NAME"
|
|
111
|
+
else
|
|
112
|
+
echo ""
|
|
113
|
+
echo "Launching harness-studio..."
|
|
114
|
+
echo " Main (left) : Interactive terminal — run 'claude' here"
|
|
115
|
+
echo " Dashboard (right↑) : Real-time progress"
|
|
116
|
+
echo " Monitor (right↔) : Agent lifecycle events"
|
|
117
|
+
echo " Eval (right↓) : Evaluation summaries"
|
|
118
|
+
echo ""
|
|
119
|
+
tmux attach -t "$SESSION_NAME"
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
@@ -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
|
+
}
|