@walwal-harness/cli 4.0.0-alpha.12 → 4.0.0-alpha.14
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# For consumer projects, add .worktrees/ to .gitignore
|
package/bin/init.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.14",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-dashboard-v4.sh — v4 Dashboard
|
|
3
|
-
#
|
|
2
|
+
# harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
|
|
3
|
+
# Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
|
|
4
4
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
@@ -30,42 +30,34 @@ render_header() {
|
|
|
30
30
|
now=$(date +"%H:%M:%S")
|
|
31
31
|
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
32
32
|
|
|
33
|
-
echo -e "${BOLD}
|
|
34
|
-
echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
|
|
35
|
-
echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
|
|
36
|
-
echo -e " ${DIM}${project_name} | ${now}${RESET}"
|
|
37
|
-
echo ""
|
|
33
|
+
echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
render_queue_summary() {
|
|
41
37
|
if [ ! -f "$QUEUE" ]; then
|
|
42
|
-
echo -e " ${DIM}(queue not initialized
|
|
38
|
+
echo -e " ${DIM}(queue not initialized)${RESET}"
|
|
43
39
|
return
|
|
44
40
|
fi
|
|
45
41
|
|
|
46
|
-
local ready blocked in_prog passed failed total
|
|
42
|
+
local ready blocked in_prog passed failed total
|
|
47
43
|
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
48
44
|
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
49
45
|
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
50
46
|
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
51
47
|
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
52
|
-
concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
|
|
53
48
|
ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
|
|
54
49
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
55
50
|
|
|
56
|
-
# Progress bar
|
|
57
51
|
local pct=0
|
|
58
52
|
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
59
|
-
local bar_w=
|
|
53
|
+
local bar_w=16
|
|
60
54
|
local filled=$(( pct * bar_w / 100 ))
|
|
61
55
|
local empty=$(( bar_w - filled ))
|
|
62
56
|
local bar=""
|
|
63
57
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
64
58
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
65
59
|
|
|
66
|
-
echo -e " ${
|
|
67
|
-
echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
|
|
68
|
-
echo ""
|
|
60
|
+
echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
render_teams() {
|
|
@@ -75,49 +67,44 @@ render_teams() {
|
|
|
75
67
|
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
|
|
76
68
|
if [ "${team_count:-0}" -eq 0 ]; then return; fi
|
|
77
69
|
|
|
78
|
-
echo -e " ${BOLD}Teams${RESET}"
|
|
79
|
-
|
|
80
70
|
for i in $(seq 1 "$team_count"); do
|
|
81
71
|
local t_status t_feature t_phase t_attempt
|
|
82
72
|
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
83
73
|
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
84
74
|
|
|
85
|
-
# Get phase from in_progress
|
|
86
75
|
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
87
76
|
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
88
77
|
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
89
78
|
else
|
|
90
|
-
t_phase="—"
|
|
91
|
-
t_attempt="—"
|
|
79
|
+
t_phase="—"; t_attempt=""
|
|
92
80
|
fi
|
|
93
81
|
|
|
94
82
|
local icon color
|
|
95
83
|
case "$t_status" in
|
|
96
|
-
busy) icon="▶"
|
|
97
|
-
idle) icon="○"
|
|
98
|
-
|
|
99
|
-
*) icon="?" ; color="$RESET" ;;
|
|
84
|
+
busy) icon="▶"; color="$GREEN" ;;
|
|
85
|
+
idle) icon="○"; color="$DIM" ;;
|
|
86
|
+
*) icon="?"; color="$RESET" ;;
|
|
100
87
|
esac
|
|
101
88
|
|
|
102
|
-
local
|
|
89
|
+
local phase_short=""
|
|
103
90
|
case "$t_phase" in
|
|
104
|
-
gen)
|
|
105
|
-
gate)
|
|
106
|
-
eval)
|
|
107
|
-
*)
|
|
91
|
+
gen) phase_short="${CYAN}G${RESET}" ;;
|
|
92
|
+
gate) phase_short="${YELLOW}K${RESET}" ;;
|
|
93
|
+
eval) phase_short="${MAGENTA}E${RESET}" ;;
|
|
94
|
+
*) phase_short="${DIM}-${RESET}" ;;
|
|
108
95
|
esac
|
|
109
96
|
|
|
110
|
-
printf " %b
|
|
97
|
+
printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
|
|
98
|
+
if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
|
|
99
|
+
printf " #%s" "$t_attempt"
|
|
100
|
+
fi
|
|
101
|
+
echo ""
|
|
111
102
|
done
|
|
112
|
-
echo ""
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
105
|
+
render_features() {
|
|
116
106
|
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
117
107
|
|
|
118
|
-
echo -e " ${BOLD}Features${RESET}"
|
|
119
|
-
|
|
120
|
-
# Single jq call: merge feature-list + queue state → pre-formatted lines
|
|
121
108
|
jq -r --slurpfile q "$QUEUE" '
|
|
122
109
|
($q[0].queue.passed // []) as $passed |
|
|
123
110
|
($q[0].queue.failed // []) as $failed |
|
|
@@ -125,7 +112,7 @@ render_feature_list() {
|
|
|
125
112
|
($q[0].queue.in_progress // {}) as $prog |
|
|
126
113
|
.features[] |
|
|
127
114
|
.id as $fid |
|
|
128
|
-
(.name // .description // "?" | if length >
|
|
115
|
+
(.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
129
116
|
(if ($fid | IN($passed[])) then "P"
|
|
130
117
|
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
131
118
|
elif ($fid | IN($failed[])) then "F"
|
|
@@ -138,100 +125,25 @@ render_feature_list() {
|
|
|
138
125
|
F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
139
126
|
R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
140
127
|
B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
141
|
-
I\|*)
|
|
142
|
-
team=$(echo "$st" | cut -d'|' -f2)
|
|
128
|
+
I\|*) team=$(echo "$st" | cut -d'|' -f2)
|
|
143
129
|
phase=$(echo "$st" | cut -d'|' -f3)
|
|
144
|
-
printf " ${CYAN}◐${RESET} %-6s %-
|
|
130
|
+
printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
|
|
145
131
|
*) printf " ? %-6s %s\n" "$fid" "$fname" ;;
|
|
146
132
|
esac
|
|
147
133
|
done
|
|
148
|
-
|
|
149
|
-
echo ""
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
render_user_prompts() {
|
|
153
|
-
local log_file="$PROJECT_ROOT/.harness/progress.log"
|
|
154
|
-
if [ ! -f "$log_file" ]; then return; fi
|
|
155
|
-
|
|
156
|
-
# Extract user-prompt entries (newest first, max 5)
|
|
157
|
-
local user_lines
|
|
158
|
-
user_lines=$(grep 'user-prompt' "$log_file" 2>/dev/null | tail -r 2>/dev/null | head -5)
|
|
159
|
-
|
|
160
|
-
if [ -z "$user_lines" ]; then return; fi
|
|
161
|
-
|
|
162
|
-
echo -e " ${BOLD}Manual Prompts${RESET} ${DIM}(newest first)${RESET}"
|
|
163
|
-
|
|
164
|
-
echo "$user_lines" | while IFS= read -r line; do
|
|
165
|
-
local ts detail
|
|
166
|
-
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
167
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
168
|
-
|
|
169
|
-
# Short timestamp (HH:MM or MM-DD HH:MM)
|
|
170
|
-
local short_ts
|
|
171
|
-
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
172
|
-
|
|
173
|
-
if [ ${#detail} -gt 55 ]; then detail="${detail:0:53}.."; fi
|
|
174
|
-
|
|
175
|
-
echo -e " ${BOLD}★${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
176
|
-
done
|
|
177
|
-
|
|
178
|
-
echo ""
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
render_team_activity() {
|
|
182
|
-
local log_file="$PROJECT_ROOT/.harness/progress.log"
|
|
183
|
-
if [ ! -f "$log_file" ]; then return; fi
|
|
184
|
-
|
|
185
|
-
local max_lines=8
|
|
186
|
-
|
|
187
|
-
echo -e " ${BOLD}Activity${RESET} ${DIM}(newest first)${RESET}"
|
|
188
|
-
|
|
189
|
-
# All non-user-prompt entries, newest first
|
|
190
|
-
grep -v '^#' "$log_file" 2>/dev/null | grep -v '^$' | grep -v 'user-prompt' | \
|
|
191
|
-
tail -r 2>/dev/null | head -"$max_lines" | \
|
|
192
|
-
while IFS= read -r line; do
|
|
193
|
-
local ts agent action detail
|
|
194
|
-
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
195
|
-
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
196
|
-
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
197
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
198
|
-
|
|
199
|
-
local short_ts icon color
|
|
200
|
-
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
201
|
-
|
|
202
|
-
case "$agent" in
|
|
203
|
-
dispatcher*) icon="▸"; color="$MAGENTA" ;;
|
|
204
|
-
team-*) icon="⚡"; color="$CYAN" ;;
|
|
205
|
-
manual) icon="★"; color="$BOLD" ;;
|
|
206
|
-
planner*) icon="□"; color="$YELLOW" ;;
|
|
207
|
-
gen*) icon="▶"; color="$GREEN" ;;
|
|
208
|
-
eval*) icon="✦"; color="$RED" ;;
|
|
209
|
-
system) icon="⚙"; color="$DIM" ;;
|
|
210
|
-
*) icon="·"; color="$DIM" ;;
|
|
211
|
-
esac
|
|
212
|
-
|
|
213
|
-
if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
|
|
214
|
-
|
|
215
|
-
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
216
|
-
done
|
|
217
|
-
|
|
218
|
-
echo ""
|
|
219
134
|
}
|
|
220
135
|
|
|
221
136
|
render_all() {
|
|
222
137
|
render_header
|
|
223
138
|
render_queue_summary
|
|
224
139
|
render_teams
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
render_feature_list
|
|
228
|
-
echo -e " ${DIM}Refreshing every 3s${RESET}"
|
|
140
|
+
echo ""
|
|
141
|
+
render_features
|
|
229
142
|
}
|
|
230
143
|
|
|
231
144
|
# ── Main loop ──
|
|
232
145
|
tput civis 2>/dev/null
|
|
233
146
|
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
234
|
-
|
|
235
147
|
clear
|
|
236
148
|
|
|
237
149
|
while true; do
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-prompts-v4.sh — v4 Dashboard 하단: Manual Prompts + Activity
|
|
3
|
+
# progress.log에서 user-prompt와 team 활동을 newest-first로 표시.
|
|
4
|
+
# 스크롤 가능 영역 — 프롬프트가 많아져도 상단 Progress를 가리지 않음.
|
|
5
|
+
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="${1:-}"
|
|
11
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
12
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[prompts] .harness/ not found."; exit 1; }
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
LOG_FILE="$PROJECT_ROOT/.harness/progress.log"
|
|
17
|
+
|
|
18
|
+
BOLD="\033[1m"
|
|
19
|
+
DIM="\033[2m"
|
|
20
|
+
GREEN="\033[32m"
|
|
21
|
+
YELLOW="\033[33m"
|
|
22
|
+
RED="\033[31m"
|
|
23
|
+
CYAN="\033[36m"
|
|
24
|
+
MAGENTA="\033[35m"
|
|
25
|
+
RESET="\033[0m"
|
|
26
|
+
|
|
27
|
+
LAST_LINE_COUNT=0
|
|
28
|
+
|
|
29
|
+
render_prompts() {
|
|
30
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
31
|
+
echo -e " ${DIM}(progress.log not found)${RESET}"
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
local term_h
|
|
36
|
+
term_h=$(tput lines 2>/dev/null || echo 20)
|
|
37
|
+
local max_lines=$((term_h - 3)) # Leave room for header
|
|
38
|
+
if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
|
|
39
|
+
|
|
40
|
+
echo -e "${BOLD}Prompts & Activity${RESET} ${DIM}(newest first)${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
# Read all entries, reverse, limit to terminal height
|
|
44
|
+
grep -v '^#' "$LOG_FILE" 2>/dev/null | grep -v '^$' | \
|
|
45
|
+
tail -r 2>/dev/null | head -"$max_lines" | \
|
|
46
|
+
while IFS= read -r line; do
|
|
47
|
+
local ts agent action detail
|
|
48
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
49
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
50
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
51
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
52
|
+
|
|
53
|
+
local short_ts icon color
|
|
54
|
+
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
55
|
+
|
|
56
|
+
case "$agent" in
|
|
57
|
+
user-prompt)
|
|
58
|
+
icon="★"; color="$BOLD"
|
|
59
|
+
# User prompts get full width, highlighted
|
|
60
|
+
if [ ${#detail} -gt 60 ]; then detail="${detail:0:58}.."; fi
|
|
61
|
+
echo -e " ${color}${icon} ${short_ts}${RESET} ${detail}"
|
|
62
|
+
;;
|
|
63
|
+
dispatcher*)
|
|
64
|
+
icon="▸"; color="$MAGENTA"
|
|
65
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
66
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
67
|
+
;;
|
|
68
|
+
team-*)
|
|
69
|
+
icon="⚡"; color="$CYAN"
|
|
70
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
71
|
+
# Color by action type
|
|
72
|
+
case "$action" in
|
|
73
|
+
*pass*) color="$GREEN"; icon="✓" ;;
|
|
74
|
+
*fail*) color="$RED"; icon="✗" ;;
|
|
75
|
+
*eval*) color="$MAGENTA"; icon="✦" ;;
|
|
76
|
+
*gen*) color="$CYAN"; icon="▶" ;;
|
|
77
|
+
*merge*) color="$GREEN"; icon="⊕" ;;
|
|
78
|
+
esac
|
|
79
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
80
|
+
;;
|
|
81
|
+
manual)
|
|
82
|
+
icon="★"; color="$YELLOW"
|
|
83
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
84
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
85
|
+
;;
|
|
86
|
+
*)
|
|
87
|
+
icon="·"; color="$DIM"
|
|
88
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
89
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
done
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Main loop ──
|
|
96
|
+
tput civis 2>/dev/null
|
|
97
|
+
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
98
|
+
clear
|
|
99
|
+
|
|
100
|
+
while true; do
|
|
101
|
+
buf=$(render_prompts 2>&1)
|
|
102
|
+
tput cup 0 0 2>/dev/null
|
|
103
|
+
echo "$buf"
|
|
104
|
+
tput ed 2>/dev/null
|
|
105
|
+
sleep 3
|
|
106
|
+
done
|
|
@@ -72,11 +72,16 @@ fi
|
|
|
72
72
|
PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
|
|
73
73
|
-P -F '#{pane_id}')
|
|
74
74
|
|
|
75
|
-
# Column 2: Dashboard (split right from Main, 66% remaining
|
|
75
|
+
# Column 2: Dashboard Progress (split right from Main, 66% remaining)
|
|
76
76
|
PANE_DASH=$(tmux split-window -h -p 66 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
|
|
77
77
|
-P -F '#{pane_id}' \
|
|
78
78
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
79
79
|
|
|
80
|
+
# Dashboard bottom: Prompts & Activity (split below Dashboard, 40% bottom)
|
|
81
|
+
PANE_PROMPTS=$(tmux split-window -v -p 40 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
82
|
+
-P -F '#{pane_id}' \
|
|
83
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompts-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
84
|
+
|
|
80
85
|
# Column 3: Team 1 (split right from Dashboard, 50% of remaining = 33% total)
|
|
81
86
|
PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
82
87
|
-P -F '#{pane_id}' \
|
|
@@ -96,11 +101,12 @@ PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
|
|
|
96
101
|
tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
|
|
97
102
|
|
|
98
103
|
# ── Pane titles ──
|
|
99
|
-
tmux select-pane -t "$PANE_MAIN"
|
|
100
|
-
tmux select-pane -t "$PANE_DASH"
|
|
101
|
-
tmux select-pane -t "$
|
|
102
|
-
tmux select-pane -t "$
|
|
103
|
-
tmux select-pane -t "$
|
|
104
|
+
tmux select-pane -t "$PANE_MAIN" -T "Main"
|
|
105
|
+
tmux select-pane -t "$PANE_DASH" -T "Progress"
|
|
106
|
+
tmux select-pane -t "$PANE_PROMPTS" -T "Prompts"
|
|
107
|
+
tmux select-pane -t "$PANE_T1" -T "Team 1"
|
|
108
|
+
tmux select-pane -t "$PANE_T2" -T "Team 2"
|
|
109
|
+
tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
104
110
|
|
|
105
111
|
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
106
112
|
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-team-worker.sh — Team Worker:
|
|
2
|
+
# harness-team-worker.sh — Team Worker v4: git worktree 격리 실행
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# 각 Team이 독립 worktree에서 작업하여 git 충돌 없이 병렬 실행.
|
|
5
|
+
# Feature PASS → main merge → worktree 정리.
|
|
6
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 모델
|
|
7
|
+
# Usage: bash scripts/harness-team-worker.sh <team_id> [project-root]
|
|
14
8
|
|
|
15
9
|
set -uo pipefail
|
|
16
10
|
|
|
@@ -39,10 +33,11 @@ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
|
39
33
|
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
40
34
|
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
41
35
|
QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
|
|
42
|
-
|
|
43
|
-
# ── Lock file for git operations (prevent race conditions between teams) ──
|
|
44
36
|
GIT_LOCK="$PROJECT_ROOT/.harness/.git-lock"
|
|
45
37
|
|
|
38
|
+
# Worktree base directory
|
|
39
|
+
WORKTREE_DIR="$PROJECT_ROOT/.worktrees/team-${TEAM_ID}"
|
|
40
|
+
|
|
46
41
|
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
|
47
42
|
GEN_MODEL="${GEN_MODEL:-sonnet}"
|
|
48
43
|
EVAL_MODEL="${EVAL_MODEL:-opus}"
|
|
@@ -54,7 +49,6 @@ if [ -f "$CONFIG" ]; then
|
|
|
54
49
|
if [ -n "$_em" ]; then EVAL_MODEL="$_em"; fi
|
|
55
50
|
fi
|
|
56
51
|
|
|
57
|
-
# ── ANSI helpers ──
|
|
58
52
|
BOLD="\033[1m"
|
|
59
53
|
DIM="\033[2m"
|
|
60
54
|
GREEN="\033[32m"
|
|
@@ -64,42 +58,101 @@ CYAN="\033[36m"
|
|
|
64
58
|
RESET="\033[0m"
|
|
65
59
|
|
|
66
60
|
ts() { date +"%H:%M:%S"; }
|
|
61
|
+
log() { echo -e "[$(ts)] ${BOLD}T${TEAM_ID}${RESET} $*"; }
|
|
62
|
+
log_progress() { echo "$(date +"%Y-%m-%d %H:%M") | team-${TEAM_ID} | ${1} | ${2}" >> "$PROGRESS_LOG"; }
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
# ── Git lock ──
|
|
65
|
+
acquire_git_lock() {
|
|
66
|
+
local waited=0
|
|
67
|
+
while ! mkdir "$GIT_LOCK" 2>/dev/null; do
|
|
68
|
+
sleep 0.2
|
|
69
|
+
waited=$((waited + 1))
|
|
70
|
+
if [ "$waited" -ge 150 ]; then rm -rf "$GIT_LOCK"; mkdir "$GIT_LOCK" 2>/dev/null || true; break; fi
|
|
71
|
+
done
|
|
70
72
|
}
|
|
73
|
+
release_git_lock() { rm -rf "$GIT_LOCK" 2>/dev/null || true; }
|
|
74
|
+
|
|
75
|
+
# ── Worktree management ──
|
|
76
|
+
setup_worktree() {
|
|
77
|
+
local branch="$1"
|
|
78
|
+
|
|
79
|
+
acquire_git_lock
|
|
80
|
+
|
|
81
|
+
# Clean previous worktree if exists
|
|
82
|
+
if [ -d "$WORKTREE_DIR" ]; then
|
|
83
|
+
(cd "$PROJECT_ROOT" && git worktree remove "$WORKTREE_DIR" --force 2>/dev/null) || rm -rf "$WORKTREE_DIR"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Create fresh worktree from main
|
|
87
|
+
(cd "$PROJECT_ROOT" && git worktree add "$WORKTREE_DIR" -b "$branch" main 2>/dev/null) || \
|
|
88
|
+
(cd "$PROJECT_ROOT" && git worktree add "$WORKTREE_DIR" "$branch" 2>/dev/null) || {
|
|
89
|
+
release_git_lock
|
|
90
|
+
log "${RED}Failed to create worktree${RESET}"
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
release_git_lock
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
# Copy .harness to worktree (symlink for shared state)
|
|
97
|
+
ln -sf "$PROJECT_ROOT/.harness" "$WORKTREE_DIR/.harness" 2>/dev/null || true
|
|
98
|
+
|
|
99
|
+
log "Worktree: ${WORKTREE_DIR}"
|
|
100
|
+
return 0
|
|
74
101
|
}
|
|
75
102
|
|
|
76
|
-
|
|
77
|
-
acquire_git_lock
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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"
|
|
103
|
+
cleanup_worktree() {
|
|
104
|
+
acquire_git_lock
|
|
105
|
+
if [ -d "$WORKTREE_DIR" ]; then
|
|
106
|
+
(cd "$PROJECT_ROOT" && git worktree remove "$WORKTREE_DIR" --force 2>/dev/null) || rm -rf "$WORKTREE_DIR"
|
|
107
|
+
fi
|
|
108
|
+
release_git_lock
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
merge_to_main() {
|
|
112
|
+
local branch="$1"
|
|
113
|
+
|
|
114
|
+
acquire_git_lock
|
|
115
|
+
|
|
116
|
+
local merge_ok=false
|
|
117
|
+
|
|
118
|
+
# Try merge
|
|
119
|
+
if (cd "$PROJECT_ROOT" && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
|
|
120
|
+
merge_ok=true
|
|
121
|
+
else
|
|
122
|
+
# Conflict → abort, then try rebase in worktree
|
|
123
|
+
(cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
|
|
124
|
+
|
|
125
|
+
log "${YELLOW}Merge conflict — rebasing in worktree...${RESET}"
|
|
126
|
+
if (cd "$WORKTREE_DIR" && git rebase main 2>/dev/null); then
|
|
127
|
+
# Retry merge after rebase
|
|
128
|
+
if (cd "$PROJECT_ROOT" && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS (rebased)" 2>/dev/null); then
|
|
129
|
+
merge_ok=true
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
(cd "$WORKTREE_DIR" && git rebase --abort 2>/dev/null) || true
|
|
133
|
+
log "${RED}Rebase failed${RESET}"
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Clean up branch after merge
|
|
138
|
+
if [ "$merge_ok" = true ]; then
|
|
139
|
+
(cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
release_git_lock
|
|
143
|
+
|
|
144
|
+
[ "$merge_ok" = true ]
|
|
93
145
|
}
|
|
94
146
|
|
|
95
|
-
# ── Pre-eval gate ──
|
|
147
|
+
# ── Pre-eval gate (runs in worktree) ──
|
|
96
148
|
run_pre_eval_gate() {
|
|
97
|
-
local
|
|
149
|
+
local work_dir="$WORKTREE_DIR"
|
|
98
150
|
|
|
151
|
+
# Resolve cwd within worktree
|
|
99
152
|
if [ -f "$CONFIG" ]; then
|
|
100
153
|
_cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
|
|
101
154
|
if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
|
|
102
|
-
|
|
155
|
+
work_dir="$WORKTREE_DIR/$_cwd"
|
|
103
156
|
fi
|
|
104
157
|
fi
|
|
105
158
|
|
|
@@ -107,26 +160,24 @@ run_pre_eval_gate() {
|
|
|
107
160
|
if [ -f "$CONFIG" ]; then
|
|
108
161
|
mapfile -t checks < <(jq -r '.flow.pre_eval_gate.frontend_checks[]' "$CONFIG" 2>/dev/null)
|
|
109
162
|
fi
|
|
110
|
-
|
|
111
163
|
if [ ${#checks[@]} -eq 0 ]; then
|
|
112
164
|
checks=("npx tsc --noEmit" "npx eslint src/")
|
|
113
165
|
fi
|
|
114
166
|
|
|
115
|
-
local all_pass=true
|
|
167
|
+
local all_pass=true
|
|
116
168
|
for cmd in "${checks[@]}"; do
|
|
117
|
-
if (cd "$
|
|
169
|
+
if (cd "$work_dir" && timeout 120s bash -c "$cmd" >/dev/null 2>&1); then
|
|
118
170
|
log " ${GREEN}✓${RESET} $cmd"
|
|
119
171
|
else
|
|
120
172
|
log " ${RED}✗${RESET} $cmd"
|
|
121
173
|
all_pass=false
|
|
122
|
-
fail_cmds+="$cmd; "
|
|
123
174
|
fi
|
|
124
175
|
done
|
|
125
176
|
|
|
126
177
|
[ "$all_pass" = true ]
|
|
127
178
|
}
|
|
128
179
|
|
|
129
|
-
# ── Build
|
|
180
|
+
# ── Build prompts ──
|
|
130
181
|
build_gen_prompt() {
|
|
131
182
|
local fid="$1" attempt="$2" feedback="${3:-}"
|
|
132
183
|
|
|
@@ -161,7 +212,7 @@ RULES:
|
|
|
161
212
|
- Implement ONLY this single feature
|
|
162
213
|
- Do NOT modify code belonging to other features
|
|
163
214
|
- Follow existing code patterns and CONVENTIONS.md
|
|
164
|
-
- When done, stage and commit
|
|
215
|
+
- When done, stage and commit: git add -A && git commit -m 'feat(${fid}): ${fname}'
|
|
165
216
|
PROMPT
|
|
166
217
|
|
|
167
218
|
if [ "$attempt" -gt 1 ] && [ -n "$feedback" ]; then
|
|
@@ -175,7 +226,6 @@ RETRY
|
|
|
175
226
|
fi
|
|
176
227
|
}
|
|
177
228
|
|
|
178
|
-
# ── Build evaluator prompt ──
|
|
179
229
|
build_eval_prompt() {
|
|
180
230
|
local fid="$1"
|
|
181
231
|
|
|
@@ -218,15 +268,12 @@ FEEDBACK: one paragraph summary
|
|
|
218
268
|
PROMPT
|
|
219
269
|
}
|
|
220
270
|
|
|
221
|
-
# ── Parse eval result (macOS-compatible, no grep -P) ──
|
|
222
271
|
parse_eval_result() {
|
|
223
272
|
local output="$1"
|
|
224
|
-
|
|
225
273
|
local verdict score feedback
|
|
226
274
|
verdict=$(echo "$output" | grep -E '^VERDICT:' | sed 's/VERDICT:[[:space:]]*//' | head -1)
|
|
227
275
|
score=$(echo "$output" | grep -E '^SCORE:' | sed 's/SCORE:[[:space:]]*//' | head -1)
|
|
228
276
|
feedback=$(echo "$output" | grep -E '^FEEDBACK:' | sed 's/FEEDBACK:[[:space:]]*//' | head -1)
|
|
229
|
-
|
|
230
277
|
echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
|
|
231
278
|
}
|
|
232
279
|
|
|
@@ -237,14 +284,12 @@ log "${CYAN}Team ${TEAM_ID} started${RESET} (gen=${GEN_MODEL}, eval=${EVAL_MODEL
|
|
|
237
284
|
log_progress "start" "Team ${TEAM_ID} worker started"
|
|
238
285
|
|
|
239
286
|
while true; do
|
|
240
|
-
# ── Dequeue
|
|
287
|
+
# ── Dequeue ──
|
|
241
288
|
feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
|
|
242
289
|
|
|
243
290
|
if [ -z "$feature_id" ] || [[ "$feature_id" == "["* ]]; then
|
|
244
291
|
log "${DIM}No features ready. Waiting 10s...${RESET}"
|
|
245
292
|
sleep 10
|
|
246
|
-
|
|
247
|
-
# Check if completely done
|
|
248
293
|
remaining=$(jq '(.queue.ready | length) + (.queue.blocked | length) + (.queue.in_progress | length)' "$QUEUE" 2>/dev/null || echo "1")
|
|
249
294
|
if [ "${remaining}" -eq 0 ] 2>/dev/null; then
|
|
250
295
|
log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
|
|
@@ -254,16 +299,16 @@ while true; do
|
|
|
254
299
|
continue
|
|
255
300
|
fi
|
|
256
301
|
|
|
257
|
-
log "${CYAN}▶
|
|
302
|
+
log "${CYAN}▶ ${feature_id}${RESET}"
|
|
258
303
|
log_progress "dequeue" "${feature_id}"
|
|
259
304
|
|
|
260
|
-
# ──
|
|
305
|
+
# ── Setup worktree ──
|
|
261
306
|
branch="feature/${feature_id}"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
307
|
+
if ! setup_worktree "$branch"; then
|
|
308
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
309
|
+
log_progress "fail" "${feature_id} worktree setup failed"
|
|
310
|
+
continue
|
|
311
|
+
fi
|
|
267
312
|
|
|
268
313
|
# ── Gen→Eval Loop ──
|
|
269
314
|
attempt=1
|
|
@@ -271,119 +316,82 @@ while true; do
|
|
|
271
316
|
passed=false
|
|
272
317
|
|
|
273
318
|
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
|
274
|
-
log "${BOLD}──
|
|
319
|
+
log "${BOLD}── ${feature_id} attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
|
|
275
320
|
|
|
276
|
-
# ── Generate ──
|
|
277
|
-
log "Gen
|
|
321
|
+
# ── Generate (in worktree) ──
|
|
322
|
+
log "Gen (${GEN_MODEL})..."
|
|
278
323
|
bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
279
324
|
|
|
280
325
|
gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
|
|
281
326
|
|
|
282
327
|
gen_start=$(date +%s)
|
|
283
|
-
|
|
284
|
-
gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" \
|
|
328
|
+
gen_output=$(cd "$WORKTREE_DIR" && claude -p "$gen_prompt" \
|
|
285
329
|
--dangerously-skip-permissions \
|
|
286
330
|
--model "$GEN_MODEL" \
|
|
287
331
|
--output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
|
|
288
332
|
gen_elapsed=$(( $(date +%s) - gen_start ))
|
|
289
333
|
|
|
290
|
-
files_changed=$(cd "$
|
|
334
|
+
files_changed=$(cd "$WORKTREE_DIR" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
|
|
291
335
|
log "Gen done (${gen_elapsed}s) — ${files_changed} files"
|
|
292
|
-
log_progress "gen" "${feature_id}
|
|
336
|
+
log_progress "gen" "${feature_id} #${attempt}: ${files_changed} files, ${gen_elapsed}s"
|
|
293
337
|
|
|
294
|
-
# Auto-commit
|
|
295
|
-
(cd "$
|
|
338
|
+
# Auto-commit in worktree
|
|
339
|
+
(cd "$WORKTREE_DIR" && git add -A && git commit -m "feat(${feature_id}): attempt ${attempt}" --no-verify 2>/dev/null) || true
|
|
296
340
|
|
|
297
|
-
# ── Pre-eval gate ──
|
|
298
|
-
log "
|
|
341
|
+
# ── Pre-eval gate (in worktree) ──
|
|
342
|
+
log "Gate..."
|
|
299
343
|
bash "$QUEUE_MGR" update_phase "$feature_id" "gate" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
300
344
|
|
|
301
|
-
if ! run_pre_eval_gate
|
|
302
|
-
log "${RED}Gate FAIL
|
|
303
|
-
eval_feedback="Pre-eval gate failed: type check or lint errors.
|
|
345
|
+
if ! run_pre_eval_gate; then
|
|
346
|
+
log "${RED}Gate FAIL${RESET}"
|
|
347
|
+
eval_feedback="Pre-eval gate failed: type check or lint errors."
|
|
304
348
|
attempt=$((attempt + 1))
|
|
305
349
|
continue
|
|
306
350
|
fi
|
|
307
351
|
|
|
308
|
-
# ── Evaluate ──
|
|
309
|
-
log "Eval
|
|
352
|
+
# ── Evaluate (in worktree) ──
|
|
353
|
+
log "Eval (${EVAL_MODEL})..."
|
|
310
354
|
bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
311
355
|
|
|
312
356
|
eval_prompt=$(build_eval_prompt "$feature_id")
|
|
313
357
|
|
|
314
358
|
eval_start=$(date +%s)
|
|
315
|
-
|
|
316
|
-
eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" \
|
|
359
|
+
eval_output=$(cd "$WORKTREE_DIR" && claude -p "$eval_prompt" \
|
|
317
360
|
--dangerously-skip-permissions \
|
|
318
361
|
--model "$EVAL_MODEL" \
|
|
319
362
|
--output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
|
|
320
363
|
eval_elapsed=$(( $(date +%s) - eval_start ))
|
|
321
364
|
|
|
322
|
-
# Parse result
|
|
323
365
|
result_line=$(parse_eval_result "$eval_output")
|
|
324
366
|
verdict=$(echo "$result_line" | cut -d'|' -f1)
|
|
325
367
|
score=$(echo "$result_line" | cut -d'|' -f2)
|
|
326
368
|
feedback=$(echo "$result_line" | cut -d'|' -f3-)
|
|
327
369
|
|
|
328
|
-
log_progress "eval" "${feature_id}
|
|
370
|
+
log_progress "eval" "${feature_id} #${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
|
|
329
371
|
|
|
330
372
|
if [ "$verdict" = "PASS" ]; then
|
|
331
|
-
log "${GREEN}${BOLD}✓ PASS
|
|
373
|
+
log "${GREEN}${BOLD}✓ PASS ${score}/3.00${RESET} (${eval_elapsed}s)"
|
|
332
374
|
passed=true
|
|
333
375
|
break
|
|
334
376
|
else
|
|
335
|
-
log "${RED}✗ FAIL
|
|
377
|
+
log "${RED}✗ FAIL ${score}/3.00${RESET} (${eval_elapsed}s)"
|
|
336
378
|
log "${DIM} ${feedback}${RESET}"
|
|
337
379
|
eval_feedback="$feedback"
|
|
338
380
|
attempt=$((attempt + 1))
|
|
339
381
|
fi
|
|
340
382
|
done
|
|
341
383
|
|
|
342
|
-
#
|
|
343
|
-
# Phase 3: Branch merge with conflict handling
|
|
344
|
-
# ══════════════════════════════════════════
|
|
384
|
+
# ── Result ──
|
|
345
385
|
if [ "$passed" = true ]; then
|
|
346
|
-
log "Merging
|
|
347
|
-
acquire_git_lock
|
|
348
|
-
|
|
349
|
-
merge_ok=false
|
|
350
|
-
|
|
351
|
-
# Attempt 1: straight merge
|
|
352
|
-
if (cd "$PROJECT_ROOT" && git checkout main 2>/dev/null && git merge --no-ff "$branch" -m "merge: ${feature_id} PASS" 2>/dev/null); then
|
|
353
|
-
merge_ok=true
|
|
354
|
-
else
|
|
355
|
-
# Attempt 2: abort failed merge, rebase, re-eval gate, then merge
|
|
356
|
-
log "${YELLOW}Conflict detected — rebasing ${branch} onto main...${RESET}"
|
|
357
|
-
(cd "$PROJECT_ROOT" && git merge --abort 2>/dev/null) || true
|
|
358
|
-
(cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
|
|
359
|
-
|
|
360
|
-
if (cd "$PROJECT_ROOT" && git rebase main 2>/dev/null); then
|
|
361
|
-
log "Rebase OK. Re-running gate..."
|
|
362
|
-
|
|
363
|
-
if run_pre_eval_gate "$feature_id"; then
|
|
364
|
-
log "Gate still PASS after rebase."
|
|
365
|
-
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
|
|
366
|
-
merge_ok=true
|
|
367
|
-
fi
|
|
368
|
-
else
|
|
369
|
-
log "${RED}Gate FAIL after rebase — needs re-gen${RESET}"
|
|
370
|
-
fi
|
|
371
|
-
else
|
|
372
|
-
log "${RED}Rebase failed — conflicts too complex${RESET}"
|
|
373
|
-
(cd "$PROJECT_ROOT" && git rebase --abort 2>/dev/null) || true
|
|
374
|
-
fi
|
|
375
|
-
fi
|
|
376
|
-
|
|
377
|
-
release_git_lock
|
|
378
|
-
|
|
379
|
-
if [ "$merge_ok" = true ]; then
|
|
380
|
-
# Clean up feature branch
|
|
381
|
-
(cd "$PROJECT_ROOT" && git branch -d "$branch" 2>/dev/null) || true
|
|
386
|
+
log "Merging → main..."
|
|
382
387
|
|
|
388
|
+
if merge_to_main "$branch"; then
|
|
389
|
+
# Cleanup worktree after successful merge
|
|
390
|
+
cleanup_worktree
|
|
383
391
|
bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
384
|
-
log_progress "pass" "${feature_id} merged
|
|
392
|
+
log_progress "pass" "${feature_id} merged & cleaned"
|
|
385
393
|
|
|
386
|
-
# Update feature-list.json
|
|
394
|
+
# Update feature-list.json
|
|
387
395
|
if [ -f "$FEATURES" ]; then
|
|
388
396
|
jq --arg fid "$feature_id" '
|
|
389
397
|
.features |= map(
|
|
@@ -396,18 +404,15 @@ while true; do
|
|
|
396
404
|
|
|
397
405
|
log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
|
|
398
406
|
else
|
|
399
|
-
|
|
400
|
-
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
407
|
+
cleanup_worktree
|
|
401
408
|
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
409
|
+
log "${RED}Merge failed → ${feature_id} FAILED${RESET}"
|
|
402
410
|
log_progress "merge-fail" "${feature_id}"
|
|
403
411
|
fi
|
|
404
|
-
|
|
405
412
|
else
|
|
406
|
-
|
|
407
|
-
acquire_git_lock
|
|
408
|
-
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
409
|
-
release_git_lock
|
|
413
|
+
cleanup_worktree
|
|
410
414
|
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
415
|
+
log "${RED}${BOLD}✗ ${feature_id} FAILED (${MAX_ATTEMPTS} attempts)${RESET}"
|
|
411
416
|
log_progress "fail" "${feature_id} after ${MAX_ATTEMPTS} attempts"
|
|
412
417
|
fi
|
|
413
418
|
|