@walwal-harness/cli 4.0.0-alpha.1 → 4.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/templates/config.json +11 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-v4.sh +108 -43
- package/scripts/harness-queue-manager.sh +59 -5
- package/scripts/harness-session-start.sh +18 -0
- package/scripts/harness-studio-v4.sh +36 -29
- package/scripts/harness-team-worker.sh +169 -113
- package/scripts/harness-user-prompt-submit.sh +31 -1
- package/skills/evaluator-functional/SKILL.md +26 -0
- package/skills/generator-frontend/SKILL.md +17 -0
|
@@ -209,6 +209,17 @@
|
|
|
209
209
|
"frontend_cwd": "",
|
|
210
210
|
"timeout_seconds": 120,
|
|
211
211
|
"on_fail": "reroute_to_generator"
|
|
212
|
+
},
|
|
213
|
+
"parallel": {
|
|
214
|
+
"comment": "v4 Parallel Agent Teams 설정. npx walwal-harness v4 로 실행.",
|
|
215
|
+
"enabled": false,
|
|
216
|
+
"concurrency": 3,
|
|
217
|
+
"max_attempts_per_feature": 3,
|
|
218
|
+
"gen_model": "sonnet",
|
|
219
|
+
"eval_model": "opus",
|
|
220
|
+
"branch_strategy": "feature-branch",
|
|
221
|
+
"merge_on_pass": true,
|
|
222
|
+
"rebase_on_conflict": true
|
|
212
223
|
}
|
|
213
224
|
},
|
|
214
225
|
"evaluation": {
|
package/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.10",
|
|
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"
|
|
@@ -44,18 +44,21 @@ render_queue_summary() {
|
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
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)
|
|
47
|
+
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
48
|
+
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
49
|
+
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
50
|
+
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
51
|
+
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
52
|
+
concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
|
|
53
|
+
ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
|
|
53
54
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
54
55
|
|
|
55
56
|
# Progress bar
|
|
56
57
|
local pct=0
|
|
57
58
|
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
58
|
-
local bar_w=20
|
|
59
|
+
local bar_w=20
|
|
60
|
+
local filled=$(( pct * bar_w / 100 ))
|
|
61
|
+
local empty=$(( bar_w - filled ))
|
|
59
62
|
local bar=""
|
|
60
63
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
61
64
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
@@ -112,45 +115,106 @@ render_teams() {
|
|
|
112
115
|
render_feature_list() {
|
|
113
116
|
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
114
117
|
|
|
115
|
-
local total
|
|
116
|
-
total=$(jq '.features | length' "$FEATURES" 2>/dev/null)
|
|
117
|
-
if [ "${total:-0}" -eq 0 ]; then return; fi
|
|
118
|
-
|
|
119
118
|
echo -e " ${BOLD}Features${RESET}"
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
# Single jq call: merge feature-list + queue state → pre-formatted lines
|
|
121
|
+
jq -r --slurpfile q "$QUEUE" '
|
|
122
|
+
($q[0].queue.passed // []) as $passed |
|
|
123
|
+
($q[0].queue.failed // []) as $failed |
|
|
124
|
+
($q[0].queue.ready // []) as $ready |
|
|
125
|
+
($q[0].queue.in_progress // {}) as $prog |
|
|
126
|
+
.features[] |
|
|
127
|
+
.id as $fid |
|
|
128
|
+
(.name // .description // "?" | if length > 22 then .[0:20] + ".." else . end) as $fname |
|
|
129
|
+
(if ($fid | IN($passed[])) then "P"
|
|
130
|
+
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
131
|
+
elif ($fid | IN($failed[])) then "F"
|
|
132
|
+
elif ($fid | IN($ready[])) then "R"
|
|
133
|
+
else "B" end) as $st |
|
|
134
|
+
"\($st)\t\($fid)\t\($fname)"
|
|
135
|
+
' "$FEATURES" 2>/dev/null | while IFS=$'\t' read -r st fid fname; do
|
|
136
|
+
case "$st" in
|
|
137
|
+
P) printf " ${GREEN}●${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
138
|
+
F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
139
|
+
R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
140
|
+
B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
141
|
+
I\|*) # in_progress: extract team and phase
|
|
142
|
+
team=$(echo "$st" | cut -d'|' -f2)
|
|
143
|
+
phase=$(echo "$st" | cut -d'|' -f3)
|
|
144
|
+
printf " ${CYAN}◐${RESET} %-6s %-18s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
|
|
145
|
+
*) printf " ? %-6s %s\n" "$fid" "$fname" ;;
|
|
146
|
+
esac
|
|
147
|
+
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}"
|
|
149
163
|
|
|
150
|
-
|
|
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}')
|
|
151
168
|
|
|
152
|
-
|
|
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}"
|
|
153
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
|
+
|
|
154
218
|
echo ""
|
|
155
219
|
}
|
|
156
220
|
|
|
@@ -158,6 +222,8 @@ render_all() {
|
|
|
158
222
|
render_header
|
|
159
223
|
render_queue_summary
|
|
160
224
|
render_teams
|
|
225
|
+
render_user_prompts
|
|
226
|
+
render_team_activity
|
|
161
227
|
render_feature_list
|
|
162
228
|
echo -e " ${DIM}Refreshing every 3s${RESET}"
|
|
163
229
|
}
|
|
@@ -169,8 +235,7 @@ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
|
169
235
|
clear
|
|
170
236
|
|
|
171
237
|
while true; do
|
|
172
|
-
|
|
173
|
-
buf=$(render_all 2>/dev/null)
|
|
238
|
+
buf=$(render_all 2>&1)
|
|
174
239
|
tput cup 0 0 2>/dev/null
|
|
175
240
|
echo "$buf"
|
|
176
241
|
tput ed 2>/dev/null
|
|
@@ -42,6 +42,25 @@ fi
|
|
|
42
42
|
FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
43
43
|
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
44
44
|
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
45
|
+
QUEUE_LOCK="$PROJECT_ROOT/.harness/.queue-lock"
|
|
46
|
+
|
|
47
|
+
# ── Atomic queue lock — prevent race conditions between teams ──
|
|
48
|
+
acquire_queue_lock() {
|
|
49
|
+
local max_wait=30 waited=0
|
|
50
|
+
while ! mkdir "$QUEUE_LOCK" 2>/dev/null; do
|
|
51
|
+
sleep 0.1
|
|
52
|
+
waited=$((waited + 1))
|
|
53
|
+
if [ "$waited" -ge $((max_wait * 10)) ]; then
|
|
54
|
+
rm -rf "$QUEUE_LOCK"
|
|
55
|
+
mkdir "$QUEUE_LOCK" 2>/dev/null || true
|
|
56
|
+
break
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
release_queue_lock() {
|
|
62
|
+
rm -rf "$QUEUE_LOCK" 2>/dev/null || true
|
|
63
|
+
}
|
|
45
64
|
|
|
46
65
|
# ── Concurrency from config ──
|
|
47
66
|
CONCURRENCY=3
|
|
@@ -122,12 +141,14 @@ cmd_dequeue() {
|
|
|
122
141
|
if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
|
|
123
142
|
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
124
143
|
|
|
144
|
+
acquire_queue_lock
|
|
145
|
+
|
|
125
146
|
local feature
|
|
126
147
|
feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
|
|
127
148
|
|
|
128
149
|
if [ -z "$feature" ]; then
|
|
150
|
+
release_queue_lock
|
|
129
151
|
echo "[queue] No features in ready queue."
|
|
130
|
-
# Check if all done
|
|
131
152
|
local in_prog blocked
|
|
132
153
|
in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
|
|
133
154
|
blocked=$(jq '.queue.blocked | length' "$QUEUE")
|
|
@@ -137,13 +158,13 @@ cmd_dequeue() {
|
|
|
137
158
|
return 1
|
|
138
159
|
fi
|
|
139
160
|
|
|
140
|
-
# Move feature from ready → in_progress, assign to team
|
|
141
161
|
jq --arg fid "$feature" --arg tid "$team_id" '
|
|
142
162
|
.queue.ready -= [$fid] |
|
|
143
163
|
.queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
|
|
144
164
|
.teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
|
|
145
165
|
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
146
166
|
|
|
167
|
+
release_queue_lock
|
|
147
168
|
echo "$feature"
|
|
148
169
|
}
|
|
149
170
|
|
|
@@ -155,11 +176,11 @@ cmd_pass() {
|
|
|
155
176
|
if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
|
|
156
177
|
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
157
178
|
|
|
158
|
-
|
|
179
|
+
acquire_queue_lock
|
|
180
|
+
|
|
159
181
|
local team_id
|
|
160
182
|
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
161
183
|
|
|
162
|
-
# Move from in_progress → passed, free team, unblock dependents
|
|
163
184
|
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
164
185
|
# Remove from in_progress
|
|
165
186
|
del(.queue.in_progress[$fid]) |
|
|
@@ -188,6 +209,7 @@ cmd_pass() {
|
|
|
188
209
|
|
|
189
210
|
local newly_ready
|
|
190
211
|
newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
|
|
212
|
+
release_queue_lock
|
|
191
213
|
echo "[queue] $fid PASSED. Ready: [$newly_ready]"
|
|
192
214
|
}
|
|
193
215
|
|
|
@@ -199,6 +221,8 @@ cmd_fail() {
|
|
|
199
221
|
if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
|
|
200
222
|
if [ ! -f "$QUEUE" ]; then exit 1; fi
|
|
201
223
|
|
|
224
|
+
acquire_queue_lock
|
|
225
|
+
|
|
202
226
|
local team_id
|
|
203
227
|
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
204
228
|
|
|
@@ -211,6 +235,7 @@ cmd_fail() {
|
|
|
211
235
|
else . end)
|
|
212
236
|
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
213
237
|
|
|
238
|
+
release_queue_lock
|
|
214
239
|
echo "[queue] $fid FAILED."
|
|
215
240
|
}
|
|
216
241
|
|
|
@@ -239,6 +264,8 @@ cmd_update_phase() {
|
|
|
239
264
|
exit 1
|
|
240
265
|
fi
|
|
241
266
|
|
|
267
|
+
acquire_queue_lock
|
|
268
|
+
|
|
242
269
|
local jq_expr
|
|
243
270
|
jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
|
|
244
271
|
if [ -n "$attempt" ]; then
|
|
@@ -246,6 +273,7 @@ cmd_update_phase() {
|
|
|
246
273
|
fi
|
|
247
274
|
|
|
248
275
|
jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
276
|
+
release_queue_lock
|
|
249
277
|
}
|
|
250
278
|
|
|
251
279
|
# ══════════════════════════════════════════
|
|
@@ -297,6 +325,31 @@ cmd_status() {
|
|
|
297
325
|
fi
|
|
298
326
|
}
|
|
299
327
|
|
|
328
|
+
# ══════════════════════════════════════════
|
|
329
|
+
# recover — Move stale in_progress back to ready (after studio restart)
|
|
330
|
+
# ══════════════════════════════════════════
|
|
331
|
+
cmd_recover() {
|
|
332
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Not initialized."; return; fi
|
|
333
|
+
|
|
334
|
+
local stale_count
|
|
335
|
+
stale_count=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
|
|
336
|
+
|
|
337
|
+
if [ "${stale_count:-0}" -eq 0 ]; then
|
|
338
|
+
echo "[queue] No stale in_progress entries."
|
|
339
|
+
return
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Move all in_progress → ready, reset all teams to idle
|
|
343
|
+
jq '
|
|
344
|
+
.queue.ready += [.queue.in_progress | keys[]] |
|
|
345
|
+
.queue.ready |= unique |
|
|
346
|
+
.queue.in_progress = {} |
|
|
347
|
+
.teams |= with_entries(.value = { status: "idle", feature: null, branch: null, pid: null })
|
|
348
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
349
|
+
|
|
350
|
+
echo "[queue] Recovered ${stale_count} stale features back to ready queue."
|
|
351
|
+
}
|
|
352
|
+
|
|
300
353
|
# ── Dispatch ──
|
|
301
354
|
case "$CMD" in
|
|
302
355
|
init) cmd_init ;;
|
|
@@ -305,9 +358,10 @@ case "$CMD" in
|
|
|
305
358
|
fail) cmd_fail "$@" ;;
|
|
306
359
|
requeue) cmd_requeue "$@" ;;
|
|
307
360
|
update_phase) cmd_update_phase "$@" ;;
|
|
361
|
+
recover) cmd_recover ;;
|
|
308
362
|
status) cmd_status ;;
|
|
309
363
|
*)
|
|
310
|
-
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|update_phase|status> [args]"
|
|
364
|
+
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|recover|update_phase|status> [args]"
|
|
311
365
|
exit 1
|
|
312
366
|
;;
|
|
313
367
|
esac
|
|
@@ -26,6 +26,24 @@ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
|
26
26
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
27
27
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
28
28
|
|
|
29
|
+
# ─────────────────────────────────────────
|
|
30
|
+
# v4 Parallel Mode — feature-queue.json이 존재하면 v4 안내
|
|
31
|
+
# ─────────────────────────────────────────
|
|
32
|
+
FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
33
|
+
if [ -f "$FEATURE_QUEUE" ]; then
|
|
34
|
+
passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
35
|
+
total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
36
|
+
in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
37
|
+
failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
38
|
+
|
|
39
|
+
echo "# Harness v4 — Parallel Agent Teams active"
|
|
40
|
+
echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
|
|
41
|
+
echo "# Teams are running autonomously. You are the orchestrator."
|
|
42
|
+
echo "# Role: Monitor dashboard, resolve failures, manual interventions only."
|
|
43
|
+
echo "# Do NOT run /harness-generator-* or /harness-evaluator-* — Teams handle Gen+Eval."
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
29
47
|
# ─────────────────────────────────────────
|
|
30
48
|
# init 상태: 첫 안내
|
|
31
49
|
# ─────────────────────────────────────────
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-studio-v4.sh — Harness Studio v4:
|
|
2
|
+
# harness-studio-v4.sh — Harness Studio v4: 3-Column Layout
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
# │
|
|
6
|
-
# │
|
|
7
|
-
# │
|
|
8
|
-
#
|
|
9
|
-
# │
|
|
10
|
-
#
|
|
11
|
-
#
|
|
4
|
+
# ┌──────────────┬──────────────┬──────────────┐
|
|
5
|
+
# │ │ │ Team 1 │
|
|
6
|
+
# │ │ ├──────────────┤
|
|
7
|
+
# │ Main │ Dashboard │ Team 2 │
|
|
8
|
+
# │ (Claude) │ (read-only) ├──────────────┤
|
|
9
|
+
# │ │ │ Team 3 │
|
|
10
|
+
# └──────────────┴──────────────┴──────────────┘
|
|
11
|
+
#
|
|
12
|
+
# Main: 사용자가 직접 Claude Code를 실행하는 대화형 세션
|
|
13
|
+
# Dashboard: feature-queue + team status 자동 갱신 (입력 불가)
|
|
14
|
+
# Team 1~3: claude -p headless worker (입력 불가, 로그만 표시)
|
|
12
15
|
#
|
|
13
16
|
# Usage:
|
|
14
17
|
# bash scripts/harness-studio-v4.sh [project-root]
|
|
@@ -20,7 +23,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
20
23
|
SESSION_NAME="harness-v4"
|
|
21
24
|
|
|
22
25
|
PROJECT_ROOT=""
|
|
23
|
-
KILL=false
|
|
24
26
|
|
|
25
27
|
for arg in "$@"; do
|
|
26
28
|
case "$arg" in
|
|
@@ -52,45 +54,50 @@ echo "Session: $SESSION_NAME"
|
|
|
52
54
|
|
|
53
55
|
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
54
56
|
|
|
55
|
-
# ── Initialize
|
|
57
|
+
# ── Initialize or recover queue ──
|
|
56
58
|
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
57
59
|
if [ ! -f "$QUEUE" ]; then
|
|
58
60
|
echo "Initializing feature queue..."
|
|
59
61
|
bash "$SCRIPT_DIR/harness-queue-manager.sh" init "$PROJECT_ROOT"
|
|
62
|
+
else
|
|
63
|
+
echo "Recovering stale queue state..."
|
|
64
|
+
bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT"
|
|
60
65
|
fi
|
|
61
66
|
|
|
62
67
|
# ══════════════════════════════════════════
|
|
63
|
-
#
|
|
68
|
+
# 3-Column Layout (Main | Dashboard | Teams)
|
|
64
69
|
# ══════════════════════════════════════════
|
|
65
70
|
|
|
66
|
-
# 1
|
|
67
|
-
|
|
71
|
+
# Column 1: Main (interactive shell — user runs claude here)
|
|
72
|
+
PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
|
|
73
|
+
-P -F '#{pane_id}')
|
|
74
|
+
|
|
75
|
+
# Column 2: Dashboard (split right from Main, 66% remaining → 33% each of 3 cols)
|
|
76
|
+
PANE_DASH=$(tmux split-window -h -p 66 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
|
|
68
77
|
-P -F '#{pane_id}' \
|
|
69
78
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
70
79
|
|
|
71
|
-
#
|
|
80
|
+
# Column 3: Team 1 (split right from Dashboard, 50% of remaining = 33% total)
|
|
72
81
|
PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
73
82
|
-P -F '#{pane_id}' \
|
|
74
83
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'")
|
|
75
84
|
|
|
76
|
-
#
|
|
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)
|
|
85
|
+
# Team 2 (split below Team 1)
|
|
82
86
|
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
|
|
83
87
|
-P -F '#{pane_id}' \
|
|
84
88
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
|
|
85
89
|
|
|
86
|
-
#
|
|
90
|
+
# Team 3 (split below Team 2)
|
|
87
91
|
PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
|
|
88
92
|
-P -F '#{pane_id}' \
|
|
89
93
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
|
|
90
94
|
|
|
95
|
+
# ── Launch Claude in Main pane ──
|
|
96
|
+
tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
|
|
97
|
+
|
|
91
98
|
# ── Pane titles ──
|
|
99
|
+
tmux select-pane -t "$PANE_MAIN" -T "Main"
|
|
92
100
|
tmux select-pane -t "$PANE_DASH" -T "Dashboard"
|
|
93
|
-
tmux select-pane -t "$PANE_CTRL" -T "Control"
|
|
94
101
|
tmux select-pane -t "$PANE_T1" -T "Team 1"
|
|
95
102
|
tmux select-pane -t "$PANE_T2" -T "Team 2"
|
|
96
103
|
tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
@@ -98,18 +105,18 @@ tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
|
98
105
|
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
99
106
|
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
100
107
|
|
|
101
|
-
# Focus
|
|
102
|
-
tmux select-pane -t "$
|
|
108
|
+
# ── Focus Main pane ──
|
|
109
|
+
tmux select-pane -t "$PANE_MAIN"
|
|
103
110
|
|
|
104
|
-
# Attach
|
|
111
|
+
# ── Attach ──
|
|
105
112
|
if [ -n "${TMUX:-}" ]; then
|
|
106
113
|
tmux switch-client -t "$SESSION_NAME"
|
|
107
114
|
else
|
|
108
115
|
echo ""
|
|
109
116
|
echo "Launching Harness Studio v4..."
|
|
110
|
-
echo "
|
|
111
|
-
echo "
|
|
112
|
-
echo " Team 1-3 (right)
|
|
117
|
+
echo " Main (left) : Interactive — run 'claude' here"
|
|
118
|
+
echo " Dashboard (mid) : Feature Queue + Team status (auto-refresh)"
|
|
119
|
+
echo " Team 1-3 (right) : Parallel workers (headless, log only)"
|
|
113
120
|
echo ""
|
|
114
121
|
tmux attach -t "$SESSION_NAME"
|
|
115
122
|
fi
|
|
@@ -40,11 +40,13 @@ CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
|
40
40
|
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
41
41
|
QUEUE_MGR="$SCRIPT_DIR/harness-queue-manager.sh"
|
|
42
42
|
|
|
43
|
+
# ── Lock file for git operations (prevent race conditions between teams) ──
|
|
44
|
+
GIT_LOCK="$PROJECT_ROOT/.harness/.git-lock"
|
|
45
|
+
|
|
43
46
|
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
|
44
47
|
GEN_MODEL="${GEN_MODEL:-sonnet}"
|
|
45
48
|
EVAL_MODEL="${EVAL_MODEL:-opus}"
|
|
46
49
|
|
|
47
|
-
# Read models from config if available
|
|
48
50
|
if [ -f "$CONFIG" ]; then
|
|
49
51
|
_gm=$(jq -r '.agents["generator-frontend"].model // empty' "$CONFIG" 2>/dev/null)
|
|
50
52
|
_em=$(jq -r '.agents["evaluator-functional"].model // empty' "$CONFIG" 2>/dev/null)
|
|
@@ -68,20 +70,33 @@ log() {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
log_progress() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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"
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
# ── Pre-eval gate ──
|
|
78
96
|
run_pre_eval_gate() {
|
|
79
|
-
local feature_id="$1"
|
|
80
97
|
local cwd="$PROJECT_ROOT"
|
|
81
98
|
|
|
82
|
-
# Read frontend_cwd from config
|
|
83
99
|
if [ -f "$CONFIG" ]; then
|
|
84
|
-
local _cwd
|
|
85
100
|
_cwd=$(jq -r '.flow.pre_eval_gate.frontend_cwd // empty' "$CONFIG" 2>/dev/null)
|
|
86
101
|
if [ -n "$_cwd" ] && [ "$_cwd" != "null" ]; then
|
|
87
102
|
cwd="$PROJECT_ROOT/$_cwd"
|
|
@@ -97,85 +112,86 @@ run_pre_eval_gate() {
|
|
|
97
112
|
checks=("npx tsc --noEmit" "npx eslint src/")
|
|
98
113
|
fi
|
|
99
114
|
|
|
100
|
-
local all_pass=true
|
|
115
|
+
local all_pass=true fail_cmds=""
|
|
101
116
|
for cmd in "${checks[@]}"; do
|
|
102
|
-
if
|
|
103
|
-
log "${
|
|
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"
|
|
104
121
|
all_pass=false
|
|
122
|
+
fail_cmds+="$cmd; "
|
|
105
123
|
fi
|
|
106
124
|
done
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
log "${GREEN}Gate: tsc ✓ eslint ✓${RESET}"
|
|
110
|
-
return 0
|
|
111
|
-
else
|
|
112
|
-
return 1
|
|
113
|
-
fi
|
|
126
|
+
[ "$all_pass" = true ]
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
# ── Build generator prompt ──
|
|
117
130
|
build_gen_prompt() {
|
|
118
|
-
local
|
|
119
|
-
local attempt="$2"
|
|
120
|
-
local eval_feedback="${3:-}"
|
|
131
|
+
local fid="$1" attempt="$2" feedback="${3:-}"
|
|
121
132
|
|
|
122
|
-
local
|
|
123
|
-
|
|
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 // []')
|
|
124
140
|
|
|
125
|
-
local
|
|
126
|
-
|
|
127
|
-
feature_desc=$(echo "$feature_json" | jq -r '.description // ""')
|
|
128
|
-
ac_json=$(echo "$feature_json" | jq -c '.ac // []')
|
|
141
|
+
local project_name
|
|
142
|
+
project_name=$(jq -r '.project_name // ""' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
|
|
129
143
|
|
|
130
|
-
|
|
144
|
+
cat <<PROMPT
|
|
145
|
+
You are Generator-Frontend for a harness engineering project.
|
|
131
146
|
|
|
132
|
-
PROJECT: $
|
|
147
|
+
PROJECT: ${project_name}
|
|
133
148
|
CONVENTIONS: Read CONVENTIONS.md if it exists.
|
|
134
149
|
|
|
135
|
-
YOUR TASK: Implement ONLY feature ${
|
|
136
|
-
Description: ${
|
|
150
|
+
YOUR TASK: Implement ONLY feature ${fid}: ${fname}
|
|
151
|
+
Description: ${fdesc}
|
|
152
|
+
Dependencies (already implemented): ${deps_json}
|
|
137
153
|
Acceptance Criteria: ${ac_json}
|
|
138
154
|
|
|
139
155
|
Read these files for context:
|
|
140
|
-
- .harness/actions/feature-list.json (filter to ${
|
|
156
|
+
- .harness/actions/feature-list.json (filter to ${fid})
|
|
141
157
|
- .harness/actions/api-contract.json (relevant endpoints)
|
|
142
158
|
- .harness/actions/plan.md (overall design)
|
|
143
159
|
|
|
144
160
|
RULES:
|
|
145
|
-
- Implement ONLY this feature
|
|
161
|
+
- Implement ONLY this single feature
|
|
162
|
+
- Do NOT modify code belonging to other features
|
|
146
163
|
- Follow existing code patterns and CONVENTIONS.md
|
|
147
|
-
-
|
|
148
|
-
|
|
164
|
+
- When done, stage and commit with: git add -A && git commit -m 'feat(${fid}): ${fname}'
|
|
165
|
+
PROMPT
|
|
149
166
|
|
|
150
|
-
if [ "$attempt" -gt 1 ] && [ -n "$
|
|
151
|
-
|
|
167
|
+
if [ "$attempt" -gt 1 ] && [ -n "$feedback" ]; then
|
|
168
|
+
cat <<RETRY
|
|
152
169
|
|
|
153
170
|
PREVIOUS EVAL FEEDBACK (attempt ${attempt}):
|
|
154
|
-
${
|
|
171
|
+
${feedback}
|
|
155
172
|
|
|
156
|
-
Fix the issues
|
|
173
|
+
Fix the issues above. Focus specifically on the failed criteria.
|
|
174
|
+
RETRY
|
|
157
175
|
fi
|
|
158
|
-
|
|
159
|
-
echo "$prompt"
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
# ── Build evaluator prompt ──
|
|
163
179
|
build_eval_prompt() {
|
|
164
|
-
local
|
|
165
|
-
|
|
166
|
-
local feature_json
|
|
167
|
-
feature_json=$(jq --arg fid "$feature_id" '.features[] | select(.id == $fid)' "$FEATURES" 2>/dev/null)
|
|
180
|
+
local fid="$1"
|
|
168
181
|
|
|
169
|
-
local
|
|
170
|
-
|
|
171
|
-
|
|
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 // []')
|
|
172
187
|
|
|
173
188
|
local passed_list
|
|
174
189
|
passed_list=$(jq -r '.queue.passed // [] | join(", ")' "$QUEUE" 2>/dev/null)
|
|
175
190
|
|
|
176
|
-
|
|
191
|
+
cat <<PROMPT
|
|
192
|
+
You are Evaluator-Functional for a harness engineering project.
|
|
177
193
|
|
|
178
|
-
TASK: Evaluate feature ${
|
|
194
|
+
TASK: Evaluate feature ${fid}: ${fname}
|
|
179
195
|
|
|
180
196
|
Acceptance Criteria to verify:
|
|
181
197
|
${ac_json}
|
|
@@ -192,23 +208,24 @@ R5: Error handling & edge cases (15%)
|
|
|
192
208
|
PASS threshold: 2.80 / 3.00
|
|
193
209
|
FAIL: any AC not met, any regression failure
|
|
194
210
|
|
|
195
|
-
|
|
211
|
+
You MUST output this exact block (parseable by automation):
|
|
196
212
|
---EVAL-RESULT---
|
|
197
|
-
FEATURE: ${
|
|
213
|
+
FEATURE: ${fid}
|
|
198
214
|
VERDICT: PASS or FAIL
|
|
199
215
|
SCORE: X.XX
|
|
200
|
-
FEEDBACK:
|
|
201
|
-
---END-EVAL-RESULT---
|
|
216
|
+
FEEDBACK: one paragraph summary
|
|
217
|
+
---END-EVAL-RESULT---
|
|
218
|
+
PROMPT
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
# ── Parse eval result ──
|
|
221
|
+
# ── Parse eval result (macOS-compatible, no grep -P) ──
|
|
205
222
|
parse_eval_result() {
|
|
206
223
|
local output="$1"
|
|
207
224
|
|
|
208
225
|
local verdict score feedback
|
|
209
|
-
verdict=$(echo "$output" | grep -
|
|
210
|
-
score=$(echo "$output" | grep -
|
|
211
|
-
feedback=$(echo "$output" |
|
|
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)
|
|
212
229
|
|
|
213
230
|
echo "${verdict:-UNKNOWN}|${score:-0.00}|${feedback:-no feedback}"
|
|
214
231
|
}
|
|
@@ -223,14 +240,13 @@ while true; do
|
|
|
223
240
|
# ── Dequeue next feature ──
|
|
224
241
|
feature_id=$(bash "$QUEUE_MGR" dequeue "$TEAM_ID" "$PROJECT_ROOT" 2>/dev/null)
|
|
225
242
|
|
|
226
|
-
if [ -z "$feature_id" ] || [[ "$feature_id" == "[
|
|
227
|
-
log "${DIM}No features
|
|
243
|
+
if [ -z "$feature_id" ] || [[ "$feature_id" == "["* ]]; then
|
|
244
|
+
log "${DIM}No features ready. Waiting 10s...${RESET}"
|
|
228
245
|
sleep 10
|
|
229
246
|
|
|
230
247
|
# Check if completely done
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if [ "${remaining:-1}" -eq 0 ]; then
|
|
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
|
|
234
250
|
log "${GREEN}${BOLD}ALL FEATURES COMPLETE. Team ${TEAM_ID} exiting.${RESET}"
|
|
235
251
|
log_progress "complete" "All features done"
|
|
236
252
|
exit 0
|
|
@@ -241,16 +257,18 @@ while true; do
|
|
|
241
257
|
log "${CYAN}▶ Dequeued ${feature_id}${RESET}"
|
|
242
258
|
log_progress "dequeue" "${feature_id}"
|
|
243
259
|
|
|
244
|
-
# ── Create feature branch ──
|
|
245
|
-
|
|
246
|
-
|
|
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) || \
|
|
247
264
|
(cd "$PROJECT_ROOT" && git checkout "$branch" 2>/dev/null) || true
|
|
265
|
+
release_git_lock
|
|
248
266
|
log "Branch: ${branch}"
|
|
249
267
|
|
|
250
268
|
# ── Gen→Eval Loop ──
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
attempt=1
|
|
270
|
+
eval_feedback=""
|
|
271
|
+
passed=false
|
|
254
272
|
|
|
255
273
|
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
|
256
274
|
log "${BOLD}── Attempt ${attempt}/${MAX_ATTEMPTS} ──${RESET}"
|
|
@@ -259,18 +277,21 @@ while true; do
|
|
|
259
277
|
log "Gen ${feature_id} (${GEN_MODEL})..."
|
|
260
278
|
bash "$QUEUE_MGR" update_phase "$feature_id" "gen" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
261
279
|
|
|
262
|
-
local gen_prompt
|
|
263
280
|
gen_prompt=$(build_gen_prompt "$feature_id" "$attempt" "$eval_feedback")
|
|
264
281
|
|
|
265
|
-
|
|
266
|
-
|
|
282
|
+
gen_start=$(date +%s)
|
|
283
|
+
log "${DIM} claude -p --dangerously-skip-permissions --model ${GEN_MODEL}${RESET}"
|
|
284
|
+
gen_output=$(cd "$PROJECT_ROOT" && claude -p "$gen_prompt" \
|
|
285
|
+
--dangerously-skip-permissions \
|
|
286
|
+
--model "$GEN_MODEL" \
|
|
287
|
+
--output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
|
|
288
|
+
gen_elapsed=$(( $(date +%s) - gen_start ))
|
|
267
289
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files"
|
|
290
|
+
files_changed=$(cd "$PROJECT_ROOT" && git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
|
|
291
|
+
log "Gen done (${gen_elapsed}s) — ${files_changed} files"
|
|
292
|
+
log_progress "gen" "${feature_id} attempt ${attempt}: ${files_changed} files, ${gen_elapsed}s"
|
|
272
293
|
|
|
273
|
-
# Auto-commit
|
|
294
|
+
# Auto-commit
|
|
274
295
|
(cd "$PROJECT_ROOT" && git add -A && git commit -m "feat(${feature_id}): gen attempt ${attempt}" --no-verify 2>/dev/null) || true
|
|
275
296
|
|
|
276
297
|
# ── Pre-eval gate ──
|
|
@@ -288,72 +309,107 @@ while true; do
|
|
|
288
309
|
log "Eval ${feature_id} (${EVAL_MODEL})..."
|
|
289
310
|
bash "$QUEUE_MGR" update_phase "$feature_id" "eval" "$attempt" "$PROJECT_ROOT" 2>/dev/null
|
|
290
311
|
|
|
291
|
-
local eval_prompt
|
|
292
312
|
eval_prompt=$(build_eval_prompt "$feature_id")
|
|
293
313
|
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
eval_start=$(date +%s)
|
|
315
|
+
log "${DIM} claude -p --dangerously-skip-permissions --model ${EVAL_MODEL}${RESET}"
|
|
316
|
+
eval_output=$(cd "$PROJECT_ROOT" && claude -p "$eval_prompt" \
|
|
317
|
+
--dangerously-skip-permissions \
|
|
318
|
+
--model "$EVAL_MODEL" \
|
|
319
|
+
--output-format text 2>&1 | tee /dev/stderr) 2>&1 || true
|
|
320
|
+
eval_elapsed=$(( $(date +%s) - eval_start ))
|
|
296
321
|
|
|
297
322
|
# Parse result
|
|
298
|
-
local result_line verdict score feedback
|
|
299
323
|
result_line=$(parse_eval_result "$eval_output")
|
|
300
324
|
verdict=$(echo "$result_line" | cut -d'|' -f1)
|
|
301
325
|
score=$(echo "$result_line" | cut -d'|' -f2)
|
|
302
326
|
feedback=$(echo "$result_line" | cut -d'|' -f3-)
|
|
303
327
|
|
|
304
|
-
log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score})"
|
|
328
|
+
log_progress "eval" "${feature_id} attempt ${attempt}: ${verdict} (${score}) ${eval_elapsed}s"
|
|
305
329
|
|
|
306
330
|
if [ "$verdict" = "PASS" ]; then
|
|
307
|
-
log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00"
|
|
331
|
+
log "${GREEN}${BOLD}✓ PASS${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
|
|
308
332
|
passed=true
|
|
309
333
|
break
|
|
310
334
|
else
|
|
311
|
-
log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00"
|
|
335
|
+
log "${RED}✗ FAIL${RESET} ${feature_id} — ${score}/3.00 (${eval_elapsed}s)"
|
|
312
336
|
log "${DIM} ${feedback}${RESET}"
|
|
313
337
|
eval_feedback="$feedback"
|
|
314
338
|
attempt=$((attempt + 1))
|
|
315
339
|
fi
|
|
316
340
|
done
|
|
317
341
|
|
|
318
|
-
#
|
|
342
|
+
# ══════════════════════════════════════════
|
|
343
|
+
# Phase 3: Branch merge with conflict handling
|
|
344
|
+
# ══════════════════════════════════════════
|
|
319
345
|
if [ "$passed" = true ]; then
|
|
320
|
-
# Merge to main
|
|
321
346
|
log "Merging ${branch} → main..."
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
382
|
+
|
|
383
|
+
bash "$QUEUE_MGR" pass "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
384
|
+
log_progress "pass" "${feature_id} merged to main"
|
|
385
|
+
|
|
386
|
+
# Update feature-list.json passes
|
|
387
|
+
if [ -f "$FEATURES" ]; then
|
|
388
|
+
jq --arg fid "$feature_id" '
|
|
389
|
+
.features |= map(
|
|
390
|
+
if .id == $fid then
|
|
391
|
+
.passes = ((.passes // []) + ["generator-frontend", "evaluator-functional"] | unique)
|
|
392
|
+
else . end
|
|
393
|
+
)
|
|
394
|
+
' "$FEATURES" > "${FEATURES}.tmp" && mv "${FEATURES}.tmp" "$FEATURES"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
log "${GREEN}${BOLD}✓ ${feature_id} DONE${RESET}"
|
|
398
|
+
else
|
|
399
|
+
log "${RED}${BOLD}Merge failed — ${feature_id} marked as failed${RESET}"
|
|
400
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
401
|
+
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
402
|
+
log_progress "merge-fail" "${feature_id}"
|
|
346
403
|
fi
|
|
347
404
|
|
|
348
405
|
else
|
|
349
406
|
log "${RED}${BOLD}✗ ${feature_id} FAILED after ${MAX_ATTEMPTS} attempts${RESET}"
|
|
407
|
+
acquire_git_lock
|
|
408
|
+
(cd "$PROJECT_ROOT" && git checkout main 2>/dev/null) || true
|
|
409
|
+
release_git_lock
|
|
350
410
|
bash "$QUEUE_MGR" fail "$feature_id" "$PROJECT_ROOT" 2>/dev/null
|
|
351
411
|
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
412
|
fi
|
|
356
413
|
|
|
357
|
-
# Brief pause before next feature
|
|
358
414
|
sleep 2
|
|
359
415
|
done
|
|
@@ -51,7 +51,37 @@ current_agent=${CURRENT_AGENT} (running) 인데 /harness-${REQUESTED_SKILL} 호
|
|
|
51
51
|
fi
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
-
# ──
|
|
54
|
+
# ── v4 Parallel Mode 감지 ──
|
|
55
|
+
FEATURE_QUEUE="$CWD/.harness/actions/feature-queue.json"
|
|
56
|
+
if [ -f "$FEATURE_QUEUE" ]; then
|
|
57
|
+
V4_PASSED=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
58
|
+
V4_TOTAL=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
59
|
+
V4_FAILED=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
60
|
+
|
|
61
|
+
# Log user prompt to progress.log (truncated to 80 chars)
|
|
62
|
+
PROGRESS_LOG="$CWD/.harness/progress.log"
|
|
63
|
+
if [ -n "$PROMPT" ] && [ -f "$PROGRESS_LOG" ]; then
|
|
64
|
+
PROMPT_SHORT=$(echo "$PROMPT" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-80)
|
|
65
|
+
# Skip logging for empty or very short prompts
|
|
66
|
+
if [ ${#PROMPT_SHORT} -gt 2 ]; then
|
|
67
|
+
echo "$(date +"%Y-%m-%d %H:%M") | user-prompt | input | ${PROMPT_SHORT}" >> "$PROGRESS_LOG"
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
cat <<EOF
|
|
72
|
+
[harness-v4] ${V4_PASSED}/${V4_TOTAL} features passed | ${V4_FAILED} failed
|
|
73
|
+
|
|
74
|
+
## v4 Parallel Mode Active
|
|
75
|
+
- 3 Agent Teams이 feature-queue에서 자율적으로 Gen→Eval 루프 실행 중
|
|
76
|
+
- 당신은 **오케스트레이터** 역할: 대시보드 모니터링, 실패 대응, 수동 개입
|
|
77
|
+
- /harness-generator-*, /harness-evaluator-* 스킬 호출 금지 (Teams가 처리)
|
|
78
|
+
- 할 수 있는 것: 코드 리뷰, 아키텍처 결정, failed feature 분석, requeue 판단
|
|
79
|
+
- skip: "harness skip" 시 일반 대화
|
|
80
|
+
EOF
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Compact context 주입 (v3 mode) ──
|
|
55
85
|
cat <<EOF
|
|
56
86
|
[harness] S${SPRINT_NUM} | ${PIPELINE} | agent=${CURRENT_AGENT} (${AGENT_STATUS}) | next=${NEXT_AGENT}
|
|
57
87
|
${CONTEXT_WARNING}
|
|
@@ -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 연동, 성공 기준
|