@walwal-harness/cli 3.7.4 → 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/bin/init.js +34 -2
- package/package.json +1 -1
- package/scripts/harness-control-v4.sh +97 -0
- package/scripts/harness-dashboard-v4.sh +243 -0
- package/scripts/harness-queue-manager.sh +367 -0
- package/scripts/harness-session-start.sh +18 -0
- package/scripts/harness-studio-v4.sh +122 -0
- package/scripts/harness-team-worker.sh +415 -0
- 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
|
@@ -0,0 +1,367 @@
|
|
|
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
|
+
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
|
+
}
|
|
64
|
+
|
|
65
|
+
# ── Concurrency from config ──
|
|
66
|
+
CONCURRENCY=3
|
|
67
|
+
if [ -f "$CONFIG" ]; then
|
|
68
|
+
_c=$(jq -r '.flow.parallel.concurrency // 3' "$CONFIG" 2>/dev/null)
|
|
69
|
+
if [ "$_c" -gt 0 ] 2>/dev/null; then CONCURRENCY=$_c; fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ══════════════════════════════════════════
|
|
73
|
+
# init — Build queue from feature-list.json
|
|
74
|
+
# ══════════════════════════════════════════
|
|
75
|
+
cmd_init() {
|
|
76
|
+
if [ ! -f "$FEATURES" ]; then
|
|
77
|
+
echo "[queue] feature-list.json not found."
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Build dependency graph and topological sort
|
|
82
|
+
# Output: feature-queue.json with ready (no deps) and blocked (has deps)
|
|
83
|
+
jq --argjson concurrency "$CONCURRENCY" '
|
|
84
|
+
# Build passed set (features already passed by evaluator)
|
|
85
|
+
def passed_set:
|
|
86
|
+
[.features[] | select(
|
|
87
|
+
((.passes // []) | length > 0) and
|
|
88
|
+
((.passes // []) | any(. == "evaluator-functional"))
|
|
89
|
+
) | .id] ;
|
|
90
|
+
|
|
91
|
+
# Separate ready vs blocked
|
|
92
|
+
def classify(passed):
|
|
93
|
+
reduce .features[] as $f (
|
|
94
|
+
{ ready: [], blocked: {} };
|
|
95
|
+
($f.depends_on // []) as $deps |
|
|
96
|
+
if ($f.id | IN(passed[])) then . # already passed, skip
|
|
97
|
+
elif ($deps | length == 0) then
|
|
98
|
+
.ready += [$f.id]
|
|
99
|
+
elif (passed | length > 0) and ($deps | all(. as $d | $d | IN(passed[]))) then
|
|
100
|
+
.ready += [$f.id] # all deps satisfied
|
|
101
|
+
else
|
|
102
|
+
.blocked[$f.id] = ($deps - passed)
|
|
103
|
+
end
|
|
104
|
+
) ;
|
|
105
|
+
|
|
106
|
+
passed_set as $passed |
|
|
107
|
+
classify($passed) as $classified |
|
|
108
|
+
{
|
|
109
|
+
version: "4.0",
|
|
110
|
+
concurrency: $concurrency,
|
|
111
|
+
queue: {
|
|
112
|
+
ready: $classified.ready,
|
|
113
|
+
blocked: $classified.blocked,
|
|
114
|
+
in_progress: {},
|
|
115
|
+
passed: $passed,
|
|
116
|
+
failed: []
|
|
117
|
+
},
|
|
118
|
+
teams: (
|
|
119
|
+
[range(1; $concurrency + 1)] | map({
|
|
120
|
+
key: (. | tostring),
|
|
121
|
+
value: { status: "idle", feature: null, branch: null, pid: null }
|
|
122
|
+
}) | from_entries
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
' "$FEATURES" > "$QUEUE"
|
|
126
|
+
|
|
127
|
+
local ready_count blocked_count passed_count
|
|
128
|
+
ready_count=$(jq '.queue.ready | length' "$QUEUE")
|
|
129
|
+
blocked_count=$(jq '.queue.blocked | length' "$QUEUE")
|
|
130
|
+
passed_count=$(jq '.queue.passed | length' "$QUEUE")
|
|
131
|
+
|
|
132
|
+
echo "[queue] Initialized: $ready_count ready, $blocked_count blocked, $passed_count already passed"
|
|
133
|
+
echo "[queue] Concurrency: $CONCURRENCY teams"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ══════════════════════════════════════════
|
|
137
|
+
# dequeue — Assign next ready feature to team
|
|
138
|
+
# ══════════════════════════════════════════
|
|
139
|
+
cmd_dequeue() {
|
|
140
|
+
local team_id="${1:-}"
|
|
141
|
+
if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
|
|
142
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
143
|
+
|
|
144
|
+
acquire_queue_lock
|
|
145
|
+
|
|
146
|
+
local feature
|
|
147
|
+
feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
|
|
148
|
+
|
|
149
|
+
if [ -z "$feature" ]; then
|
|
150
|
+
release_queue_lock
|
|
151
|
+
echo "[queue] No features in ready queue."
|
|
152
|
+
local in_prog blocked
|
|
153
|
+
in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
|
|
154
|
+
blocked=$(jq '.queue.blocked | length' "$QUEUE")
|
|
155
|
+
if [ "$in_prog" -eq 0 ] && [ "$blocked" -eq 0 ]; then
|
|
156
|
+
echo "[queue] ALL FEATURES COMPLETE."
|
|
157
|
+
fi
|
|
158
|
+
return 1
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
jq --arg fid "$feature" --arg tid "$team_id" '
|
|
162
|
+
.queue.ready -= [$fid] |
|
|
163
|
+
.queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
|
|
164
|
+
.teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
|
|
165
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
166
|
+
|
|
167
|
+
release_queue_lock
|
|
168
|
+
echo "$feature"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# ══════════════════════════════════════════
|
|
172
|
+
# pass — Mark feature as passed, unblock dependents
|
|
173
|
+
# ══════════════════════════════════════════
|
|
174
|
+
cmd_pass() {
|
|
175
|
+
local fid="${1:-}"
|
|
176
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
|
|
177
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
178
|
+
|
|
179
|
+
acquire_queue_lock
|
|
180
|
+
|
|
181
|
+
local team_id
|
|
182
|
+
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
183
|
+
|
|
184
|
+
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
185
|
+
# Remove from in_progress
|
|
186
|
+
del(.queue.in_progress[$fid]) |
|
|
187
|
+
|
|
188
|
+
# Add to passed
|
|
189
|
+
.queue.passed += [$fid] |
|
|
190
|
+
.queue.passed |= unique |
|
|
191
|
+
|
|
192
|
+
# Free team
|
|
193
|
+
(if $tid != "0" then
|
|
194
|
+
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
195
|
+
else . end) |
|
|
196
|
+
|
|
197
|
+
# Unblock dependents: for each blocked feature, remove $fid from its deps
|
|
198
|
+
# If deps become empty, move to ready
|
|
199
|
+
.queue.blocked as $blocked |
|
|
200
|
+
reduce ($blocked | keys[]) as $blocked_fid (
|
|
201
|
+
.;
|
|
202
|
+
.queue.blocked[$blocked_fid] -= [$fid] |
|
|
203
|
+
if (.queue.blocked[$blocked_fid] | length) == 0 then
|
|
204
|
+
del(.queue.blocked[$blocked_fid]) |
|
|
205
|
+
.queue.ready += [$blocked_fid]
|
|
206
|
+
else . end
|
|
207
|
+
)
|
|
208
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
209
|
+
|
|
210
|
+
local newly_ready
|
|
211
|
+
newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
|
|
212
|
+
release_queue_lock
|
|
213
|
+
echo "[queue] $fid PASSED. Ready: [$newly_ready]"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ══════════════════════════════════════════
|
|
217
|
+
# fail — Mark feature as failed
|
|
218
|
+
# ══════════════════════════════════════════
|
|
219
|
+
cmd_fail() {
|
|
220
|
+
local fid="${1:-}"
|
|
221
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
|
|
222
|
+
if [ ! -f "$QUEUE" ]; then exit 1; fi
|
|
223
|
+
|
|
224
|
+
acquire_queue_lock
|
|
225
|
+
|
|
226
|
+
local team_id
|
|
227
|
+
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
228
|
+
|
|
229
|
+
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
230
|
+
del(.queue.in_progress[$fid]) |
|
|
231
|
+
.queue.failed += [$fid] |
|
|
232
|
+
.queue.failed |= unique |
|
|
233
|
+
(if $tid != "0" then
|
|
234
|
+
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
235
|
+
else . end)
|
|
236
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
237
|
+
|
|
238
|
+
release_queue_lock
|
|
239
|
+
echo "[queue] $fid FAILED."
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# ══════════════════════════════════════════
|
|
243
|
+
# requeue — Move failed feature back to ready
|
|
244
|
+
# ══════════════════════════════════════════
|
|
245
|
+
cmd_requeue() {
|
|
246
|
+
local fid="${1:-}"
|
|
247
|
+
if [ -z "$fid" ]; then echo "[queue] Usage: requeue <feature_id>"; exit 1; fi
|
|
248
|
+
|
|
249
|
+
jq --arg fid "$fid" '
|
|
250
|
+
.queue.failed -= [$fid] |
|
|
251
|
+
.queue.ready += [$fid]
|
|
252
|
+
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
253
|
+
|
|
254
|
+
echo "[queue] $fid requeued to ready."
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# ══════════════════════════════════════════
|
|
258
|
+
# update_phase — Update in_progress feature phase/attempt
|
|
259
|
+
# ══════════════════════════════════════════
|
|
260
|
+
cmd_update_phase() {
|
|
261
|
+
local fid="${1:-}" phase="${2:-}" attempt="${3:-}"
|
|
262
|
+
if [ -z "$fid" ] || [ -z "$phase" ]; then
|
|
263
|
+
echo "[queue] Usage: update_phase <feature_id> <phase> [attempt]"
|
|
264
|
+
exit 1
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
acquire_queue_lock
|
|
268
|
+
|
|
269
|
+
local jq_expr
|
|
270
|
+
jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
|
|
271
|
+
if [ -n "$attempt" ]; then
|
|
272
|
+
jq_expr="$jq_expr | .queue.in_progress[\"$fid\"].attempt = ($attempt | tonumber)"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
276
|
+
release_queue_lock
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# ══════════════════════════════════════════
|
|
280
|
+
# status — Print queue state
|
|
281
|
+
# ══════════════════════════════════════════
|
|
282
|
+
cmd_status() {
|
|
283
|
+
if [ ! -f "$QUEUE" ]; then
|
|
284
|
+
echo "[queue] Not initialized. Run: bash scripts/harness-queue-manager.sh init"
|
|
285
|
+
return
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
local ready blocked in_prog passed failed
|
|
289
|
+
ready=$(jq -r '.queue.ready | length' "$QUEUE")
|
|
290
|
+
blocked=$(jq -r '.queue.blocked | length' "$QUEUE")
|
|
291
|
+
in_prog=$(jq -r '.queue.in_progress | length' "$QUEUE")
|
|
292
|
+
passed=$(jq -r '.queue.passed | length' "$QUEUE")
|
|
293
|
+
failed=$(jq -r '.queue.failed | length' "$QUEUE")
|
|
294
|
+
local total=$((ready + blocked + in_prog + passed + failed))
|
|
295
|
+
|
|
296
|
+
echo ""
|
|
297
|
+
echo " Feature Queue ($passed/$total done)"
|
|
298
|
+
echo " ─────────────────────────────"
|
|
299
|
+
echo " Ready: $ready"
|
|
300
|
+
echo " Blocked: $blocked"
|
|
301
|
+
echo " In Progress: $in_prog"
|
|
302
|
+
echo " Passed: $passed"
|
|
303
|
+
echo " Failed: $failed"
|
|
304
|
+
echo ""
|
|
305
|
+
|
|
306
|
+
# Team status
|
|
307
|
+
local team_count
|
|
308
|
+
team_count=$(jq '.teams | length' "$QUEUE")
|
|
309
|
+
echo " Teams"
|
|
310
|
+
echo " ─────────────────────────────"
|
|
311
|
+
for i in $(seq 1 "$team_count"); do
|
|
312
|
+
local t_status t_feature
|
|
313
|
+
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE")
|
|
314
|
+
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE")
|
|
315
|
+
printf " Team %d: %-6s %s\n" "$i" "$t_status" "$t_feature"
|
|
316
|
+
done
|
|
317
|
+
echo ""
|
|
318
|
+
|
|
319
|
+
# In-progress details
|
|
320
|
+
if [ "$in_prog" -gt 0 ]; then
|
|
321
|
+
echo " In Progress"
|
|
322
|
+
echo " ─────────────────────────────"
|
|
323
|
+
jq -r '.queue.in_progress | to_entries[] | " \(.key): team \(.value.team) — \(.value.phase) (attempt \(.value.attempt))"' "$QUEUE"
|
|
324
|
+
echo ""
|
|
325
|
+
fi
|
|
326
|
+
}
|
|
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
|
+
|
|
353
|
+
# ── Dispatch ──
|
|
354
|
+
case "$CMD" in
|
|
355
|
+
init) cmd_init ;;
|
|
356
|
+
dequeue) cmd_dequeue "$@" ;;
|
|
357
|
+
pass) cmd_pass "$@" ;;
|
|
358
|
+
fail) cmd_fail "$@" ;;
|
|
359
|
+
requeue) cmd_requeue "$@" ;;
|
|
360
|
+
update_phase) cmd_update_phase "$@" ;;
|
|
361
|
+
recover) cmd_recover ;;
|
|
362
|
+
status) cmd_status ;;
|
|
363
|
+
*)
|
|
364
|
+
echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|recover|update_phase|status> [args]"
|
|
365
|
+
exit 1
|
|
366
|
+
;;
|
|
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
|
# ─────────────────────────────────────────
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-studio-v4.sh — Harness Studio v4: 3-Column Layout
|
|
3
|
+
#
|
|
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 (입력 불가, 로그만 표시)
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# bash scripts/harness-studio-v4.sh [project-root]
|
|
18
|
+
# bash scripts/harness-studio-v4.sh --kill
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
SESSION_NAME="harness-v4"
|
|
24
|
+
|
|
25
|
+
PROJECT_ROOT=""
|
|
26
|
+
|
|
27
|
+
for arg in "$@"; do
|
|
28
|
+
case "$arg" in
|
|
29
|
+
--kill)
|
|
30
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "Killed." || echo "No session."
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
if [ -d "$arg" ]; then PROJECT_ROOT="$arg"; fi
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
40
|
+
dir="$(pwd)"
|
|
41
|
+
while [ "$dir" != "/" ]; do
|
|
42
|
+
if [ -d "$dir/.harness" ]; then PROJECT_ROOT="$dir"; break; fi
|
|
43
|
+
dir="$(dirname "$dir")"
|
|
44
|
+
done
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
48
|
+
echo "Error: .harness/ not found."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "Project: $PROJECT_ROOT"
|
|
53
|
+
echo "Session: $SESSION_NAME"
|
|
54
|
+
|
|
55
|
+
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
56
|
+
|
|
57
|
+
# ── Initialize or recover queue ──
|
|
58
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
59
|
+
if [ ! -f "$QUEUE" ]; then
|
|
60
|
+
echo "Initializing feature queue..."
|
|
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"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ══════════════════════════════════════════
|
|
68
|
+
# 3-Column Layout (Main | Dashboard | Teams)
|
|
69
|
+
# ══════════════════════════════════════════
|
|
70
|
+
|
|
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" \
|
|
77
|
+
-P -F '#{pane_id}' \
|
|
78
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
79
|
+
|
|
80
|
+
# Column 3: Team 1 (split right from Dashboard, 50% of remaining = 33% total)
|
|
81
|
+
PANE_T1=$(tmux split-window -h -p 50 -t "$PANE_DASH" -c "$PROJECT_ROOT" \
|
|
82
|
+
-P -F '#{pane_id}' \
|
|
83
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'")
|
|
84
|
+
|
|
85
|
+
# Team 2 (split below Team 1)
|
|
86
|
+
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
|
|
87
|
+
-P -F '#{pane_id}' \
|
|
88
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
|
|
89
|
+
|
|
90
|
+
# Team 3 (split below Team 2)
|
|
91
|
+
PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
|
|
92
|
+
-P -F '#{pane_id}' \
|
|
93
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
|
|
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
|
+
|
|
98
|
+
# ── Pane titles ──
|
|
99
|
+
tmux select-pane -t "$PANE_MAIN" -T "Main"
|
|
100
|
+
tmux select-pane -t "$PANE_DASH" -T "Dashboard"
|
|
101
|
+
tmux select-pane -t "$PANE_T1" -T "Team 1"
|
|
102
|
+
tmux select-pane -t "$PANE_T2" -T "Team 2"
|
|
103
|
+
tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
104
|
+
|
|
105
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
106
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
107
|
+
|
|
108
|
+
# ── Focus Main pane ──
|
|
109
|
+
tmux select-pane -t "$PANE_MAIN"
|
|
110
|
+
|
|
111
|
+
# ── Attach ──
|
|
112
|
+
if [ -n "${TMUX:-}" ]; then
|
|
113
|
+
tmux switch-client -t "$SESSION_NAME"
|
|
114
|
+
else
|
|
115
|
+
echo ""
|
|
116
|
+
echo "Launching Harness Studio v4..."
|
|
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)"
|
|
120
|
+
echo ""
|
|
121
|
+
tmux attach -t "$SESSION_NAME"
|
|
122
|
+
fi
|