@walwal-harness/cli 5.9.5 → 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 CHANGED
@@ -69,25 +69,31 @@ npx walwal-harness team
69
69
  #### 대시보드 구성
70
70
 
71
71
  ```
72
- ┌────────────────────┬──────────────────────────┬───────────┐
73
- │ Dashboard │ Gotcha & Memory │ TEAM 1 │
74
- │ - pipeline/sprint │ - 활성 에이전트 gotcha │ Gen|Eval │
75
- │ - feature 진행도 │ - 나머지 에이전트 요약 ├───────────┤
76
- │ - queue 상태 │ - SHARED MEMORY │ TEAM 2 │
77
- │ │ (memory.md 최근 N) │ Gen|Eval │
78
- ├────────────────────┤ ├───────────┤
79
- │ Archive Prompt │ │ TEAM 3 │
80
- │ (완료 feature 요약)│ │ Gen|Eval │
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
- | **Gotcha & Memory** | 활성 에이전트의 누적 실수 + 나머지 요약 + 공유 메모리 | `harness-gotcha-memory.sh` |
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
@@ -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.5",
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"
@@ -236,14 +236,23 @@ render_team_queue_summary() {
236
236
  failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
237
237
 
238
238
  # passed 는 queue.passed ∪ feature-list.json 의 self-passed 를 dedup 합집합으로 계산
239
- # (과거 sprint 에서 PASS 되어 queue 에서 빠진 feature 도 카운트)
239
+ # total 도 feature-list IDs ∪ queue 전체 IDs 합집합으로 계산 — 새 sprint feature 가
240
+ # queue 에만 추가되고 feature-list 에 누락되어도 누락 없이 카운트 (예: F-800+ Sprint 8)
240
241
  if [ -f "$FEATURES" ]; then
241
242
  passed=$(jq -r --slurpfile q "$QUEUE" '
242
243
  ($q[0].queue.passed // []) as $qp |
243
244
  ([.features[] | select((.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) | .id]) as $sp |
244
245
  ($qp + $sp | unique | length)
245
246
  ' "$FEATURES" 2>/dev/null || echo 0)
246
- total=$(jq '.features | length' "$FEATURES" 2>/dev/null || echo 0)
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)
247
256
  else
248
257
  passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
249
258
  total=$((ready + blocked + in_prog + passed + failed))
@@ -260,6 +269,24 @@ render_team_queue_summary() {
260
269
  for ((i=0; i<empty; i++)); do bar+="░"; done
261
270
 
262
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
263
290
  }
264
291
 
265
292
  render_team_status() {
@@ -313,19 +340,23 @@ render_team_features() {
313
340
  echo ""
314
341
  echo -e "${BOLD}Features${RESET}"
315
342
 
343
+ # feature-list IDs ∪ queue 전체 IDs 합집합을 iterate.
344
+ # feature-list 에 등록 안 된 queue-only feature 는 name="(queue)" 로 표시.
316
345
  jq -r --slurpfile q "$QUEUE" '
317
346
  ($q[0].queue.passed // []) as $passed |
318
- ($q[0].queue.failed // []) as $failed |
347
+ ($q[0].queue.failed // []) as $failed_raw |
348
+ (if ($failed_raw | type) == "object" then ($failed_raw | keys) else $failed_raw end) as $failed |
319
349
  ($q[0].queue.ready // []) as $ready |
320
350
  ($q[0].queue.in_progress // {}) as $prog |
321
351
  ($q[0].queue.blocked // {}) as $blocked |
322
- .features[] |
323
- .id as $fid |
324
- (.name // .title // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
325
- # passed 판정: queue.passed 또는 feature.passes 에 evaluator-functional/visual/code-quality 가 있으면 PASS.
326
- # 과거 sprint 에서 PASS 된 feature 가 새 sprint queue 재생성 시 queue.passed 에서 누락되어도
327
- # feature-list.json 의 passes 배열은 이력으로 남아있으므로, 거기서도 검사한다.
328
- ((.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) as $self_passed |
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 |
329
360
  (if ($fid | IN($passed[])) or $self_passed then "P"
330
361
  elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
331
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