@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.20

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.
@@ -133,62 +133,15 @@
133
133
  ]
134
134
  }
135
135
  },
136
- "generator-frontend-flutter": {
137
- "role": "Flutter 앱 개발 — Riverpod, integrated_data_layer(Retrofit), i18n(ARB), build_runner",
138
- "skill": "harness-generator-frontend-flutter",
139
- "inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
140
- "outputs": ["code:flutter", "actions/sprint-contract.md"],
141
- "order": 2,
142
- "fe_stack": "flutter",
143
- "model": "sonnet",
144
- "thinking_mode": null
145
- },
146
- "evaluator-functional-flutter": {
147
- "role": "Flutter 앱 검증 — flutter analyze, flutter test, build_runner 일관성, 안티패턴 정적 검증",
148
- "skill": "harness-evaluator-functional-flutter",
149
- "tools": ["bash:flutter", "bash:dart"],
150
- "inputs": ["actions/sprint-contract.md"],
151
- "outputs": ["actions/evaluation-functional.md"],
152
- "fe_stack": "flutter",
153
- "model": "opus",
154
- "thinking_mode": "ultrathink"
155
- }
156
136
  },
157
137
  "flow": {
158
138
  "sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
159
139
  "pipeline_selection": {
160
- "comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정. fe_stack 필드로 React/Flutter를 구분. harness-next.sh가 pipeline.json.fe_stack 을 읽어 generator-frontend/evaluator-functional 을 Flutter 변형으로 치환한다.",
140
+ "comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.",
161
141
  "pipelines": {
162
142
  "FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
163
143
  "FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
164
144
  "BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
165
- },
166
- "fe_stack_substitution": {
167
- "comment": "pipeline.json.fe_stack + fe_target 에 따라 FE 에이전트 치환. Flutter Web 은 React 와 동일한 Playwright 기반 evaluator 를 사용한다.",
168
- "flutter": {
169
- "_doc": "fe_target = web | mobile | desktop. by_target 으로 분기.",
170
- "by_target": {
171
- "web": {
172
- "_doc": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 Playwright evaluator 사용 가능",
173
- "generator-frontend": "generator-frontend-flutter",
174
- "evaluator-functional": "evaluator-functional",
175
- "evaluator-visual": "evaluator-visual"
176
- },
177
- "mobile": {
178
- "_doc": "Flutter Mobile (Android/iOS) — 브라우저 없음, 정적 분석 evaluator 사용",
179
- "generator-frontend": "generator-frontend-flutter",
180
- "evaluator-functional": "evaluator-functional-flutter",
181
- "evaluator-visual": "__skip__"
182
- },
183
- "desktop": {
184
- "_doc": "Flutter Desktop (macOS/Windows/Linux) — 브라우저 없음, 정적 분석 evaluator 사용",
185
- "generator-frontend": "generator-frontend-flutter",
186
- "evaluator-functional": "evaluator-functional-flutter",
187
- "evaluator-visual": "__skip__"
188
- }
189
- },
190
- "_default_target": "mobile"
191
- }
192
145
  }
193
146
  },
194
147
  "sprint_execution": {
@@ -0,0 +1 @@
1
+ # For consumer projects, add .worktrees/ to .gitignore
package/bin/init.js CHANGED
@@ -231,6 +231,7 @@ function installScripts() {
231
231
  'harness-control-v4.sh',
232
232
  'harness-queue-manager.sh',
233
233
  'harness-team-worker.sh',
234
+ 'harness-prompts-v4.sh',
234
235
  ]);
235
236
 
236
237
  if (fs.existsSync(scriptsSrc)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "4.0.0-alpha.2",
3
+ "version": "4.0.0-alpha.20",
4
4
  "description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # harness-dashboard-v4.sh — v4 Dashboard: Feature Queue + Team Status
3
- # Auto-refresh 3초 간격. feature-queue.json + feature-list.json 시각화.
2
+ # harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
3
+ # Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
4
4
 
5
5
  set -uo pipefail
6
6
 
@@ -30,39 +30,34 @@ render_header() {
30
30
  now=$(date +"%H:%M:%S")
31
31
  project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
32
32
 
33
- echo -e "${BOLD}╔════════════════════════════════════════════════╗${RESET}"
34
- echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
35
- echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
36
- echo -e " ${DIM}${project_name} | ${now}${RESET}"
37
- echo ""
33
+ echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
38
34
  }
39
35
 
40
36
  render_queue_summary() {
41
37
  if [ ! -f "$QUEUE" ]; then
42
- echo -e " ${DIM}(queue not initialized — run 'init' in Control)${RESET}"
38
+ echo -e " ${DIM}(queue not initialized)${RESET}"
43
39
  return
44
40
  fi
45
41
 
46
- local ready blocked in_prog passed failed total concurrency
47
- ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null)
48
- blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null)
49
- in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null)
50
- passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null)
51
- failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null)
52
- concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null)
42
+ local ready blocked in_prog passed failed total
43
+ ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
44
+ blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
45
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
46
+ passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
47
+ failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
48
+ ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
53
49
  total=$((ready + blocked + in_prog + passed + failed))
54
50
 
55
- # Progress bar
56
51
  local pct=0
57
52
  if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
58
- local bar_w=20 filled=$(( pct * bar_w / 100 )) empty=$(( bar_w - filled ))
53
+ local bar_w=16
54
+ local filled=$(( pct * bar_w / 100 ))
55
+ local empty=$(( bar_w - filled ))
59
56
  local bar=""
60
57
  for ((i=0; i<filled; i++)); do bar+="█"; done
61
58
  for ((i=0; i<empty; i++)); do bar+="░"; done
62
59
 
63
- echo -e " ${BOLD}Queue${RESET} ${bar} ${passed}/${total} (${pct}%) ${DIM}concurrency=${concurrency}${RESET}"
64
- echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
65
- echo ""
60
+ 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}"
66
61
  }
67
62
 
68
63
  render_teams() {
@@ -72,105 +67,87 @@ render_teams() {
72
67
  team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
73
68
  if [ "${team_count:-0}" -eq 0 ]; then return; fi
74
69
 
75
- echo -e " ${BOLD}Teams${RESET}"
76
-
77
70
  for i in $(seq 1 "$team_count"); do
78
71
  local t_status t_feature t_phase t_attempt
79
72
  t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
80
73
  t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
81
74
 
82
- # Get phase from in_progress
83
75
  if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
84
76
  t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
85
77
  t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
86
78
  else
87
- t_phase="—"
88
- t_attempt="—"
79
+ t_phase="—"; t_attempt=""
89
80
  fi
90
81
 
91
82
  local icon color
92
83
  case "$t_status" in
93
- busy) icon="▶" ; color="$GREEN" ;;
94
- idle) icon="○" ; color="$DIM" ;;
95
- paused) icon="" ; color="$YELLOW" ;;
96
- *) icon="?" ; color="$RESET" ;;
84
+ busy) icon="▶"; color="$GREEN" ;;
85
+ idle) icon="○"; color="$DIM" ;;
86
+ *) icon="?"; color="$RESET" ;;
97
87
  esac
98
88
 
99
- local phase_display=""
89
+ local phase_short=""
100
90
  case "$t_phase" in
101
- gen) phase_display="${CYAN}GEN${RESET}" ;;
102
- gate) phase_display="${YELLOW}GATE${RESET}" ;;
103
- eval) phase_display="${MAGENTA}EVAL${RESET}" ;;
104
- *) phase_display="${DIM}${t_phase}${RESET}" ;;
91
+ gen) phase_short="${CYAN}G${RESET}" ;;
92
+ gate) phase_short="${YELLOW}K${RESET}" ;;
93
+ eval) phase_short="${MAGENTA}E${RESET}" ;;
94
+ *) phase_short="${DIM}-${RESET}" ;;
105
95
  esac
106
96
 
107
- printf " %b %b Team %d %-8s %b attempt %s\n" "$color" "$icon" "$i" "$t_feature" "$phase_display" "$t_attempt"
97
+ printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
98
+ if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
99
+ printf " #%s" "$t_attempt"
100
+ fi
101
+ echo ""
108
102
  done
109
- echo ""
110
103
  }
111
104
 
112
- render_feature_list() {
105
+ render_features() {
113
106
  if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
114
107
 
115
- local total
116
- total=$(jq '.features | length' "$FEATURES" 2>/dev/null)
117
- if [ "${total:-0}" -eq 0 ]; then return; fi
118
-
119
- echo -e " ${BOLD}Features${RESET}"
120
-
121
- local i=0
122
- while [ "$i" -lt "$total" ]; do
123
- local fid fname status_icon
124
- fid=$(jq -r ".features[$i].id" "$FEATURES" 2>/dev/null)
125
- fname=$(jq -r ".features[$i].name // .features[$i].description // \"\"" "$FEATURES" 2>/dev/null)
126
- if [ ${#fname} -gt 22 ]; then fname="${fname:0:20}.."; fi
127
-
128
- # Determine status from queue
129
- local in_passed in_failed in_progress in_ready in_blocked
130
- in_passed=$(jq -r --arg f "$fid" '.queue.passed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
131
- in_failed=$(jq -r --arg f "$fid" '.queue.failed // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
132
- in_progress=$(jq -r --arg f "$fid" '.queue.in_progress[$f] // empty' "$QUEUE" 2>/dev/null)
133
- in_ready=$(jq -r --arg f "$fid" '.queue.ready // [] | map(select(. == $f)) | length' "$QUEUE" 2>/dev/null)
134
-
135
- if [ "${in_passed:-0}" -gt 0 ]; then
136
- status_icon="${GREEN}●${RESET}"
137
- elif [ -n "$in_progress" ] && [ "$in_progress" != "" ]; then
138
- local team phase
139
- team=$(echo "$in_progress" | jq -r '.team // "?"' 2>/dev/null)
140
- phase=$(echo "$in_progress" | jq -r '.phase // "?"' 2>/dev/null)
141
- status_icon="${CYAN}◐${RESET} T${team}:${phase}"
142
- elif [ "${in_failed:-0}" -gt 0 ]; then
143
- status_icon="${RED}✗${RESET}"
144
- elif [ "${in_ready:-0}" -gt 0 ]; then
145
- status_icon="${YELLOW}○${RESET}"
146
- else
147
- status_icon="${DIM}◌${RESET}" # blocked
148
- fi
149
-
150
- printf " %b %-6s %-24s\n" "$status_icon" "$fid" "$fname"
151
-
152
- i=$((i + 1))
108
+ jq -r --slurpfile q "$QUEUE" '
109
+ ($q[0].queue.passed // []) as $passed |
110
+ ($q[0].queue.failed // []) as $failed |
111
+ ($q[0].queue.ready // []) as $ready |
112
+ ($q[0].queue.in_progress // {}) as $prog |
113
+ .features[] |
114
+ .id as $fid |
115
+ (.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
116
+ (if ($fid | IN($passed[])) then "P"
117
+ elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
118
+ elif ($fid | IN($failed[])) then "F"
119
+ elif ($fid | IN($ready[])) then "R"
120
+ else "B" end) as $st |
121
+ "\($st)\t\($fid)\t\($fname)"
122
+ ' "$FEATURES" 2>/dev/null | while IFS=$'\t' read -r st fid fname; do
123
+ case "$st" in
124
+ P) printf " ${GREEN}●${RESET} %-6s %s\n" "$fid" "$fname" ;;
125
+ F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
126
+ R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
127
+ B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
128
+ I\|*) team=$(echo "$st" | cut -d'|' -f2)
129
+ phase=$(echo "$st" | cut -d'|' -f3)
130
+ printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
131
+ *) printf " ? %-6s %s\n" "$fid" "$fname" ;;
132
+ esac
153
133
  done
154
- echo ""
155
134
  }
156
135
 
157
136
  render_all() {
158
137
  render_header
159
138
  render_queue_summary
160
139
  render_teams
161
- render_feature_list
162
- echo -e " ${DIM}Refreshing every 3s${RESET}"
140
+ echo ""
141
+ render_features
163
142
  }
164
143
 
165
144
  # ── Main loop ──
166
145
  tput civis 2>/dev/null
167
146
  trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
168
-
169
147
  clear
170
148
 
171
149
  while true; do
172
- local buf
173
- buf=$(render_all 2>/dev/null)
150
+ buf=$(render_all 2>&1)
174
151
  tput cup 0 0 2>/dev/null
175
152
  echo "$buf"
176
153
  tput ed 2>/dev/null
@@ -59,21 +59,10 @@ if [ -f "$PIPELINE_JSON" ]; then
59
59
  fi
60
60
 
61
61
  # ─────────────────────────────────────────
62
- # fe_stack + fe_target 치환 헬퍼
63
- # pipeline_selection.pipelines 에서 읽은 에이전트명을 fe_stack/fe_target 에 따라 치환
64
- # - react: 그대로
65
- # - flutter+web: generator-frontend → generator-frontend-flutter 만 치환, eval 은 그대로 (Playwright 사용 가능)
66
- # - flutter+mobile/desktop: eval 도 정적 분석용으로 치환, evaluator-visual 은 __skip__
62
+ # fe_stack 치환 (no-op Flutter 지원 제거됨, 하위 호환용 stub)
67
63
  # ─────────────────────────────────────────
68
64
  substitute_fe_stack() {
69
- local agent="$1"
70
- if [ "$fe_stack" != "flutter" ]; then
71
- echo "$agent"
72
- return
73
- fi
74
- local sub
75
- sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
76
- echo "$sub"
65
+ echo "$1"
77
66
  }
78
67
 
79
68
  # ─────────────────────────────────────────
@@ -105,7 +94,7 @@ run_pre_eval_gate() {
105
94
  # current_agent가 generator가 아닌 경우 (예: dispatcher로 리라우팅된 상태),
106
95
  # completed_agents에서 마지막 generator를 찾는다
107
96
  case "$source_agent" in
108
- generator-frontend|generator-frontend-flutter)
97
+ generator-frontend)
109
98
  location="frontend"
110
99
  checks_key="frontend_checks"
111
100
  ;;
@@ -118,7 +107,7 @@ run_pre_eval_gate() {
118
107
  local last_gen
119
108
  last_gen=$(jq -r '.completed_agents // [] | map(select(startswith("generator-"))) | last // empty' "$PROGRESS" 2>/dev/null)
120
109
  case "$last_gen" in
121
- generator-frontend|generator-frontend-flutter)
110
+ generator-frontend)
122
111
  location="frontend"
123
112
  checks_key="frontend_checks"
124
113
  ;;
@@ -0,0 +1,106 @@
1
+ #!/bin/bash
2
+ # harness-prompts-v4.sh — v4 Dashboard 하단: Manual Prompts + Activity
3
+ # progress.log에서 user-prompt와 team 활동을 newest-first로 표시.
4
+ # 스크롤 가능 영역 — 프롬프트가 많아져도 상단 Progress를 가리지 않음.
5
+
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+
10
+ PROJECT_ROOT="${1:-}"
11
+ if [ -z "$PROJECT_ROOT" ]; then
12
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
13
+ PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[prompts] .harness/ not found."; exit 1; }
14
+ fi
15
+
16
+ LOG_FILE="$PROJECT_ROOT/.harness/progress.log"
17
+
18
+ BOLD="\033[1m"
19
+ DIM="\033[2m"
20
+ GREEN="\033[32m"
21
+ YELLOW="\033[33m"
22
+ RED="\033[31m"
23
+ CYAN="\033[36m"
24
+ MAGENTA="\033[35m"
25
+ RESET="\033[0m"
26
+
27
+ LAST_LINE_COUNT=0
28
+
29
+ render_prompts() {
30
+ if [ ! -f "$LOG_FILE" ]; then
31
+ echo -e " ${DIM}(progress.log not found)${RESET}"
32
+ return
33
+ fi
34
+
35
+ local term_h
36
+ term_h=$(tput lines 2>/dev/null || echo 20)
37
+ local max_lines=$((term_h - 3)) # Leave room for header
38
+ if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
39
+
40
+ echo -e "${BOLD}Prompts & Activity${RESET} ${DIM}(newest first)${RESET}"
41
+ echo ""
42
+
43
+ # Read all entries, reverse, limit to terminal height
44
+ grep -v '^#' "$LOG_FILE" 2>/dev/null | grep -v '^$' | \
45
+ tail -r 2>/dev/null | head -"$max_lines" | \
46
+ while IFS= read -r line; do
47
+ local ts agent action detail
48
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
49
+ agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
50
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
51
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
52
+
53
+ local short_ts icon color
54
+ short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
55
+
56
+ case "$agent" in
57
+ user-prompt)
58
+ icon="★"; color="$BOLD"
59
+ # User prompts get full width, highlighted
60
+ if [ ${#detail} -gt 60 ]; then detail="${detail:0:58}.."; fi
61
+ echo -e " ${color}${icon} ${short_ts}${RESET} ${detail}"
62
+ ;;
63
+ dispatcher*)
64
+ icon="▸"; color="$MAGENTA"
65
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
66
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
67
+ ;;
68
+ team-*)
69
+ icon="⚡"; color="$CYAN"
70
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
71
+ # Color by action type
72
+ case "$action" in
73
+ *pass*) color="$GREEN"; icon="✓" ;;
74
+ *fail*) color="$RED"; icon="✗" ;;
75
+ *eval*) color="$MAGENTA"; icon="✦" ;;
76
+ *gen*) color="$CYAN"; icon="▶" ;;
77
+ *merge*) color="$GREEN"; icon="⊕" ;;
78
+ esac
79
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
80
+ ;;
81
+ manual)
82
+ icon="★"; color="$YELLOW"
83
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
84
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${detail}"
85
+ ;;
86
+ *)
87
+ icon="·"; color="$DIM"
88
+ if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
89
+ echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
90
+ ;;
91
+ esac
92
+ done
93
+ }
94
+
95
+ # ── Main loop ──
96
+ tput civis 2>/dev/null
97
+ trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
98
+ clear
99
+
100
+ while true; do
101
+ buf=$(render_prompts 2>&1)
102
+ tput cup 0 0 2>/dev/null
103
+ echo "$buf"
104
+ tput ed 2>/dev/null
105
+ sleep 3
106
+ done
@@ -42,6 +42,25 @@ fi
42
42
  FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
43
43
  QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
44
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
+ }
45
64
 
46
65
  # ── Concurrency from config ──
47
66
  CONCURRENCY=3
@@ -122,12 +141,14 @@ cmd_dequeue() {
122
141
  if [ -z "$team_id" ]; then echo "[queue] Usage: dequeue <team_id>"; exit 1; fi
123
142
  if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
124
143
 
144
+ acquire_queue_lock
145
+
125
146
  local feature
126
147
  feature=$(jq -r '.queue.ready[0] // empty' "$QUEUE")
127
148
 
128
149
  if [ -z "$feature" ]; then
150
+ release_queue_lock
129
151
  echo "[queue] No features in ready queue."
130
- # Check if all done
131
152
  local in_prog blocked
132
153
  in_prog=$(jq '.queue.in_progress | length' "$QUEUE")
133
154
  blocked=$(jq '.queue.blocked | length' "$QUEUE")
@@ -137,13 +158,13 @@ cmd_dequeue() {
137
158
  return 1
138
159
  fi
139
160
 
140
- # Move feature from ready → in_progress, assign to team
141
161
  jq --arg fid "$feature" --arg tid "$team_id" '
142
162
  .queue.ready -= [$fid] |
143
163
  .queue.in_progress[$fid] = { team: ($tid | tonumber), phase: "gen", attempt: 1 } |
144
164
  .teams[$tid] = { status: "busy", feature: $fid, branch: ("feature/" + $fid), pid: null }
145
165
  ' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
146
166
 
167
+ release_queue_lock
147
168
  echo "$feature"
148
169
  }
149
170
 
@@ -155,11 +176,11 @@ cmd_pass() {
155
176
  if [ -z "$fid" ]; then echo "[queue] Usage: pass <feature_id>"; exit 1; fi
156
177
  if [ ! -f "$QUEUE" ]; then echo "[queue] Run 'init' first."; exit 1; fi
157
178
 
158
- # Get team that was working on this feature
179
+ acquire_queue_lock
180
+
159
181
  local team_id
160
182
  team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
161
183
 
162
- # Move from in_progress → passed, free team, unblock dependents
163
184
  jq --arg fid "$fid" --arg tid "${team_id:-0}" '
164
185
  # Remove from in_progress
165
186
  del(.queue.in_progress[$fid]) |
@@ -188,6 +209,7 @@ cmd_pass() {
188
209
 
189
210
  local newly_ready
190
211
  newly_ready=$(jq -r '.queue.ready | join(", ")' "$QUEUE")
212
+ release_queue_lock
191
213
  echo "[queue] $fid PASSED. Ready: [$newly_ready]"
192
214
  }
193
215
 
@@ -199,6 +221,8 @@ cmd_fail() {
199
221
  if [ -z "$fid" ]; then echo "[queue] Usage: fail <feature_id>"; exit 1; fi
200
222
  if [ ! -f "$QUEUE" ]; then exit 1; fi
201
223
 
224
+ acquire_queue_lock
225
+
202
226
  local team_id
203
227
  team_id=$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].team // empty' "$QUEUE")
204
228
 
@@ -211,6 +235,7 @@ cmd_fail() {
211
235
  else . end)
212
236
  ' "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
213
237
 
238
+ release_queue_lock
214
239
  echo "[queue] $fid FAILED."
215
240
  }
216
241
 
@@ -239,6 +264,8 @@ cmd_update_phase() {
239
264
  exit 1
240
265
  fi
241
266
 
267
+ acquire_queue_lock
268
+
242
269
  local jq_expr
243
270
  jq_expr=".queue.in_progress[\"$fid\"].phase = \"$phase\""
244
271
  if [ -n "$attempt" ]; then
@@ -246,6 +273,7 @@ cmd_update_phase() {
246
273
  fi
247
274
 
248
275
  jq "$jq_expr" "$QUEUE" > "${QUEUE}.tmp" && mv "${QUEUE}.tmp" "$QUEUE"
276
+ release_queue_lock
249
277
  }
250
278
 
251
279
  # ══════════════════════════════════════════
@@ -297,6 +325,31 @@ cmd_status() {
297
325
  fi
298
326
  }
299
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
+
300
353
  # ── Dispatch ──
301
354
  case "$CMD" in
302
355
  init) cmd_init ;;
@@ -305,9 +358,10 @@ case "$CMD" in
305
358
  fail) cmd_fail "$@" ;;
306
359
  requeue) cmd_requeue "$@" ;;
307
360
  update_phase) cmd_update_phase "$@" ;;
361
+ recover) cmd_recover ;;
308
362
  status) cmd_status ;;
309
363
  *)
310
- echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|update_phase|status> [args]"
364
+ echo "Usage: harness-queue-manager.sh <init|dequeue|pass|fail|requeue|recover|update_phase|status> [args]"
311
365
  exit 1
312
366
  ;;
313
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
  # ─────────────────────────────────────────