@walwal-harness/cli 6.0.4 → 6.0.5
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/CHANGELOG.md +21 -0
- package/bin/init.js +121 -4
- package/package.json +1 -1
- package/scripts/harness-archive.sh +0 -109
- package/scripts/harness-dashboard-up.sh +0 -72
- package/scripts/harness-dashboard.sh +0 -509
- package/scripts/harness-goal-init.sh +0 -72
- package/scripts/harness-goal-show.sh +0 -37
- package/scripts/harness-gotcha-memory.sh +0 -348
- package/scripts/harness-gotcha-register.sh +0 -393
- package/scripts/harness-monitor.sh +0 -398
- package/scripts/harness-next.sh +0 -546
- package/scripts/harness-progress-set.sh +0 -41
- package/scripts/harness-prompt-history.sh +0 -164
- package/scripts/harness-queue-manager.sh +0 -694
- package/scripts/harness-session-start.sh +0 -129
- package/scripts/harness-statusline.sh +0 -139
- package/scripts/harness-tmux.sh +0 -372
- package/scripts/harness-user-prompt-submit.sh +0 -105
- package/scripts/init-agents-md.sh +0 -337
- package/scripts/init-ref-docs.sh +0 -262
- package/scripts/lib/harness-audit.sh +0 -113
- package/scripts/lib/harness-guardrail.sh +0 -148
- package/scripts/lib/harness-keywait.sh +0 -57
- package/scripts/lib/harness-render-progress.sh +0 -346
- package/scripts/scan-project.sh +0 -367
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# harness-queue-manager.sh — Feature Queue Manager (v5 unified)
|
|
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.team.concurrency // .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
|
-
# Usage: init [sprint_number]
|
|
75
|
-
# sprint_number: optional, filter features by sprint (default: all)
|
|
76
|
-
# ══════════════════════════════════════════
|
|
77
|
-
cmd_init() {
|
|
78
|
-
local sprint_filter="${1:-all}"
|
|
79
|
-
|
|
80
|
-
if [ ! -f "$FEATURES" ]; then
|
|
81
|
-
echo "[queue] feature-list.json not found."
|
|
82
|
-
exit 1
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
# Build dependency graph and topological sort
|
|
86
|
-
# Output: feature-queue.json with ready (no deps) and blocked (has deps)
|
|
87
|
-
jq --argjson concurrency "$CONCURRENCY" --arg sprint "$sprint_filter" '
|
|
88
|
-
# Build passed set (features already passed by evaluator)
|
|
89
|
-
def passed_set:
|
|
90
|
-
[.features[] | select(
|
|
91
|
-
((.passes // []) | length > 0) and
|
|
92
|
-
((.passes // []) | any(. == "evaluator-functional"))
|
|
93
|
-
) | .id] ;
|
|
94
|
-
|
|
95
|
-
# Filter features by sprint if specified
|
|
96
|
-
def target_features:
|
|
97
|
-
if $sprint == "all" then .features
|
|
98
|
-
else [.features[] | select((.sprint // 1) == ($sprint | tonumber))]
|
|
99
|
-
end ;
|
|
100
|
-
|
|
101
|
-
# Separate ready vs blocked
|
|
102
|
-
def classify(features; passed):
|
|
103
|
-
reduce features[] as $f (
|
|
104
|
-
{ ready: [], blocked: {} };
|
|
105
|
-
($f.depends_on // []) as $deps |
|
|
106
|
-
if ($f.id | IN(passed[])) then . # already passed, skip
|
|
107
|
-
elif ($deps | length == 0) then
|
|
108
|
-
.ready += [$f.id]
|
|
109
|
-
elif (passed | length > 0) and ($deps | all(. as $d | $d | IN(passed[]))) then
|
|
110
|
-
.ready += [$f.id] # all deps satisfied
|
|
111
|
-
else
|
|
112
|
-
.blocked[$f.id] = ($deps - passed)
|
|
113
|
-
end
|
|
114
|
-
) ;
|
|
115
|
-
|
|
116
|
-
passed_set as $passed |
|
|
117
|
-
target_features as $targets |
|
|
118
|
-
classify($targets; $passed) as $classified |
|
|
119
|
-
{
|
|
120
|
-
version: "5.0",
|
|
121
|
-
concurrency: $concurrency,
|
|
122
|
-
current_sprint: (if $sprint == "all" then null else ($sprint | tonumber) end),
|
|
123
|
-
queue: {
|
|
124
|
-
ready: $classified.ready,
|
|
125
|
-
blocked: $classified.blocked,
|
|
126
|
-
in_progress: {},
|
|
127
|
-
passed: $passed,
|
|
128
|
-
failed: []
|
|
129
|
-
},
|
|
130
|
-
teams: (
|
|
131
|
-
[range(1; $concurrency + 1)] | map({
|
|
132
|
-
key: (. | tostring),
|
|
133
|
-
value: { status: "idle", feature: null, branch: null, pid: null }
|
|
134
|
-
}) | from_entries
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
' "$FEATURES" > "$QUEUE"
|
|
138
|
-
|
|
139
|
-
local ready_count blocked_count passed_count
|
|
140
|
-
ready_count=$(jq '.queue.ready | length' "$QUEUE")
|
|
141
|
-
blocked_count=$(jq '.queue.blocked | length' "$QUEUE")
|
|
142
|
-
passed_count=$(jq '.queue.passed | length' "$QUEUE")
|
|
143
|
-
|
|
144
|
-
echo "[queue] Initialized (sprint=$sprint_filter): $ready_count ready, $blocked_count blocked, $passed_count already passed"
|
|
145
|
-
echo "[queue] Concurrency: $CONCURRENCY teams"
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
# ══════════════════════════════════════════
|
|
149
|
-
# next-sprint — Auto-advance to next sprint
|
|
150
|
-
# Checks if current sprint is complete, loads next sprint features
|
|
151
|
-
# ══════════════════════════════════════════
|
|
152
|
-
cmd_next_sprint() {
|
|
153
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
154
|
-
if [ ! -f "$FEATURES" ]; then echo "[queue] feature-list.json not found."; exit 1; fi
|
|
155
|
-
|
|
156
|
-
acquire_queue_lock
|
|
157
|
-
|
|
158
|
-
local ready in_prog failed
|
|
159
|
-
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
160
|
-
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
161
|
-
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
162
|
-
|
|
163
|
-
# Current sprint still has work
|
|
164
|
-
if [ "$ready" -gt 0 ] || [ "$in_prog" -gt 0 ]; then
|
|
165
|
-
release_queue_lock
|
|
166
|
-
echo "[queue] Current sprint still active: $ready ready, $in_prog in progress"
|
|
167
|
-
return 1
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
# Failed features block advancement
|
|
171
|
-
if [ "$failed" -gt 0 ]; then
|
|
172
|
-
release_queue_lock
|
|
173
|
-
echo "[queue] Cannot advance: $failed failed features. Requeue or fix them first."
|
|
174
|
-
return 1
|
|
175
|
-
fi
|
|
176
|
-
|
|
177
|
-
# Find current sprint number
|
|
178
|
-
local current_sprint
|
|
179
|
-
current_sprint=$(jq -r '.current_sprint // 0' "$QUEUE" 2>/dev/null)
|
|
180
|
-
if [ "$current_sprint" = "null" ] || [ "$current_sprint" = "0" ]; then
|
|
181
|
-
# Detect from passed features
|
|
182
|
-
current_sprint=$(jq -r --slurpfile q "$QUEUE" '
|
|
183
|
-
[.features[] | select(.id as $fid | $q[0].queue.passed | index($fid)) | .sprint // 1] | max // 1
|
|
184
|
-
' "$FEATURES" 2>/dev/null)
|
|
185
|
-
fi
|
|
186
|
-
|
|
187
|
-
local next_sprint=$((current_sprint + 1))
|
|
188
|
-
|
|
189
|
-
# Check if next sprint features exist
|
|
190
|
-
local next_count
|
|
191
|
-
next_count=$(jq --arg s "$next_sprint" '[.features[] | select((.sprint // 1) == ($s | tonumber))] | length' "$FEATURES" 2>/dev/null)
|
|
192
|
-
|
|
193
|
-
if [ "$next_count" -eq 0 ]; then
|
|
194
|
-
release_queue_lock
|
|
195
|
-
echo "[queue] ALL SPRINTS COMPLETE. No Sprint $next_sprint features found."
|
|
196
|
-
echo "[queue] Total passed: $(jq '.queue.passed | length' "$QUEUE")"
|
|
197
|
-
return 0
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
release_queue_lock
|
|
201
|
-
|
|
202
|
-
# Re-init with next sprint (preserves passed from previous sprints)
|
|
203
|
-
echo "[queue] Sprint $current_sprint complete! Advancing to Sprint $next_sprint ($next_count features)"
|
|
204
|
-
cmd_init "$next_sprint"
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
# ══════════════════════════════════════════
|
|
208
|
-
# dequeue — Assign next ready feature to team
|
|
209
|
-
# ══════════════════════════════════════════
|
|
210
|
-
cmd_dequeue() {
|
|
211
|
-
local team_id="${1:-}"
|
|
212
|
-
if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
|
|
213
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
214
|
-
|
|
215
|
-
acquire_queue_lock
|
|
216
|
-
|
|
217
|
-
local feature
|
|
218
|
-
feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
|
|
219
|
-
|
|
220
|
-
if [ -z "$feature" ]; then
|
|
221
|
-
release_queue_lock
|
|
222
|
-
echo "[queue] No features in ready queue."
|
|
223
|
-
local in_prog blocked
|
|
224
|
-
in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
|
|
225
|
-
blocked=$(jq '.queue.blocked | length' "$QUEUE")
|
|
226
|
-
if [ "$in_prog" -eq 0 ] && [ "$blocked" -eq 0 ]; then
|
|
227
|
-
echo "[queue] ALL FEATURES COMPLETE."
|
|
228
|
-
fi
|
|
229
|
-
return 1
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
jq --arg fid "$feature" --arg tid "$team_id" '
|
|
233
|
-
.queue.ready -= [$fid] |
|
|
234
|
-
.queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
|
|
235
|
-
.teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
|
|
236
|
-
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
237
|
-
|
|
238
|
-
release_queue_lock
|
|
239
|
-
echo "$feature"
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# ══════════════════════════════════════════
|
|
243
|
-
# pass — Mark feature as passed, unblock dependents
|
|
244
|
-
# ══════════════════════════════════════════
|
|
245
|
-
cmd_pass() {
|
|
246
|
-
local fid="${1:-}"
|
|
247
|
-
if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
|
|
248
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
|
|
249
|
-
|
|
250
|
-
acquire_queue_lock
|
|
251
|
-
|
|
252
|
-
local team_id
|
|
253
|
-
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
254
|
-
|
|
255
|
-
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
256
|
-
# Remove from in_progress
|
|
257
|
-
del(.queue.in_progress[$fid]) |
|
|
258
|
-
|
|
259
|
-
# Add to passed
|
|
260
|
-
.queue.passed += [$fid] |
|
|
261
|
-
.queue.passed |= unique |
|
|
262
|
-
|
|
263
|
-
# Free team
|
|
264
|
-
(if $tid != "0" then
|
|
265
|
-
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
266
|
-
else . end) |
|
|
267
|
-
|
|
268
|
-
# Unblock dependents: for each blocked feature, remove $fid from its deps
|
|
269
|
-
# If deps become empty, move to ready
|
|
270
|
-
.queue.blocked as $blocked |
|
|
271
|
-
reduce ($blocked | keys[]) as $blocked_fid (
|
|
272
|
-
.;
|
|
273
|
-
.queue.blocked[$blocked_fid] -= [$fid] |
|
|
274
|
-
if (.queue.blocked[$blocked_fid] | length) == 0 then
|
|
275
|
-
del(.queue.blocked[$blocked_fid]) |
|
|
276
|
-
.queue.ready += [$blocked_fid]
|
|
277
|
-
else . end
|
|
278
|
-
)
|
|
279
|
-
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
280
|
-
|
|
281
|
-
local newly_ready
|
|
282
|
-
newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
|
|
283
|
-
release_queue_lock
|
|
284
|
-
echo "[queue] $fid PASSED. Ready: [$newly_ready]"
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
# ══════════════════════════════════════════
|
|
288
|
-
# fail — Mark feature as failed
|
|
289
|
-
# ══════════════════════════════════════════
|
|
290
|
-
cmd_fail() {
|
|
291
|
-
local fid="${1:-}"
|
|
292
|
-
if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
|
|
293
|
-
if [ ! -f "$QUEUE" ]; then exit 1; fi
|
|
294
|
-
|
|
295
|
-
acquire_queue_lock
|
|
296
|
-
|
|
297
|
-
local team_id
|
|
298
|
-
team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
|
|
299
|
-
|
|
300
|
-
jq --arg fid "$fid" --arg tid "${team_id:-0}" '
|
|
301
|
-
del(.queue.in_progress[$fid]) |
|
|
302
|
-
.queue.failed += [$fid] |
|
|
303
|
-
.queue.failed |= unique |
|
|
304
|
-
(if $tid != "0" then
|
|
305
|
-
.teams[$tid] = { status: "idle", feature: null, branch: null, pid: null }
|
|
306
|
-
else . end)
|
|
307
|
-
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
308
|
-
|
|
309
|
-
release_queue_lock
|
|
310
|
-
echo "[queue] $fid FAILED."
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
# ══════════════════════════════════════════
|
|
314
|
-
# requeue — Move failed feature back to ready
|
|
315
|
-
# ══════════════════════════════════════════
|
|
316
|
-
cmd_requeue() {
|
|
317
|
-
local fid="${1:-}"
|
|
318
|
-
if [ -z "$fid" ]; then echo "[queue] Usage: requeue <feature_id>"; exit 1; fi
|
|
319
|
-
|
|
320
|
-
acquire_queue_lock
|
|
321
|
-
|
|
322
|
-
jq --arg fid "$fid" '
|
|
323
|
-
.queue.failed -= [$fid] |
|
|
324
|
-
.queue.ready += [$fid]
|
|
325
|
-
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
326
|
-
|
|
327
|
-
release_queue_lock
|
|
328
|
-
echo "[queue] $fid requeued to ready."
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
# ══════════════════════════════════════════
|
|
332
|
-
# update_phase — Update in_progress feature phase/attempt
|
|
333
|
-
# ══════════════════════════════════════════
|
|
334
|
-
cmd_update_phase() {
|
|
335
|
-
local fid="${1:-}" phase="${2:-}" attempt="${3:-}"
|
|
336
|
-
if [ -z "$fid" ] || [ -z "$phase" ]; then
|
|
337
|
-
echo "[queue] Usage: update_phase <feature_id> <phase> [attempt]"
|
|
338
|
-
exit 1
|
|
339
|
-
fi
|
|
340
|
-
|
|
341
|
-
acquire_queue_lock
|
|
342
|
-
|
|
343
|
-
local jq_expr
|
|
344
|
-
jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
|
|
345
|
-
if [ -n "$attempt" ]; then
|
|
346
|
-
jq_expr="$jq_expr | .queue.in_progress[\"$fid\"].attempt = ($attempt | tonumber)"
|
|
347
|
-
fi
|
|
348
|
-
|
|
349
|
-
jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
350
|
-
release_queue_lock
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
# ══════════════════════════════════════════
|
|
354
|
-
# status — Print queue state
|
|
355
|
-
# ══════════════════════════════════════════
|
|
356
|
-
cmd_status() {
|
|
357
|
-
if [ ! -f "$QUEUE" ]; then
|
|
358
|
-
echo "[queue] Not initialized. Run: bash scripts/harness-queue-manager.sh init"
|
|
359
|
-
return
|
|
360
|
-
fi
|
|
361
|
-
|
|
362
|
-
local ready blocked in_prog passed failed
|
|
363
|
-
ready=$(jq -r '.queue.ready | length' "$QUEUE")
|
|
364
|
-
blocked=$(jq -r '.queue.blocked | length' "$QUEUE")
|
|
365
|
-
in_prog=$(jq -r '.queue.in_progress | length' "$QUEUE")
|
|
366
|
-
passed=$(jq -r '.queue.passed | length' "$QUEUE")
|
|
367
|
-
failed=$(jq -r '.queue.failed | length' "$QUEUE")
|
|
368
|
-
local total=$((ready + blocked + in_prog + passed + failed))
|
|
369
|
-
|
|
370
|
-
echo ""
|
|
371
|
-
echo " Feature Queue ($passed/$total done)"
|
|
372
|
-
echo " ─────────────────────────────"
|
|
373
|
-
echo " Ready: $ready"
|
|
374
|
-
echo " Blocked: $blocked"
|
|
375
|
-
echo " In Progress: $in_prog"
|
|
376
|
-
echo " Passed: $passed"
|
|
377
|
-
echo " Failed: $failed"
|
|
378
|
-
echo ""
|
|
379
|
-
|
|
380
|
-
# Team status
|
|
381
|
-
local team_count
|
|
382
|
-
team_count=$(jq '.teams | length' "$QUEUE")
|
|
383
|
-
echo " Teams"
|
|
384
|
-
echo " ─────────────────────────────"
|
|
385
|
-
for i in $(seq 1 "$team_count"); do
|
|
386
|
-
local t_status t_feature
|
|
387
|
-
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE")
|
|
388
|
-
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE")
|
|
389
|
-
printf " Team %d: %-6s %s\n" "$i" "$t_status" "$t_feature"
|
|
390
|
-
done
|
|
391
|
-
echo ""
|
|
392
|
-
|
|
393
|
-
# In-progress details
|
|
394
|
-
if [ "$in_prog" -gt 0 ]; then
|
|
395
|
-
echo " In Progress"
|
|
396
|
-
echo " ─────────────────────────────"
|
|
397
|
-
jq -r '.queue.in_progress | to_entries[] | " \(.key): team \(.value.team) — \(.value.phase) (attempt \(.value.attempt))"' "$QUEUE"
|
|
398
|
-
echo ""
|
|
399
|
-
fi
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
# ══════════════════════════════════════════
|
|
403
|
-
# recover — Move stale in_progress back to ready (after studio restart)
|
|
404
|
-
# ══════════════════════════════════════════
|
|
405
|
-
cmd_recover() {
|
|
406
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Not initialized."; return; fi
|
|
407
|
-
|
|
408
|
-
local stale_count
|
|
409
|
-
stale_count=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
|
|
410
|
-
|
|
411
|
-
if [ "${stale_count:-0}" -eq 0 ]; then
|
|
412
|
-
echo "[queue] No stale in_progress entries."
|
|
413
|
-
return
|
|
414
|
-
fi
|
|
415
|
-
|
|
416
|
-
# Move all in_progress → ready, reset all teams to idle
|
|
417
|
-
jq '
|
|
418
|
-
.queue.ready += [.queue.in_progress | keys[]] |
|
|
419
|
-
.queue.ready |= unique |
|
|
420
|
-
.queue.in_progress = {} |
|
|
421
|
-
.teams |= with_entries(.value = { status: "idle", feature: null, branch: null, pid: null })
|
|
422
|
-
' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
423
|
-
|
|
424
|
-
echo "[queue] Recovered ${stale_count} stale features back to ready queue."
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
# ══════════════════════════════════════════
|
|
428
|
-
# auto-dispatch — Pair all idle teams with ready features in one atomic step.
|
|
429
|
-
# Output: JSON array of {team, feature} on stdout.
|
|
430
|
-
# Atomically moves ready → in_progress + teams[tid] = busy for each pair.
|
|
431
|
-
# Dependencies are already honored (ready queue only holds unblocked features).
|
|
432
|
-
# ══════════════════════════════════════════
|
|
433
|
-
cmd_auto_dispatch() {
|
|
434
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first." >&2; exit 1; fi
|
|
435
|
-
|
|
436
|
-
acquire_queue_lock
|
|
437
|
-
|
|
438
|
-
local result
|
|
439
|
-
result=$(jq '
|
|
440
|
-
. as $root |
|
|
441
|
-
($root.teams | to_entries | map(select(.value.status == "idle")) | map(.key)) as $idle |
|
|
442
|
-
($root.queue.ready) as $ready |
|
|
443
|
-
(if ($idle | length) < ($ready | length) then ($idle | length) else ($ready | length) end) as $n |
|
|
444
|
-
[range(0; $n) | { team: ($idle[.] | tonumber), feature: $ready[.] }] as $pairs |
|
|
445
|
-
(reduce $pairs[] as $p ($root;
|
|
446
|
-
.queue.ready -= [$p.feature] |
|
|
447
|
-
.queue.in_progress[$p.feature] = { team: $p.team, phase: "gen", attempt: 1 } |
|
|
448
|
-
.teams[($p.team | tostring)] = {
|
|
449
|
-
status: "busy",
|
|
450
|
-
feature: $p.feature,
|
|
451
|
-
branch: ("feature/" + $p.feature),
|
|
452
|
-
pid: null
|
|
453
|
-
}
|
|
454
|
-
)) as $updated |
|
|
455
|
-
{ state: $updated, pairs: $pairs }
|
|
456
|
-
' "$QUEUE")
|
|
457
|
-
|
|
458
|
-
local new_state pairs
|
|
459
|
-
new_state=$(echo "$result" | jq '.state')
|
|
460
|
-
pairs=$(echo "$result" | jq -c '.pairs')
|
|
461
|
-
|
|
462
|
-
# Write updated state
|
|
463
|
-
echo "$new_state" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
|
|
464
|
-
|
|
465
|
-
release_queue_lock
|
|
466
|
-
|
|
467
|
-
# Emit pairs (single JSON line) for caller
|
|
468
|
-
echo "$pairs"
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
# ══════════════════════════════════════════
|
|
472
|
-
# idle-slots — Report how many teams are idle + how many ready features wait.
|
|
473
|
-
# No state mutation. Useful for dashboards / quick checks.
|
|
474
|
-
# ══════════════════════════════════════════
|
|
475
|
-
cmd_idle_slots() {
|
|
476
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first." >&2; exit 1; fi
|
|
477
|
-
jq '{
|
|
478
|
-
idle_teams: [.teams | to_entries[] | select(.value.status == "idle") | .key],
|
|
479
|
-
ready_features: .queue.ready,
|
|
480
|
-
dispatchable: (
|
|
481
|
-
(if ([.teams | to_entries[] | select(.value.status == "idle") | .key] | length) <
|
|
482
|
-
(.queue.ready | length)
|
|
483
|
-
then [.teams | to_entries[] | select(.value.status == "idle") | .key] | length
|
|
484
|
-
else .queue.ready | length
|
|
485
|
-
end)
|
|
486
|
-
)
|
|
487
|
-
}' "$QUEUE"
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
# ── Rate-Limit Hold checkpoint (v5.6.7+) ──
|
|
491
|
-
CHECKPOINT="$PROJECT_ROOT/.harness/actions/team-checkpoint.json"
|
|
492
|
-
|
|
493
|
-
cmd_hold() {
|
|
494
|
-
# Args: <reason> [retry_seconds=600]
|
|
495
|
-
local reason="${1:-rate_limit}"
|
|
496
|
-
local retry_secs="${2:-600}"
|
|
497
|
-
local now_iso retry_iso
|
|
498
|
-
now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
499
|
-
retry_iso="$(date -u -v+"${retry_secs}"S +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
500
|
-
|| date -u -d "+${retry_secs} seconds" +%Y-%m-%dT%H:%M:%SZ)"
|
|
501
|
-
acquire_queue_lock
|
|
502
|
-
trap release_queue_lock EXIT
|
|
503
|
-
local in_progress pending
|
|
504
|
-
in_progress="$(jq '[.teams[] | select(.current_feature != null) |
|
|
505
|
-
{team: .team_id, feature: .current_feature, phase: (.current_phase // "gen")}]' "$QUEUE")"
|
|
506
|
-
pending="$(jq '[.features[] | select(.status == "ready") | .id]' "$QUEUE")"
|
|
507
|
-
local tmp="${CHECKPOINT}.tmp"
|
|
508
|
-
jq -n --arg ts "$now_iso" --arg retry "$retry_iso" --arg reason "$reason" \
|
|
509
|
-
--argjson inprog "$in_progress" --argjson ready "$pending" \
|
|
510
|
-
'{timestamp:$ts, retry_at:$retry, last_error:$reason,
|
|
511
|
-
in_progress:$inprog, ready_features:$ready,
|
|
512
|
-
hold_count: 1, escalate_after: 72}' > "$tmp" && mv "$tmp" "$CHECKPOINT"
|
|
513
|
-
release_queue_lock
|
|
514
|
-
trap - EXIT
|
|
515
|
-
echo "[hold] checkpoint written — retry_at=$retry_iso reason=$reason"
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
cmd_resume_probe() {
|
|
519
|
-
# Lightweight probe — called on ScheduleWakeup resume.
|
|
520
|
-
# Exits 0 if ready to resume, 1 if still held, 2 if escalated.
|
|
521
|
-
[ ! -f "$CHECKPOINT" ] && { echo "[resume] no checkpoint — nothing to resume"; exit 0; }
|
|
522
|
-
local hold_count escalate_after
|
|
523
|
-
hold_count="$(jq -r '.hold_count // 1' "$CHECKPOINT")"
|
|
524
|
-
escalate_after="$(jq -r '.escalate_after // 72' "$CHECKPOINT")"
|
|
525
|
-
if [ "$hold_count" -ge "$escalate_after" ]; then
|
|
526
|
-
echo "[resume] ESCALATED — held $hold_count cycles (max=$escalate_after). Manual intervention required."
|
|
527
|
-
exit 2
|
|
528
|
-
fi
|
|
529
|
-
# Probe: minimal claude call — if rate-limited, exits non-zero quickly.
|
|
530
|
-
if command -v claude >/dev/null 2>&1; then
|
|
531
|
-
if echo "ping" | timeout 30 claude -p "reply only: pong" >/dev/null 2>&1; then
|
|
532
|
-
echo "[resume] probe OK — clearing hold"
|
|
533
|
-
rm -f "$CHECKPOINT"
|
|
534
|
-
exit 0
|
|
535
|
-
fi
|
|
536
|
-
fi
|
|
537
|
-
# Still held — increment and report
|
|
538
|
-
local tmp="${CHECKPOINT}.tmp"
|
|
539
|
-
jq '.hold_count = (.hold_count + 1) | .last_probe_at = now | todate' "$CHECKPOINT" > "$tmp" && mv "$tmp" "$CHECKPOINT"
|
|
540
|
-
echo "[resume] still held (cycle $((hold_count+1))/$escalate_after) — schedule another wake-up"
|
|
541
|
-
exit 1
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
# ══════════════════════════════════════════
|
|
545
|
-
# enqueue — Append feature to feature-list.json AND insert into queue (canonical path).
|
|
546
|
-
#
|
|
547
|
-
# Lead 가 ad-hoc 핫픽스 / 새 sprint feature 를 추가할 때 jq 로 큐를 직접 편집하면
|
|
548
|
-
# feature-list.json 과 큐가 갈라진다 (실제 사고: F-716, F-800~F-806 이 큐에만 등록).
|
|
549
|
-
# 이 명령은 feature-list append → queue insert (deps 미해소면 blocked) 를 한 트랜잭션으로 처리.
|
|
550
|
-
#
|
|
551
|
-
# Usage: enqueue <feature-json-file> # 파일 경로
|
|
552
|
-
# enqueue - # stdin 으로 JSON
|
|
553
|
-
#
|
|
554
|
-
# 입력 JSON 최소 필드: {id, title, sprint, depends_on?, layer?, service?, acceptance_criteria?}
|
|
555
|
-
# ══════════════════════════════════════════
|
|
556
|
-
cmd_enqueue() {
|
|
557
|
-
local input="${1:-}"
|
|
558
|
-
if [ -z "$input" ]; then
|
|
559
|
-
echo "[queue] enqueue: feature JSON 파일 경로 또는 '-' (stdin) 필요" >&2
|
|
560
|
-
exit 1
|
|
561
|
-
fi
|
|
562
|
-
if [ ! -f "$FEATURES" ]; then echo "[queue] feature-list.json not found." >&2; exit 1; fi
|
|
563
|
-
if [ ! -f "$QUEUE" ]; then echo "[queue] feature-queue.json not found. init 먼저." >&2; exit 1; fi
|
|
564
|
-
|
|
565
|
-
local feat_json
|
|
566
|
-
if [ "$input" = "-" ]; then
|
|
567
|
-
feat_json=$(cat)
|
|
568
|
-
else
|
|
569
|
-
feat_json=$(cat "$input")
|
|
570
|
-
fi
|
|
571
|
-
|
|
572
|
-
if ! echo "$feat_json" | jq -e '.id and .title and .sprint' >/dev/null 2>&1; then
|
|
573
|
-
echo "[queue] enqueue: id/title/sprint 필수 필드 누락" >&2
|
|
574
|
-
exit 1
|
|
575
|
-
fi
|
|
576
|
-
|
|
577
|
-
local fid
|
|
578
|
-
fid=$(echo "$feat_json" | jq -r '.id')
|
|
579
|
-
|
|
580
|
-
acquire_queue_lock
|
|
581
|
-
|
|
582
|
-
if jq -e --arg id "$fid" '.features[] | select(.id == $id)' "$FEATURES" >/dev/null 2>&1; then
|
|
583
|
-
release_queue_lock
|
|
584
|
-
echo "[queue] enqueue: $fid 이 feature-list.json 에 이미 존재" >&2
|
|
585
|
-
exit 1
|
|
586
|
-
fi
|
|
587
|
-
|
|
588
|
-
if jq -e --arg id "$fid" '
|
|
589
|
-
($id | IN(.queue.ready[]?,.queue.passed[]?)) or
|
|
590
|
-
(.queue.in_progress[$id]?) or (.queue.blocked[$id]?) or
|
|
591
|
-
(.queue.failed[$id]? // (.queue.failed | type == "array" and ($id | IN(.queue.failed[]?))))
|
|
592
|
-
' "$QUEUE" >/dev/null 2>&1; then
|
|
593
|
-
release_queue_lock
|
|
594
|
-
echo "[queue] enqueue: $fid 이 큐에 이미 존재" >&2
|
|
595
|
-
exit 1
|
|
596
|
-
fi
|
|
597
|
-
|
|
598
|
-
local tmp_features="${FEATURES}.tmp.$$"
|
|
599
|
-
if ! jq --argjson new "$feat_json" '.features += [$new]' "$FEATURES" > "$tmp_features" 2>/dev/null; then
|
|
600
|
-
rm -f "$tmp_features"; release_queue_lock
|
|
601
|
-
echo "[queue] enqueue: feature-list.json 업데이트 실패" >&2; exit 1
|
|
602
|
-
fi
|
|
603
|
-
mv "$tmp_features" "$FEATURES"
|
|
604
|
-
|
|
605
|
-
local deps unmet
|
|
606
|
-
deps=$(echo "$feat_json" | jq -c '.depends_on // []')
|
|
607
|
-
unmet=$(jq --argjson d "$deps" '
|
|
608
|
-
($d - (.queue.passed // [])) | length
|
|
609
|
-
' "$QUEUE" 2>/dev/null || echo 0)
|
|
610
|
-
|
|
611
|
-
local tmp_queue="${QUEUE}.tmp.$$"
|
|
612
|
-
if [ "${unmet:-0}" -gt 0 ]; then
|
|
613
|
-
jq --arg id "$fid" --argjson deps "$deps" '.queue.blocked[$id] = $deps' "$QUEUE" > "$tmp_queue" 2>/dev/null
|
|
614
|
-
else
|
|
615
|
-
jq --arg id "$fid" '.queue.ready += [$id]' "$QUEUE" > "$tmp_queue" 2>/dev/null
|
|
616
|
-
fi
|
|
617
|
-
if [ ! -s "$tmp_queue" ]; then
|
|
618
|
-
rm -f "$tmp_queue"; release_queue_lock
|
|
619
|
-
echo "[queue] enqueue: feature-queue.json 업데이트 실패" >&2; exit 1
|
|
620
|
-
fi
|
|
621
|
-
mv "$tmp_queue" "$QUEUE"
|
|
622
|
-
|
|
623
|
-
release_queue_lock
|
|
624
|
-
|
|
625
|
-
if [ "${unmet:-0}" -gt 0 ]; then
|
|
626
|
-
echo "[queue] enqueue: $fid → blocked (unmet deps: ${unmet})"
|
|
627
|
-
else
|
|
628
|
-
echo "[queue] enqueue: $fid → ready"
|
|
629
|
-
fi
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
# ══════════════════════════════════════════
|
|
633
|
-
# integrity — feature-list ↔ queue divergence 검사.
|
|
634
|
-
# 큐에 있지만 feature-list 에 없는 feature ID 를 출력. 0건이면 종료코드 0.
|
|
635
|
-
# ══════════════════════════════════════════
|
|
636
|
-
cmd_integrity() {
|
|
637
|
-
if [ ! -f "$FEATURES" ] || [ ! -f "$QUEUE" ]; then
|
|
638
|
-
echo "[integrity] feature-list 또는 feature-queue 없음"
|
|
639
|
-
exit 0
|
|
640
|
-
fi
|
|
641
|
-
local orphans
|
|
642
|
-
orphans=$(jq -r --slurpfile q "$QUEUE" '
|
|
643
|
-
([.features[].id]) as $fl |
|
|
644
|
-
(($q[0].queue.ready // []) +
|
|
645
|
-
($q[0].queue.passed // []) +
|
|
646
|
-
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
647
|
-
(($q[0].queue.in_progress // {}) | keys) +
|
|
648
|
-
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
649
|
-
($qids - $fl) | unique | .[]
|
|
650
|
-
' "$FEATURES" 2>/dev/null)
|
|
651
|
-
|
|
652
|
-
if [ -z "$orphans" ]; then
|
|
653
|
-
echo "[integrity] OK — feature-list ↔ queue 동기화됨"
|
|
654
|
-
exit 0
|
|
655
|
-
fi
|
|
656
|
-
echo "[integrity] ⚠ feature-list 에 없는 queue-only feature 발견:"
|
|
657
|
-
echo "$orphans" | sed 's/^/ - /'
|
|
658
|
-
echo "[integrity] 복구 방법:"
|
|
659
|
-
echo " 1) Planner 가 해당 feature 를 feature-list.json 에 sprint 지정해서 추가하거나"
|
|
660
|
-
echo " 2) bash scripts/harness-queue-manager.sh enqueue <feature.json> 로 canonical path 사용"
|
|
661
|
-
exit 1
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
cmd_hold_status() {
|
|
665
|
-
if [ ! -f "$CHECKPOINT" ]; then
|
|
666
|
-
echo '{"held":false}'
|
|
667
|
-
return
|
|
668
|
-
fi
|
|
669
|
-
jq '. + {held:true}' "$CHECKPOINT"
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
# ── Dispatch ──
|
|
673
|
-
case "$CMD" in
|
|
674
|
-
init) cmd_init "$@" ;;
|
|
675
|
-
dequeue) cmd_dequeue "$@" ;;
|
|
676
|
-
auto-dispatch) cmd_auto_dispatch ;;
|
|
677
|
-
idle-slots) cmd_idle_slots ;;
|
|
678
|
-
pass) cmd_pass "$@" ;;
|
|
679
|
-
fail) cmd_fail "$@" ;;
|
|
680
|
-
requeue) cmd_requeue "$@" ;;
|
|
681
|
-
update_phase) cmd_update_phase "$@" ;;
|
|
682
|
-
recover) cmd_recover ;;
|
|
683
|
-
next-sprint) cmd_next_sprint ;;
|
|
684
|
-
status) cmd_status ;;
|
|
685
|
-
hold) cmd_hold "$@" ;;
|
|
686
|
-
resume-probe) cmd_resume_probe ;;
|
|
687
|
-
hold-status) cmd_hold_status ;;
|
|
688
|
-
enqueue) cmd_enqueue "$@" ;;
|
|
689
|
-
integrity) cmd_integrity ;;
|
|
690
|
-
*)
|
|
691
|
-
echo "Usage: harness-queue-manager.sh <init|dequeue|auto-dispatch|idle-slots|pass|fail|requeue|recover|next-sprint|update_phase|status|hold|resume-probe|hold-status|enqueue|integrity> [args]"
|
|
692
|
-
exit 1
|
|
693
|
-
;;
|
|
694
|
-
esac
|