@walwal-harness/cli 3.7.3 → 4.0.0-alpha.1

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