@walwal-harness/cli 5.9.5 → 6.0.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "5.9.5",
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.",
3
+ "version": "6.0.0",
4
+ "description": "Production harness for AI agent engineering — NEXUS-adapted company metaphor (Dispatcher/CEO + Conductor + Meeting-Manager + COO/Planner + CTO + CQO + Service-Ops). Solo/Team mode, Brainstormer, Planner, Generator(BE/FE/Designer/DevOps), Evaluator chain (Code-Quality → Functional → Visual + Architecture/Security). Supports React, Next.js, and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
7
7
  },
@@ -24,6 +24,13 @@
24
24
  "planner",
25
25
  "generator",
26
26
  "evaluator",
27
+ "nexus",
28
+ "ceo",
29
+ "cto",
30
+ "cqo",
31
+ "conductor",
32
+ "meeting-manager",
33
+ "service-ops",
27
34
  "nestjs",
28
35
  "react",
29
36
  "flutter",
@@ -45,6 +52,7 @@
45
52
  "scripts/",
46
53
  "commands/",
47
54
  "assets/",
48
- "gotchas/"
55
+ "gotchas/",
56
+ "CHANGELOG.md"
49
57
  ]
50
58
  }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # walwal-harness — Brick Office dashboard launcher
3
+ #
4
+ # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
+ # walwal-harness CLI 패키지에는 R3F + Next.js 무게 때문에 dashboard 가 미포함이고,
6
+ # 본 스크립트가 git sparse-checkout 으로 apps/harness-dashboard/ 만 가져와서
7
+ # 사용자 프로젝트 외부 (~/.walwal-harness/dashboard) 에 격리 설치 후 dev 실행한다.
8
+ #
9
+ # Usage:
10
+ # bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
11
+ # bash scripts/harness-dashboard-up.sh --port 3050 # override port
12
+ # HARNESS_ROOT=/path/to/project bash ... # explicit harness root
13
+ set -euo pipefail
14
+
15
+ REPO_URL="${WALWAL_HARNESS_REPO:-https://github.com/30eggis/walwal-harness.git}"
16
+ DASHBOARD_PATH="apps/harness-dashboard"
17
+ LOCAL_DIR="${HOME}/.walwal-harness/dashboard"
18
+ PORT="${PORT:-3001}"
19
+
20
+ while [[ $# -gt 0 ]]; do
21
+ case "$1" in
22
+ --port) PORT="$2"; shift 2 ;;
23
+ --reinstall) rm -rf "$LOCAL_DIR"; shift ;;
24
+ --help|-h)
25
+ sed -n '2,16p' "$0"
26
+ exit 0 ;;
27
+ *) echo "[brick-office] unknown arg: $1" >&2; exit 2 ;;
28
+ esac
29
+ done
30
+
31
+ if [[ -z "${HARNESS_ROOT:-}" ]]; then
32
+ HARNESS_ROOT="$(pwd)"
33
+ fi
34
+ if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
35
+ echo "[brick-office] FAIL: ${HARNESS_ROOT} 에 .harness/ 가 없습니다."
36
+ echo " 먼저 'npx walwal-harness' 로 초기화하세요."
37
+ exit 3
38
+ fi
39
+
40
+ echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
41
+ echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
42
+
43
+ if [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
44
+ echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
45
+ mkdir -p "${LOCAL_DIR}"
46
+ cd "${LOCAL_DIR}"
47
+ git init -q
48
+ git remote add origin "${REPO_URL}" 2>/dev/null || true
49
+ git config core.sparseCheckout true
50
+ echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
51
+ git fetch --depth=1 origin main -q
52
+ git checkout main -q
53
+ echo "[brick-office] 다운로드 완료."
54
+ fi
55
+
56
+ cd "${LOCAL_DIR}/${DASHBOARD_PATH}"
57
+
58
+ if [[ ! -d node_modules ]]; then
59
+ echo "[brick-office] npm install (한 번만 실행, ~30s)..."
60
+ npm install --no-audit --no-fund --loglevel=error
61
+ fi
62
+
63
+ echo ""
64
+ echo "╔══════════════════════════════════════════════════════════╗"
65
+ echo "║ Brick Office — http://localhost:${PORT}"
66
+ echo "║ ║"
67
+ echo "║ HARNESS_ROOT 이 가리키는 프로젝트의 .harness/ 상태를 ║"
68
+ echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료. ║"
69
+ echo "╚══════════════════════════════════════════════════════════╝"
70
+ echo ""
71
+
72
+ HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -p "${PORT}"
@@ -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"
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # harness-goal-init.sh — 새 GOAL 발급 (CEO=Dispatcher 전용)
3
+ # 사용법: bash scripts/harness-goal-init.sh "<title>"
4
+ #
5
+ # 동작:
6
+ # 1. .harness/actions/goals.md 가 없으면 template로 생성
7
+ # 2. 다음 GOAL ID(G-N) 발급 → progress.json.goals.list 에 등록
8
+ # 3. progress.json.goals.active_id 갱신
9
+ # 4. CTO 협의 미완료 상태(cto_feasibility=null, owner_confirmed=false)
10
+ #
11
+ # 후속:
12
+ # - CTO는 cto_feasibility 의견을 협의 후 같은 항목에 채워줌
13
+ # - Owner 최종 확인 시 dispatcher 가 owner_confirmed=true 로 갱신
14
+ set -euo pipefail
15
+
16
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
17
+ GOAL_FILE="$ROOT/.harness/actions/goals.md"
18
+ TEMPLATE="$ROOT/.harness/actions/goals.md.template"
19
+ PROGRESS="$ROOT/.harness/progress.json"
20
+
21
+ TITLE="${1:-}"
22
+ if [ -z "$TITLE" ]; then
23
+ echo "usage: $0 \"<title>\"" >&2
24
+ exit 1
25
+ fi
26
+
27
+ mkdir -p "$ROOT/.harness/actions"
28
+
29
+ # 1. 파일이 없으면 template 복사 후 ISO 시각 채움
30
+ if [ ! -f "$GOAL_FILE" ]; then
31
+ if [ ! -f "$TEMPLATE" ]; then
32
+ echo "template missing: $TEMPLATE" >&2; exit 1
33
+ fi
34
+ ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
35
+ sed "s|<ISO>|$ISO|g" "$TEMPLATE" > "$GOAL_FILE"
36
+ fi
37
+
38
+ # 2. 다음 GOAL ID 계산
39
+ LAST_N=$(jq -r '.goals.list | map(.id // "G-0") | map(sub("G-"; "")) | map(tonumber? // 0) | max // 0' "$PROGRESS" 2>/dev/null || echo 0)
40
+ NEXT_N=$((LAST_N + 1))
41
+ GOAL_ID="G-${NEXT_N}"
42
+ ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
43
+
44
+ # 3. progress.json 에 등록 (partial update)
45
+ jq --arg id "$GOAL_ID" --arg title "$TITLE" --arg iso "$ISO" '
46
+ .goals.list += [{
47
+ "id": $id,
48
+ "title": $title,
49
+ "status": "draft",
50
+ "owner_confirmed": false,
51
+ "cto_feasibility": null,
52
+ "created_at": $iso
53
+ }] |
54
+ .goals.active_id = $id |
55
+ .updated_at = $iso
56
+ ' "$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
57
+
58
+ # 4. goals.md 에 GOAL 섹션 append (CEO가 이후 본문 채움)
59
+ cat >> "$GOAL_FILE" <<EOF
60
+
61
+ ## $GOAL_ID — $TITLE
62
+ status: draft
63
+ owner_confirmed: false
64
+ cto_feasibility: null
65
+ created_at: $ISO
66
+
67
+ (CEO가 success_metrics·deadline·kpis·runbook 채움)
68
+ EOF
69
+
70
+ echo "$GOAL_ID 발급 완료: $TITLE"
71
+ echo "→ $GOAL_FILE 에서 본문 작성"
72
+ echo "→ CTO 협의 후 owner 확정 시 owner_confirmed=true"
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # harness-goal-show.sh — 현재 active GOAL + 적합도 요약 출력
3
+ set -euo pipefail
4
+
5
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
6
+ PROGRESS="$ROOT/.harness/progress.json"
7
+ GOAL_FILE="$ROOT/.harness/actions/goals.md"
8
+
9
+ if [ ! -f "$PROGRESS" ]; then
10
+ echo "progress.json 없음. bash init.sh 먼저 실행." >&2; exit 1
11
+ fi
12
+
13
+ ACTIVE_ID=$(jq -r '.goals.active_id // ""' "$PROGRESS")
14
+ ADHERENCE=$(jq -r '.goals.current_adherence // "n/a"' "$PROGRESS")
15
+
16
+ if [ -z "$ACTIVE_ID" ]; then
17
+ echo "Active GOAL 없음. CEO(Dispatcher)가 첫 발화에서 발급."
18
+ echo " bash scripts/harness-goal-init.sh \"<title>\""
19
+ exit 0
20
+ fi
21
+
22
+ echo "Active GOAL: $ACTIVE_ID"
23
+ echo "Adherence: $ADHERENCE"
24
+ echo ""
25
+
26
+ # progress.json 의 GOAL 메타 출력
27
+ jq -r --arg id "$ACTIVE_ID" '
28
+ .goals.list[] | select(.id == $id) |
29
+ "title: \(.title)",
30
+ "status: \(.status)",
31
+ "owner_confirmed: \(.owner_confirmed)",
32
+ "cto_feasibility: \(.cto_feasibility // "pending")",
33
+ "created_at: \(.created_at // "?")"
34
+ ' "$PROGRESS"
35
+
36
+ echo ""
37
+ echo "본문: $GOAL_FILE 에서 ## $ACTIVE_ID 섹션 참조"
@@ -415,25 +415,24 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
415
415
  # ── Collect artifacts ──
416
416
  FEATURE_LIST="$PROJECT_ROOT/.harness/actions/feature-list.json"
417
417
 
418
- local -a artifacts_ready=()
418
+ artifacts_ready=()
419
419
  for f in plan.md feature-list.json api-contract.json sprint-contract.md evaluation-functional.md evaluation-visual.md; do
420
420
  if [ -f "$PROJECT_ROOT/.harness/actions/$f" ]; then
421
421
  artifacts_ready+=("$f")
422
422
  fi
423
423
  done
424
- local artifacts_json
425
424
  artifacts_json=$(printf '%s\n' "${artifacts_ready[@]}" | jq -R . | jq -s .)
426
425
 
427
426
  # Collect focus features (incomplete ones)
428
- local focus_features="[]"
427
+ focus_features="[]"
429
428
  if [ -f "$FEATURE_LIST" ]; then
430
429
  focus_features=$(jq '[.features[]? | select(.passes == null or (.passes | length) == 0 or ((.passes // []) | map(select(. == "evaluator-functional")) | length == 0)) | .id] | .[0:5]' "$FEATURE_LIST" 2>/dev/null || echo "[]")
431
430
  fi
432
431
 
433
432
  # ── Regression data ──
434
- local regression_source="null"
435
- local prev_sprint=$((sprint_num - 1))
436
- local prev_archive="$PROJECT_ROOT/.harness/archive/sprint-$(printf '%03d' $prev_sprint)"
433
+ regression_source="null"
434
+ prev_sprint=$((sprint_num - 1))
435
+ prev_archive="$PROJECT_ROOT/.harness/archive/sprint-$(printf '%03d' $prev_sprint)"
437
436
  if [ "$prev_sprint" -ge 1 ] && [ -d "$prev_archive" ]; then
438
437
  if [ -f "$prev_archive/feature-list.json" ]; then
439
438
  regression_source=$(jq '{
@@ -445,8 +444,8 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
445
444
  fi
446
445
 
447
446
  # ── Eval-specific config ──
448
- local eval_config="null"
449
- local cross_validation_data="null"
447
+ eval_config="null"
448
+ cross_validation_data="null"
450
449
  case "$next_agent" in
451
450
  evaluator-*)
452
451
  eval_config=$(jq '{
@@ -462,15 +461,15 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
462
461
  esac
463
462
 
464
463
  # ── Cross-Validation (chain: code-quality → functional → visual) ──
465
- local cross_validation_from_code_quality="null"
464
+ cross_validation_from_code_quality="null"
466
465
  if [ "$next_agent" = "evaluator-functional" ] || [ "$next_agent" = "evaluator-visual" ]; then
467
- local cq_eval="$PROJECT_ROOT/.harness/actions/evaluation-code-quality.md"
466
+ cq_eval="$PROJECT_ROOT/.harness/actions/evaluation-code-quality.md"
468
467
  if [ -f "$cq_eval" ]; then
469
468
  cross_validation_from_code_quality=$(sed -n '/```json/,/```/p' "$cq_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "code-quality" or .cross_validation_from_code_quality)' 2>/dev/null || echo "null")
470
469
  fi
471
470
  fi
472
471
  if [ "$next_agent" = "evaluator-visual" ]; then
473
- local func_eval="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
472
+ func_eval="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
474
473
  if [ -f "$func_eval" ]; then
475
474
  cross_validation_data=$(sed -n '/```json/,/```/p' "$func_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "functional")' 2>/dev/null || echo "null")
476
475
  fi
@@ -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