@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.
- package/README.md +46 -22
- package/assets/templates/AGENTS.md.template +31 -0
- package/assets/templates/HARNESS.md +180 -6
- package/assets/templates/config.json +205 -14
- package/assets/templates/evaluation-functional.md.template +119 -0
- package/assets/templates/evaluation-visual.md.template +151 -0
- package/assets/templates/memory.md +20 -0
- package/assets/templates/progress.json.template +6 -0
- package/package.json +5 -2
- package/scripts/harness-next.sh +266 -6
- package/scripts/harness-session-start.sh +15 -12
- package/scripts/harness-statusline.sh +93 -0
- package/scripts/harness-user-prompt-submit.sh +31 -72
- package/scripts/lib/harness-guardrail.sh +143 -0
- package/scripts/lib/harness-render-progress.sh +8 -3
- package/scripts/scan-project.sh +33 -3
- package/skills/dispatcher/references/pipeline-definitions.md +20 -9
- package/skills/evaluator-functional-flutter/SKILL.md +10 -3
- package/skills/generator-frontend-flutter/SKILL.md +36 -2
- package/skills/generator-frontend-flutter/references/anti-patterns.md +44 -12
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +273 -0
- package/skills/planner/references/fe-stack-detection.md +62 -15
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-session-start.sh — SessionStart 훅
|
|
3
|
-
#
|
|
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
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
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 플래그 확인
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
[
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
package/scripts/scan-project.sh
CHANGED
|
@@ -62,11 +62,15 @@ fi
|
|
|
62
62
|
|
|
63
63
|
# Frontend
|
|
64
64
|
# Flutter 우선 감지 — pubspec.yaml 존재하면 Flutter 프로젝트로 판정 (TECH_LANG도 보정)
|
|
65
|
-
FE_STACK="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 |
|
|
37
|
-
|
|
38
|
-
| `react` | `generator-frontend` | `evaluator-functional` (Playwright
|
|
39
|
-
| `flutter`| `generator-frontend-flutter` |
|
|
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
|
-
|
|
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"
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
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.
|
|
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
|
|