@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.
@@ -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 4-pane monitor)
572
- npx walwal-harness studio --ai Studio + AI eval summary (API cost)
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.7.4",
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 연동, 성공 기준