@walwal-harness/cli 2.0.0 → 2.3.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.
Files changed (29) hide show
  1. package/assets/templates/config.json +41 -1
  2. package/bin/init.js +93 -7
  3. package/package.json +5 -2
  4. package/scripts/harness-next.sh +36 -1
  5. package/scripts/harness-user-prompt-submit.sh +106 -0
  6. package/scripts/lib/harness-render-progress.sh +15 -1
  7. package/scripts/scan-project.sh +24 -2
  8. package/skills/brainstorming/SKILL.md +200 -0
  9. package/skills/brainstorming/references/attribution.md +109 -0
  10. package/skills/brainstorming/references/spec-document-reviewer-prompt.md +49 -0
  11. package/skills/brainstorming/references/visual-companion.md +287 -0
  12. package/skills/brainstorming/scripts/frame-template.html +214 -0
  13. package/skills/brainstorming/scripts/helper.js +88 -0
  14. package/skills/brainstorming/scripts/server.cjs +354 -0
  15. package/skills/brainstorming/scripts/start-server.sh +148 -0
  16. package/skills/brainstorming/scripts/stop-server.sh +56 -0
  17. package/skills/dispatcher/SKILL.md +114 -2
  18. package/skills/dispatcher/references/pipeline-definitions.md +53 -5
  19. package/skills/evaluator-functional-flutter/SKILL.md +198 -0
  20. package/skills/evaluator-functional-flutter/references/ia-compliance.md +77 -0
  21. package/skills/evaluator-functional-flutter/references/scoring-rubric.md +132 -0
  22. package/skills/evaluator-functional-flutter/references/static-check-rules.md +99 -0
  23. package/skills/generator-frontend-flutter/SKILL.md +138 -0
  24. package/skills/generator-frontend-flutter/references/anti-patterns.md +288 -0
  25. package/skills/generator-frontend-flutter/references/api-layer-pattern.md +233 -0
  26. package/skills/generator-frontend-flutter/references/i18n-pattern.md +102 -0
  27. package/skills/generator-frontend-flutter/references/riverpod-pattern.md +199 -0
  28. package/skills/planner/SKILL.md +23 -1
  29. package/skills/planner/references/fe-stack-detection.md +131 -0
@@ -12,9 +12,20 @@
12
12
  "outputs": ["actions/pipeline.json"],
13
13
  "always_first": true
14
14
  },
15
+ "brainstorming": {
16
+ "role": "사용자의 러프한 요구사항을 대화형으로 구체화하여 Planner가 바로 쓸 수 있는 brainstorm-spec.md로 변환. Dispatcher가 사용자에게 '브레인스토밍 필요?' 확인 후에만 호출되는 조건부 에이전트.",
17
+ "skill": "harness-brainstorming",
18
+ "inputs": ["(user conversation)"],
19
+ "outputs": ["actions/brainstorm-spec.md"],
20
+ "conditional": true,
21
+ "invoked_by": "dispatcher (user opt-in)",
22
+ "next_on_complete": "planner",
23
+ "attribution": "Derived from obra/superpowers skills/brainstorming (MIT License)"
24
+ },
15
25
  "planner": {
16
26
  "role": "제품 사양 + API 계약서 + 서비스 분할 설계",
17
27
  "skill": "harness-planner",
28
+ "inputs": ["actions/brainstorm-spec.md (optional)"],
18
29
  "outputs": [
19
30
  "actions/plan.md",
20
31
  "actions/feature-list.json",
@@ -48,16 +59,40 @@
48
59
  "tools": ["playwright:browser_take_screenshot", "playwright:browser_resize", "playwright:browser_snapshot"],
49
60
  "inputs": ["actions/sprint-contract.md", "actions/evaluation-functional.md"],
50
61
  "outputs": ["actions/evaluation-visual.md"]
62
+ },
63
+ "generator-frontend-flutter": {
64
+ "role": "Flutter 앱 개발 — Riverpod, integrated_data_layer(Retrofit), i18n(ARB), build_runner",
65
+ "skill": "harness-generator-frontend-flutter",
66
+ "inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
67
+ "outputs": ["code:flutter", "actions/sprint-contract.md"],
68
+ "order": 2,
69
+ "fe_stack": "flutter"
70
+ },
71
+ "evaluator-functional-flutter": {
72
+ "role": "Flutter 앱 검증 — flutter analyze, flutter test, build_runner 일관성, 안티패턴 정적 검증",
73
+ "skill": "harness-evaluator-functional-flutter",
74
+ "tools": ["bash:flutter", "bash:dart"],
75
+ "inputs": ["actions/sprint-contract.md"],
76
+ "outputs": ["actions/evaluation-functional.md"],
77
+ "fe_stack": "flutter"
51
78
  }
52
79
  },
53
80
  "flow": {
54
81
  "sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
55
82
  "pipeline_selection": {
56
- "comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정. 이하 sequence에서 스킵 대상 제외.",
83
+ "comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정. fe_stack 필드로 React/Flutter를 구분. harness-next.sh가 pipeline.json.fe_stack 을 읽어 generator-frontend/evaluator-functional 을 Flutter 변형으로 치환한다.",
57
84
  "pipelines": {
58
85
  "FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
59
86
  "FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
60
87
  "BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
88
+ },
89
+ "fe_stack_substitution": {
90
+ "comment": "pipeline.json.fe_stack=flutter 이면 아래 치환을 적용. evaluator-visual은 skip.",
91
+ "flutter": {
92
+ "generator-frontend": "generator-frontend-flutter",
93
+ "evaluator-functional": "evaluator-functional-flutter",
94
+ "evaluator-visual": "__skip__"
95
+ }
61
96
  }
62
97
  },
63
98
  "sprint_execution": {
@@ -75,6 +110,11 @@
75
110
  "state_log": ".harness/progress.log",
76
111
  "next_prompt_file": ".harness/next-prompt.txt"
77
112
  },
113
+ "behavior": {
114
+ "comment": "하네스 동작 플래그. UserPromptSubmit 훅이 이 값을 읽어 자동 라우팅을 결정한다.",
115
+ "auto_route_dispatcher": true,
116
+ "auto_route_dispatcher_description": "true 이면 사용자의 모든 프롬프트가 harness-dispatcher 경유로 분류/라우팅 된다. 사용자가 'harness skip' 등을 말하면 단일 메시지 한정으로 건너뛴다."
117
+ },
78
118
  "paths": {
79
119
  "actions": ".harness/actions",
80
120
  "archive": ".harness/archive",
package/bin/init.js CHANGED
@@ -253,24 +253,102 @@ function installSessionHook() {
253
253
  if (!settings.hooks) settings.hooks = {};
254
254
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
255
255
 
256
- // Check if our hook is already installed
256
+ // Migrate any legacy flat entries (created by older walwal-harness versions)
257
+ // into the correct matcher + hooks-array shape that Claude Code expects.
257
258
  const hookCmd = 'bash scripts/harness-session-start.sh';
259
+ let migrated = false;
260
+ settings.hooks.SessionStart = settings.hooks.SessionStart
261
+ .map((entry) => {
262
+ if (entry && typeof entry === 'object' && Array.isArray(entry.hooks)) {
263
+ return entry; // already in correct shape
264
+ }
265
+ if (entry && typeof entry === 'object' && entry.type === 'command' && entry.command) {
266
+ migrated = true;
267
+ return { matcher: '', hooks: [{ type: entry.type, command: entry.command }] };
268
+ }
269
+ return entry;
270
+ })
271
+ .filter(Boolean);
272
+
273
+ // Check if our hook is already installed (inside any matcher group)
258
274
  const alreadyInstalled = settings.hooks.SessionStart.some(
259
- h => h.command && h.command.includes('harness-session-start')
275
+ (entry) =>
276
+ entry &&
277
+ Array.isArray(entry.hooks) &&
278
+ entry.hooks.some(
279
+ (h) => h && h.command && h.command.includes('harness-session-start')
280
+ )
260
281
  );
261
282
 
262
283
  if (!alreadyInstalled) {
263
284
  settings.hooks.SessionStart.push({
264
- type: 'command',
265
- command: hookCmd
285
+ matcher: '',
286
+ hooks: [{ type: 'command', command: hookCmd }]
266
287
  });
267
288
  fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
268
289
  log('SessionStart hook installed in .claude/settings.json');
290
+ } else if (migrated) {
291
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
292
+ log('SessionStart hook migrated to matcher + hooks-array format');
269
293
  } else {
270
294
  log('SessionStart hook already installed');
271
295
  }
272
296
  }
273
297
 
298
+ // ─────────────────────────────────────────
299
+ // 3c. UserPromptSubmit hook (auto dispatcher routing)
300
+ // ─────────────────────────────────────────
301
+ function installUserPromptSubmitHook() {
302
+ log('Installing UserPromptSubmit hook (auto dispatcher routing)...');
303
+
304
+ const settingsDir = path.join(PROJECT_ROOT, '.claude');
305
+ const settingsFile = path.join(settingsDir, 'settings.json');
306
+
307
+ ensureDir(settingsDir);
308
+
309
+ let settings = {};
310
+ if (fileExists(settingsFile)) {
311
+ try {
312
+ settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
313
+ } catch (e) {
314
+ log('WARNING: Could not parse existing .claude/settings.json, creating new');
315
+ }
316
+ }
317
+
318
+ if (!settings.hooks) settings.hooks = {};
319
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
320
+
321
+ const hookCmd = 'bash scripts/harness-user-prompt-submit.sh';
322
+
323
+ // Detect existing harness UserPromptSubmit hook (any shape)
324
+ const alreadyInstalled = settings.hooks.UserPromptSubmit.some((entry) => {
325
+ if (!entry || typeof entry !== 'object') return false;
326
+ if (Array.isArray(entry.hooks)) {
327
+ return entry.hooks.some(
328
+ (h) => h && h.command && h.command.includes('harness-user-prompt-submit')
329
+ );
330
+ }
331
+ if (entry.type === 'command' && entry.command) {
332
+ return entry.command.includes('harness-user-prompt-submit');
333
+ }
334
+ return false;
335
+ });
336
+
337
+ if (!alreadyInstalled) {
338
+ settings.hooks.UserPromptSubmit.push({
339
+ matcher: '',
340
+ hooks: [{ type: 'command', command: hookCmd }]
341
+ });
342
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
343
+ log('UserPromptSubmit hook installed in .claude/settings.json');
344
+ log(' → All prompts will be routed through harness-dispatcher');
345
+ log(' → Opt-out per message: say "harness skip" or "without harness"');
346
+ log(' → Disable globally: set .harness/config.json behavior.auto_route_dispatcher = false');
347
+ } else {
348
+ log('UserPromptSubmit hook already installed');
349
+ }
350
+ }
351
+
274
352
  // ─────────────────────────────────────────
275
353
  // 4. AGENTS.md + CLAUDE.md
276
354
  // ─────────────────────────────────────────
@@ -429,9 +507,16 @@ What it does:
429
507
  1. Scaffolds .harness/ directory (actions, archive, gotchas, config)
430
508
  2. Installs skills to .claude/skills/ (dispatcher, planner, generators, evaluators)
431
509
  3. Copies helper scripts to scripts/
432
- 4. Creates AGENTS.md + CLAUDE.md symlink
433
- 5. Checks Playwright MCP configuration
434
- 6. Checks recommended external skills (Vercel, design skills)
510
+ 4. Registers SessionStart hook (boot-time progress render)
511
+ 5. Registers UserPromptSubmit hook (auto-route every prompt through harness-dispatcher)
512
+ 6. Creates AGENTS.md + CLAUDE.md symlink
513
+ 7. Checks Playwright MCP configuration
514
+ 8. Checks recommended external skills (Vercel, design skills)
515
+
516
+ Auto routing:
517
+ Every user prompt is routed through harness-dispatcher.
518
+ Per-message opt-out: say "harness skip" or "harness 없이".
519
+ Global disable: edit .harness/config.json → behavior.auto_route_dispatcher = false
435
520
 
436
521
  After init:
437
522
  1. Restart Claude Code session (exit and re-enter) for skills to load
@@ -463,6 +548,7 @@ function main() {
463
548
  installSkills();
464
549
  installScripts();
465
550
  installSessionHook();
551
+ installUserPromptSubmitHook();
466
552
  setupAgentsMd();
467
553
  checkPlaywrightMcp();
468
554
  checkRecommendedSkills();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "2.0.0",
4
- "description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual) with Gotcha management",
3
+ "version": "2.3.0",
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"
7
7
  },
@@ -16,11 +16,14 @@
16
16
  "ai-agent",
17
17
  "claude-code",
18
18
  "skill",
19
+ "brainstorming",
19
20
  "planner",
20
21
  "generator",
21
22
  "evaluator",
22
23
  "nestjs",
23
24
  "react",
25
+ "flutter",
26
+ "riverpod",
24
27
  "playwright"
25
28
  ],
26
29
  "author": "",
@@ -18,6 +18,7 @@ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || {
18
18
 
19
19
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
20
20
  CONFIG="$PROJECT_ROOT/.harness/config.json"
21
+ PIPELINE_JSON="$PROJECT_ROOT/.harness/actions/pipeline.json"
21
22
  NEXT_PROMPT="$PROJECT_ROOT/.harness/next-prompt.txt"
22
23
 
23
24
  check_jq || exit 1
@@ -40,6 +41,29 @@ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS")
40
41
  retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
41
42
  max_retries=$(jq -r '.flow.max_retries_per_sprint // 10' "$CONFIG" 2>/dev/null || echo 10)
42
43
 
44
+ # fe_stack 치환 (Flutter 지원) — pipeline.json 에서 읽음
45
+ fe_stack="react"
46
+ if [ -f "$PIPELINE_JSON" ]; then
47
+ fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
48
+ fi
49
+
50
+ # ─────────────────────────────────────────
51
+ # fe_stack 치환 헬퍼
52
+ # pipeline_selection.pipelines에서 읽은 에이전트명을 fe_stack에 따라 치환
53
+ # - react: 그대로
54
+ # - flutter: generator-frontend → generator-frontend-flutter 등, __skip__ 은 건너뜀
55
+ # ─────────────────────────────────────────
56
+ substitute_fe_stack() {
57
+ local agent="$1"
58
+ if [ "$fe_stack" != "flutter" ]; then
59
+ echo "$agent"
60
+ return
61
+ fi
62
+ local sub
63
+ sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
64
+ echo "$sub"
65
+ }
66
+
43
67
  # ─────────────────────────────────────────
44
68
  # Determine next agent
45
69
  # ─────────────────────────────────────────
@@ -73,8 +97,18 @@ compute_next_agent() {
73
97
  return
74
98
  fi
75
99
 
100
+ local -a raw_agents
101
+ mapfile -t raw_agents < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null | sed 's/:.*//')
102
+
103
+ # fe_stack 치환 + __skip__ 필터링
76
104
  local -a agents
77
- mapfile -t agents < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null | sed 's/:.*//')
105
+ for a in "${raw_agents[@]}"; do
106
+ local sub
107
+ sub=$(substitute_fe_stack "$a")
108
+ if [ "$sub" != "__skip__" ]; then
109
+ agents+=("$sub")
110
+ fi
111
+ done
78
112
 
79
113
  local found=false
80
114
  for agent in "${agents[@]}"; do
@@ -82,6 +116,7 @@ compute_next_agent() {
82
116
  echo "$agent"
83
117
  return
84
118
  fi
119
+ # current 비교 시에도 치환된 이름으로 (Flutter 변형 에이전트가 실행되는 경우)
85
120
  if [ "$agent" = "$current" ]; then
86
121
  found=true
87
122
  fi
@@ -0,0 +1,106 @@
1
+ #!/bin/bash
2
+ # harness-user-prompt-submit.sh
3
+ # ─────────────────────────────────────────
4
+ # Claude Code UserPromptSubmit hook
5
+ # 모든 사용자 프롬프트를 harness-dispatcher 로 라우팅하도록 Claude 에게
6
+ # 지시하는 컨텍스트를 stdout 에 주입한다.
7
+ #
8
+ # 활성 조건:
9
+ # 1) 현재 cwd 에 .harness/config.json 존재
10
+ # 2) .harness/config.json 의 behavior.auto_route_dispatcher != false
11
+ #
12
+ # 비활성 상황에서는 아무것도 출력하지 않고 exit 0 (pass-through).
13
+ # ─────────────────────────────────────────
14
+ set -e
15
+
16
+ # stdin 의 JSON payload 읽기 (Claude Code 가 {prompt, cwd, session_id, ...} 전달)
17
+ INPUT=$(cat)
18
+
19
+ # cwd 추출 (payload 에 없으면 PWD fallback)
20
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
21
+ if [ -z "$CWD" ]; then
22
+ CWD="$PWD"
23
+ fi
24
+
25
+ # 조건 1: 하네스 초기화 확인
26
+ if [ ! -f "$CWD/.harness/config.json" ]; then
27
+ exit 0
28
+ fi
29
+
30
+ # 조건 2: opt-out 플래그 확인 (기본값 true)
31
+ AUTO_ROUTE="true"
32
+ if command -v jq >/dev/null 2>&1; then
33
+ AUTO_ROUTE=$(jq -r '.behavior.auto_route_dispatcher // true' "$CWD/.harness/config.json" 2>/dev/null || echo "true")
34
+ fi
35
+ if [ "$AUTO_ROUTE" != "true" ]; then
36
+ exit 0
37
+ fi
38
+
39
+ # 프롬프트 내용 추출 (opt-out 문구 감지용)
40
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)
41
+ # 사용자가 명시적으로 건너뛰기 요청하면 pass-through
42
+ if echo "$PROMPT" | grep -qiE "harness\s*(skip|off|bypass|없이)|without\s*harness|just\s*(answer|chat|reply)"; then
43
+ exit 0
44
+ fi
45
+
46
+ # 현재 세션 상태 읽기
47
+ PIPELINE="none"
48
+ CURRENT_AGENT="none"
49
+ NEXT_AGENT="none"
50
+ SPRINT_NUM="0"
51
+ SPRINT_STATUS="init"
52
+ FE_STACK="react"
53
+
54
+ if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
55
+ PIPELINE=$(jq -r '.pipeline // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
56
+ CURRENT_AGENT=$(jq -r '.current_agent // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
57
+ NEXT_AGENT=$(jq -r '.next_agent // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
58
+ SPRINT_NUM=$(jq -r '.sprint.number // 0' "$CWD/.harness/progress.json" 2>/dev/null || echo "0")
59
+ SPRINT_STATUS=$(jq -r '.sprint.status // "init"' "$CWD/.harness/progress.json" 2>/dev/null || echo "init")
60
+ fi
61
+
62
+ if [ -f "$CWD/.harness/actions/pipeline.json" ] && command -v jq >/dev/null 2>&1; then
63
+ FE_STACK=$(jq -r '.fe_stack // "react"' "$CWD/.harness/actions/pipeline.json" 2>/dev/null || echo "react")
64
+ fi
65
+
66
+ # ─────────────────────────────────────────
67
+ # Context 주입 (stdout → Claude 컨텍스트)
68
+ # ─────────────────────────────────────────
69
+ cat <<EOF
70
+ [walwal-harness] Auto-routing is ACTIVE for this project.
71
+
72
+ 이 프로젝트는 walwal-harness 가 초기화되어 있으며, auto_route_dispatcher 플래그가
73
+ 켜져 있습니다. 사용자의 모든 프롬프트는 **harness-dispatcher** 스킬을 통해
74
+ 분류/라우팅된 뒤 처리되어야 합니다.
75
+
76
+ ## 분류 규칙 (Dispatcher Request Classification)
77
+ - **기능 요청** (만들어줘, 추가, 고쳐줘, PRD, OpenAPI) → Pipeline Flow
78
+ - **실수 지적** (아니, 잘못됐어, 그렇게 하면 안 돼, X로 해야지) → Gotcha Flow
79
+ - **혼합** → Gotcha 먼저 기록 → Pipeline 이어서
80
+ - **메타/인사/Claude 자체 질문** → 짧게 일반 응답 (dispatcher skip 허용)
81
+
82
+ ## 현재 harness 세션 상태
83
+ - pipeline: $PIPELINE
84
+ - current_agent: $CURRENT_AGENT
85
+ - next_agent: $NEXT_AGENT
86
+ - sprint: $SPRINT_NUM ($SPRINT_STATUS)
87
+ - fe_stack: $FE_STACK
88
+
89
+ ## 지시
90
+ 1. 먼저 위 상태를 읽고, 사용자 메시지가 어떤 분류에 해당하는지 판단
91
+ 2. **pipeline 이 'none' 또는 dispatcher 미실행 상태**면 harness-dispatcher 스킬 호출
92
+ 3. **pipeline 이 이미 활성**이면:
93
+ - 실수 지적이면 → dispatcher 로 gotcha 기록
94
+ - 기능 연속 작업이면 → next_agent 또는 current_agent 의 컨텍스트로 계속
95
+ 4. 응답 전 반드시 \`.harness/progress.json\` 을 읽고 세션 경계 프로토콜 준수
96
+
97
+ ## 이 라우팅을 건너뛰려면
98
+ - **단일 메시지**: 사용자가 "harness skip", "without harness", "harness 없이", "just answer" 등을 명시
99
+ - **전역 비활성**: \`.harness/config.json\` 에서 \`behavior.auto_route_dispatcher = false\` 설정
100
+
101
+ ## 참고
102
+ 이 안내는 \`scripts/harness-user-prompt-submit.sh\` UserPromptSubmit 훅에 의해
103
+ 매 프롬프트마다 자동 주입됩니다. 중복 안내처럼 보여도 무시하지 말고 위 규칙을 따르세요.
104
+ EOF
105
+
106
+ exit 0
@@ -282,12 +282,17 @@ render_progress() {
282
282
  render_agent_bar() {
283
283
  local PROGRESS="${1:-.}/.harness/progress.json"
284
284
  local CONFIG="${1:-.}/.harness/config.json"
285
+ local PIPELINE_JSON="${1:-.}/.harness/actions/pipeline.json"
285
286
 
286
287
  if [ ! -f "$PROGRESS" ] || [ ! -f "$CONFIG" ]; then return 1; fi
287
288
 
288
- local pipeline current_agent
289
+ local pipeline current_agent fe_stack
289
290
  pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
290
291
  current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
292
+ fe_stack="react"
293
+ if [ -f "$PIPELINE_JSON" ]; then
294
+ fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
295
+ fi
291
296
 
292
297
  local completed_agents
293
298
  completed_agents=$(jq -r '.completed_agents // [] | .[]' "$PROGRESS")
@@ -299,6 +304,15 @@ render_agent_bar() {
299
304
 
300
305
  while IFS= read -r agent; do
301
306
  agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
307
+
308
+ # fe_stack 치환 적용
309
+ if [ "$fe_stack" = "flutter" ]; then
310
+ local sub
311
+ sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
312
+ if [ "$sub" = "__skip__" ]; then continue; fi
313
+ agent="$sub"
314
+ fi
315
+
302
316
  if [ "$first" = true ]; then
303
317
  first=false
304
318
  else
@@ -61,7 +61,13 @@ elif [ -f "${PROJECT_ROOT}/pom.xml" ] || [ -f "${PROJECT_ROOT}/build.gradle" ];
61
61
  fi
62
62
 
63
63
  # Frontend
64
- if [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
64
+ # Flutter 우선 감지 — pubspec.yaml 존재하면 Flutter 프로젝트로 판정 (TECH_LANG도 보정)
65
+ FE_STACK="react" # react | flutter (기본값은 react 계열)
66
+ if [ -f "${PROJECT_ROOT}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/pubspec.yaml" 2>/dev/null; then
67
+ TECH_FRONTEND="flutter"
68
+ TECH_LANG="dart"
69
+ FE_STACK="flutter"
70
+ elif [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
65
71
  TECH_FRONTEND="nextjs"
66
72
  elif [ -f "${PROJECT_ROOT}/vite.config.ts" ] || [ -f "${PROJECT_ROOT}/vite.config.js" ]; then
67
73
  TECH_FRONTEND="vite-react"
@@ -75,11 +81,26 @@ fi
75
81
  if [ -d "${PROJECT_ROOT}/apps/web" ]; then
76
82
  if [ -f "${PROJECT_ROOT}/apps/web/next.config.js" ] || [ -f "${PROJECT_ROOT}/apps/web/next.config.ts" ]; then
77
83
  TECH_FRONTEND="nextjs"
84
+ FE_STACK="react"
78
85
  elif [ -f "${PROJECT_ROOT}/apps/web/vite.config.ts" ]; then
79
86
  TECH_FRONTEND="vite-react"
87
+ FE_STACK="react"
80
88
  fi
81
89
  fi
82
90
 
91
+ # Flutter 서브디렉토리 감지 (monorepo 또는 서브 프로젝트 케이스)
92
+ if [ "$TECH_FRONTEND" = "unknown" ]; then
93
+ # 대표적인 Flutter 서브폴더 이름을 얕게 탐색
94
+ for d in apps/mobile mobile clue_mobile_app flutter_app; do
95
+ if [ -f "${PROJECT_ROOT}/${d}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/${d}/pubspec.yaml" 2>/dev/null; then
96
+ TECH_FRONTEND="flutter"
97
+ TECH_LANG="dart"
98
+ FE_STACK="flutter"
99
+ break
100
+ fi
101
+ done
102
+ fi
103
+
83
104
  # Database
84
105
  if grep -rq "typeorm\|prisma\|sequelize\|knex" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
85
106
  if grep -q "pg\|postgres" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
@@ -203,6 +224,7 @@ cat > "$OUTPUT" << JSONEOF
203
224
  "tech_stack": {
204
225
  "backend": "${TECH_BACKEND}",
205
226
  "frontend": "${TECH_FRONTEND}",
227
+ "fe_stack": "${FE_STACK}",
206
228
  "database": "${TECH_DB}",
207
229
  "monorepo": "${TECH_MONOREPO}",
208
230
  "language": "${TECH_LANG}"
@@ -260,7 +282,7 @@ echo "=== Scan Complete ==="
260
282
  echo "Output: ${OUTPUT}"
261
283
  echo ""
262
284
  echo "--- Summary ---"
263
- echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} / ${TECH_DB}"
285
+ echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}) / ${TECH_DB}"
264
286
  echo "Monorepo: ${TECH_MONOREPO}"
265
287
  echo "OpenAPI: ${OPENAPI}"
266
288
  echo "Git: ${GIT_INIT} (${GIT_COMMITS} commits, branch: ${GIT_BRANCH})"