@walwal-harness/cli 2.4.0 → 3.2.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.
@@ -1,31 +1,34 @@
1
1
  #!/bin/bash
2
- # harness-session-start.sh — SessionStart 훅
3
- # Claude Code 세션 시작 시 자동 실행되어 현재 하네스 상태를 출력한다.
4
- # .claude/settings.json의 SessionStart 훅으로 등록된다.
2
+ # harness-session-start.sh — SessionStart 훅 (compact)
3
+ # statusline이 상시 상태를 표시하므로, 여기서는 핵심 안내만 출력.
5
4
 
6
5
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
6
  LIB="$SCRIPT_DIR/lib/harness-render-progress.sh"
8
7
 
9
- # lib이 없으면 silent exit (훅이므로 에러 출력하지 않음)
10
8
  if [ ! -f "$LIB" ]; then exit 0; fi
11
9
  source "$LIB"
12
-
13
- # jq 없으면 silent exit
14
10
  command -v jq &>/dev/null || exit 0
15
11
 
16
- # .harness/ 찾기
17
12
  PROJECT_ROOT="$(resolve_harness_root "." 2>/dev/null)" || exit 0
18
-
19
13
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
20
14
  [ -f "$PROGRESS" ] || exit 0
21
15
 
22
- # init 상태면 간단 안내만
23
16
  sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
17
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
18
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
19
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
20
+
21
+ # init 상태: 간단 안내
24
22
  if [ "$sprint_status" = "init" ]; then
25
23
  echo "# Harness ready — say \"하네스 엔지니어링 시작\" or /harness-dispatcher"
26
24
  exit 0
27
25
  fi
28
26
 
29
- # Feature-level 프로그래스 출력
30
- render_progress "$PROJECT_ROOT"
31
- render_agent_bar "$PROJECT_ROOT"
27
+ # 활성 세션: 다음 액션만 안내 (상세 프로그래스는 statusline에서 상시 표시)
28
+ if [ "$agent_status" = "blocked" ]; then
29
+ echo "# Harness BLOCKED — user intervention required. Run: bash scripts/harness-next.sh"
30
+ elif [ "$next_agent" != "none" ] && [ "$next_agent" != "null" ]; then
31
+ echo "# Harness: next → /harness-${next_agent}"
32
+ elif [ "$current_agent" != "none" ] && [ "$current_agent" != "null" ]; then
33
+ echo "# Harness: ${current_agent} [${agent_status}]"
34
+ fi
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ # harness-statusline.sh — Claude Code statusline hook
3
+ # 터미널 하단에 항상 고정되는 1줄 compact 상태 표시.
4
+ # stdin: Claude Code JSON payload (model, context_window, cost 등)
5
+ # stdout: 상태 문자열 (Claude Code가 터미널 하단에 렌더링)
6
+
7
+ # Read Claude Code session data from stdin
8
+ input=$(cat)
9
+
10
+ # Extract Claude Code built-in data
11
+ context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null | cut -d. -f1)
12
+ cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' 2>/dev/null)
13
+
14
+ # Resolve project root (walk up from cwd)
15
+ CWD=$(echo "$input" | jq -r '.workspace.current_dir // empty' 2>/dev/null)
16
+ if [ -z "$CWD" ]; then CWD="$PWD"; fi
17
+
18
+ PROJECT_ROOT="$CWD"
19
+ while [ "$PROJECT_ROOT" != "/" ]; do
20
+ if [ -d "$PROJECT_ROOT/.harness" ]; then break; fi
21
+ PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
22
+ done
23
+
24
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
+ FEATURE_LIST="$PROJECT_ROOT/.harness/actions/feature-list.json"
26
+ PIPELINE_JSON="$PROJECT_ROOT/.harness/actions/pipeline.json"
27
+
28
+ # No harness → minimal status
29
+ if [ ! -f "$PROGRESS" ]; then
30
+ echo "harness: not initialized | ctx ${context_pct}%"
31
+ exit 0
32
+ fi
33
+
34
+ # Read harness state
35
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
36
+ sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
37
+ pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS" 2>/dev/null)
38
+ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
39
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
40
+ next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
41
+ retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS" 2>/dev/null)
42
+
43
+ # Pipeline short name
44
+ case "$pipeline" in
45
+ FULLSTACK) pl="FULL" ;;
46
+ FE-ONLY) pl="FE" ;;
47
+ BE-ONLY) pl="BE" ;;
48
+ null|"?") pl="?" ;;
49
+ *) pl="$pipeline" ;;
50
+ esac
51
+
52
+ # Agent short name (strip harness prefix)
53
+ agent_short="${current_agent#generator-}"
54
+ agent_short="${agent_short#evaluator-}"
55
+ if [ "$current_agent" = "none" ] || [ "$current_agent" = "null" ]; then
56
+ agent_short="$next_agent"
57
+ if [ "$agent_short" = "none" ] || [ "$agent_short" = "null" ]; then
58
+ agent_short="idle"
59
+ fi
60
+ fi
61
+
62
+ # Feature progress
63
+ total_features=0
64
+ completed_features=0
65
+ if [ -f "$FEATURE_LIST" ]; then
66
+ total_features=$(jq '.features | length' "$FEATURE_LIST" 2>/dev/null || echo 0)
67
+ completed_features=$(jq '[.features[]? | select(
68
+ (.passes // []) | (
69
+ (map(select(. == "evaluator-functional")) | length > 0) and
70
+ (map(select(. == "evaluator-visual")) | length > 0)
71
+ )
72
+ )] | length' "$FEATURE_LIST" 2>/dev/null || echo 0)
73
+ fi
74
+
75
+ # Status indicator
76
+ status_icon=""
77
+ case "$agent_status" in
78
+ running) status_icon=">" ;;
79
+ completed) status_icon="v" ;;
80
+ failed) status_icon="x" ;;
81
+ blocked) status_icon="!" ;;
82
+ *) status_icon="-" ;;
83
+ esac
84
+
85
+ # Retry indicator
86
+ retry_str=""
87
+ if [ "$retry_count" -gt 0 ]; then
88
+ retry_str=" R${retry_count}"
89
+ fi
90
+
91
+ # Build compact status line
92
+ # Format: [S1] FULL | >backend | 2/5 feat | R0 | ctx 45% | $1.23
93
+ echo "[S${sprint_num}] ${pl} | ${status_icon}${agent_short}${retry_str} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
@@ -1,55 +1,31 @@
1
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
- # ─────────────────────────────────────────
2
+ # harness-user-prompt-submit.sh — UserPromptSubmit hook (compact v3.2)
3
+ # 핵심 상태 + 라우팅 지시만 주입. 장황한 설명 제거.
14
4
  set -e
15
5
 
16
- # stdin 의 JSON payload 읽기 (Claude Code 가 {prompt, cwd, session_id, ...} 전달)
17
6
  INPUT=$(cat)
18
-
19
- # cwd 추출 (payload 에 없으면 PWD fallback)
20
7
  CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
21
- if [ -z "$CWD" ]; then
22
- CWD="$PWD"
23
- fi
8
+ if [ -z "$CWD" ]; then CWD="$PWD"; fi
24
9
 
25
10
  # 조건 1: 하네스 초기화 확인
26
- if [ ! -f "$CWD/.harness/config.json" ]; then
27
- exit 0
28
- fi
11
+ if [ ! -f "$CWD/.harness/config.json" ]; then exit 0; fi
29
12
 
30
- # 조건 2: opt-out 플래그 확인 (기본값 true)
13
+ # 조건 2: opt-out 플래그 확인
31
14
  AUTO_ROUTE="true"
32
15
  if command -v jq >/dev/null 2>&1; then
33
16
  AUTO_ROUTE=$(jq -r '.behavior.auto_route_dispatcher // true' "$CWD/.harness/config.json" 2>/dev/null || echo "true")
34
17
  fi
35
- if [ "$AUTO_ROUTE" != "true" ]; then
36
- exit 0
37
- fi
18
+ if [ "$AUTO_ROUTE" != "true" ]; then exit 0; fi
38
19
 
39
- # 프롬프트 내용 추출 (opt-out 문구 감지용)
20
+ # 사용자 skip 감지
40
21
  PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)
41
- # 사용자가 명시적으로 건너뛰기 요청하면 pass-through
42
22
  if echo "$PROMPT" | grep -qiE "harness\s*(skip|off|bypass|없이)|without\s*harness|just\s*(answer|chat|reply)"; then
43
23
  exit 0
44
24
  fi
45
25
 
46
26
  # 현재 세션 상태 읽기
47
- PIPELINE="none"
48
- CURRENT_AGENT="none"
49
- NEXT_AGENT="none"
50
- SPRINT_NUM="0"
51
- SPRINT_STATUS="init"
52
- FE_STACK="react"
27
+ PIPELINE="none"; CURRENT_AGENT="none"; NEXT_AGENT="none"
28
+ SPRINT_NUM="0"; SPRINT_STATUS="init"; AGENT_STATUS="pending"
53
29
 
54
30
  if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
55
31
  PIPELINE=$(jq -r '.pipeline // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
@@ -57,50 +33,33 @@ if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
57
33
  NEXT_AGENT=$(jq -r '.next_agent // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
58
34
  SPRINT_NUM=$(jq -r '.sprint.number // 0' "$CWD/.harness/progress.json" 2>/dev/null || echo "0")
59
35
  SPRINT_STATUS=$(jq -r '.sprint.status // "init"' "$CWD/.harness/progress.json" 2>/dev/null || echo "init")
36
+ AGENT_STATUS=$(jq -r '.agent_status // "pending"' "$CWD/.harness/progress.json" 2>/dev/null || echo "pending")
60
37
  fi
61
38
 
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")
39
+ # ── 컨텍스트 분리 가드레일 ──
40
+ # 현재 에이전트가 활성인데 다른 에이전트 스킬을 호출하려는 경우 경고
41
+ CONTEXT_WARNING=""
42
+ if [ "$CURRENT_AGENT" != "none" ] && [ "$CURRENT_AGENT" != "null" ] && [ "$AGENT_STATUS" = "running" ]; then
43
+ # 프롬프트에서 /harness-* 패턴 추출
44
+ REQUESTED_SKILL=$(echo "$PROMPT" | grep -oE '/harness-[a-z-]+' | head -1 | sed 's|/harness-||')
45
+ if [ -n "$REQUESTED_SKILL" ] && [ "$REQUESTED_SKILL" != "$CURRENT_AGENT" ]; then
46
+ CONTEXT_WARNING="
47
+ ## !! Context Isolation Warning !!
48
+ current_agent=${CURRENT_AGENT} (running) 인데 /harness-${REQUESTED_SKILL} 호출 감지.
49
+ 한 세션에서 다른 에이전트를 실행하면 컨텍스트가 오염됩니다.
50
+ 현재 에이전트를 먼저 완료(completed)하거나, 새 세션을 시작하세요."
51
+ fi
64
52
  fi
65
53
 
66
- # ─────────────────────────────────────────
67
- # Context 주입 (stdout → Claude 컨텍스트)
68
- # ─────────────────────────────────────────
54
+ # ── Compact context 주입 ──
69
55
  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
- 매 프롬프트마다 자동 주입됩니다. 중복 안내처럼 보여도 무시하지 말고 위 규칙을 따르세요.
56
+ [harness] S${SPRINT_NUM} | ${PIPELINE} | agent=${CURRENT_AGENT} (${AGENT_STATUS}) | next=${NEXT_AGENT}
57
+ ${CONTEXT_WARNING}
58
+ ## Route
59
+ - pipeline=none/dispatcher 미실행 → harness-dispatcher 스킬 호출
60
+ - 기능 요청 → pipeline flow | 실수 지적 → gotcha flow | 메타 질문 → 짧게 응답 (skip)
61
+ - 활성 pipeline → next_agent/current_agent 컨텍스트로 계속
62
+ - skip: "harness skip", "just answer" 등 명시 시 단일 메시지 건너뜀
104
63
  EOF
105
64
 
106
65
  exit 0
@@ -0,0 +1,143 @@
1
+ #!/bin/bash
2
+ # harness-guardrail.sh — 런타임 파일 소유권 검증
3
+ # 에이전트 전환 시 이전 에이전트가 권한 밖 파일을 수정했는지 git diff로 검증.
4
+ #
5
+ # 사용법: source lib/harness-guardrail.sh && verify_file_ownership "$PROJECT_ROOT"
6
+ # 반환값: 0=통과, 1=위반 발견 (위반 목록을 stdout에 출력)
7
+
8
+ # ─────────────────────────────────────────
9
+ # Agent → 허용 경로 매핑
10
+ # ─────────────────────────────────────────
11
+ get_allowed_paths() {
12
+ local agent="$1"
13
+ case "$agent" in
14
+ planner)
15
+ echo "AGENTS.md"
16
+ echo ".harness/actions/"
17
+ echo ".harness/progress.json"
18
+ echo ".harness/progress.log"
19
+ ;;
20
+ generator-backend)
21
+ echo "apps/gateway/"
22
+ echo "apps/service-"
23
+ echo "libs/"
24
+ echo ".harness/actions/sprint-contract.md"
25
+ echo ".harness/actions/feature-list.json"
26
+ echo ".harness/progress.json"
27
+ echo "package.json"
28
+ echo "nest-cli.json"
29
+ echo "tsconfig"
30
+ echo "docker-compose"
31
+ ;;
32
+ generator-frontend|generator-frontend-flutter)
33
+ echo "apps/web/"
34
+ echo "apps/flutter/"
35
+ echo ".harness/actions/sprint-contract.md"
36
+ echo ".harness/actions/feature-list.json"
37
+ echo ".harness/progress.json"
38
+ echo "package.json"
39
+ echo "tsconfig"
40
+ echo "pubspec"
41
+ ;;
42
+ evaluator-functional|evaluator-functional-flutter)
43
+ echo ".harness/actions/evaluation-functional.md"
44
+ echo ".harness/progress.json"
45
+ echo "tests/"
46
+ echo "test/"
47
+ echo "e2e/"
48
+ echo "playwright"
49
+ ;;
50
+ evaluator-visual)
51
+ echo ".harness/actions/evaluation-visual.md"
52
+ echo ".harness/progress.json"
53
+ ;;
54
+ dispatcher)
55
+ echo ".harness/"
56
+ ;;
57
+ *)
58
+ echo "**" # unknown agent — allow all (safe fallback)
59
+ ;;
60
+ esac
61
+ }
62
+
63
+ # ─────────────────────────────────────────
64
+ # verify_file_ownership — git diff 기반 검증
65
+ #
66
+ # Args:
67
+ # $1 — project root
68
+ #
69
+ # Reads:
70
+ # .harness/progress.json (current_agent)
71
+ #
72
+ # Returns: 0=pass, 1=violation found
73
+ # ─────────────────────────────────────────
74
+ verify_file_ownership() {
75
+ local PROJECT_ROOT="$1"
76
+ local PROGRESS="$PROJECT_ROOT/.harness/progress.json"
77
+
78
+ if [ ! -f "$PROGRESS" ]; then return 0; fi
79
+
80
+ local agent
81
+ agent=$(jq -r '.current_agent // "null"' "$PROGRESS" 2>/dev/null)
82
+ if [ "$agent" = "null" ] || [ -z "$agent" ]; then return 0; fi
83
+
84
+ # git이 없으면 skip
85
+ if ! command -v git &>/dev/null; then return 0; fi
86
+
87
+ # 마지막 커밋 이후 변경된 파일 목록
88
+ local -a changed_files
89
+ mapfile -t changed_files < <(cd "$PROJECT_ROOT" && git diff --name-only HEAD 2>/dev/null)
90
+
91
+ # staged 파일도 포함
92
+ local -a staged_files
93
+ mapfile -t staged_files < <(cd "$PROJECT_ROOT" && git diff --name-only --cached 2>/dev/null)
94
+
95
+ # 합치기 (중복 제거)
96
+ local -a all_files
97
+ mapfile -t all_files < <(printf '%s\n' "${changed_files[@]}" "${staged_files[@]}" | sort -u | grep -v '^$')
98
+
99
+ if [ ${#all_files[@]} -eq 0 ]; then return 0; fi
100
+
101
+ # 허용 경로 목록
102
+ local -a allowed
103
+ mapfile -t allowed < <(get_allowed_paths "$agent")
104
+
105
+ # 위반 검사
106
+ local -a violations=()
107
+ for file in "${all_files[@]}"; do
108
+ local is_allowed=false
109
+ for pattern in "${allowed[@]}"; do
110
+ if [[ "$file" == $pattern* ]]; then
111
+ is_allowed=true
112
+ break
113
+ fi
114
+ done
115
+ if [ "$is_allowed" = false ]; then
116
+ violations+=("$file")
117
+ fi
118
+ done
119
+
120
+ if [ ${#violations[@]} -eq 0 ]; then
121
+ return 0
122
+ fi
123
+
124
+ # 위반 보고
125
+ echo ""
126
+ echo " ── Guardrail Violation ────────────────"
127
+ echo " Agent '$agent' modified files outside its allowed paths:"
128
+ echo ""
129
+ for v in "${violations[@]}"; do
130
+ echo " ✗ $v"
131
+ done
132
+ echo ""
133
+ echo " Allowed paths for $agent:"
134
+ for p in "${allowed[@]}"; do
135
+ echo " ✓ $p*"
136
+ done
137
+ echo ""
138
+ echo " Action: Review these changes before proceeding."
139
+ echo " ─────────────────────────────────────────"
140
+ echo ""
141
+
142
+ return 1
143
+ }
@@ -286,12 +286,17 @@ render_agent_bar() {
286
286
 
287
287
  if [ ! -f "$PROGRESS" ] || [ ! -f "$CONFIG" ]; then return 1; fi
288
288
 
289
- local pipeline current_agent fe_stack
289
+ local pipeline current_agent fe_stack fe_target
290
290
  pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
291
291
  current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
292
292
  fe_stack="react"
293
+ fe_target="web"
293
294
  if [ -f "$PIPELINE_JSON" ]; then
294
295
  fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
296
+ fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
297
+ if [ -z "$fe_target" ]; then
298
+ fe_target=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}._default_target // \"web\"" "$CONFIG" 2>/dev/null || echo "web")
299
+ fi
295
300
  fi
296
301
 
297
302
  local completed_agents
@@ -305,10 +310,10 @@ render_agent_bar() {
305
310
  while IFS= read -r agent; do
306
311
  agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
307
312
 
308
- # fe_stack 치환 적용
313
+ # fe_stack + fe_target 치환 적용
309
314
  if [ "$fe_stack" = "flutter" ]; then
310
315
  local sub
311
- sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
316
+ sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
312
317
  if [ "$sub" = "__skip__" ]; then continue; fi
313
318
  agent="$sub"
314
319
  fi
@@ -62,11 +62,15 @@ fi
62
62
 
63
63
  # Frontend
64
64
  # Flutter 우선 감지 — pubspec.yaml 존재하면 Flutter 프로젝트로 판정 (TECH_LANG도 보정)
65
- FE_STACK="react" # react | flutter (기본값은 react 계열)
65
+ FE_STACK="react" # react | flutter (기본값은 react 계열)
66
+ FE_TARGET="web" # web | mobile | desktop (Flutter 의 컴파일 타겟; React 는 항상 web)
67
+ FLUTTER_ROOT="" # Flutter 프로젝트 루트 경로 (web/mobile/desktop 감지용)
68
+
66
69
  if [ -f "${PROJECT_ROOT}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/pubspec.yaml" 2>/dev/null; then
67
70
  TECH_FRONTEND="flutter"
68
71
  TECH_LANG="dart"
69
72
  FE_STACK="flutter"
73
+ FLUTTER_ROOT="${PROJECT_ROOT}"
70
74
  elif [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
71
75
  TECH_FRONTEND="nextjs"
72
76
  elif [ -f "${PROJECT_ROOT}/vite.config.ts" ] || [ -f "${PROJECT_ROOT}/vite.config.js" ]; then
@@ -91,16 +95,41 @@ fi
91
95
  # Flutter 서브디렉토리 감지 (monorepo 또는 서브 프로젝트 케이스)
92
96
  if [ "$TECH_FRONTEND" = "unknown" ]; then
93
97
  # 대표적인 Flutter 서브폴더 이름을 얕게 탐색
94
- for d in apps/mobile mobile clue_mobile_app flutter_app; do
98
+ for d in apps/mobile apps/web mobile clue_mobile_app flutter_app; do
95
99
  if [ -f "${PROJECT_ROOT}/${d}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/${d}/pubspec.yaml" 2>/dev/null; then
96
100
  TECH_FRONTEND="flutter"
97
101
  TECH_LANG="dart"
98
102
  FE_STACK="flutter"
103
+ FLUTTER_ROOT="${PROJECT_ROOT}/${d}"
99
104
  break
100
105
  fi
101
106
  done
102
107
  fi
103
108
 
109
+ # Flutter fe_target 감지 (web / mobile / desktop)
110
+ # - web/index.html 존재 → web
111
+ # - android/ 또는 ios/ 존재 + web/ 없음 → mobile
112
+ # - macos/ 또는 windows/ 또는 linux/ 존재 + 위 둘 없음 → desktop
113
+ # - 동시 존재 (멀티 타겟) → web 우선 (사용자가 Planner에서 변경 가능)
114
+ if [ "$FE_STACK" = "flutter" ] && [ -n "$FLUTTER_ROOT" ]; then
115
+ HAS_WEB=false
116
+ HAS_MOBILE=false
117
+ HAS_DESKTOP=false
118
+ [ -f "${FLUTTER_ROOT}/web/index.html" ] && HAS_WEB=true
119
+ { [ -d "${FLUTTER_ROOT}/android" ] || [ -d "${FLUTTER_ROOT}/ios" ]; } && HAS_MOBILE=true
120
+ { [ -d "${FLUTTER_ROOT}/macos" ] || [ -d "${FLUTTER_ROOT}/windows" ] || [ -d "${FLUTTER_ROOT}/linux" ]; } && HAS_DESKTOP=true
121
+
122
+ if [ "$HAS_WEB" = "true" ]; then
123
+ FE_TARGET="web"
124
+ elif [ "$HAS_MOBILE" = "true" ]; then
125
+ FE_TARGET="mobile"
126
+ elif [ "$HAS_DESKTOP" = "true" ]; then
127
+ FE_TARGET="desktop"
128
+ else
129
+ FE_TARGET="unknown"
130
+ fi
131
+ fi
132
+
104
133
  # Database
105
134
  if grep -rq "typeorm\|prisma\|sequelize\|knex" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
106
135
  if grep -q "pg\|postgres" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
@@ -225,6 +254,7 @@ cat > "$OUTPUT" << JSONEOF
225
254
  "backend": "${TECH_BACKEND}",
226
255
  "frontend": "${TECH_FRONTEND}",
227
256
  "fe_stack": "${FE_STACK}",
257
+ "fe_target": "${FE_TARGET}",
228
258
  "database": "${TECH_DB}",
229
259
  "monorepo": "${TECH_MONOREPO}",
230
260
  "language": "${TECH_LANG}"
@@ -282,7 +312,7 @@ echo "=== Scan Complete ==="
282
312
  echo "Output: ${OUTPUT}"
283
313
  echo ""
284
314
  echo "--- Summary ---"
285
- echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}) / ${TECH_DB}"
315
+ echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}, fe_target=${FE_TARGET}) / ${TECH_DB}"
286
316
  echo "Monorepo: ${TECH_MONOREPO}"
287
317
  echo "OpenAPI: ${OPENAPI}"
288
318
  echo "Git: ${GIT_INIT} (${GIT_COMMITS} commits, branch: ${GIT_BRANCH})"
@@ -31,20 +31,31 @@ docmeta:
31
31
  > **FE Stack 차원**: 모든 FE 관련 파이프라인은 `fe_stack` 필드로 React/Flutter를 구분한다.
32
32
  > Planner가 scan-result.json(`tech_stack.fe_stack`) 또는 사용자 질문으로 확정한다.
33
33
 
34
- ## fe_stack 스위치 매트릭스
34
+ ## fe_stack + fe_target 스위치 매트릭스
35
35
 
36
- | fe_stack | FE Generator | FE Evaluator(Functional) | Evaluator-Visual |
37
- |----------|--------------|--------------------------|------------------|
38
- | `react` | `generator-frontend` | `evaluator-functional` (Playwright MCP) | `evaluator-visual` |
39
- | `flutter`| `generator-frontend-flutter` | `evaluator-functional-flutter` (flutter analyze/test) | **SKIP** (브라우저 없음) |
36
+ | fe_stack | fe_target | FE Generator | Eval-Functional | Eval-Visual |
37
+ |----------|-----------|--------------|----------------|-------------|
38
+ | `react` | (n/a) | `generator-frontend` | `evaluator-functional` (Playwright) | `evaluator-visual` |
39
+ | `flutter`| **`web`** | `generator-frontend-flutter` | **`evaluator-functional`** (Playwright!) | **`evaluator-visual`** (Playwright!) |
40
+ | `flutter`| `mobile` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | **SKIP** |
41
+ | `flutter`| `desktop` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | **SKIP** |
40
42
 
41
- Dispatcher는 `next_agent`를 설정할 때 다음 규칙으로 치환한다:
43
+ > **Flutter Web**: 컴파일 결과가 HTML+JS+CSS 이므로 React 경로의 Playwright evaluator 가 정상 동작.
44
+ > Generator-Frontend-Flutter 의 Self-Verification 에서 `flutter analyze` / `flutter test` /
45
+ > `flutter build web --release` 가 이미 통과한 상태로 handoff 됨이 전제.
46
+
47
+ Dispatcher는 `next_agent`를 설정할 때 `pipeline.json` 의 `fe_stack` + `fe_target` 두 값을 보고 치환한다:
42
48
 
43
49
  ```
44
50
  if pipeline.json.fe_stack == "flutter":
45
- "generator-frontend" → "generator-frontend-flutter"
46
- "evaluator-functional" (FE 단계) → "evaluator-functional-flutter"
47
- "evaluator-visual" → skip (agents_skipped로 이동)
51
+ "generator-frontend" → "generator-frontend-flutter" (모든 fe_target 공통)
52
+
53
+ if fe_target == "web":
54
+ "evaluator-functional" → "evaluator-functional" (그대로, Playwright 사용)
55
+ "evaluator-visual" → "evaluator-visual" (그대로)
56
+ elif fe_target in ("mobile", "desktop"):
57
+ "evaluator-functional" → "evaluator-functional-flutter"
58
+ "evaluator-visual" → __skip__
48
59
  ```
49
60
 
50
61
  ## FE-ONLY
@@ -6,9 +6,16 @@ disable-model-invocation: true
6
6
 
7
7
  # Evaluator-Functional-Flutter — Dart/Flutter 정적·동적 검증
8
8
 
9
- > **주의**: Flutter 앱은 브라우저가 아니므로 Playwright MCP(`browser_*`)를 쓸 수 없다.
10
- > 이 에이전트는 `flutter analyze`, `flutter test`, `dart format`, 정적 grep 검증, 생성 파일 일관성으로 평가한다.
11
- > 실기기/시뮬레이터 UI 검증은 사람 QA 또는 별도 파이프라인으로 위임.
9
+ > **이 에이전트는 `fe_target = mobile` 또는 `desktop` 일 때만 사용된다.**
10
+ >
11
+ > Flutter Mobile/Desktop 앱은 브라우저가 아니므로 Playwright MCP(`browser_*`)를 쓸 수 없다.
12
+ > 이 에이전트는 `flutter analyze`, `flutter test`, `dart format`, 정적 grep 검증, 생성 파일
13
+ > 일관성으로 평가한다. 실기기/시뮬레이터 UI 검증은 사람 QA 또는 별도 파이프라인으로 위임.
14
+ >
15
+ > **`fe_target = web` 인 Flutter Web 프로젝트는 이 에이전트가 아니라 일반 `evaluator-functional`
16
+ > 과 `evaluator-visual` 이 Playwright 로 검증한다** (컴파일 결과가 HTML+JS+CSS 이므로).
17
+ > 이 경우 Generator-Frontend-Flutter 의 Self-Verification 단계에서 `flutter analyze` /
18
+ > `flutter test` / `flutter build web --release` 가 이미 통과했음이 전제된다.
12
19
 
13
20
  ## Session Boundary Protocol
14
21
 
@@ -33,7 +33,10 @@ disable-model-invocation: true
33
33
  4. `pwd` + `.harness/progress.json` + `git log --oneline -20`
34
34
  5. `.harness/actions/api-contract.json` 읽기 — **서버 API 계약이 Source of Truth**
35
35
  6. `.harness/actions/feature-list.json` — `layer: "frontend"` 필터
36
- 7. `pubspec.yaml` 확인 — Flutter 버전, Riverpod/Retrofit/json_serializable 의존성 존재 확인
36
+ 7. `.harness/actions/pipeline.json` 의 **`fe_target`** 확인 (`web` | `mobile` | `desktop`)
37
+ - 이 값에 따라 허용되는 API, 빌드 명령, 안티패턴이 달라진다 (아래 "fe_target 분기" 섹션 참조)
38
+ 8. `pubspec.yaml` 확인 — Flutter 버전, Riverpod/Retrofit/json_serializable 의존성 존재 확인
39
+ - `fe_target = web` 인 경우 `flutter config --enable-web` 활성화 + `web/index.html` 존재 확인
37
40
 
38
41
  ## AGENTS.md — 읽기 전용
39
42
 
@@ -60,7 +63,38 @@ Backend 코드, `.harness/`, `AGENTS.md` 수정 금지.
60
63
  | [API Layer Pattern](references/api-layer-pattern.md) | integrated_data_layer 구조, Request/Response, Retrofit | API 연동 시 |
61
64
  | [Riverpod Pattern](references/riverpod-pattern.md) | Page+VM 쌍, NotifierProvider, family 패턴 | 페이지/위젯 구현 시 |
62
65
  | [i18n Pattern](references/i18n-pattern.md) | ARB 파일, LocaleAssist, 키 네이밍 | 문자열 추가 시 |
63
- | [Anti-Patterns](references/anti-patterns.md) | 금지 API, 하드코딩, bridges/, StatefulWidget 직접 호출 | 구현 완료 후 셀프 체크 |
66
+ | [Anti-Patterns](references/anti-patterns.md) | 금지 API, 하드코딩, bridges/, StatefulWidget 직접 호출 (fe_target 별 차이 포함) | 구현 완료 후 셀프 체크 |
67
+ | [Flutter Web Pattern](references/flutter-web-pattern.md) | Web 전용 — `dart:html`/`package:web` 허용, 라우팅, 빌드, hosting | `fe_target = web` 일 때만 |
68
+
69
+ ## fe_target 분기
70
+
71
+ `pipeline.json.fe_target` 에 따라 적용되는 규칙이 다르다:
72
+
73
+ ### `fe_target = web` (Flutter Web)
74
+
75
+ - **빌드**: `flutter build web --release` / 개발 서버: `flutter run -d chrome`
76
+ - **HTML/JS 인터롭 허용**: `dart:html`, `package:web`, `dart:js_interop` 사용 가능
77
+ - 단, 가능하면 cross-platform 코드를 우선하고 web 전용 코드는 `if (kIsWeb)` 가드 또는 conditional import
78
+ - **라우팅**: `go_router` 권장 (URL 동기화). `Navigator 1.0` 의 `MaterialApp.routes` 도 OK 지만 hash routing 주의
79
+ - **자산**: `web/index.html`, `web/manifest.json`, `web/icons/` 관리. SEO 가 필요하면 `<meta>` 태그 명시
80
+ - **CORS**: 백엔드 API 가 `localhost:포트` 에서 다른 origin 일 수 있으므로 CORS 설정 확인
81
+ - **Eval 흐름**: Playwright 기반 `evaluator-functional` + `evaluator-visual` 이 정상 작동 (브라우저 E2E + 시각 검증)
82
+ - 상세 → [Flutter Web Pattern](references/flutter-web-pattern.md)
83
+
84
+ ### `fe_target = mobile` (Android / iOS)
85
+
86
+ - **빌드**: `flutter build apk` / `flutter build ios` / 개발: `flutter run -d <device>`
87
+ - **HTML/JS 인터롭 금지**: `dart:html`, `package:web`, `universal_html` 직접 참조 → 빌드 실패
88
+ - **플랫폼 채널**: 네이티브 기능 사용 시 `MethodChannel` 또는 검증된 plugin 사용
89
+ - **권한**: `AndroidManifest.xml` / `Info.plist` 에 권한 명시
90
+ - **Eval 흐름**: 정적 분석 기반 `evaluator-functional-flutter` 사용 (Playwright 불가)
91
+
92
+ ### `fe_target = desktop` (macOS / Windows / Linux)
93
+
94
+ - **빌드**: `flutter build macos` / `flutter build windows` / `flutter build linux`
95
+ - **HTML/JS 인터롭 금지**: 모바일과 동일
96
+ - **윈도우 관리**: `window_manager` 등 플러그인으로 윈도우 크기/위치 제어
97
+ - **Eval 흐름**: 정적 분석 기반 `evaluator-functional-flutter` 사용
64
98
 
65
99
  ## 핵심 규칙
66
100