@walwal-harness/cli 4.0.0-alpha.9 → 4.0.0-beta.10

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.
Files changed (29) hide show
  1. package/README.md +235 -273
  2. package/assets/templates/config.json +1 -48
  3. package/assets/templates/gitignore-append.txt +1 -0
  4. package/bin/init.js +139 -67
  5. package/package.json +4 -2
  6. package/scripts/harness-dashboard-v4.sh +66 -82
  7. package/scripts/harness-monitor.sh +202 -67
  8. package/scripts/harness-next.sh +4 -15
  9. package/scripts/harness-prompt-history.sh +126 -0
  10. package/scripts/harness-studio-setup.sh +143 -0
  11. package/scripts/harness-studio.sh +66 -0
  12. package/scripts/harness-tmux-v4.sh +136 -0
  13. package/scripts/harness-user-prompt-submit.sh +13 -0
  14. package/skills/dispatcher/SKILL.md +7 -2
  15. package/skills/team-action/SKILL.md +217 -0
  16. package/skills/team-stop/SKILL.md +19 -0
  17. package/scripts/harness-control-v4.sh +0 -97
  18. package/scripts/harness-studio-v4.sh +0 -122
  19. package/scripts/harness-team-worker.sh +0 -415
  20. package/skills/evaluator-functional-flutter/SKILL.md +0 -206
  21. package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
  22. package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
  23. package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
  24. package/skills/generator-frontend-flutter/SKILL.md +0 -173
  25. package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
  26. package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
  27. package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
  28. package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
  29. package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
@@ -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
@@ -129,17 +129,56 @@ function scaffoldHarness() {
129
129
  }
130
130
  }
131
131
 
132
- // Copy config.json
132
+ // config.json — ALWAYS update (harness system file, not user data)
133
+ // But preserve user's custom settings (pre_eval_gate.frontend_cwd, behavior, etc.)
133
134
  const configSrc = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
134
135
  const configDest = path.join(HARNESS_DIR, 'config.json');
135
- if (fs.existsSync(configSrc) && (!fileExists(configDest) || isForce)) {
136
- copyFile(configSrc, configDest);
136
+ if (fs.existsSync(configSrc)) {
137
+ if (fileExists(configDest) && !isForce) {
138
+ // Merge: keep user's customizations, update harness structure
139
+ try {
140
+ const existing = JSON.parse(fs.readFileSync(configDest, 'utf8'));
141
+ const template = JSON.parse(fs.readFileSync(configSrc, 'utf8'));
142
+ // Preserve user customizations
143
+ const userPreserve = {
144
+ behavior: existing.behavior,
145
+ 'flow.pre_eval_gate.frontend_cwd': existing?.flow?.pre_eval_gate?.frontend_cwd,
146
+ 'flow.pre_eval_gate.backend_cwd': existing?.flow?.pre_eval_gate?.backend_cwd,
147
+ 'flow.pre_eval_gate.frontend_checks': existing?.flow?.pre_eval_gate?.frontend_checks,
148
+ 'flow.pre_eval_gate.backend_checks': existing?.flow?.pre_eval_gate?.backend_checks,
149
+ };
150
+ // Write template, then re-apply user settings
151
+ fs.writeFileSync(configDest, JSON.stringify(template, null, 2) + '\n');
152
+ // Re-apply preserved user settings
153
+ const merged = JSON.parse(fs.readFileSync(configDest, 'utf8'));
154
+ if (userPreserve.behavior) merged.behavior = userPreserve.behavior;
155
+ if (userPreserve['flow.pre_eval_gate.frontend_cwd']) {
156
+ merged.flow.pre_eval_gate.frontend_cwd = userPreserve['flow.pre_eval_gate.frontend_cwd'];
157
+ }
158
+ if (userPreserve['flow.pre_eval_gate.backend_cwd']) {
159
+ merged.flow.pre_eval_gate.backend_cwd = userPreserve['flow.pre_eval_gate.backend_cwd'];
160
+ }
161
+ if (userPreserve['flow.pre_eval_gate.frontend_checks']) {
162
+ merged.flow.pre_eval_gate.frontend_checks = userPreserve['flow.pre_eval_gate.frontend_checks'];
163
+ }
164
+ if (userPreserve['flow.pre_eval_gate.backend_checks']) {
165
+ merged.flow.pre_eval_gate.backend_checks = userPreserve['flow.pre_eval_gate.backend_checks'];
166
+ }
167
+ fs.writeFileSync(configDest, JSON.stringify(merged, null, 2) + '\n');
168
+ log('config.json updated (user settings preserved)');
169
+ } catch (e) {
170
+ copyFile(configSrc, configDest);
171
+ log('config.json replaced (merge failed)');
172
+ }
173
+ } else {
174
+ copyFile(configSrc, configDest);
175
+ }
137
176
  }
138
177
 
139
- // Copy HARNESS.md
178
+ // HARNESS.md — ALWAYS update
140
179
  const harnessMdSrc = path.join(PKG_ROOT, 'assets', 'templates', 'HARNESS.md');
141
180
  const harnessMdDest = path.join(HARNESS_DIR, 'HARNESS.md');
142
- if (fs.existsSync(harnessMdSrc) && (!fileExists(harnessMdDest) || isForce)) {
181
+ if (fs.existsSync(harnessMdSrc)) {
143
182
  copyFile(harnessMdSrc, harnessMdDest);
144
183
  }
145
184
 
@@ -186,6 +225,17 @@ function installSkills() {
186
225
  return;
187
226
  }
188
227
 
228
+ // harness- 프리픽스 스킬 전체 삭제 후 재복사 — 잔류 방지
229
+ if (fs.existsSync(CLAUDE_SKILLS_DIR)) {
230
+ const existing = fs.readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true });
231
+ for (const entry of existing) {
232
+ if (entry.isDirectory() && entry.name.startsWith('harness-')) {
233
+ fs.rmSync(path.join(CLAUDE_SKILLS_DIR, entry.name), { recursive: true, force: true });
234
+ }
235
+ }
236
+ log(' Cleared existing harness-* skills');
237
+ }
238
+
189
239
  const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
190
240
  .filter(d => d.isDirectory())
191
241
  .map(d => d.name);
@@ -193,13 +243,8 @@ function installSkills() {
193
243
  for (const skill of skills) {
194
244
  const src = path.join(skillsSrc, skill);
195
245
  const dest = path.join(CLAUDE_SKILLS_DIR, `harness-${skill}`);
196
-
197
- if (!fileExists(dest) || isForce) {
198
- copyDir(src, dest);
199
- log(` Installed: harness-${skill}`);
200
- } else {
201
- log(` Skipped (exists): harness-${skill}`);
202
- }
246
+ copyDir(src, dest);
247
+ log(` Installed: harness-${skill}`);
203
248
  }
204
249
 
205
250
  log('Skills installation complete');
@@ -214,51 +259,28 @@ function installScripts() {
214
259
  const scriptsSrc = path.join(PKG_ROOT, 'scripts');
215
260
  const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
216
261
 
217
- // Core scripts are ALWAYS overwritten on update (not user-editable)
218
- // These contain harness logic that must stay in sync with the CLI version
219
- const coreScripts = new Set([
220
- 'harness-next.sh',
221
- 'harness-session-start.sh',
222
- 'harness-statusline.sh',
223
- 'harness-user-prompt-submit.sh',
224
- 'harness-dashboard.sh',
225
- 'harness-monitor.sh',
226
- 'harness-eval-watcher.sh',
227
- 'harness-tmux.sh',
228
- 'harness-control.sh',
229
- 'harness-studio-v4.sh',
230
- 'harness-dashboard-v4.sh',
231
- 'harness-control-v4.sh',
232
- 'harness-queue-manager.sh',
233
- 'harness-team-worker.sh',
234
- ]);
262
+ // 전체 삭제 재복사 버전 잔류 파일 방지
263
+ if (fs.existsSync(scriptsDest)) {
264
+ fs.rmSync(scriptsDest, { recursive: true, force: true });
265
+ log(' Cleared existing scripts/');
266
+ }
235
267
 
236
268
  if (fs.existsSync(scriptsSrc)) {
237
- ensureDir(scriptsDest);
238
- const entries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
239
- for (const entry of entries) {
240
- const srcPath = path.join(scriptsSrc, entry.name);
241
- const destPath = path.join(scriptsDest, entry.name);
242
- if (entry.isDirectory()) {
243
- // lib/ and other subdirectories — always overwrite
244
- copyDir(srcPath, destPath);
245
- try {
246
- const subFiles = fs.readdirSync(destPath);
247
- for (const f of subFiles) {
248
- if (f.endsWith('.sh')) {
249
- fs.chmodSync(path.join(destPath, f), '755');
250
- }
251
- }
252
- } catch (e) {}
253
- } else {
254
- // Core scripts: always overwrite. Others: skip if exists (unless --force)
255
- const isCore = coreScripts.has(entry.name);
256
- if (isCore || !fileExists(destPath) || isForce) {
257
- copyFile(srcPath, destPath);
258
- try { fs.chmodSync(destPath, '755'); } catch (e) {}
269
+ copyDir(scriptsSrc, scriptsDest);
270
+
271
+ // chmod +x for all .sh files (recursive)
272
+ function chmodRecursive(dir) {
273
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
274
+ for (const entry of entries) {
275
+ const full = path.join(dir, entry.name);
276
+ if (entry.isDirectory()) {
277
+ chmodRecursive(full);
278
+ } else if (entry.name.endsWith('.sh')) {
279
+ try { fs.chmodSync(full, '755'); } catch (e) {}
259
280
  }
260
281
  }
261
282
  }
283
+ chmodRecursive(scriptsDest);
262
284
  }
263
285
 
264
286
  log('Scripts installation complete');
@@ -424,6 +446,28 @@ function installUserPromptSubmitHook() {
424
446
  // ─────────────────────────────────────────
425
447
  // 4. AGENTS.md + CLAUDE.md
426
448
  // ─────────────────────────────────────────
449
+ // ─────────────────────────────────────────
450
+ // 3d. Agent Teams env var
451
+ // ─────────────────────────────────────────
452
+ function installAgentTeamsEnv() {
453
+ const settingsPath = path.join(PROJECT_ROOT, '.claude', 'settings.json');
454
+ let settings = {};
455
+ if (fileExists(settingsPath)) {
456
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
457
+ }
458
+
459
+ if (!settings.env) settings.env = {};
460
+
461
+ if (settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] !== '1') {
462
+ settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
463
+ ensureDir(path.dirname(settingsPath));
464
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
465
+ log('Agent Teams enabled (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)');
466
+ } else {
467
+ log('Agent Teams already enabled');
468
+ }
469
+ }
470
+
427
471
  function setupAgentsMd() {
428
472
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
429
473
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
@@ -575,7 +619,7 @@ Usage:
575
619
  npx walwal-harness --force Re-initialize (overwrites existing files)
576
620
  npx walwal-harness studio Launch Harness Studio v3 (tmux 5-pane)
577
621
  npx walwal-harness studio --ai Studio v3 + AI eval summary
578
- npx walwal-harness v4 Launch Studio v4 (3 Parallel Agent Teams)
622
+ npx walwal-harness v4 Enable Agent Teams (set env var + init queue)
579
623
  npx walwal-harness --help Show this help
580
624
 
581
625
  What it does:
@@ -626,24 +670,51 @@ function runStudio() {
626
670
  }
627
671
 
628
672
  function runStudioV4() {
629
- const scriptsDir = path.join(PKG_ROOT, 'scripts');
630
- const tmuxScript = path.join(scriptsDir, 'harness-studio-v4.sh');
631
-
632
- if (!fs.existsSync(tmuxScript)) {
633
- log('ERROR: harness-studio-v4.sh not found. Update @walwal-harness/cli to >= 4.0.0');
634
- process.exit(1);
673
+ // Enable Agent Teams in project settings
674
+ const settingsPath = path.join(PROJECT_ROOT, '.claude', 'settings.json');
675
+ let settings = {};
676
+ if (fileExists(settingsPath)) {
677
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
635
678
  }
636
679
 
637
- try {
638
- execSync('which tmux', { stdio: 'ignore' });
639
- } catch {
640
- log('ERROR: tmux is required. Install with: brew install tmux');
641
- process.exit(1);
680
+ if (!settings.env) settings.env = {};
681
+ settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
682
+ ensureDir(path.dirname(settingsPath));
683
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
684
+
685
+ // Initialize feature queue if feature-list.json exists
686
+ const featureList = path.join(PROJECT_ROOT, '.harness', 'actions', 'feature-list.json');
687
+ const featureQueue = path.join(PROJECT_ROOT, '.harness', 'actions', 'feature-queue.json');
688
+ const queueMgr = path.join(PKG_ROOT, 'scripts', 'harness-queue-manager.sh');
689
+
690
+ if (fs.existsSync(featureList) && fs.existsSync(queueMgr)) {
691
+ if (!fs.existsSync(featureQueue)) {
692
+ log('Initializing feature queue...');
693
+ try { execSync(`bash "${queueMgr}" init "${PROJECT_ROOT}"`, { stdio: 'inherit' }); } catch (e) {}
694
+ } else {
695
+ log('Recovering feature queue...');
696
+ try { execSync(`bash "${queueMgr}" recover "${PROJECT_ROOT}"`, { stdio: 'inherit' }); } catch (e) {}
697
+ }
642
698
  }
643
699
 
644
- const cmd = `bash "${tmuxScript}" "${PROJECT_ROOT}"`.trim();
645
- log('Launching Harness Studio v4 (Parallel Agent Teams)...');
646
- execSync(cmd, { stdio: 'inherit' });
700
+ console.log('');
701
+ log('╔═══════════════════════════════════════════════════════════╗');
702
+ log('║ Agent Teams v4 ENABLED ║');
703
+ log('╠═══════════════════════════════════════════════════════════╣');
704
+ log('║ ║');
705
+ log('║ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 set in ║');
706
+ log('║ .claude/settings.json ║');
707
+ log('║ ║');
708
+ log('║ Next steps: ║');
709
+ log('║ 1. Restart Claude Code (/exit → re-enter) ║');
710
+ log('║ 2. Run Planner: "하네스 엔지니어링 시작" ║');
711
+ log('║ 3. Start Teams: /harness-team-action ║');
712
+ log('║ ║');
713
+ log('║ Or use --teammate-mode tmux for split panes: ║');
714
+ log('║ $ claude --teammate-mode tmux ║');
715
+ log('║ ║');
716
+ log('╚═══════════════════════════════════════════════════════════╝');
717
+ console.log('');
647
718
  }
648
719
 
649
720
  function main() {
@@ -679,6 +750,7 @@ function main() {
679
750
  installSessionHook();
680
751
  installStatusline();
681
752
  installUserPromptSubmitHook();
753
+ installAgentTeamsEnv();
682
754
  setupAgentsMd();
683
755
  checkPlaywrightMcp();
684
756
  checkRecommendedSkills();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "4.0.0-alpha.9",
3
+ "version": "4.0.0-beta.10",
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"
@@ -9,7 +9,9 @@
9
9
  "postinstall": "node bin/init.js --auto",
10
10
  "init": "node bin/init.js",
11
11
  "scan": "bash scripts/scan-project.sh .",
12
- "reset": "node bin/init.js --force"
12
+ "reset": "node bin/init.js --force",
13
+ "studio": "bash scripts/harness-studio.sh",
14
+ "studio:kill": "bash scripts/harness-studio.sh --kill"
13
15
  },
14
16
  "keywords": [
15
17
  "harness-engineering",
@@ -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,42 +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
42
+ local ready blocked in_prog passed failed total
47
43
  ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
48
44
  blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
49
45
  in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
50
46
  passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
51
47
  failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
52
- concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
53
48
  ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
54
49
  total=$((ready + blocked + in_prog + passed + failed))
55
50
 
56
- # Progress bar
57
51
  local pct=0
58
52
  if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
59
- local bar_w=20
53
+ local bar_w=16
60
54
  local filled=$(( pct * bar_w / 100 ))
61
55
  local empty=$(( bar_w - filled ))
62
56
  local bar=""
63
57
  for ((i=0; i<filled; i++)); do bar+="█"; done
64
58
  for ((i=0; i<empty; i++)); do bar+="░"; done
65
59
 
66
- echo -e " ${BOLD}Queue${RESET} ${bar} ${passed}/${total} (${pct}%) ${DIM}concurrency=${concurrency}${RESET}"
67
- echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
68
- 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}"
69
61
  }
70
62
 
71
63
  render_teams() {
@@ -75,49 +67,44 @@ render_teams() {
75
67
  team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
76
68
  if [ "${team_count:-0}" -eq 0 ]; then return; fi
77
69
 
78
- echo -e " ${BOLD}Teams${RESET}"
79
-
80
70
  for i in $(seq 1 "$team_count"); do
81
71
  local t_status t_feature t_phase t_attempt
82
72
  t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
83
73
  t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
84
74
 
85
- # Get phase from in_progress
86
75
  if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
87
76
  t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
88
77
  t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
89
78
  else
90
- t_phase="—"
91
- t_attempt="—"
79
+ t_phase="—"; t_attempt=""
92
80
  fi
93
81
 
94
82
  local icon color
95
83
  case "$t_status" in
96
- busy) icon="▶" ; color="$GREEN" ;;
97
- idle) icon="○" ; color="$DIM" ;;
98
- paused) icon="" ; color="$YELLOW" ;;
99
- *) icon="?" ; color="$RESET" ;;
84
+ busy) icon="▶"; color="$GREEN" ;;
85
+ idle) icon="○"; color="$DIM" ;;
86
+ *) icon="?"; color="$RESET" ;;
100
87
  esac
101
88
 
102
- local phase_display=""
89
+ local phase_short=""
103
90
  case "$t_phase" in
104
- gen) phase_display="${CYAN}GEN${RESET}" ;;
105
- gate) phase_display="${YELLOW}GATE${RESET}" ;;
106
- eval) phase_display="${MAGENTA}EVAL${RESET}" ;;
107
- *) 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}" ;;
108
95
  esac
109
96
 
110
- 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 ""
111
102
  done
112
- echo ""
113
103
  }
114
104
 
115
- render_feature_list() {
105
+ render_features() {
116
106
  if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
117
107
 
118
- echo -e " ${BOLD}Features${RESET}"
119
-
120
- # Single jq call: merge feature-list + queue state → pre-formatted lines
121
108
  jq -r --slurpfile q "$QUEUE" '
122
109
  ($q[0].queue.passed // []) as $passed |
123
110
  ($q[0].queue.failed // []) as $failed |
@@ -125,7 +112,7 @@ render_feature_list() {
125
112
  ($q[0].queue.in_progress // {}) as $prog |
126
113
  .features[] |
127
114
  .id as $fid |
128
- (.name // .description // "?" | if length > 22 then .[0:20] + ".." else . end) as $fname |
115
+ (.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
129
116
  (if ($fid | IN($passed[])) then "P"
130
117
  elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
131
118
  elif ($fid | IN($failed[])) then "F"
@@ -138,73 +125,70 @@ render_feature_list() {
138
125
  F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
139
126
  R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
140
127
  B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
141
- I\|*) # in_progress: extract team and phase
142
- team=$(echo "$st" | cut -d'|' -f2)
128
+ I\|*) team=$(echo "$st" | cut -d'|' -f2)
143
129
  phase=$(echo "$st" | cut -d'|' -f3)
144
- printf " ${CYAN}◐${RESET} %-6s %-18s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
130
+ printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
145
131
  *) printf " ? %-6s %s\n" "$fid" "$fname" ;;
146
132
  esac
147
133
  done
148
-
149
- echo ""
150
134
  }
151
135
 
152
- render_prompt_history() {
153
- local log_file="$PROJECT_ROOT/.harness/progress.log"
154
- if [ ! -f "$log_file" ]; then return; fi
155
-
156
- # Get terminal height to limit display
157
- local term_h
158
- term_h=$(tput lines 2>/dev/null || echo 50)
159
- local max_lines=10 # show latest 10 entries
160
-
161
- echo -e " ${BOLD}Prompt History${RESET} ${DIM}(newest first)${RESET}"
162
-
163
- # Read non-comment lines, reverse (newest first), take max_lines
164
- grep -v '^#' "$log_file" 2>/dev/null | grep -v '^$' | tail -r 2>/dev/null | head -"$max_lines" | \
165
- while IFS= read -r line; do
166
- # Parse: date | agent | action | detail
167
- local ts agent action detail
168
- ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
169
- agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
170
- action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
171
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
172
-
173
- local short_ts icon color
174
- short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
175
-
176
- case "$agent" in
177
- dispatcher*|dispatch) icon="▸"; color="$MAGENTA" ;;
178
- team-*) icon="⚡"; color="$CYAN" ;;
179
- manual|user) icon="★"; color="$BOLD" ;;
180
- planner*) icon="□"; color="$YELLOW" ;;
181
- generator*|gen*) icon="▶"; color="$GREEN" ;;
182
- eval*) icon="✦"; color="$RED" ;;
183
- system) icon="⚙"; color="$DIM" ;;
184
- *) icon="·"; color="$DIM" ;;
185
- esac
136
+ render_bottleneck_alert() {
137
+ if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
186
138
 
187
- if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
139
+ local failed_list blocked_count idle_teams in_prog
140
+ failed_list=$(jq -r '.queue.failed[]' "$QUEUE" 2>/dev/null)
141
+ blocked_count=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
142
+ in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
143
+ idle_teams=0
188
144
 
189
- echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
145
+ # Count idle teams
146
+ local team_count
147
+ team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 0)
148
+ for i in $(seq 1 "$team_count"); do
149
+ local ts
150
+ ts=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
151
+ if [ "$ts" = "idle" ]; then idle_teams=$((idle_teams + 1)); fi
190
152
  done
191
153
 
192
- echo ""
154
+ if [ -z "$failed_list" ]; then return; fi
155
+
156
+ # Check each failed feature for blocking impact
157
+ while IFS= read -r fid; do
158
+ [ -z "$fid" ] && continue
159
+ # Count how many blocked features depend on this fid
160
+ local deps_on_this
161
+ deps_on_this=$(jq --arg fid "$fid" '[.queue.blocked | to_entries[] | select(.value | index($fid))] | length' "$QUEUE" 2>/dev/null || echo 0)
162
+
163
+ if [ "$deps_on_this" -gt 0 ]; then
164
+ echo ""
165
+ echo -e " ${RED}${BOLD}⚠ BOTTLENECK${RESET} ${RED}${fid}${RESET} failed → ${YELLOW}${deps_on_this} features blocked${RESET}"
166
+ if [ "$idle_teams" -gt 0 ]; then
167
+ echo -e " ${BOLD}→ requeue:${RESET} bash scripts/harness-queue-manager.sh requeue ${fid} ."
168
+ fi
169
+ fi
170
+ done <<< "$failed_list"
171
+
172
+ # All teams idle + nothing in progress = stalled
173
+ if [ "$idle_teams" -eq "$team_count" ] && [ "$in_prog" -eq 0 ] && [ "$blocked_count" -gt 0 ]; then
174
+ echo ""
175
+ echo -e " ${RED}${BOLD}!! STALLED${RESET} — All teams idle, ${blocked_count} features blocked"
176
+ echo -e " ${BOLD}→ requeue failed features or check dependency graph${RESET}"
177
+ fi
193
178
  }
194
179
 
195
180
  render_all() {
196
181
  render_header
197
182
  render_queue_summary
198
183
  render_teams
199
- render_prompt_history
200
- render_feature_list
201
- echo -e " ${DIM}Refreshing every 3s${RESET}"
184
+ render_bottleneck_alert
185
+ echo ""
186
+ render_features
202
187
  }
203
188
 
204
189
  # ── Main loop ──
205
190
  tput civis 2>/dev/null
206
191
  trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
207
-
208
192
  clear
209
193
 
210
194
  while true; do