@walwal-harness/cli 3.7.4 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/init.js +34 -2
- package/package.json +1 -1
- package/scripts/harness-control-v4.sh +97 -0
- package/scripts/harness-dashboard-v4.sh +178 -0
- package/scripts/harness-queue-manager.sh +313 -0
- package/scripts/harness-studio-v4.sh +115 -0
- package/scripts/harness-team-worker.sh +359 -0
package/bin/init.js
CHANGED
|
@@ -226,6 +226,11 @@ function installScripts() {
|
|
|
226
226
|
'harness-eval-watcher.sh',
|
|
227
227
|
'harness-tmux.sh',
|
|
228
228
|
'harness-control.sh',
|
|
229
|
+
'harness-studio-v4.sh',
|
|
230
|
+
'harness-dashboard-v4.sh',
|
|
231
|
+
'harness-control-v4.sh',
|
|
232
|
+
'harness-queue-manager.sh',
|
|
233
|
+
'harness-team-worker.sh',
|
|
229
234
|
]);
|
|
230
235
|
|
|
231
236
|
if (fs.existsSync(scriptsSrc)) {
|
|
@@ -568,8 +573,9 @@ function showHelp() {
|
|
|
568
573
|
Usage:
|
|
569
574
|
npx walwal-harness Initialize project for harness engineering
|
|
570
575
|
npx walwal-harness --force Re-initialize (overwrites existing files)
|
|
571
|
-
npx walwal-harness studio Launch Harness Studio (tmux
|
|
572
|
-
npx walwal-harness studio --ai Studio + AI eval summary
|
|
576
|
+
npx walwal-harness studio Launch Harness Studio v3 (tmux 5-pane)
|
|
577
|
+
npx walwal-harness studio --ai Studio v3 + AI eval summary
|
|
578
|
+
npx walwal-harness v4 Launch Studio v4 (3 Parallel Agent Teams)
|
|
573
579
|
npx walwal-harness --help Show this help
|
|
574
580
|
|
|
575
581
|
What it does:
|
|
@@ -619,6 +625,27 @@ function runStudio() {
|
|
|
619
625
|
execSync(cmd, { stdio: 'inherit' });
|
|
620
626
|
}
|
|
621
627
|
|
|
628
|
+
function runStudioV4() {
|
|
629
|
+
const scriptsDir = path.join(PKG_ROOT, 'scripts');
|
|
630
|
+
const tmuxScript = path.join(scriptsDir, 'harness-studio-v4.sh');
|
|
631
|
+
|
|
632
|
+
if (!fs.existsSync(tmuxScript)) {
|
|
633
|
+
log('ERROR: harness-studio-v4.sh not found. Update @walwal-harness/cli to >= 4.0.0');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
execSync('which tmux', { stdio: 'ignore' });
|
|
639
|
+
} catch {
|
|
640
|
+
log('ERROR: tmux is required. Install with: brew install tmux');
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const cmd = `bash "${tmuxScript}" "${PROJECT_ROOT}"`.trim();
|
|
645
|
+
log('Launching Harness Studio v4 (Parallel Agent Teams)...');
|
|
646
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
647
|
+
}
|
|
648
|
+
|
|
622
649
|
function main() {
|
|
623
650
|
if (isHelp) {
|
|
624
651
|
showHelp();
|
|
@@ -630,6 +657,11 @@ function main() {
|
|
|
630
657
|
return;
|
|
631
658
|
}
|
|
632
659
|
|
|
660
|
+
if (subcommand === 'studio-v4' || subcommand === 'v4') {
|
|
661
|
+
runStudioV4();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
633
665
|
const pkg = require(path.join(PKG_ROOT, 'package.json'));
|
|
634
666
|
console.log('');
|
|
635
667
|
console.log('╔══════════════════════════════════════╗');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-alpha.1",
|
|
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,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-control-v4.sh — v4 Control Center
|
|
3
|
+
#
|
|
4
|
+
# Commands:
|
|
5
|
+
# init Initialize feature queue
|
|
6
|
+
# start Launch all idle team workers
|
|
7
|
+
# pause <team> Pause team worker
|
|
8
|
+
# resume <team> Resume team worker
|
|
9
|
+
# assign <fid> <t> Force-assign feature to team
|
|
10
|
+
# requeue <fid> Move failed feature back to ready
|
|
11
|
+
# concurrency <N> Change parallel team count
|
|
12
|
+
# status / s Show queue status
|
|
13
|
+
# log <message> Add manual note
|
|
14
|
+
# help / h Show help
|
|
15
|
+
# quit / q Exit
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
+
|
|
21
|
+
PROJECT_ROOT="${1:-}"
|
|
22
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
23
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
24
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[control] .harness/ not found."; exit 1; }
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
28
|
+
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
29
|
+
QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
|
|
30
|
+
|
|
31
|
+
BOLD="\033[1m"
|
|
32
|
+
DIM="\033[2m"
|
|
33
|
+
GREEN="\033[32m"
|
|
34
|
+
YELLOW="\033[33m"
|
|
35
|
+
RED="\033[31m"
|
|
36
|
+
CYAN="\033[36m"
|
|
37
|
+
RESET="\033[0m"
|
|
38
|
+
|
|
39
|
+
cmd_init() {
|
|
40
|
+
bash "$QUEUE_MGR" init "$PROJECT_ROOT"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cmd_status() {
|
|
44
|
+
bash "$QUEUE_MGR" status "$PROJECT_ROOT"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cmd_requeue() {
|
|
48
|
+
local fid="${1:-}"
|
|
49
|
+
if [ -z "$fid" ]; then echo -e " ${RED}Usage: requeue <feature_id>${RESET}"; return; fi
|
|
50
|
+
bash "$QUEUE_MGR" requeue "$fid" "$PROJECT_ROOT"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cmd_log() {
|
|
54
|
+
local msg="$1"
|
|
55
|
+
if [ -z "$msg" ]; then echo -e " ${RED}Usage: log <message>${RESET}"; return; fi
|
|
56
|
+
local ts
|
|
57
|
+
ts=$(date +"%Y-%m-%d")
|
|
58
|
+
echo "${ts} | manual | note | ${msg}" >> "$PROGRESS_LOG"
|
|
59
|
+
echo -e " ${GREEN}Logged:${RESET} ${msg}"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
show_help() {
|
|
63
|
+
echo ""
|
|
64
|
+
echo -e " ${BOLD}Harness v4 Control${RESET}"
|
|
65
|
+
echo -e " ${CYAN}init${RESET} Initialize feature queue from feature-list.json"
|
|
66
|
+
echo -e " ${CYAN}status${RESET} / ${CYAN}s${RESET} Show queue + team status"
|
|
67
|
+
echo -e " ${CYAN}requeue${RESET} <fid> Move failed feature back to ready"
|
|
68
|
+
echo -e " ${CYAN}log${RESET} <message> Add manual note to progress.log"
|
|
69
|
+
echo -e " ${CYAN}help${RESET} / ${CYAN}h${RESET} Show this help"
|
|
70
|
+
echo -e " ${CYAN}quit${RESET} / ${CYAN}q${RESET} Exit control"
|
|
71
|
+
echo ""
|
|
72
|
+
echo -e " ${DIM}Teams auto-start when studio launches.${RESET}"
|
|
73
|
+
echo -e " ${DIM}Workers auto-dequeue from the ready queue.${RESET}"
|
|
74
|
+
echo ""
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ── Main ──
|
|
78
|
+
echo ""
|
|
79
|
+
echo -e " ${BOLD}Harness v4 Control${RESET} ${DIM}(type 'help' for commands)${RESET}"
|
|
80
|
+
echo ""
|
|
81
|
+
|
|
82
|
+
while true; do
|
|
83
|
+
echo -ne " ${BOLD}v4>${RESET} "
|
|
84
|
+
read -r input || exit 0
|
|
85
|
+
input=$(echo "$input" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
86
|
+
|
|
87
|
+
case "$input" in
|
|
88
|
+
init) cmd_init ;;
|
|
89
|
+
status|s) cmd_status ;;
|
|
90
|
+
requeue\ *) cmd_requeue "${input#requeue }" ;;
|
|
91
|
+
log\ *) cmd_log "${input#log }" ;;
|
|
92
|
+
help|h) show_help ;;
|
|
93
|
+
quit|q) echo -e " ${DIM}Goodbye.${RESET}"; exit 0 ;;
|
|
94
|
+
"") ;; # empty
|
|
95
|
+
*) echo -e " ${DIM}Unknown command. Type 'help'.${RESET}" ;;
|
|
96
|
+
esac
|
|
97
|
+
done
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-dashboard-v4.sh — v4 Dashboard: Feature Queue + Team Status
|
|
3
|
+
# Auto-refresh 3초 간격. feature-queue.json + feature-list.json 시각화.
|
|
4
|
+
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="${1:-}"
|
|
11
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
12
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[dash] .harness/ not found."; exit 1; }
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
16
|
+
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
17
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
18
|
+
|
|
19
|
+
BOLD="\033[1m"
|
|
20
|
+
DIM="\033[2m"
|
|
21
|
+
GREEN="\033[32m"
|
|
22
|
+
YELLOW="\033[33m"
|
|
23
|
+
RED="\033[31m"
|
|
24
|
+
CYAN="\033[36m"
|
|
25
|
+
MAGENTA="\033[35m"
|
|
26
|
+
RESET="\033[0m"
|
|
27
|
+
|
|
28
|
+
render_header() {
|
|
29
|
+
local now project_name
|
|
30
|
+
now=$(date +"%H:%M:%S")
|
|
31
|
+
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
32
|
+
|
|
33
|
+
echo -e "${BOLD}╔════════════════════════════════════════════════╗${RESET}"
|
|
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 ""
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render_queue_summary() {
|
|
41
|
+
if [ ! -f "$QUEUE" ]; then
|
|
42
|
+
echo -e " ${DIM}(queue not initialized — run 'init' in Control)${RESET}"
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
local ready blocked in_prog passed failed total concurrency
|
|
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
|
+
concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null)
|
|
53
|
+
total=$((ready + blocked + in_prog + passed + failed))
|
|
54
|
+
|
|
55
|
+
# Progress bar
|
|
56
|
+
local pct=0
|
|
57
|
+
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
58
|
+
local bar_w=20 filled=$(( pct * bar_w / 100 )) empty=$(( bar_w - filled ))
|
|
59
|
+
local bar=""
|
|
60
|
+
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
61
|
+
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
62
|
+
|
|
63
|
+
echo -e " ${BOLD}Queue${RESET} ${bar} ${passed}/${total} (${pct}%) ${DIM}concurrency=${concurrency}${RESET}"
|
|
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 ""
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
render_teams() {
|
|
69
|
+
if [ ! -f "$QUEUE" ]; then return; fi
|
|
70
|
+
|
|
71
|
+
local team_count
|
|
72
|
+
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
|
|
73
|
+
if [ "${team_count:-0}" -eq 0 ]; then return; fi
|
|
74
|
+
|
|
75
|
+
echo -e " ${BOLD}Teams${RESET}"
|
|
76
|
+
|
|
77
|
+
for i in $(seq 1 "$team_count"); do
|
|
78
|
+
local t_status t_feature t_phase t_attempt
|
|
79
|
+
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
80
|
+
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
81
|
+
|
|
82
|
+
# Get phase from in_progress
|
|
83
|
+
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
84
|
+
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
85
|
+
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
86
|
+
else
|
|
87
|
+
t_phase="—"
|
|
88
|
+
t_attempt="—"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
local icon color
|
|
92
|
+
case "$t_status" in
|
|
93
|
+
busy) icon="▶" ; color="$GREEN" ;;
|
|
94
|
+
idle) icon="○" ; color="$DIM" ;;
|
|
95
|
+
paused) icon="⏸" ; color="$YELLOW" ;;
|
|
96
|
+
*) icon="?" ; color="$RESET" ;;
|
|
97
|
+
esac
|
|
98
|
+
|
|
99
|
+
local phase_display=""
|
|
100
|
+
case "$t_phase" in
|
|
101
|
+
gen) phase_display="${CYAN}GEN${RESET}" ;;
|
|
102
|
+
gate) phase_display="${YELLOW}GATE${RESET}" ;;
|
|
103
|
+
eval) phase_display="${MAGENTA}EVAL${RESET}" ;;
|
|
104
|
+
*) phase_display="${DIM}${t_phase}${RESET}" ;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
printf " %b %b Team %d %-8s %b attempt %s\n" "$color" "$icon" "$i" "$t_feature" "$phase_display" "$t_attempt"
|
|
108
|
+
done
|
|
109
|
+
echo ""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render_feature_list() {
|
|
113
|
+
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
114
|
+
|
|
115
|
+
local total
|
|
116
|
+
total=$(jq '.features | length' "$FEATURES" 2>/dev/null)
|
|
117
|
+
if [ "${total:-0}" -eq 0 ]; then return; fi
|
|
118
|
+
|
|
119
|
+
echo -e " ${BOLD}Features${RESET}"
|
|
120
|
+
|
|
121
|
+
local i=0
|
|
122
|
+
while [ "$i" -lt "$total" ]; do
|
|
123
|
+
local fid fname status_icon
|
|
124
|
+
fid=$(jq -r ".features[$i].id" "$FEATURES" 2>/dev/null)
|
|
125
|
+
fname=$(jq -r ".features[$i].name // .features[$i].description // \"\"" "$FEATURES" 2>/dev/null)
|
|
126
|
+
if [ ${#fname} -gt 22 ]; then fname="${fname:0:20}.."; fi
|
|
127
|
+
|
|
128
|
+
# Determine status from queue
|
|
129
|
+
local in_passed in_failed in_progress in_ready in_blocked
|
|
130
|
+
in_passed=$(jq -r --arg f "$fid" '.queue.passed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
|
|
131
|
+
in_failed=$(jq -r --arg f "$fid" '.queue.failed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
|
|
132
|
+
in_progress=$(jq -r --arg f "$fid" '.queue.in_progress[$f] // empty' "$QUEUE" 2>/dev/null)
|
|
133
|
+
in_ready=$(jq -r --arg f "$fid" '.queue.ready // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
|
|
134
|
+
|
|
135
|
+
if [ "${in_passed:-0}" -gt 0 ]; then
|
|
136
|
+
status_icon="${GREEN}●${RESET}"
|
|
137
|
+
elif [ -n "$in_progress" ] && [ "$in_progress" != "" ]; then
|
|
138
|
+
local team phase
|
|
139
|
+
team=$(echo "$in_progress" | jq -r '.team // "?"' 2>/dev/null)
|
|
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))
|
|
153
|
+
done
|
|
154
|
+
echo ""
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render_all() {
|
|
158
|
+
render_header
|
|
159
|
+
render_queue_summary
|
|
160
|
+
render_teams
|
|
161
|
+
render_feature_list
|
|
162
|
+
echo -e " ${DIM}Refreshing every 3s${RESET}"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ── Main loop ──
|
|
166
|
+
tput civis 2>/dev/null
|
|
167
|
+
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
168
|
+
|
|
169
|
+
clear
|
|
170
|
+
|
|
171
|
+
while true; do
|
|
172
|
+
local buf
|
|
173
|
+
buf=$(render_all 2>/dev/null)
|
|
174
|
+
tput cup 0 0 2>/dev/null
|
|
175
|
+
echo "$buf"
|
|
176
|
+
tput ed 2>/dev/null
|
|
177
|
+
sleep 3
|
|
178
|
+
done
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-queue-manager.sh — Feature Queue Manager (v4.0)
|
|
3
|
+
#
|
|
4
|
+
# feature-list.json에서 depends_on 그래프를 읽어 topological sort 후
|
|
5
|
+
# feature-queue.json을 생성/관리한다.
|
|
6
|
+
#
|
|
7
|
+
# Commands:
|
|
8
|
+
# init feature-list.json → feature-queue.json 초기 생성
|
|
9
|
+
# dequeue <team> ready 큐에서 feature를 꺼내 team에 배정
|
|
10
|
+
# pass <fid> feature를 passed로 이동, blocked→ready 전이
|
|
11
|
+
# fail <fid> feature를 failed로 이동
|
|
12
|
+
# requeue <fid> failed feature를 ready로 복귀
|
|
13
|
+
# status 현재 큐 상태 출력
|
|
14
|
+
#
|
|
15
|
+
# Usage: bash scripts/harness-queue-manager.sh <command> [args...] [project-root]
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
# ── Resolve project root ──
|
|
20
|
+
resolve_root() {
|
|
21
|
+
local dir="${1:-.}"
|
|
22
|
+
dir="$(cd "$dir" 2>/dev/null && pwd || echo "$dir")"
|
|
23
|
+
while [ "$dir" != "/" ]; do
|
|
24
|
+
if [ -d "$dir/.harness" ]; then echo "$dir"; return 0; fi
|
|
25
|
+
dir="$(dirname "$dir")"
|
|
26
|
+
done
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
CMD="${1:-status}"
|
|
31
|
+
shift || true
|
|
32
|
+
|
|
33
|
+
# Last arg might be project root
|
|
34
|
+
PROJECT_ROOT=""
|
|
35
|
+
for arg in "$@"; do
|
|
36
|
+
if [ -d "$arg/.harness" ]; then PROJECT_ROOT="$arg"; fi
|
|
37
|
+
done
|
|
38
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
39
|
+
PROJECT_ROOT="$(resolve_root ".")" || { echo "[queue] .harness/ not found."; exit 1; }
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
43
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
44
|
+
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
45
|
+
|
|
46
|
+
# ── Concurrency from config ──
|
|
47
|
+
CONCURRENCY=3
|
|
48
|
+
if [ -f "$CONFIG" ]; then
|
|
49
|
+
_c=$(jq -r '.flow.parallel.concurrency // 3' "$CONFIG" 2>/dev/null)
|
|
50
|
+
if [ "$_c" -gt 0 ] 2>/dev/null; then CONCURRENCY=$_c; fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ══════════════════════════════════════════
|
|
54
|
+
# init — Build queue from feature-list.json
|
|
55
|
+
# ══════════════════════════════════════════
|
|
56
|
+
cmd_init() {
|
|
57
|
+
if [ ! -f "$FEATURES" ]; then
|
|
58
|
+
echo "[queue] feature-list.json not found."
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Build dependency graph and topological sort
|
|
63
|
+
# Output: feature-queue.json with ready (no deps) and blocked (has deps)
|
|
64
|
+
jq --argjson concurrency "$CONCURRENCY" '
|
|
65
|
+
# Build passed set (features already passed by evaluator)
|
|
66
|
+
def passed_set:
|
|
67
|
+
[.features[] | select(
|
|
68
|
+
((.passes // []) | length > 0) and
|
|
69
|
+
((.passes // []) | any(. == "evaluator-functional"))
|
|
70
|
+
) | .id] ;
|
|
71
|
+
|
|
72
|
+
# Separate ready vs blocked
|
|
73
|
+
def classify(passed):
|
|
74
|
+
reduce .features[] as $f (
|
|
75
|
+
{ ready: [], blocked: {} };
|
|
76
|
+
($f.depends_on // []) as $deps |
|
|
77
|
+
if ($f.id | IN(passed[])) then . # already passed, skip
|
|
78
|
+
elif ($deps | length == 0) then
|
|
79
|
+
.ready += [$f.id]
|
|
80
|
+
elif (passed | length > 0) and ($deps | all(. as $d | $d | IN(passed[]))) then
|
|
81
|
+
.ready += [$f.id] # all deps satisfied
|
|
82
|
+
else
|
|
83
|
+
.blocked[$f.id] = ($deps - passed)
|
|
84
|
+
end
|
|
85
|
+
) ;
|
|
86
|
+
|
|
87
|
+
passed_set as $passed |
|
|
88
|
+
classify($passed) as $classified |
|
|
89
|
+
{
|
|
90
|
+
version: "4.0",
|
|
91
|
+
concurrency: $concurrency,
|
|
92
|
+
queue: {
|
|
93
|
+
ready: $classified.ready,
|
|
94
|
+
blocked: $classified.blocked,
|
|
95
|
+
in_progress: {},
|
|
96
|
+
passed: $passed,
|
|
97
|
+
failed: []
|
|
98
|
+
},
|
|
99
|
+
teams: (
|
|
100
|
+
[range(1; $concurrency + 1)] | map({
|
|
101
|
+
key: (. | tostring),
|
|
102
|
+
value: { status: "idle", feature: null, branch: null, pid: null }
|
|
103
|
+
}) | from_entries
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
' "$FEATURES" > "$QUEUE"
|
|
107
|
+
|
|
108
|
+
local ready_count blocked_count passed_count
|
|
109
|
+
ready_count=$(jq '.queue.ready | length' "$QUEUE")
|
|
110
|
+
blocked_count=$(jq '.queue.blocked | length' "$QUEUE")
|
|
111
|
+
passed_count=$(jq '.queue.passed | length' "$QUEUE")
|
|
112
|
+
|
|
113
|
+
echo "[queue] Initialized: $ready_count ready, $blocked_count blocked, $passed_count already passed"
|
|
114
|
+
echo "[queue] Concurrency: $CONCURRENCY teams"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# ══════════════════════════════════════════
|
|
118
|
+
# dequeue — Assign next ready feature to team
|
|
119
|
+
# ══════════════════════════════════════════
|
|
120
|
+
cmd_dequeue() {
|
|
121
|
+
local team_id="${1:-}"
|
|
122
|
+
if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
|
|
123
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
124
|
+
|
|
125
|
+
local feature
|
|
126
|
+
feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
|
|
127
|
+
|
|
128
|
+
if [ -z "$feature" ]; then
|
|
129
|
+
echo "[queue] No features in ready queue."
|
|
130
|
+
# Check if all done
|
|
131
|
+
local in_prog blocked
|
|
132
|
+
in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
|
|
133
|
+
blocked=$(jq '.queue.blocked | length' "$QUEUE")
|
|
134
|
+
if [ "$in_prog" -eq 0 ] && [ "$blocked" -eq 0 ]; then
|
|
135
|
+
echo "[queue] ALL FEATURES COMPLETE."
|
|
136
|
+
fi
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Move feature from ready → in_progress, assign to team
|
|
141
|
+
jq --arg fid "$feature" --arg tid "$team_id" '
|
|
142
|
+
.queue.ready -= [$fid] |
|
|
143
|
+
.queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
|
|
144
|
+
.teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
|
|
145
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
146
|
+
|
|
147
|
+
echo "$feature"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ══════════════════════════════════════════
|
|
151
|
+
# pass — Mark feature as passed, unblock dependents
|
|
152
|
+
# ══════════════════════════════════════════
|
|
153
|
+
cmd_pass() {
|
|
154
|
+
local fid="${1:-}"
|
|
155
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
|
|
156
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
157
|
+
|
|
158
|
+
# Get team that was working on this feature
|
|
159
|
+
local team_id
|
|
160
|
+
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
161
|
+
|
|
162
|
+
# Move from in_progress → passed, free team, unblock dependents
|
|
163
|
+
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
164
|
+
# Remove from in_progress
|
|
165
|
+
del(.queue.in_progress[$fid]) |
|
|
166
|
+
|
|
167
|
+
# Add to passed
|
|
168
|
+
.queue.passed += [$fid] |
|
|
169
|
+
.queue.passed |= unique |
|
|
170
|
+
|
|
171
|
+
# Free team
|
|
172
|
+
(if $tid != "0" then
|
|
173
|
+
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
174
|
+
else . end) |
|
|
175
|
+
|
|
176
|
+
# Unblock dependents: for each blocked feature, remove $fid from its deps
|
|
177
|
+
# If deps become empty, move to ready
|
|
178
|
+
.queue.blocked as $blocked |
|
|
179
|
+
reduce ($blocked | keys[]) as $blocked_fid (
|
|
180
|
+
.;
|
|
181
|
+
.queue.blocked[$blocked_fid] -= [$fid] |
|
|
182
|
+
if (.queue.blocked[$blocked_fid] | length) == 0 then
|
|
183
|
+
del(.queue.blocked[$blocked_fid]) |
|
|
184
|
+
.queue.ready += [$blocked_fid]
|
|
185
|
+
else . end
|
|
186
|
+
)
|
|
187
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
188
|
+
|
|
189
|
+
local newly_ready
|
|
190
|
+
newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
|
|
191
|
+
echo "[queue] $fid PASSED. Ready: [$newly_ready]"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ══════════════════════════════════════════
|
|
195
|
+
# fail — Mark feature as failed
|
|
196
|
+
# ══════════════════════════════════════════
|
|
197
|
+
cmd_fail() {
|
|
198
|
+
local fid="${1:-}"
|
|
199
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
|
|
200
|
+
if [ ! -f "$QUEUE" ]; then exit 1; fi
|
|
201
|
+
|
|
202
|
+
local team_id
|
|
203
|
+
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
204
|
+
|
|
205
|
+
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
206
|
+
del(.queue.in_progress[$fid]) |
|
|
207
|
+
.queue.failed += [$fid] |
|
|
208
|
+
.queue.failed |= unique |
|
|
209
|
+
(if $tid != "0" then
|
|
210
|
+
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
211
|
+
else . end)
|
|
212
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
213
|
+
|
|
214
|
+
echo "[queue] $fid FAILED."
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# ══════════════════════════════════════════
|
|
218
|
+
# requeue — Move failed feature back to ready
|
|
219
|
+
# ══════════════════════════════════════════
|
|
220
|
+
cmd_requeue() {
|
|
221
|
+
local fid="${1:-}"
|
|
222
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: requeue <feature_id>"; exit 1; fi
|
|
223
|
+
|
|
224
|
+
jq --arg fid "$fid" '
|
|
225
|
+
.queue.failed -= [$fid] |
|
|
226
|
+
.queue.ready += [$fid]
|
|
227
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
228
|
+
|
|
229
|
+
echo "[queue] $fid requeued to ready."
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# ══════════════════════════════════════════
|
|
233
|
+
# update_phase — Update in_progress feature phase/attempt
|
|
234
|
+
# ══════════════════════════════════════════
|
|
235
|
+
cmd_update_phase() {
|
|
236
|
+
local fid="${1:-}" phase="${2:-}" attempt="${3:-}"
|
|
237
|
+
if [ -z "$fid" ] || [ -z "$phase" ]; then
|
|
238
|
+
echo "[queue] Usage: update_phase <feature_id> <phase> [attempt]"
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
local jq_expr
|
|
243
|
+
jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
|
|
244
|
+
if [ -n "$attempt" ]; then
|
|
245
|
+
jq_expr="$jq_expr | .queue.in_progress[\"$fid\"].attempt = ($attempt | tonumber)"
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# ══════════════════════════════════════════
|
|
252
|
+
# status — Print queue state
|
|
253
|
+
# ══════════════════════════════════════════
|
|
254
|
+
cmd_status() {
|
|
255
|
+
if [ ! -f "$QUEUE" ]; then
|
|
256
|
+
echo "[queue] Not initialized. Run: bash scripts/harness-queue-manager.sh init"
|
|
257
|
+
return
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
local ready blocked in_prog passed failed
|
|
261
|
+
ready=$(jq -r '.queue.ready | length' "$QUEUE")
|
|
262
|
+
blocked=$(jq -r '.queue.blocked | length' "$QUEUE")
|
|
263
|
+
in_prog=$(jq -r '.queue.in_progress | length' "$QUEUE")
|
|
264
|
+
passed=$(jq -r '.queue.passed | length' "$QUEUE")
|
|
265
|
+
failed=$(jq -r '.queue.failed | length' "$QUEUE")
|
|
266
|
+
local total=$((ready + blocked + in_prog + passed + failed))
|
|
267
|
+
|
|
268
|
+
echo ""
|
|
269
|
+
echo " Feature Queue ($passed/$total done)"
|
|
270
|
+
echo " ─────────────────────────────"
|
|
271
|
+
echo " Ready: $ready"
|
|
272
|
+
echo " Blocked: $blocked"
|
|
273
|
+
echo " In Progress: $in_prog"
|
|
274
|
+
echo " Passed: $passed"
|
|
275
|
+
echo " Failed: $failed"
|
|
276
|
+
echo ""
|
|
277
|
+
|
|
278
|
+
# Team status
|
|
279
|
+
local team_count
|
|
280
|
+
team_count=$(jq '.teams | length' "$QUEUE")
|
|
281
|
+
echo " Teams"
|
|
282
|
+
echo " ─────────────────────────────"
|
|
283
|
+
for i in $(seq 1 "$team_count"); do
|
|
284
|
+
local t_status t_feature
|
|
285
|
+
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE")
|
|
286
|
+
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE")
|
|
287
|
+
printf " Team %d: %-6s %s\n" "$i" "$t_status" "$t_feature"
|
|
288
|
+
done
|
|
289
|
+
echo ""
|
|
290
|
+
|
|
291
|
+
# In-progress details
|
|
292
|
+
if [ "$in_prog" -gt 0 ]; then
|
|
293
|
+
echo " In Progress"
|
|
294
|
+
echo " ─────────────────────────────"
|
|
295
|
+
jq -r '.queue.in_progress | to_entries[] | " \(.key): team \(.value.team) — \(.value.phase) (attempt \(.value.attempt))"' "$QUEUE"
|
|
296
|
+
echo ""
|
|
297
|
+
fi
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# ── Dispatch ──
|
|
301
|
+
case "$CMD" in
|
|
302
|
+
init) cmd_init ;;
|
|
303
|
+
dequeue) cmd_dequeue "$@" ;;
|
|
304
|
+
pass) cmd_pass "$@" ;;
|
|
305
|
+
fail) cmd_fail "$@" ;;
|
|
306
|
+
requeue) cmd_requeue "$@" ;;
|
|
307
|
+
update_phase) cmd_update_phase "$@" ;;
|
|
308
|
+
status) cmd_status ;;
|
|
309
|
+
*)
|
|
310
|
+
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|update_phase|status> [args]"
|
|
311
|
+
exit 1
|
|
312
|
+
;;
|
|
313
|
+
esac
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-studio-v4.sh — Harness Studio v4: Parallel Agent Teams
|
|
3
|
+
#
|
|
4
|
+
# ┌──────────────────────┬─────────────────────────┐
|
|
5
|
+
# │ Dashboard │ Team 1 (worker log) │
|
|
6
|
+
# │ (Queue + Teams + ├─────────────────────────┤
|
|
7
|
+
# │ Feature status) │ Team 2 (worker log) │
|
|
8
|
+
# ├──────────────────────┤ │
|
|
9
|
+
# │ Control ├─────────────────────────┤
|
|
10
|
+
# │ harness> _ │ Team 3 (worker log) │
|
|
11
|
+
# └──────────────────────┴─────────────────────────┘
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bash scripts/harness-studio-v4.sh [project-root]
|
|
15
|
+
# bash scripts/harness-studio-v4.sh --kill
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
+
SESSION_NAME="harness-v4"
|
|
21
|
+
|
|
22
|
+
PROJECT_ROOT=""
|
|
23
|
+
KILL=false
|
|
24
|
+
|
|
25
|
+
for arg in "$@"; do
|
|
26
|
+
case "$arg" in
|
|
27
|
+
--kill)
|
|
28
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Killed." || echo "No session."
|
|
29
|
+
exit 0
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
38
|
+
dir="$(pwd)"
|
|
39
|
+
while [ "$dir" != "/" ]; do
|
|
40
|
+
if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
|
|
41
|
+
dir="$(dirname "$dir")"
|
|
42
|
+
done
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
46
|
+
echo "Error: .harness/ not found."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "Project: $PROJECT_ROOT"
|
|
51
|
+
echo "Session: $SESSION_NAME"
|
|
52
|
+
|
|
53
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
54
|
+
|
|
55
|
+
# ── Initialize queue if not exists ──
|
|
56
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
57
|
+
if [ ! -f "$QUEUE" ]; then
|
|
58
|
+
echo "Initializing feature queue..."
|
|
59
|
+
bash "$SCRIPT_DIR/harness-queue-manager.sh" init "$PROJECT_ROOT"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ══════════════════════════════════════════
|
|
63
|
+
# Build 5-pane layout using explicit pane IDs
|
|
64
|
+
# ══════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
# 1. Dashboard (top-left)
|
|
67
|
+
PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 200 -y 50 \
|
|
68
|
+
-P -F '#{pane_id}' \
|
|
69
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
70
|
+
|
|
71
|
+
# 2. Team 1 (top-right)
|
|
72
|
+
PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
73
|
+
-P -F '#{pane_id}' \
|
|
74
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'")
|
|
75
|
+
|
|
76
|
+
# 3. Control (bottom-left, 25% of left)
|
|
77
|
+
PANE_CTRL=$(tmux split-window -v -p 25 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
78
|
+
-P -F '#{pane_id}' \
|
|
79
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-control-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
80
|
+
|
|
81
|
+
# 4. Team 2 (middle-right, split from Team 1)
|
|
82
|
+
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
|
|
83
|
+
-P -F '#{pane_id}' \
|
|
84
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
|
|
85
|
+
|
|
86
|
+
# 5. Team 3 (bottom-right, split from Team 2)
|
|
87
|
+
PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
|
|
88
|
+
-P -F '#{pane_id}' \
|
|
89
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
|
|
90
|
+
|
|
91
|
+
# ── Pane titles ──
|
|
92
|
+
tmux select-pane -t "$PANE_DASH" -T "Dashboard"
|
|
93
|
+
tmux select-pane -t "$PANE_CTRL" -T "Control"
|
|
94
|
+
tmux select-pane -t "$PANE_T1" -T "Team 1"
|
|
95
|
+
tmux select-pane -t "$PANE_T2" -T "Team 2"
|
|
96
|
+
tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
97
|
+
|
|
98
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
99
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
100
|
+
|
|
101
|
+
# Focus Control
|
|
102
|
+
tmux select-pane -t "$PANE_CTRL"
|
|
103
|
+
|
|
104
|
+
# Attach
|
|
105
|
+
if [ -n "${TMUX:-}" ]; then
|
|
106
|
+
tmux switch-client -t "$SESSION_NAME"
|
|
107
|
+
else
|
|
108
|
+
echo ""
|
|
109
|
+
echo "Launching Harness Studio v4..."
|
|
110
|
+
echo " Dashboard (left↑) : Feature Queue + Team status"
|
|
111
|
+
echo " Control (left↓) : start/pause/assign/requeue"
|
|
112
|
+
echo " Team 1-3 (right) : Parallel worker logs"
|
|
113
|
+
echo ""
|
|
114
|
+
tmux attach -t "$SESSION_NAME"
|
|
115
|
+
fi
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-team-worker.sh — Team Worker: Feature-level Gen→Eval loop (v4.0)
|
|
3
|
+
#
|
|
4
|
+
# 1 Team = 1 프로세스. Feature Queue에서 feature를 꺼내
|
|
5
|
+
# Gen→Gate→Eval 루프를 claude -p 헤드리스로 자율 실행한다.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/harness-team-worker.sh <team_id> [project-root]
|
|
9
|
+
#
|
|
10
|
+
# Environment:
|
|
11
|
+
# MAX_ATTEMPTS=3 Feature당 최대 Gen→Eval 시도 횟수
|
|
12
|
+
# GEN_MODEL=sonnet Generator 모델
|
|
13
|
+
# EVAL_MODEL=opus Evaluator 모델
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
TEAM_ID="${1:?Usage: harness-team-worker.sh <team_id> [project-root]}"
|
|
18
|
+
shift || true
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
|
|
22
|
+
# ── Resolve project root ──
|
|
23
|
+
PROJECT_ROOT="${1:-}"
|
|
24
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
25
|
+
dir="$(pwd)"
|
|
26
|
+
while [ "$dir" != "/" ]; do
|
|
27
|
+
if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
|
|
28
|
+
dir="$(dirname "$dir")"
|
|
29
|
+
done
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
33
|
+
echo "[T${TEAM_ID}] .harness/ not found."
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
38
|
+
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
39
|
+
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
40
|
+
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
41
|
+
QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
|
|
42
|
+
|
|
43
|
+
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
|
44
|
+
GEN_MODEL="${GEN_MODEL:-sonnet}"
|
|
45
|
+
EVAL_MODEL="${EVAL_MODEL:-opus}"
|
|
46
|
+
|
|
47
|
+
# Read models from config if available
|
|
48
|
+
if [ -f "$CONFIG" ]; then
|
|
49
|
+
_gm=$(jq -r '.agents["generator-frontend"].model // empty' "$CONFIG" 2>/dev/null)
|
|
50
|
+
_em=$(jq -r '.agents["evaluator-functional"].model // empty' "$CONFIG" 2>/dev/null)
|
|
51
|
+
if [ -n "$_gm" ]; then GEN_MODEL="$_gm"; fi
|
|
52
|
+
if [ -n "$_em" ]; then EVAL_MODEL="$_em"; fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# ── ANSI helpers ──
|
|
56
|
+
BOLD="\033[1m"
|
|
57
|
+
DIM="\033[2m"
|
|
58
|
+
GREEN="\033[32m"
|
|
59
|
+
YELLOW="\033[33m"
|
|
60
|
+
RED="\033[31m"
|
|
61
|
+
CYAN="\033[36m"
|
|
62
|
+
RESET="\033[0m"
|
|
63
|
+
|
|
64
|
+
ts() { date +"%H:%M:%S"; }
|
|
65
|
+
|
|
66
|
+
log() {
|
|
67
|
+
echo -e "[$(ts)] ${BOLD}T${TEAM_ID}${RESET} $*"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log_progress() {
|
|
71
|
+
local action="$1" detail="$2"
|
|
72
|
+
local now
|
|
73
|
+
now=$(date +"%Y-%m-%d")
|
|
74
|
+
echo "${now} | team-${TEAM_ID} | ${action} | ${detail}" >> "$PROGRESS_LOG"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ── Pre-eval gate ──
|
|
78
|
+
run_pre_eval_gate() {
|
|
79
|
+
local feature_id="$1"
|
|
80
|
+
local cwd="$PROJECT_ROOT"
|
|
81
|
+
|
|
82
|
+
# Read frontend_cwd from config
|
|
83
|
+
if [ -f "$CONFIG" ]; then
|
|
84
|
+
local _cwd
|
|
85
|
+
_cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
|
|
86
|
+
if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
|
|
87
|
+
cwd="$PROJECT_ROOT/$_cwd"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
local checks=()
|
|
92
|
+
if [ -f "$CONFIG" ]; then
|
|
93
|
+
mapfile -t checks < <(jq -r '.flow.pre_eval_gate.frontend_checks[]' "$CONFIG" 2>/dev/null)
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [ ${#checks[@]} -eq 0 ]; then
|
|
97
|
+
checks=("npx tsc --noEmit" "npx eslint src/")
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
local all_pass=true
|
|
101
|
+
for cmd in "${checks[@]}"; do
|
|
102
|
+
if ! (cd "$cwd" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
|
|
103
|
+
log "${RED}Gate FAIL:${RESET} $cmd"
|
|
104
|
+
all_pass=false
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
|
|
108
|
+
if [ "$all_pass" = true ]; then
|
|
109
|
+
log "${GREEN}Gate: tsc ✓ eslint ✓${RESET}"
|
|
110
|
+
return 0
|
|
111
|
+
else
|
|
112
|
+
return 1
|
|
113
|
+
fi
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ── Build generator prompt ──
|
|
117
|
+
build_gen_prompt() {
|
|
118
|
+
local feature_id="$1"
|
|
119
|
+
local attempt="$2"
|
|
120
|
+
local eval_feedback="${3:-}"
|
|
121
|
+
|
|
122
|
+
local feature_json
|
|
123
|
+
feature_json=$(jq --arg fid "$feature_id" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
|
|
124
|
+
|
|
125
|
+
local feature_name feature_desc ac_json
|
|
126
|
+
feature_name=$(echo "$feature_json" | jq -r '.name // .description // ""')
|
|
127
|
+
feature_desc=$(echo "$feature_json" | jq -r '.description // ""')
|
|
128
|
+
ac_json=$(echo "$feature_json" | jq -c '.ac // []')
|
|
129
|
+
|
|
130
|
+
local prompt="You are Generator-Frontend for a harness engineering project.
|
|
131
|
+
|
|
132
|
+
PROJECT: $(jq -r '.project_name // ""' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
|
|
133
|
+
CONVENTIONS: Read CONVENTIONS.md if it exists.
|
|
134
|
+
|
|
135
|
+
YOUR TASK: Implement ONLY feature ${feature_id}: ${feature_name}
|
|
136
|
+
Description: ${feature_desc}
|
|
137
|
+
Acceptance Criteria: ${ac_json}
|
|
138
|
+
|
|
139
|
+
Read these files for context:
|
|
140
|
+
- .harness/actions/feature-list.json (filter to ${feature_id})
|
|
141
|
+
- .harness/actions/api-contract.json (relevant endpoints)
|
|
142
|
+
- .harness/actions/plan.md (overall design)
|
|
143
|
+
|
|
144
|
+
RULES:
|
|
145
|
+
- Implement ONLY this feature, do not touch other features' code
|
|
146
|
+
- Follow existing code patterns and CONVENTIONS.md
|
|
147
|
+
- Commit your changes with message: 'feat(${feature_id}): ${feature_name}'
|
|
148
|
+
- Do NOT create tests (evaluator will handle that)"
|
|
149
|
+
|
|
150
|
+
if [ "$attempt" -gt 1 ] && [ -n "$eval_feedback" ]; then
|
|
151
|
+
prompt="$prompt
|
|
152
|
+
|
|
153
|
+
PREVIOUS EVAL FEEDBACK (attempt ${attempt}):
|
|
154
|
+
${eval_feedback}
|
|
155
|
+
|
|
156
|
+
Fix the issues from the feedback above. Focus specifically on the failed criteria."
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
echo "$prompt"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# ── Build evaluator prompt ──
|
|
163
|
+
build_eval_prompt() {
|
|
164
|
+
local feature_id="$1"
|
|
165
|
+
|
|
166
|
+
local feature_json
|
|
167
|
+
feature_json=$(jq --arg fid "$feature_id" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
|
|
168
|
+
|
|
169
|
+
local feature_name ac_json
|
|
170
|
+
feature_name=$(echo "$feature_json" | jq -r '.name // .description // ""')
|
|
171
|
+
ac_json=$(echo "$feature_json" | jq -c '.ac // []')
|
|
172
|
+
|
|
173
|
+
local passed_list
|
|
174
|
+
passed_list=$(jq -r '.queue.passed // [] | join(", ")' "$QUEUE" 2>/dev/null)
|
|
175
|
+
|
|
176
|
+
echo "You are Evaluator-Functional for a harness engineering project.
|
|
177
|
+
|
|
178
|
+
TASK: Evaluate feature ${feature_id}: ${feature_name}
|
|
179
|
+
|
|
180
|
+
Acceptance Criteria to verify:
|
|
181
|
+
${ac_json}
|
|
182
|
+
|
|
183
|
+
Previously passed features (regression check): [${passed_list}]
|
|
184
|
+
|
|
185
|
+
SCORING RUBRIC (R1-R5):
|
|
186
|
+
R1: API Contract compliance (25%)
|
|
187
|
+
R2: Acceptance Criteria full pass (25%)
|
|
188
|
+
R3: Negative tests (20%)
|
|
189
|
+
R4: E2E scenario (15%)
|
|
190
|
+
R5: Error handling & edge cases (15%)
|
|
191
|
+
|
|
192
|
+
PASS threshold: 2.80 / 3.00
|
|
193
|
+
FAIL: any AC not met, any regression failure
|
|
194
|
+
|
|
195
|
+
OUTPUT FORMAT (must be parseable):
|
|
196
|
+
---EVAL-RESULT---
|
|
197
|
+
FEATURE: ${feature_id}
|
|
198
|
+
VERDICT: PASS or FAIL
|
|
199
|
+
SCORE: X.XX
|
|
200
|
+
FEEDBACK: <one paragraph summary of issues or confirmation>
|
|
201
|
+
---END-EVAL-RESULT---"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── Parse eval result ──
|
|
205
|
+
parse_eval_result() {
|
|
206
|
+
local output="$1"
|
|
207
|
+
|
|
208
|
+
local verdict score feedback
|
|
209
|
+
verdict=$(echo "$output" | grep -oP 'VERDICT:\s*\K\w+' | head -1)
|
|
210
|
+
score=$(echo "$output" | grep -oP 'SCORE:\s*\K[0-9.]+' | head -1)
|
|
211
|
+
feedback=$(echo "$output" | sed -n 's/^FEEDBACK:\s*//p' | head -1)
|
|
212
|
+
|
|
213
|
+
echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ══════════════════════════════════════════
|
|
217
|
+
# Main Worker Loop
|
|
218
|
+
# ══════════════════════════════════════════
|
|
219
|
+
log "${CYAN}Team ${TEAM_ID} started${RESET} (gen=${GEN_MODEL}, eval=${EVAL_MODEL}, max=${MAX_ATTEMPTS})"
|
|
220
|
+
log_progress "start" "Team ${TEAM_ID} worker started"
|
|
221
|
+
|
|
222
|
+
while true; do
|
|
223
|
+
# ── Dequeue next feature ──
|
|
224
|
+
feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
|
|
225
|
+
|
|
226
|
+
if [ -z "$feature_id" ] || [[ "$feature_id" == "[queue]"* ]]; then
|
|
227
|
+
log "${DIM}No features in queue. Waiting 10s...${RESET}"
|
|
228
|
+
sleep 10
|
|
229
|
+
|
|
230
|
+
# Check if completely done
|
|
231
|
+
local remaining
|
|
232
|
+
remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null)
|
|
233
|
+
if [ "${remaining:-1}" -eq 0 ]; then
|
|
234
|
+
log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
|
|
235
|
+
log_progress "complete" "All features done"
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
continue
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
log "${CYAN}▶ Dequeued ${feature_id}${RESET}"
|
|
242
|
+
log_progress "dequeue" "${feature_id}"
|
|
243
|
+
|
|
244
|
+
# ── Create feature branch ──
|
|
245
|
+
local branch="feature/${feature_id}"
|
|
246
|
+
(cd "$PROJECT_ROOT" && git checkout -b "$branch" main 2>/dev/null) || \
|
|
247
|
+
(cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
|
|
248
|
+
log "Branch: ${branch}"
|
|
249
|
+
|
|
250
|
+
# ── Gen→Eval Loop ──
|
|
251
|
+
local attempt=1
|
|
252
|
+
local eval_feedback=""
|
|
253
|
+
local passed=false
|
|
254
|
+
|
|
255
|
+
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
|
256
|
+
log "${BOLD}── Attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
|
|
257
|
+
|
|
258
|
+
# ── Generate ──
|
|
259
|
+
log "Gen ${feature_id} (${GEN_MODEL})..."
|
|
260
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
261
|
+
|
|
262
|
+
local gen_prompt
|
|
263
|
+
gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
|
|
264
|
+
|
|
265
|
+
local gen_output
|
|
266
|
+
gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" --model "$GEN_MODEL" --output-format text 2>&1) || true
|
|
267
|
+
|
|
268
|
+
local files_changed
|
|
269
|
+
files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only | wc -l | tr -d ' ')
|
|
270
|
+
log "Gen complete — ${files_changed} files changed"
|
|
271
|
+
log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files"
|
|
272
|
+
|
|
273
|
+
# Auto-commit gen output
|
|
274
|
+
(cd "$PROJECT_ROOT" && git add -A && git commit -m "feat(${feature_id}): gen attempt ${attempt}" --no-verify 2>/dev/null) || true
|
|
275
|
+
|
|
276
|
+
# ── Pre-eval gate ──
|
|
277
|
+
log "Pre-eval gate..."
|
|
278
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "gate" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
279
|
+
|
|
280
|
+
if ! run_pre_eval_gate "$feature_id"; then
|
|
281
|
+
log "${RED}Gate FAIL — retrying gen${RESET}"
|
|
282
|
+
eval_feedback="Pre-eval gate failed: type check or lint errors. Fix compilation and lint issues."
|
|
283
|
+
attempt=$((attempt + 1))
|
|
284
|
+
continue
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# ── Evaluate ──
|
|
288
|
+
log "Eval ${feature_id} (${EVAL_MODEL})..."
|
|
289
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
290
|
+
|
|
291
|
+
local eval_prompt
|
|
292
|
+
eval_prompt=$(build_eval_prompt "$feature_id")
|
|
293
|
+
|
|
294
|
+
local eval_output
|
|
295
|
+
eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" --model "$EVAL_MODEL" --output-format text 2>&1) || true
|
|
296
|
+
|
|
297
|
+
# Parse result
|
|
298
|
+
local result_line verdict score feedback
|
|
299
|
+
result_line=$(parse_eval_result "$eval_output")
|
|
300
|
+
verdict=$(echo "$result_line" | cut -d'|' -f1)
|
|
301
|
+
score=$(echo "$result_line" | cut -d'|' -f2)
|
|
302
|
+
feedback=$(echo "$result_line" | cut -d'|' -f3-)
|
|
303
|
+
|
|
304
|
+
log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score})"
|
|
305
|
+
|
|
306
|
+
if [ "$verdict" = "PASS" ]; then
|
|
307
|
+
log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00"
|
|
308
|
+
passed=true
|
|
309
|
+
break
|
|
310
|
+
else
|
|
311
|
+
log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00"
|
|
312
|
+
log "${DIM} ${feedback}${RESET}"
|
|
313
|
+
eval_feedback="$feedback"
|
|
314
|
+
attempt=$((attempt + 1))
|
|
315
|
+
fi
|
|
316
|
+
done
|
|
317
|
+
|
|
318
|
+
# ── Result processing ──
|
|
319
|
+
if [ "$passed" = true ]; then
|
|
320
|
+
# Merge to main
|
|
321
|
+
log "Merging ${branch} → main..."
|
|
322
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null) || {
|
|
323
|
+
log "${YELLOW}Merge conflict — rebasing...${RESET}"
|
|
324
|
+
(cd "$PROJECT_ROOT" && git checkout "$branch" && git rebase main 2>/dev/null && \
|
|
325
|
+
git checkout main && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null) || {
|
|
326
|
+
log "${RED}Merge failed after rebase. Manual intervention needed.${RESET}"
|
|
327
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
328
|
+
log_progress "merge-fail" "${feature_id}"
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Mark passed in queue
|
|
334
|
+
bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
335
|
+
log_progress "pass" "${feature_id} merged to main"
|
|
336
|
+
|
|
337
|
+
# Update feature-list.json passes
|
|
338
|
+
if [ -f "$FEATURES" ]; then
|
|
339
|
+
jq --arg fid "$feature_id" '
|
|
340
|
+
.features |= map(
|
|
341
|
+
if .id == $fid then
|
|
342
|
+
.passes = ((.passes // []) + ["generator-frontend", "evaluator-functional"] | unique)
|
|
343
|
+
else . end
|
|
344
|
+
)
|
|
345
|
+
' "$FEATURES" > "${FEATURES}.tmp" && mv "${FEATURES}.tmp" "$FEATURES"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
else
|
|
349
|
+
log "${RED}${BOLD}✗ ${feature_id} FAILED after ${MAX_ATTEMPTS} attempts${RESET}"
|
|
350
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
351
|
+
log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
|
|
352
|
+
|
|
353
|
+
# Return to main
|
|
354
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Brief pause before next feature
|
|
358
|
+
sleep 2
|
|
359
|
+
done
|