@walwal-harness/cli 5.9.4 → 5.9.6
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/README.md +34 -11
- package/commands/harness-team.md +40 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard.sh +62 -11
- package/scripts/harness-queue-manager.sh +123 -1
- package/scripts/harness-tmux.sh +19 -1
package/README.md
CHANGED
|
@@ -69,25 +69,31 @@ npx walwal-harness team
|
|
|
69
69
|
#### 대시보드 구성
|
|
70
70
|
|
|
71
71
|
```
|
|
72
|
-
|
|
73
|
-
│ Dashboard │
|
|
74
|
-
│ - pipeline/sprint │
|
|
75
|
-
│ - feature 진행도 │
|
|
76
|
-
│ - queue 상태 │
|
|
77
|
-
│ │
|
|
78
|
-
├────────────────────┤
|
|
79
|
-
│ Archive Prompt │
|
|
80
|
-
│ (완료 feature 요약)│
|
|
81
|
-
|
|
72
|
+
┌────────────────────┬───────────────┬───────────┐
|
|
73
|
+
│ Dashboard │ Gotchas │ TEAM 1 │
|
|
74
|
+
│ - pipeline/sprint │ (활성 에이전트)│ Gen|Eval │
|
|
75
|
+
│ - feature 진행도 ├───────────────┤ │
|
|
76
|
+
│ - queue 상태 │ Conventions ├───────────┤
|
|
77
|
+
│ │ (하우스 스타일)│ TEAM 2 │
|
|
78
|
+
├────────────────────┤ │ Gen|Eval │
|
|
79
|
+
│ Archive Prompt ├───────────────┤ │
|
|
80
|
+
│ (완료 feature 요약)│ Memory ├───────────┤
|
|
81
|
+
│ │ (공유 교훈) │ TEAM 3 │
|
|
82
|
+
│ │ │ Gen|Eval │
|
|
83
|
+
└────────────────────┴───────────────┴───────────┘
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
| 패널 | 내용 | 소스 |
|
|
85
87
|
|------|------|------|
|
|
86
88
|
| **Dashboard** | Pipeline · Sprint · Feature passes · Queue R:B:P · Retry | `harness-dashboard.sh` |
|
|
87
|
-
| **
|
|
89
|
+
| **Gotchas** | 활성 에이전트의 누적 실수 (`[G-NNN]`) — v5.9.1 부터 독립 패널 | `harness-gotcha-memory.sh --mode gotcha` |
|
|
90
|
+
| **Conventions** | 하우스 스타일 (`[C-NNN]`) — v5.9.1 부터 독립 패널, 독립 스크롤 | `harness-gotcha-memory.sh --mode conventions` |
|
|
91
|
+
| **Memory** | 공유 교훈 (`memory.md`) — v5.9.1 부터 독립 패널 | `harness-gotcha-memory.sh --mode memory` |
|
|
88
92
|
| **TEAM 1–3** | 각 워커의 현재 feature · phase(Gen/Eval) · 실시간 stdout | `harness-queue-manager.sh` worker loop |
|
|
89
93
|
| **Archive Prompt** | 직전 완료 feature 요약 (다음 팀 컨텍스트 주입용) | archive 디렉토리 |
|
|
90
94
|
|
|
95
|
+
> **v5.9.1+** Rules 컬럼이 3분할(Gotchas/Conventions/Memory)되어 각각 독립 스크롤됩니다. tmux/iTerm2 모두 동일한 레이아웃을 보장합니다.
|
|
96
|
+
|
|
91
97
|
##### Feature 상태 아이콘 (v5.6.4+)
|
|
92
98
|
|
|
93
99
|
| 아이콘 | 상태 | 의미 |
|
|
@@ -220,6 +226,16 @@ Dispatcher 자동 분류:
|
|
|
220
226
|
|
|
221
227
|
→ Dispatcher 가 `memory.md` 에 `### [M-NNN] ...` 로 기록. Planner 리뷰 후 `unverified → verified` 로 승격.
|
|
222
228
|
|
|
229
|
+
#### 동적 Gotcha/Convention 자동 등록 (v5.9.0+)
|
|
230
|
+
|
|
231
|
+
Worker 가 `gen-report-*.md` / `evaluation-*.md` 본문에 `gotcha_candidates` / `convention_candidates` 블록을 작성하면, Lead 가 PASS merge 직후 자동으로 dedup append:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
bash scripts/harness-gotcha-register.sh . --scan-all
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
→ 다음 worker spawn 전에 갱신된 gotchas/conventions 가 file system 에 반영. **한 sprint 안에서 발견된 실수를 같은 sprint 의 다음 worker 가 즉시 회피**할 수 있게 됨. Generator 도 mandatory — 모든 에이전트가 후보를 자기 보고서에 남기는 것을 강제합니다.
|
|
238
|
+
|
|
223
239
|
#### 주의 — 데이터 보존
|
|
224
240
|
|
|
225
241
|
`npm install` postinstall 은 **누적 엔트리(`[G-NNN]` 또는 `[C-NNN]`)가 있는 파일을 절대 덮어쓰지 않습니다**. 스캐폴드 템플릿인 경우에만 갱신됩니다. v5.5.2 이전 버전은 gotchas 에 이 버그가 있었으므로 `5.6.0+` 사용을 권장합니다.
|
|
@@ -470,6 +486,13 @@ jq '.mode = "solo"' .harness/progress.json > /tmp/p.json && mv /tmp/p.json .harn
|
|
|
470
486
|
- v5.5.2 에서 해결 (postinstall 이 누적 엔트리를 절대 덮어쓰지 않도록 수정).
|
|
471
487
|
- 반드시 `5.5.2+` 사용.
|
|
472
488
|
|
|
489
|
+
### Dashboard 헤더가 SOLO 인데 실제로는 팀 모드로 돌고 있음
|
|
490
|
+
- v5.9.5 에서 해결. `feature-queue.json.queue.in_progress > 0` 이면 dashboard refresh / tmux 재기동 시 자동으로 `mode=team` 으로 self-heal 합니다.
|
|
491
|
+
- 그 이전 버전: `progress.json` 의 `mode` 만 직접 수정하거나 새 세션을 열어 SessionStart 훅의 heal 을 트리거.
|
|
492
|
+
|
|
493
|
+
### `progress.json` 손상 시 dashboard crash
|
|
494
|
+
- v5.9.4 에서 해결. invalid JSON 인 경우 안내 메시지로 graceful degrade. 복구 가이드는 dashboard 본문에 inline 표시됩니다.
|
|
495
|
+
|
|
473
496
|
---
|
|
474
497
|
|
|
475
498
|
## License
|
package/commands/harness-team.md
CHANGED
|
@@ -447,6 +447,46 @@ Lead에게 반환 **첫 줄에 반드시 `RATE_LIMIT` 태그 포함**:
|
|
|
447
447
|
|
|
448
448
|
---
|
|
449
449
|
|
|
450
|
+
## Ad-hoc Feature 추가 — Canonical Path (필수)
|
|
451
|
+
|
|
452
|
+
Lead 가 sprint 진행 중 핫픽스 / 새 sprint feature 를 추가해야 할 때 **`feature-queue.json` 을 jq 로 직접 편집하면 안 된다**. feature-list.json 과 큐가 갈라져서 dashboard 가 누락 표시하고, Planner/Evaluator 가 AC 를 찾지 못한다.
|
|
453
|
+
|
|
454
|
+
**금지**:
|
|
455
|
+
```bash
|
|
456
|
+
# ❌ 절대 금지 — feature-list 우회
|
|
457
|
+
jq '.queue.ready += ["F-XXX"]' .harness/actions/feature-queue.json > /tmp/q.json && mv /tmp/q.json .harness/actions/feature-queue.json
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Canonical**:
|
|
461
|
+
```bash
|
|
462
|
+
# ✅ 옵션 1 — 새 sprint 의 정식 feature 들이면 Planner 재호출
|
|
463
|
+
# Planner 가 feature-list.json 에 sprint=N 으로 append → next-sprint 가 큐 채움.
|
|
464
|
+
|
|
465
|
+
# ✅ 옵션 2 — 단발 핫픽스면 enqueue 명령 사용 (feature-list append + 큐 insert 원자적)
|
|
466
|
+
cat > /tmp/feat.json <<EOF
|
|
467
|
+
{
|
|
468
|
+
"id": "F-XXX",
|
|
469
|
+
"title": "auth refresh token lifecycle hotfix",
|
|
470
|
+
"sprint": 8,
|
|
471
|
+
"depends_on": [],
|
|
472
|
+
"layer": "fe",
|
|
473
|
+
"service": "frontend",
|
|
474
|
+
"acceptance_criteria": [{"id":"AC-1","description":"...","type":"manual","verify":{"tool":"grep","steps":["..."]}}]
|
|
475
|
+
}
|
|
476
|
+
EOF
|
|
477
|
+
bash scripts/harness-queue-manager.sh enqueue /tmp/feat.json
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**무결성 체크** (Lead 루프 진입 직전 권장):
|
|
481
|
+
```bash
|
|
482
|
+
bash scripts/harness-queue-manager.sh integrity
|
|
483
|
+
# 종료 코드 0 = OK, 1 = orphan feature 발견 (feature-list 에 없는 queue 항목)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
이 룰을 위반한 사례: Sprint 8 (2026-04-29) 에 F-716, F-800~F-806 이 jq 직접 편집으로 큐에만 등록되어 dashboard Features 패널에서 800 번대가 모두 누락된 적 있음. **dashboard 는 v5.9.6+ 에서 union 렌더 + orphan 경고로 가시화하지만, 가시화는 차단이 아님 — Lead 가 위 canonical path 만 쓸 것**.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
450
490
|
## 핵심 원칙
|
|
451
491
|
|
|
452
492
|
### Worker = 1 Feature Only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "5.9.
|
|
3
|
+
"version": "5.9.6",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Solo/Team mode, Planner, Generator(BE/FE), Evaluator chain (Code-Quality → Functional → Visual), optional Brainstormer. Supports React, Next.js, and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -46,7 +46,27 @@ get_term_width() {
|
|
|
46
46
|
|
|
47
47
|
get_mode() {
|
|
48
48
|
if [ -f "$PROGRESS" ] && command -v jq &>/dev/null; then
|
|
49
|
-
|
|
49
|
+
local raw_mode queue active
|
|
50
|
+
raw_mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
|
|
51
|
+
queue="$(dirname "$PROGRESS")/actions/feature-queue.json"
|
|
52
|
+
if [ "$raw_mode" != "team" ] && [ -f "$queue" ]; then
|
|
53
|
+
active=$(jq -r '(.queue.in_progress | length) // 0' "$queue" 2>/dev/null || echo 0)
|
|
54
|
+
if [ "${active:-0}" -gt 0 ]; then
|
|
55
|
+
local heal_teams=$active
|
|
56
|
+
[ "$heal_teams" -gt 3 ] && heal_teams=3
|
|
57
|
+
local SCRIPT_DIR_LOCAL
|
|
58
|
+
SCRIPT_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
59
|
+
bash "$SCRIPT_DIR_LOCAL/harness-progress-set.sh" "$(dirname "$(dirname "$PROGRESS")")" \
|
|
60
|
+
".mode = \"team\" | .team_state.active_teams = ${heal_teams} | .team_state.paused_at = null" \
|
|
61
|
+
2>/dev/null
|
|
62
|
+
if [ -f "$(dirname "$PROGRESS")/progress.log" ]; then
|
|
63
|
+
echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | dashboard reset ${raw_mode}→team (queue had ${active} in_progress)" >> "$(dirname "$PROGRESS")/progress.log"
|
|
64
|
+
fi
|
|
65
|
+
echo "team"
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
echo "$raw_mode"
|
|
50
70
|
else
|
|
51
71
|
echo "solo"
|
|
52
72
|
fi
|
|
@@ -216,14 +236,23 @@ render_team_queue_summary() {
|
|
|
216
236
|
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
217
237
|
|
|
218
238
|
# passed 는 queue.passed ∪ feature-list.json 의 self-passed 를 dedup 합집합으로 계산
|
|
219
|
-
#
|
|
239
|
+
# total 도 feature-list IDs ∪ queue 전체 IDs 합집합으로 계산 — 새 sprint feature 가
|
|
240
|
+
# queue 에만 추가되고 feature-list 에 누락되어도 누락 없이 카운트 (예: F-800+ Sprint 8)
|
|
220
241
|
if [ -f "$FEATURES" ]; then
|
|
221
242
|
passed=$(jq -r --slurpfile q "$QUEUE" '
|
|
222
243
|
($q[0].queue.passed // []) as $qp |
|
|
223
244
|
([.features[] | select((.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) | .id]) as $sp |
|
|
224
245
|
($qp + $sp | unique | length)
|
|
225
246
|
' "$FEATURES" 2>/dev/null || echo 0)
|
|
226
|
-
total=$(jq
|
|
247
|
+
total=$(jq -r --slurpfile q "$QUEUE" '
|
|
248
|
+
([.features[].id]) as $fl |
|
|
249
|
+
(($q[0].queue.ready // []) +
|
|
250
|
+
($q[0].queue.passed // []) +
|
|
251
|
+
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
252
|
+
(($q[0].queue.in_progress // {}) | keys) +
|
|
253
|
+
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
254
|
+
($fl + $qids | unique | length)
|
|
255
|
+
' "$FEATURES" 2>/dev/null || echo 0)
|
|
227
256
|
else
|
|
228
257
|
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
229
258
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
@@ -240,6 +269,24 @@ render_team_queue_summary() {
|
|
|
240
269
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
241
270
|
|
|
242
271
|
echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
|
|
272
|
+
|
|
273
|
+
# Integrity warn — 큐에 있지만 feature-list 에 없는 orphan feature 가 있으면 경고.
|
|
274
|
+
# 발생 원인: Lead 가 jq 로 큐 직접 편집 (canonical path = enqueue 명령 사용).
|
|
275
|
+
if [ -f "$FEATURES" ]; then
|
|
276
|
+
local orphan_count
|
|
277
|
+
orphan_count=$(jq -r --slurpfile q "$QUEUE" '
|
|
278
|
+
([.features[].id]) as $fl |
|
|
279
|
+
(($q[0].queue.ready // []) +
|
|
280
|
+
($q[0].queue.passed // []) +
|
|
281
|
+
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
282
|
+
(($q[0].queue.in_progress // {}) | keys) +
|
|
283
|
+
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
284
|
+
($qids - $fl) | unique | length
|
|
285
|
+
' "$FEATURES" 2>/dev/null || echo 0)
|
|
286
|
+
if [ "${orphan_count:-0}" -gt 0 ]; then
|
|
287
|
+
echo -e " ${YELLOW}⚠ ${orphan_count} feature(s) in queue but not in feature-list.json${RESET} ${DIM}(use: queue-manager enqueue)${RESET}"
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
243
290
|
}
|
|
244
291
|
|
|
245
292
|
render_team_status() {
|
|
@@ -293,19 +340,23 @@ render_team_features() {
|
|
|
293
340
|
echo ""
|
|
294
341
|
echo -e "${BOLD}Features${RESET}"
|
|
295
342
|
|
|
343
|
+
# feature-list IDs ∪ queue 전체 IDs 합집합을 iterate.
|
|
344
|
+
# feature-list 에 등록 안 된 queue-only feature 는 name="(queue)" 로 표시.
|
|
296
345
|
jq -r --slurpfile q "$QUEUE" '
|
|
297
346
|
($q[0].queue.passed // []) as $passed |
|
|
298
|
-
($q[0].queue.failed // []) as $
|
|
347
|
+
($q[0].queue.failed // []) as $failed_raw |
|
|
348
|
+
(if ($failed_raw | type) == "object" then ($failed_raw | keys) else $failed_raw end) as $failed |
|
|
299
349
|
($q[0].queue.ready // []) as $ready |
|
|
300
350
|
($q[0].queue.in_progress // {}) as $prog |
|
|
301
351
|
($q[0].queue.blocked // {}) as $blocked |
|
|
302
|
-
.features[] |
|
|
303
|
-
.id as $
|
|
304
|
-
(.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
(
|
|
352
|
+
([.features[] | {id: .id, name: (.name // .title // .description // "?"), passes: (.passes // [])}]) as $fl |
|
|
353
|
+
([$fl[] | {(.id): .}] | add // {}) as $fl_map |
|
|
354
|
+
(([$fl[].id]) + $ready + $passed + $failed + ($prog | keys) + ($blocked | keys) | unique) as $all_ids |
|
|
355
|
+
$all_ids[] |
|
|
356
|
+
. as $fid |
|
|
357
|
+
($fl_map[$fid] // {name: "(queue)", passes: []}) as $f |
|
|
358
|
+
($f.name | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
359
|
+
(($f.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) as $self_passed |
|
|
309
360
|
(if ($fid | IN($passed[])) or $self_passed then "P"
|
|
310
361
|
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
311
362
|
elif ($fid | IN($failed[])) then "F"
|
|
@@ -541,6 +541,126 @@ cmd_resume_probe() {
|
|
|
541
541
|
exit 1
|
|
542
542
|
}
|
|
543
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
|
+
|
|
544
664
|
cmd_hold_status() {
|
|
545
665
|
if [ ! -f "$CHECKPOINT" ]; then
|
|
546
666
|
echo '{"held":false}'
|
|
@@ -565,8 +685,10 @@ case "$CMD" in
|
|
|
565
685
|
hold) cmd_hold "$@" ;;
|
|
566
686
|
resume-probe) cmd_resume_probe ;;
|
|
567
687
|
hold-status) cmd_hold_status ;;
|
|
688
|
+
enqueue) cmd_enqueue "$@" ;;
|
|
689
|
+
integrity) cmd_integrity ;;
|
|
568
690
|
*)
|
|
569
|
-
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> [args]"
|
|
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]"
|
|
570
692
|
exit 1
|
|
571
693
|
;;
|
|
572
694
|
esac
|
package/scripts/harness-tmux.sh
CHANGED
|
@@ -79,10 +79,28 @@ if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
|
79
79
|
exit 1
|
|
80
80
|
fi
|
|
81
81
|
|
|
82
|
-
# Auto-detect mode from progress.json if not specified
|
|
82
|
+
# Auto-detect mode from progress.json if not specified.
|
|
83
|
+
# Self-heal: feature-queue.json 에 in_progress 가 있으면 mode 와 무관하게 team 으로 간주.
|
|
84
|
+
# (SessionStart 훅 외 경로에서 dashboard/tmux 가 stale solo 를 표시하던 버그)
|
|
83
85
|
if [ -z "$MODE" ]; then
|
|
84
86
|
if command -v jq &>/dev/null && [ -f "$PROJECT_ROOT/.harness/progress.json" ]; then
|
|
85
87
|
MODE=$(jq -r '.mode // "solo"' "$PROJECT_ROOT/.harness/progress.json" 2>/dev/null)
|
|
88
|
+
QUEUE_FILE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
89
|
+
if [ "$MODE" != "team" ] && [ -f "$QUEUE_FILE" ]; then
|
|
90
|
+
ACTIVE_COUNT=$(jq -r '(.queue.in_progress | length) // 0' "$QUEUE_FILE" 2>/dev/null || echo 0)
|
|
91
|
+
if [ "${ACTIVE_COUNT:-0}" -gt 0 ]; then
|
|
92
|
+
HEAL_TEAMS=$ACTIVE_COUNT
|
|
93
|
+
[ "$HEAL_TEAMS" -gt 3 ] && HEAL_TEAMS=3
|
|
94
|
+
bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
|
|
95
|
+
".mode = \"team\" | .team_state.active_teams = ${HEAL_TEAMS} | .team_state.paused_at = null" \
|
|
96
|
+
2>/dev/null
|
|
97
|
+
MODE="team"
|
|
98
|
+
echo "# tmux: mode auto-healed to 'team' (queue had ${ACTIVE_COUNT} in_progress)"
|
|
99
|
+
if [ -f "$PROJECT_ROOT/.harness/progress.log" ]; then
|
|
100
|
+
echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | tmux reset solo→team (queue had ${ACTIVE_COUNT} in_progress)" >> "$PROJECT_ROOT/.harness/progress.log"
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
86
104
|
fi
|
|
87
105
|
MODE="${MODE:-solo}"
|
|
88
106
|
fi
|