@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.20
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/README.md +226 -273
- package/assets/templates/config.json +1 -48
- package/assets/templates/gitignore-append.txt +1 -0
- package/bin/init.js +1 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-v4.sh +58 -81
- package/scripts/harness-next.sh +4 -15
- package/scripts/harness-prompts-v4.sh +106 -0
- package/scripts/harness-queue-manager.sh +59 -5
- package/scripts/harness-session-start.sh +18 -0
- package/scripts/harness-studio-v4.sh +69 -69
- package/scripts/harness-team-worker.sh +136 -123
- package/scripts/harness-user-prompt-submit.sh +31 -1
- package/skills/dispatcher/SKILL.md +7 -2
- package/skills/evaluator-functional-flutter/SKILL.md +0 -206
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
- package/skills/generator-frontend-flutter/SKILL.md +0 -173
- package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
|
@@ -133,62 +133,15 @@
|
|
|
133
133
|
]
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
|
-
"generator-frontend-flutter": {
|
|
137
|
-
"role": "Flutter 앱 개발 — Riverpod, integrated_data_layer(Retrofit), i18n(ARB), build_runner",
|
|
138
|
-
"skill": "harness-generator-frontend-flutter",
|
|
139
|
-
"inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
|
|
140
|
-
"outputs": ["code:flutter", "actions/sprint-contract.md"],
|
|
141
|
-
"order": 2,
|
|
142
|
-
"fe_stack": "flutter",
|
|
143
|
-
"model": "sonnet",
|
|
144
|
-
"thinking_mode": null
|
|
145
|
-
},
|
|
146
|
-
"evaluator-functional-flutter": {
|
|
147
|
-
"role": "Flutter 앱 검증 — flutter analyze, flutter test, build_runner 일관성, 안티패턴 정적 검증",
|
|
148
|
-
"skill": "harness-evaluator-functional-flutter",
|
|
149
|
-
"tools": ["bash:flutter", "bash:dart"],
|
|
150
|
-
"inputs": ["actions/sprint-contract.md"],
|
|
151
|
-
"outputs": ["actions/evaluation-functional.md"],
|
|
152
|
-
"fe_stack": "flutter",
|
|
153
|
-
"model": "opus",
|
|
154
|
-
"thinking_mode": "ultrathink"
|
|
155
|
-
}
|
|
156
136
|
},
|
|
157
137
|
"flow": {
|
|
158
138
|
"sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
159
139
|
"pipeline_selection": {
|
|
160
|
-
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.
|
|
140
|
+
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.",
|
|
161
141
|
"pipelines": {
|
|
162
142
|
"FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
163
143
|
"FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
164
144
|
"BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
|
|
165
|
-
},
|
|
166
|
-
"fe_stack_substitution": {
|
|
167
|
-
"comment": "pipeline.json.fe_stack + fe_target 에 따라 FE 에이전트 치환. Flutter Web 은 React 와 동일한 Playwright 기반 evaluator 를 사용한다.",
|
|
168
|
-
"flutter": {
|
|
169
|
-
"_doc": "fe_target = web | mobile | desktop. by_target 으로 분기.",
|
|
170
|
-
"by_target": {
|
|
171
|
-
"web": {
|
|
172
|
-
"_doc": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 Playwright evaluator 사용 가능",
|
|
173
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
174
|
-
"evaluator-functional": "evaluator-functional",
|
|
175
|
-
"evaluator-visual": "evaluator-visual"
|
|
176
|
-
},
|
|
177
|
-
"mobile": {
|
|
178
|
-
"_doc": "Flutter Mobile (Android/iOS) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
179
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
180
|
-
"evaluator-functional": "evaluator-functional-flutter",
|
|
181
|
-
"evaluator-visual": "__skip__"
|
|
182
|
-
},
|
|
183
|
-
"desktop": {
|
|
184
|
-
"_doc": "Flutter Desktop (macOS/Windows/Linux) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
185
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
186
|
-
"evaluator-functional": "evaluator-functional-flutter",
|
|
187
|
-
"evaluator-visual": "__skip__"
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
"_default_target": "mobile"
|
|
191
|
-
}
|
|
192
145
|
}
|
|
193
146
|
},
|
|
194
147
|
"sprint_execution": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# For consumer projects, add .worktrees/ to .gitignore
|
package/bin/init.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.20",
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-dashboard-v4.sh — v4 Dashboard
|
|
3
|
-
#
|
|
2
|
+
# harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
|
|
3
|
+
# Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
|
|
4
4
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
@@ -30,39 +30,34 @@ render_header() {
|
|
|
30
30
|
now=$(date +"%H:%M:%S")
|
|
31
31
|
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
32
32
|
|
|
33
|
-
echo -e "${BOLD}
|
|
34
|
-
echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
|
|
35
|
-
echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
|
|
36
|
-
echo -e " ${DIM}${project_name} | ${now}${RESET}"
|
|
37
|
-
echo ""
|
|
33
|
+
echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
render_queue_summary() {
|
|
41
37
|
if [ ! -f "$QUEUE" ]; then
|
|
42
|
-
echo -e " ${DIM}(queue not initialized
|
|
38
|
+
echo -e " ${DIM}(queue not initialized)${RESET}"
|
|
43
39
|
return
|
|
44
40
|
fi
|
|
45
41
|
|
|
46
|
-
local ready blocked in_prog passed failed total
|
|
47
|
-
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null)
|
|
48
|
-
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null)
|
|
49
|
-
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
|
|
50
|
-
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null)
|
|
51
|
-
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null)
|
|
52
|
-
|
|
42
|
+
local ready blocked in_prog passed failed total
|
|
43
|
+
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
44
|
+
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
45
|
+
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
46
|
+
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
47
|
+
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
48
|
+
ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
|
|
53
49
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
54
50
|
|
|
55
|
-
# Progress bar
|
|
56
51
|
local pct=0
|
|
57
52
|
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
58
|
-
local bar_w=
|
|
53
|
+
local bar_w=16
|
|
54
|
+
local filled=$(( pct * bar_w / 100 ))
|
|
55
|
+
local empty=$(( bar_w - filled ))
|
|
59
56
|
local bar=""
|
|
60
57
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
61
58
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
62
59
|
|
|
63
|
-
echo -e " ${
|
|
64
|
-
echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
|
|
65
|
-
echo ""
|
|
60
|
+
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}"
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
render_teams() {
|
|
@@ -72,105 +67,87 @@ render_teams() {
|
|
|
72
67
|
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
|
|
73
68
|
if [ "${team_count:-0}" -eq 0 ]; then return; fi
|
|
74
69
|
|
|
75
|
-
echo -e " ${BOLD}Teams${RESET}"
|
|
76
|
-
|
|
77
70
|
for i in $(seq 1 "$team_count"); do
|
|
78
71
|
local t_status t_feature t_phase t_attempt
|
|
79
72
|
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
80
73
|
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
81
74
|
|
|
82
|
-
# Get phase from in_progress
|
|
83
75
|
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
84
76
|
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
85
77
|
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
86
78
|
else
|
|
87
|
-
t_phase="—"
|
|
88
|
-
t_attempt="—"
|
|
79
|
+
t_phase="—"; t_attempt=""
|
|
89
80
|
fi
|
|
90
81
|
|
|
91
82
|
local icon color
|
|
92
83
|
case "$t_status" in
|
|
93
|
-
busy) icon="▶"
|
|
94
|
-
idle) icon="○"
|
|
95
|
-
|
|
96
|
-
*) icon="?" ; color="$RESET" ;;
|
|
84
|
+
busy) icon="▶"; color="$GREEN" ;;
|
|
85
|
+
idle) icon="○"; color="$DIM" ;;
|
|
86
|
+
*) icon="?"; color="$RESET" ;;
|
|
97
87
|
esac
|
|
98
88
|
|
|
99
|
-
local
|
|
89
|
+
local phase_short=""
|
|
100
90
|
case "$t_phase" in
|
|
101
|
-
gen)
|
|
102
|
-
gate)
|
|
103
|
-
eval)
|
|
104
|
-
*)
|
|
91
|
+
gen) phase_short="${CYAN}G${RESET}" ;;
|
|
92
|
+
gate) phase_short="${YELLOW}K${RESET}" ;;
|
|
93
|
+
eval) phase_short="${MAGENTA}E${RESET}" ;;
|
|
94
|
+
*) phase_short="${DIM}-${RESET}" ;;
|
|
105
95
|
esac
|
|
106
96
|
|
|
107
|
-
printf " %b
|
|
97
|
+
printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
|
|
98
|
+
if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
|
|
99
|
+
printf " #%s" "$t_attempt"
|
|
100
|
+
fi
|
|
101
|
+
echo ""
|
|
108
102
|
done
|
|
109
|
-
echo ""
|
|
110
103
|
}
|
|
111
104
|
|
|
112
|
-
|
|
105
|
+
render_features() {
|
|
113
106
|
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
114
107
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
phase=$(echo "$in_progress" | jq -r '.phase // "?"' 2>/dev/null)
|
|
141
|
-
status_icon="${CYAN}◐${RESET} T${team}:${phase}"
|
|
142
|
-
elif [ "${in_failed:-0}" -gt 0 ]; then
|
|
143
|
-
status_icon="${RED}✗${RESET}"
|
|
144
|
-
elif [ "${in_ready:-0}" -gt 0 ]; then
|
|
145
|
-
status_icon="${YELLOW}○${RESET}"
|
|
146
|
-
else
|
|
147
|
-
status_icon="${DIM}◌${RESET}" # blocked
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
printf " %b %-6s %-24s\n" "$status_icon" "$fid" "$fname"
|
|
151
|
-
|
|
152
|
-
i=$((i + 1))
|
|
108
|
+
jq -r --slurpfile q "$QUEUE" '
|
|
109
|
+
($q[0].queue.passed // []) as $passed |
|
|
110
|
+
($q[0].queue.failed // []) as $failed |
|
|
111
|
+
($q[0].queue.ready // []) as $ready |
|
|
112
|
+
($q[0].queue.in_progress // {}) as $prog |
|
|
113
|
+
.features[] |
|
|
114
|
+
.id as $fid |
|
|
115
|
+
(.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
116
|
+
(if ($fid | IN($passed[])) then "P"
|
|
117
|
+
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
118
|
+
elif ($fid | IN($failed[])) then "F"
|
|
119
|
+
elif ($fid | IN($ready[])) then "R"
|
|
120
|
+
else "B" end) as $st |
|
|
121
|
+
"\($st)\t\($fid)\t\($fname)"
|
|
122
|
+
' "$FEATURES" 2>/dev/null | while IFS=$'\t' read -r st fid fname; do
|
|
123
|
+
case "$st" in
|
|
124
|
+
P) printf " ${GREEN}●${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
125
|
+
F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
126
|
+
R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
127
|
+
B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
128
|
+
I\|*) team=$(echo "$st" | cut -d'|' -f2)
|
|
129
|
+
phase=$(echo "$st" | cut -d'|' -f3)
|
|
130
|
+
printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
|
|
131
|
+
*) printf " ? %-6s %s\n" "$fid" "$fname" ;;
|
|
132
|
+
esac
|
|
153
133
|
done
|
|
154
|
-
echo ""
|
|
155
134
|
}
|
|
156
135
|
|
|
157
136
|
render_all() {
|
|
158
137
|
render_header
|
|
159
138
|
render_queue_summary
|
|
160
139
|
render_teams
|
|
161
|
-
|
|
162
|
-
|
|
140
|
+
echo ""
|
|
141
|
+
render_features
|
|
163
142
|
}
|
|
164
143
|
|
|
165
144
|
# ── Main loop ──
|
|
166
145
|
tput civis 2>/dev/null
|
|
167
146
|
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
168
|
-
|
|
169
147
|
clear
|
|
170
148
|
|
|
171
149
|
while true; do
|
|
172
|
-
|
|
173
|
-
buf=$(render_all 2>/dev/null)
|
|
150
|
+
buf=$(render_all 2>&1)
|
|
174
151
|
tput cup 0 0 2>/dev/null
|
|
175
152
|
echo "$buf"
|
|
176
153
|
tput ed 2>/dev/null
|
package/scripts/harness-next.sh
CHANGED
|
@@ -59,21 +59,10 @@ if [ -f "$PIPELINE_JSON" ]; then
|
|
|
59
59
|
fi
|
|
60
60
|
|
|
61
61
|
# ─────────────────────────────────────────
|
|
62
|
-
# fe_stack
|
|
63
|
-
# pipeline_selection.pipelines 에서 읽은 에이전트명을 fe_stack/fe_target 에 따라 치환
|
|
64
|
-
# - react: 그대로
|
|
65
|
-
# - flutter+web: generator-frontend → generator-frontend-flutter 만 치환, eval 은 그대로 (Playwright 사용 가능)
|
|
66
|
-
# - flutter+mobile/desktop: eval 도 정적 분석용으로 치환, evaluator-visual 은 __skip__
|
|
62
|
+
# fe_stack 치환 (no-op — Flutter 지원 제거됨, 하위 호환용 stub)
|
|
67
63
|
# ─────────────────────────────────────────
|
|
68
64
|
substitute_fe_stack() {
|
|
69
|
-
|
|
70
|
-
if [ "$fe_stack" != "flutter" ]; then
|
|
71
|
-
echo "$agent"
|
|
72
|
-
return
|
|
73
|
-
fi
|
|
74
|
-
local sub
|
|
75
|
-
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
76
|
-
echo "$sub"
|
|
65
|
+
echo "$1"
|
|
77
66
|
}
|
|
78
67
|
|
|
79
68
|
# ─────────────────────────────────────────
|
|
@@ -105,7 +94,7 @@ run_pre_eval_gate() {
|
|
|
105
94
|
# current_agent가 generator가 아닌 경우 (예: dispatcher로 리라우팅된 상태),
|
|
106
95
|
# completed_agents에서 마지막 generator를 찾는다
|
|
107
96
|
case "$source_agent" in
|
|
108
|
-
generator-frontend
|
|
97
|
+
generator-frontend)
|
|
109
98
|
location="frontend"
|
|
110
99
|
checks_key="frontend_checks"
|
|
111
100
|
;;
|
|
@@ -118,7 +107,7 @@ run_pre_eval_gate() {
|
|
|
118
107
|
local last_gen
|
|
119
108
|
last_gen=$(jq -r '.completed_agents // [] | map(select(startswith("generator-"))) | last // empty' "$PROGRESS" 2>/dev/null)
|
|
120
109
|
case "$last_gen" in
|
|
121
|
-
generator-frontend
|
|
110
|
+
generator-frontend)
|
|
122
111
|
location="frontend"
|
|
123
112
|
checks_key="frontend_checks"
|
|
124
113
|
;;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-prompts-v4.sh — v4 Dashboard 하단: Manual Prompts + Activity
|
|
3
|
+
# progress.log에서 user-prompt와 team 활동을 newest-first로 표시.
|
|
4
|
+
# 스크롤 가능 영역 — 프롬프트가 많아져도 상단 Progress를 가리지 않음.
|
|
5
|
+
|
|
6
|
+
set -uo 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 ".")" || { echo "[prompts] .harness/ not found."; exit 1; }
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
LOG_FILE="$PROJECT_ROOT/.harness/progress.log"
|
|
17
|
+
|
|
18
|
+
BOLD="\033[1m"
|
|
19
|
+
DIM="\033[2m"
|
|
20
|
+
GREEN="\033[32m"
|
|
21
|
+
YELLOW="\033[33m"
|
|
22
|
+
RED="\033[31m"
|
|
23
|
+
CYAN="\033[36m"
|
|
24
|
+
MAGENTA="\033[35m"
|
|
25
|
+
RESET="\033[0m"
|
|
26
|
+
|
|
27
|
+
LAST_LINE_COUNT=0
|
|
28
|
+
|
|
29
|
+
render_prompts() {
|
|
30
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
31
|
+
echo -e " ${DIM}(progress.log not found)${RESET}"
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
local term_h
|
|
36
|
+
term_h=$(tput lines 2>/dev/null || echo 20)
|
|
37
|
+
local max_lines=$((term_h - 3)) # Leave room for header
|
|
38
|
+
if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
|
|
39
|
+
|
|
40
|
+
echo -e "${BOLD}Prompts & Activity${RESET} ${DIM}(newest first)${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
# Read all entries, reverse, limit to terminal height
|
|
44
|
+
grep -v '^#' "$LOG_FILE" 2>/dev/null | grep -v '^$' | \
|
|
45
|
+
tail -r 2>/dev/null | head -"$max_lines" | \
|
|
46
|
+
while IFS= read -r line; do
|
|
47
|
+
local ts agent action detail
|
|
48
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
49
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
50
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
51
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
52
|
+
|
|
53
|
+
local short_ts icon color
|
|
54
|
+
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
55
|
+
|
|
56
|
+
case "$agent" in
|
|
57
|
+
user-prompt)
|
|
58
|
+
icon="★"; color="$BOLD"
|
|
59
|
+
# User prompts get full width, highlighted
|
|
60
|
+
if [ ${#detail} -gt 60 ]; then detail="${detail:0:58}.."; fi
|
|
61
|
+
echo -e " ${color}${icon} ${short_ts}${RESET} ${detail}"
|
|
62
|
+
;;
|
|
63
|
+
dispatcher*)
|
|
64
|
+
icon="▸"; color="$MAGENTA"
|
|
65
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
66
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
67
|
+
;;
|
|
68
|
+
team-*)
|
|
69
|
+
icon="⚡"; color="$CYAN"
|
|
70
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
71
|
+
# Color by action type
|
|
72
|
+
case "$action" in
|
|
73
|
+
*pass*) color="$GREEN"; icon="✓" ;;
|
|
74
|
+
*fail*) color="$RED"; icon="✗" ;;
|
|
75
|
+
*eval*) color="$MAGENTA"; icon="✦" ;;
|
|
76
|
+
*gen*) color="$CYAN"; icon="▶" ;;
|
|
77
|
+
*merge*) color="$GREEN"; icon="⊕" ;;
|
|
78
|
+
esac
|
|
79
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
80
|
+
;;
|
|
81
|
+
manual)
|
|
82
|
+
icon="★"; color="$YELLOW"
|
|
83
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
84
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
85
|
+
;;
|
|
86
|
+
*)
|
|
87
|
+
icon="·"; color="$DIM"
|
|
88
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
89
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
done
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Main loop ──
|
|
96
|
+
tput civis 2>/dev/null
|
|
97
|
+
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
98
|
+
clear
|
|
99
|
+
|
|
100
|
+
while true; do
|
|
101
|
+
buf=$(render_prompts 2>&1)
|
|
102
|
+
tput cup 0 0 2>/dev/null
|
|
103
|
+
echo "$buf"
|
|
104
|
+
tput ed 2>/dev/null
|
|
105
|
+
sleep 3
|
|
106
|
+
done
|
|
@@ -42,6 +42,25 @@ fi
|
|
|
42
42
|
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
43
43
|
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
44
44
|
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
45
|
+
QUEUE_LOCK="$PROJECT_ROOT/.harness/.queue-lock"
|
|
46
|
+
|
|
47
|
+
# ── Atomic queue lock — prevent race conditions between teams ──
|
|
48
|
+
acquire_queue_lock() {
|
|
49
|
+
local max_wait=30 waited=0
|
|
50
|
+
while ! mkdir "$QUEUE_LOCK" 2>/dev/null; do
|
|
51
|
+
sleep 0.1
|
|
52
|
+
waited=$((waited + 1))
|
|
53
|
+
if [ "$waited" -ge $((max_wait * 10)) ]; then
|
|
54
|
+
rm -rf "$QUEUE_LOCK"
|
|
55
|
+
mkdir "$QUEUE_LOCK" 2>/dev/null || true
|
|
56
|
+
break
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
release_queue_lock() {
|
|
62
|
+
rm -rf "$QUEUE_LOCK" 2>/dev/null || true
|
|
63
|
+
}
|
|
45
64
|
|
|
46
65
|
# ── Concurrency from config ──
|
|
47
66
|
CONCURRENCY=3
|
|
@@ -122,12 +141,14 @@ cmd_dequeue() {
|
|
|
122
141
|
if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
|
|
123
142
|
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
124
143
|
|
|
144
|
+
acquire_queue_lock
|
|
145
|
+
|
|
125
146
|
local feature
|
|
126
147
|
feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
|
|
127
148
|
|
|
128
149
|
if [ -z "$feature" ]; then
|
|
150
|
+
release_queue_lock
|
|
129
151
|
echo "[queue] No features in ready queue."
|
|
130
|
-
# Check if all done
|
|
131
152
|
local in_prog blocked
|
|
132
153
|
in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
|
|
133
154
|
blocked=$(jq '.queue.blocked | length' "$QUEUE")
|
|
@@ -137,13 +158,13 @@ cmd_dequeue() {
|
|
|
137
158
|
return 1
|
|
138
159
|
fi
|
|
139
160
|
|
|
140
|
-
# Move feature from ready → in_progress, assign to team
|
|
141
161
|
jq --arg fid "$feature" --arg tid "$team_id" '
|
|
142
162
|
.queue.ready -= [$fid] |
|
|
143
163
|
.queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
|
|
144
164
|
.teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
|
|
145
165
|
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
146
166
|
|
|
167
|
+
release_queue_lock
|
|
147
168
|
echo "$feature"
|
|
148
169
|
}
|
|
149
170
|
|
|
@@ -155,11 +176,11 @@ cmd_pass() {
|
|
|
155
176
|
if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
|
|
156
177
|
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
157
178
|
|
|
158
|
-
|
|
179
|
+
acquire_queue_lock
|
|
180
|
+
|
|
159
181
|
local team_id
|
|
160
182
|
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
161
183
|
|
|
162
|
-
# Move from in_progress → passed, free team, unblock dependents
|
|
163
184
|
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
164
185
|
# Remove from in_progress
|
|
165
186
|
del(.queue.in_progress[$fid]) |
|
|
@@ -188,6 +209,7 @@ cmd_pass() {
|
|
|
188
209
|
|
|
189
210
|
local newly_ready
|
|
190
211
|
newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
|
|
212
|
+
release_queue_lock
|
|
191
213
|
echo "[queue] $fid PASSED. Ready: [$newly_ready]"
|
|
192
214
|
}
|
|
193
215
|
|
|
@@ -199,6 +221,8 @@ cmd_fail() {
|
|
|
199
221
|
if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
|
|
200
222
|
if [ ! -f "$QUEUE" ]; then exit 1; fi
|
|
201
223
|
|
|
224
|
+
acquire_queue_lock
|
|
225
|
+
|
|
202
226
|
local team_id
|
|
203
227
|
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
204
228
|
|
|
@@ -211,6 +235,7 @@ cmd_fail() {
|
|
|
211
235
|
else . end)
|
|
212
236
|
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
213
237
|
|
|
238
|
+
release_queue_lock
|
|
214
239
|
echo "[queue] $fid FAILED."
|
|
215
240
|
}
|
|
216
241
|
|
|
@@ -239,6 +264,8 @@ cmd_update_phase() {
|
|
|
239
264
|
exit 1
|
|
240
265
|
fi
|
|
241
266
|
|
|
267
|
+
acquire_queue_lock
|
|
268
|
+
|
|
242
269
|
local jq_expr
|
|
243
270
|
jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
|
|
244
271
|
if [ -n "$attempt" ]; then
|
|
@@ -246,6 +273,7 @@ cmd_update_phase() {
|
|
|
246
273
|
fi
|
|
247
274
|
|
|
248
275
|
jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
276
|
+
release_queue_lock
|
|
249
277
|
}
|
|
250
278
|
|
|
251
279
|
# ══════════════════════════════════════════
|
|
@@ -297,6 +325,31 @@ cmd_status() {
|
|
|
297
325
|
fi
|
|
298
326
|
}
|
|
299
327
|
|
|
328
|
+
# ══════════════════════════════════════════
|
|
329
|
+
# recover — Move stale in_progress back to ready (after studio restart)
|
|
330
|
+
# ══════════════════════════════════════════
|
|
331
|
+
cmd_recover() {
|
|
332
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Not initialized."; return; fi
|
|
333
|
+
|
|
334
|
+
local stale_count
|
|
335
|
+
stale_count=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
|
|
336
|
+
|
|
337
|
+
if [ "${stale_count:-0}" -eq 0 ]; then
|
|
338
|
+
echo "[queue] No stale in_progress entries."
|
|
339
|
+
return
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Move all in_progress → ready, reset all teams to idle
|
|
343
|
+
jq '
|
|
344
|
+
.queue.ready += [.queue.in_progress | keys[]] |
|
|
345
|
+
.queue.ready |= unique |
|
|
346
|
+
.queue.in_progress = {} |
|
|
347
|
+
.teams |= with_entries(.value = { status: "idle", feature: null, branch: null, pid: null })
|
|
348
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
349
|
+
|
|
350
|
+
echo "[queue] Recovered ${stale_count} stale features back to ready queue."
|
|
351
|
+
}
|
|
352
|
+
|
|
300
353
|
# ── Dispatch ──
|
|
301
354
|
case "$CMD" in
|
|
302
355
|
init) cmd_init ;;
|
|
@@ -305,9 +358,10 @@ case "$CMD" in
|
|
|
305
358
|
fail) cmd_fail "$@" ;;
|
|
306
359
|
requeue) cmd_requeue "$@" ;;
|
|
307
360
|
update_phase) cmd_update_phase "$@" ;;
|
|
361
|
+
recover) cmd_recover ;;
|
|
308
362
|
status) cmd_status ;;
|
|
309
363
|
*)
|
|
310
|
-
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|update_phase|status> [args]"
|
|
364
|
+
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|recover|update_phase|status> [args]"
|
|
311
365
|
exit 1
|
|
312
366
|
;;
|
|
313
367
|
esac
|
|
@@ -26,6 +26,24 @@ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
|
26
26
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
27
27
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
28
28
|
|
|
29
|
+
# ─────────────────────────────────────────
|
|
30
|
+
# v4 Parallel Mode — feature-queue.json이 존재하면 v4 안내
|
|
31
|
+
# ─────────────────────────────────────────
|
|
32
|
+
FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
33
|
+
if [ -f "$FEATURE_QUEUE" ]; then
|
|
34
|
+
passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
35
|
+
total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
36
|
+
in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
37
|
+
failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
38
|
+
|
|
39
|
+
echo "# Harness v4 — Parallel Agent Teams active"
|
|
40
|
+
echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
|
|
41
|
+
echo "# Teams are running autonomously. You are the orchestrator."
|
|
42
|
+
echo "# Role: Monitor dashboard, resolve failures, manual interventions only."
|
|
43
|
+
echo "# Do NOT run /harness-generator-* or /harness-evaluator-* — Teams handle Gen+Eval."
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
29
47
|
# ─────────────────────────────────────────
|
|
30
48
|
# init 상태: 첫 안내
|
|
31
49
|
# ─────────────────────────────────────────
|