@walwal-harness/cli 3.7.4 → 4.0.0-alpha.2
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/config.json +11 -0
- 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 +407 -0
- package/skills/evaluator-functional/SKILL.md +26 -0
- package/skills/generator-frontend/SKILL.md +17 -0
|
@@ -209,6 +209,17 @@
|
|
|
209
209
|
"frontend_cwd": "",
|
|
210
210
|
"timeout_seconds": 120,
|
|
211
211
|
"on_fail": "reroute_to_generator"
|
|
212
|
+
},
|
|
213
|
+
"parallel": {
|
|
214
|
+
"comment": "v4 Parallel Agent Teams 설정. npx walwal-harness v4 로 실행.",
|
|
215
|
+
"enabled": false,
|
|
216
|
+
"concurrency": 3,
|
|
217
|
+
"max_attempts_per_feature": 3,
|
|
218
|
+
"gen_model": "sonnet",
|
|
219
|
+
"eval_model": "opus",
|
|
220
|
+
"branch_strategy": "feature-branch",
|
|
221
|
+
"merge_on_pass": true,
|
|
222
|
+
"rebase_on_conflict": true
|
|
212
223
|
}
|
|
213
224
|
},
|
|
214
225
|
"evaluation": {
|
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.2",
|
|
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,407 @@
|
|
|
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
|
+
# ── Lock file for git operations (prevent race conditions between teams) ──
|
|
44
|
+
GIT_LOCK="$PROJECT_ROOT/.harness/.git-lock"
|
|
45
|
+
|
|
46
|
+
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
|
47
|
+
GEN_MODEL="${GEN_MODEL:-sonnet}"
|
|
48
|
+
EVAL_MODEL="${EVAL_MODEL:-opus}"
|
|
49
|
+
|
|
50
|
+
if [ -f "$CONFIG" ]; then
|
|
51
|
+
_gm=$(jq -r '.agents["generator-frontend"].model // empty' "$CONFIG" 2>/dev/null)
|
|
52
|
+
_em=$(jq -r '.agents["evaluator-functional"].model // empty' "$CONFIG" 2>/dev/null)
|
|
53
|
+
if [ -n "$_gm" ]; then GEN_MODEL="$_gm"; fi
|
|
54
|
+
if [ -n "$_em" ]; then EVAL_MODEL="$_em"; fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ── ANSI helpers ──
|
|
58
|
+
BOLD="\033[1m"
|
|
59
|
+
DIM="\033[2m"
|
|
60
|
+
GREEN="\033[32m"
|
|
61
|
+
YELLOW="\033[33m"
|
|
62
|
+
RED="\033[31m"
|
|
63
|
+
CYAN="\033[36m"
|
|
64
|
+
RESET="\033[0m"
|
|
65
|
+
|
|
66
|
+
ts() { date +"%H:%M:%S"; }
|
|
67
|
+
|
|
68
|
+
log() {
|
|
69
|
+
echo -e "[$(ts)] ${BOLD}T${TEAM_ID}${RESET} $*"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
log_progress() {
|
|
73
|
+
echo "$(date +"%Y-%m-%d") | team-${TEAM_ID} | ${1} | ${2}" >> "$PROGRESS_LOG"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ── Git lock — serialize git checkout/merge across teams ──
|
|
77
|
+
acquire_git_lock() {
|
|
78
|
+
local max_wait=60 waited=0
|
|
79
|
+
while [ -f "$GIT_LOCK" ]; do
|
|
80
|
+
sleep 1
|
|
81
|
+
waited=$((waited + 1))
|
|
82
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
83
|
+
log "${RED}Git lock timeout (${max_wait}s). Removing stale lock.${RESET}"
|
|
84
|
+
rm -f "$GIT_LOCK"
|
|
85
|
+
break
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
echo "T${TEAM_ID}" > "$GIT_LOCK"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
release_git_lock() {
|
|
92
|
+
rm -f "$GIT_LOCK"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Pre-eval gate ──
|
|
96
|
+
run_pre_eval_gate() {
|
|
97
|
+
local cwd="$PROJECT_ROOT"
|
|
98
|
+
|
|
99
|
+
if [ -f "$CONFIG" ]; then
|
|
100
|
+
_cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
|
|
101
|
+
if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
|
|
102
|
+
cwd="$PROJECT_ROOT/$_cwd"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
local checks=()
|
|
107
|
+
if [ -f "$CONFIG" ]; then
|
|
108
|
+
mapfile -t checks < <(jq -r '.flow.pre_eval_gate.frontend_checks[]' "$CONFIG" 2>/dev/null)
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [ ${#checks[@]} -eq 0 ]; then
|
|
112
|
+
checks=("npx tsc --noEmit" "npx eslint src/")
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
local all_pass=true fail_cmds=""
|
|
116
|
+
for cmd in "${checks[@]}"; do
|
|
117
|
+
if (cd "$cwd" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
|
|
118
|
+
log " ${GREEN}✓${RESET} $cmd"
|
|
119
|
+
else
|
|
120
|
+
log " ${RED}✗${RESET} $cmd"
|
|
121
|
+
all_pass=false
|
|
122
|
+
fail_cmds+="$cmd; "
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
[ "$all_pass" = true ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ── Build generator prompt ──
|
|
130
|
+
build_gen_prompt() {
|
|
131
|
+
local fid="$1" attempt="$2" feedback="${3:-}"
|
|
132
|
+
|
|
133
|
+
local fobj
|
|
134
|
+
fobj=$(jq --arg fid "$fid" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
|
|
135
|
+
local fname fdesc ac_json deps_json
|
|
136
|
+
fname=$(echo "$fobj" | jq -r '.name // .description // ""')
|
|
137
|
+
fdesc=$(echo "$fobj" | jq -r '.description // ""')
|
|
138
|
+
ac_json=$(echo "$fobj" | jq -c '.ac // []')
|
|
139
|
+
deps_json=$(echo "$fobj" | jq -c '.depends_on // []')
|
|
140
|
+
|
|
141
|
+
local project_name
|
|
142
|
+
project_name=$(jq -r '.project_name // ""' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
|
|
143
|
+
|
|
144
|
+
cat <<PROMPT
|
|
145
|
+
You are Generator-Frontend for a harness engineering project.
|
|
146
|
+
|
|
147
|
+
PROJECT: ${project_name}
|
|
148
|
+
CONVENTIONS: Read CONVENTIONS.md if it exists.
|
|
149
|
+
|
|
150
|
+
YOUR TASK: Implement ONLY feature ${fid}: ${fname}
|
|
151
|
+
Description: ${fdesc}
|
|
152
|
+
Dependencies (already implemented): ${deps_json}
|
|
153
|
+
Acceptance Criteria: ${ac_json}
|
|
154
|
+
|
|
155
|
+
Read these files for context:
|
|
156
|
+
- .harness/actions/feature-list.json (filter to ${fid})
|
|
157
|
+
- .harness/actions/api-contract.json (relevant endpoints)
|
|
158
|
+
- .harness/actions/plan.md (overall design)
|
|
159
|
+
|
|
160
|
+
RULES:
|
|
161
|
+
- Implement ONLY this single feature
|
|
162
|
+
- Do NOT modify code belonging to other features
|
|
163
|
+
- Follow existing code patterns and CONVENTIONS.md
|
|
164
|
+
- When done, stage and commit with: git add -A && git commit -m 'feat(${fid}): ${fname}'
|
|
165
|
+
PROMPT
|
|
166
|
+
|
|
167
|
+
if [ "$attempt" -gt 1 ] && [ -n "$feedback" ]; then
|
|
168
|
+
cat <<RETRY
|
|
169
|
+
|
|
170
|
+
PREVIOUS EVAL FEEDBACK (attempt ${attempt}):
|
|
171
|
+
${feedback}
|
|
172
|
+
|
|
173
|
+
Fix the issues above. Focus specifically on the failed criteria.
|
|
174
|
+
RETRY
|
|
175
|
+
fi
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ── Build evaluator prompt ──
|
|
179
|
+
build_eval_prompt() {
|
|
180
|
+
local fid="$1"
|
|
181
|
+
|
|
182
|
+
local fobj
|
|
183
|
+
fobj=$(jq --arg fid "$fid" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
|
|
184
|
+
local fname ac_json
|
|
185
|
+
fname=$(echo "$fobj" | jq -r '.name // .description // ""')
|
|
186
|
+
ac_json=$(echo "$fobj" | jq -c '.ac // []')
|
|
187
|
+
|
|
188
|
+
local passed_list
|
|
189
|
+
passed_list=$(jq -r '.queue.passed // [] | join(", ")' "$QUEUE" 2>/dev/null)
|
|
190
|
+
|
|
191
|
+
cat <<PROMPT
|
|
192
|
+
You are Evaluator-Functional for a harness engineering project.
|
|
193
|
+
|
|
194
|
+
TASK: Evaluate feature ${fid}: ${fname}
|
|
195
|
+
|
|
196
|
+
Acceptance Criteria to verify:
|
|
197
|
+
${ac_json}
|
|
198
|
+
|
|
199
|
+
Previously passed features (regression check): [${passed_list}]
|
|
200
|
+
|
|
201
|
+
SCORING RUBRIC (R1-R5):
|
|
202
|
+
R1: API Contract compliance (25%)
|
|
203
|
+
R2: Acceptance Criteria full pass (25%)
|
|
204
|
+
R3: Negative tests (20%)
|
|
205
|
+
R4: E2E scenario (15%)
|
|
206
|
+
R5: Error handling & edge cases (15%)
|
|
207
|
+
|
|
208
|
+
PASS threshold: 2.80 / 3.00
|
|
209
|
+
FAIL: any AC not met, any regression failure
|
|
210
|
+
|
|
211
|
+
You MUST output this exact block (parseable by automation):
|
|
212
|
+
---EVAL-RESULT---
|
|
213
|
+
FEATURE: ${fid}
|
|
214
|
+
VERDICT: PASS or FAIL
|
|
215
|
+
SCORE: X.XX
|
|
216
|
+
FEEDBACK: one paragraph summary
|
|
217
|
+
---END-EVAL-RESULT---
|
|
218
|
+
PROMPT
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# ── Parse eval result (macOS-compatible, no grep -P) ──
|
|
222
|
+
parse_eval_result() {
|
|
223
|
+
local output="$1"
|
|
224
|
+
|
|
225
|
+
local verdict score feedback
|
|
226
|
+
verdict=$(echo "$output" | grep -E '^VERDICT:' | sed 's/VERDICT:[[:space:]]*//' | head -1)
|
|
227
|
+
score=$(echo "$output" | grep -E '^SCORE:' | sed 's/SCORE:[[:space:]]*//' | head -1)
|
|
228
|
+
feedback=$(echo "$output" | grep -E '^FEEDBACK:' | sed 's/FEEDBACK:[[:space:]]*//' | head -1)
|
|
229
|
+
|
|
230
|
+
echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# ══════════════════════════════════════════
|
|
234
|
+
# Main Worker Loop
|
|
235
|
+
# ══════════════════════════════════════════
|
|
236
|
+
log "${CYAN}Team ${TEAM_ID} started${RESET} (gen=${GEN_MODEL}, eval=${EVAL_MODEL}, max=${MAX_ATTEMPTS})"
|
|
237
|
+
log_progress "start" "Team ${TEAM_ID} worker started"
|
|
238
|
+
|
|
239
|
+
while true; do
|
|
240
|
+
# ── Dequeue next feature ──
|
|
241
|
+
feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
|
|
242
|
+
|
|
243
|
+
if [ -z "$feature_id" ] || [[ "$feature_id" == "["* ]]; then
|
|
244
|
+
log "${DIM}No features ready. Waiting 10s...${RESET}"
|
|
245
|
+
sleep 10
|
|
246
|
+
|
|
247
|
+
# Check if completely done
|
|
248
|
+
remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null || echo "1")
|
|
249
|
+
if [ "${remaining}" -eq 0 ] 2>/dev/null; then
|
|
250
|
+
log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
|
|
251
|
+
log_progress "complete" "All features done"
|
|
252
|
+
exit 0
|
|
253
|
+
fi
|
|
254
|
+
continue
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
log "${CYAN}▶ Dequeued ${feature_id}${RESET}"
|
|
258
|
+
log_progress "dequeue" "${feature_id}"
|
|
259
|
+
|
|
260
|
+
# ── Create feature branch (with lock) ──
|
|
261
|
+
branch="feature/${feature_id}"
|
|
262
|
+
acquire_git_lock
|
|
263
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git checkout -b "$branch" 2>/dev/null) || \
|
|
264
|
+
(cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
|
|
265
|
+
release_git_lock
|
|
266
|
+
log "Branch: ${branch}"
|
|
267
|
+
|
|
268
|
+
# ── Gen→Eval Loop ──
|
|
269
|
+
attempt=1
|
|
270
|
+
eval_feedback=""
|
|
271
|
+
passed=false
|
|
272
|
+
|
|
273
|
+
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
|
274
|
+
log "${BOLD}── Attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
|
|
275
|
+
|
|
276
|
+
# ── Generate ──
|
|
277
|
+
log "Gen ${feature_id} (${GEN_MODEL})..."
|
|
278
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
279
|
+
|
|
280
|
+
gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
|
|
281
|
+
|
|
282
|
+
gen_start=$(date +%s)
|
|
283
|
+
gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" --model "$GEN_MODEL" --output-format text 2>&1) || true
|
|
284
|
+
gen_elapsed=$(( $(date +%s) - gen_start ))
|
|
285
|
+
|
|
286
|
+
files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
|
|
287
|
+
log "Gen done (${gen_elapsed}s) — ${files_changed} files"
|
|
288
|
+
log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files, ${gen_elapsed}s"
|
|
289
|
+
|
|
290
|
+
# Auto-commit
|
|
291
|
+
(cd "$PROJECT_ROOT" && git add -A && git commit -m "feat(${feature_id}): gen attempt ${attempt}" --no-verify 2>/dev/null) || true
|
|
292
|
+
|
|
293
|
+
# ── Pre-eval gate ──
|
|
294
|
+
log "Pre-eval gate..."
|
|
295
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "gate" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
296
|
+
|
|
297
|
+
if ! run_pre_eval_gate "$feature_id"; then
|
|
298
|
+
log "${RED}Gate FAIL — retrying gen${RESET}"
|
|
299
|
+
eval_feedback="Pre-eval gate failed: type check or lint errors. Fix compilation and lint issues."
|
|
300
|
+
attempt=$((attempt + 1))
|
|
301
|
+
continue
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# ── Evaluate ──
|
|
305
|
+
log "Eval ${feature_id} (${EVAL_MODEL})..."
|
|
306
|
+
bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
307
|
+
|
|
308
|
+
eval_prompt=$(build_eval_prompt "$feature_id")
|
|
309
|
+
|
|
310
|
+
eval_start=$(date +%s)
|
|
311
|
+
eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" --model "$EVAL_MODEL" --output-format text 2>&1) || true
|
|
312
|
+
eval_elapsed=$(( $(date +%s) - eval_start ))
|
|
313
|
+
|
|
314
|
+
# Parse result
|
|
315
|
+
result_line=$(parse_eval_result "$eval_output")
|
|
316
|
+
verdict=$(echo "$result_line" | cut -d'|' -f1)
|
|
317
|
+
score=$(echo "$result_line" | cut -d'|' -f2)
|
|
318
|
+
feedback=$(echo "$result_line" | cut -d'|' -f3-)
|
|
319
|
+
|
|
320
|
+
log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
|
|
321
|
+
|
|
322
|
+
if [ "$verdict" = "PASS" ]; then
|
|
323
|
+
log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
|
|
324
|
+
passed=true
|
|
325
|
+
break
|
|
326
|
+
else
|
|
327
|
+
log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
|
|
328
|
+
log "${DIM} ${feedback}${RESET}"
|
|
329
|
+
eval_feedback="$feedback"
|
|
330
|
+
attempt=$((attempt + 1))
|
|
331
|
+
fi
|
|
332
|
+
done
|
|
333
|
+
|
|
334
|
+
# ══════════════════════════════════════════
|
|
335
|
+
# Phase 3: Branch merge with conflict handling
|
|
336
|
+
# ══════════════════════════════════════════
|
|
337
|
+
if [ "$passed" = true ]; then
|
|
338
|
+
log "Merging ${branch} → main..."
|
|
339
|
+
acquire_git_lock
|
|
340
|
+
|
|
341
|
+
merge_ok=false
|
|
342
|
+
|
|
343
|
+
# Attempt 1: straight merge
|
|
344
|
+
if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
|
|
345
|
+
merge_ok=true
|
|
346
|
+
else
|
|
347
|
+
# Attempt 2: abort failed merge, rebase, re-eval gate, then merge
|
|
348
|
+
log "${YELLOW}Conflict detected — rebasing ${branch} onto main...${RESET}"
|
|
349
|
+
(cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
|
|
350
|
+
(cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
|
|
351
|
+
|
|
352
|
+
if (cd "$PROJECT_ROOT" && git rebase main 2>/dev/null); then
|
|
353
|
+
log "Rebase OK. Re-running gate..."
|
|
354
|
+
|
|
355
|
+
if run_pre_eval_gate "$feature_id"; then
|
|
356
|
+
log "Gate still PASS after rebase."
|
|
357
|
+
if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null); then
|
|
358
|
+
merge_ok=true
|
|
359
|
+
fi
|
|
360
|
+
else
|
|
361
|
+
log "${RED}Gate FAIL after rebase — needs re-gen${RESET}"
|
|
362
|
+
fi
|
|
363
|
+
else
|
|
364
|
+
log "${RED}Rebase failed — conflicts too complex${RESET}"
|
|
365
|
+
(cd "$PROJECT_ROOT" && git rebase --abort 2>/dev/null) || true
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
release_git_lock
|
|
370
|
+
|
|
371
|
+
if [ "$merge_ok" = true ]; then
|
|
372
|
+
# Clean up feature branch
|
|
373
|
+
(cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
|
|
374
|
+
|
|
375
|
+
bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
376
|
+
log_progress "pass" "${feature_id} merged to main"
|
|
377
|
+
|
|
378
|
+
# Update feature-list.json passes
|
|
379
|
+
if [ -f "$FEATURES" ]; then
|
|
380
|
+
jq --arg fid "$feature_id" '
|
|
381
|
+
.features |= map(
|
|
382
|
+
if .id == $fid then
|
|
383
|
+
.passes = ((.passes // []) + ["generator-frontend", "evaluator-functional"] | unique)
|
|
384
|
+
else . end
|
|
385
|
+
)
|
|
386
|
+
' "$FEATURES" > "${FEATURES}.tmp" && mv "${FEATURES}.tmp" "$FEATURES"
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
|
|
390
|
+
else
|
|
391
|
+
log "${RED}${BOLD}Merge failed — ${feature_id} marked as failed${RESET}"
|
|
392
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
393
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
394
|
+
log_progress "merge-fail" "${feature_id}"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
else
|
|
398
|
+
log "${RED}${BOLD}✗ ${feature_id} FAILED after ${MAX_ATTEMPTS} attempts${RESET}"
|
|
399
|
+
acquire_git_lock
|
|
400
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
401
|
+
release_git_lock
|
|
402
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
403
|
+
log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
sleep 2
|
|
407
|
+
done
|
|
@@ -60,6 +60,32 @@ disable-model-invocation: true
|
|
|
60
60
|
5. `actions/api-contract.json` — 기대 API 동작
|
|
61
61
|
6. `.harness/progress.json`
|
|
62
62
|
|
|
63
|
+
## v4 Feature-Level Mode (Parallel Agent Teams)
|
|
64
|
+
|
|
65
|
+
v4에서 Team Worker가 `claude -p`로 호출할 때, 프롬프트에 `FEATURE_ID`가 지정된다.
|
|
66
|
+
|
|
67
|
+
### Feature-Level Rules
|
|
68
|
+
- `feature-list.json`에서 **지정된 FEATURE_ID의 AC만** 검증
|
|
69
|
+
- Regression: `feature-queue.json`의 `passed` 목록에 있는 Feature들의 AC 재검증
|
|
70
|
+
- Cross-Validation: Feature 단위에서는 skip (Sprint-End에서 수행)
|
|
71
|
+
- Visual Evaluation: Feature 단위에서는 skip (Sprint-End에서 수행)
|
|
72
|
+
- 출력 형식: `---EVAL-RESULT---` 블록 (Worker가 파싱 가능)
|
|
73
|
+
|
|
74
|
+
### Feature-Level Scoring
|
|
75
|
+
- 동일한 R1-R5 루브릭 적용
|
|
76
|
+
- PASS 기준: 2.80/3.00 (변경 없음)
|
|
77
|
+
- 1건이라도 Regression 실패 시 FAIL (변경 없음)
|
|
78
|
+
|
|
79
|
+
### Output Format (Machine-Parseable)
|
|
80
|
+
```
|
|
81
|
+
---EVAL-RESULT---
|
|
82
|
+
FEATURE: F-XXX
|
|
83
|
+
VERDICT: PASS or FAIL
|
|
84
|
+
SCORE: X.XX
|
|
85
|
+
FEEDBACK: one paragraph summary
|
|
86
|
+
---END-EVAL-RESULT---
|
|
87
|
+
```
|
|
88
|
+
|
|
63
89
|
## Evaluation Steps
|
|
64
90
|
|
|
65
91
|
### Step 0: IA Structure Compliance (GATE)
|
|
@@ -43,6 +43,23 @@ disable-model-invocation: true
|
|
|
43
43
|
|
|
44
44
|
**Backend 통합 러너가 동작 중이어야 함.** Gateway 미응답 시 → STOP.
|
|
45
45
|
|
|
46
|
+
## v4 Feature-Level Mode (Parallel Agent Teams)
|
|
47
|
+
|
|
48
|
+
v4에서 Team Worker가 `claude -p`로 호출할 때, 프롬프트에 `FEATURE_ID`가 지정된다.
|
|
49
|
+
|
|
50
|
+
### Feature-Level Rules
|
|
51
|
+
- `feature-list.json`에서 **지정된 FEATURE_ID만** 필터하여 구현
|
|
52
|
+
- 다른 Feature의 코드를 수정하지 않음
|
|
53
|
+
- `depends_on`에 명시된 Feature는 이미 구현/머지 완료된 상태
|
|
54
|
+
- Feature branch (`feature/F-XXX`)에서 작업, 완료 시 commit
|
|
55
|
+
- Sprint Contract는 작성하지 않음 (v4에서는 Feature 단위로 관리)
|
|
56
|
+
|
|
57
|
+
### Feature-Level Prompt Template
|
|
58
|
+
Worker가 전달하는 프롬프트에는 다음이 포함됨:
|
|
59
|
+
- `FEATURE_ID`, `feature_name`, `description`, `ac` (Acceptance Criteria)
|
|
60
|
+
- `depends_on` (이미 완료된 의존 Feature 목록)
|
|
61
|
+
- Eval 재시도 시: 이전 Eval의 피드백
|
|
62
|
+
|
|
46
63
|
## Sprint Workflow
|
|
47
64
|
|
|
48
65
|
1. **Sprint Contract FE 섹션 추가** — 컴포넌트, API 연동, 성공 기준
|